- 工信部備案號 滇ICP備05000110號-1
- 滇公安備案 滇53010302000111
- 增值電信業(yè)務(wù)經(jīng)營許可證 B1.B2-20181647、滇B1.B2-20190004
- 云南互聯(lián)網(wǎng)協(xié)會理事單位
- 安全聯(lián)盟認(rèn)證網(wǎng)站身份V標(biāo)記
- 域名注冊服務(wù)機(jī)構(gòu)許可:滇D3-20230001
- 代理域名注冊服務(wù)機(jī)構(gòu):新網(wǎng)數(shù)碼
有個批量調(diào)用 API 抓取數(shù)據(jù)的需求,類似爬蟲抓數(shù)據(jù)的感覺。聽到爬蟲二字,我們常常想到的是 Python, Beautiful Soup 之流,而對于簡單地抓取數(shù)據(jù)這種需求來說,一個小米加步槍就能干掉的東西,拉個加農(nóng)炮來,顯得有些大材小用。實際上,只需要圍繞著 抓取->格式轉(zhuǎn)換處理->保存 這簡單三步,然后用合適的工具或編程語言實現(xiàn)就好了。
驅(qū)動整個批量抓取過程的核心在于一個循環(huán),把所有要訪問的 URL 放在一個數(shù)組,循環(huán)遍歷一下。對于我這樣搞前端的來說,結(jié)合現(xiàn)代 JS 的 async/await 很容易就可以寫出類似下方的代碼(這里我用了 Axios 庫處理 HTTP 請求)。
// Input let read = fs.readFileSync('url-list.txt', 'utf-8'); let urlList = read.split('\n'); (async () => { for (let current = 0; current < urlList.length; current++) { const url = urlList[current]; console.log(current, url); // get let { data } = await Axios.get(url); fs.writeFileSync(`result/${url}`, JSON.stringify(data)); } })();
簡簡單單一個循環(huán),就可以解決這個問題,但問題來了,萬一中途出錯退出,再次啟動,腳本得重頭開始跑,這顯然有點不夠智能,有沒有辦法實現(xiàn)在程序中斷過后再次啟動時讓程序恢復(fù)上次的進(jìn)度?
想起 SICP 講到的遞歸與迭代的思維。迭代,實際上是用固定數(shù)目的狀態(tài)變量表示當(dāng)前程序的狀態(tài)的計算過程。迭代計算過程中,程序根據(jù)之前設(shè)定好的規(guī)則從一個狀態(tài)轉(zhuǎn)移到下一個狀態(tài),直到狀態(tài)不再滿足某個設(shè)定條件才結(jié)束。實現(xiàn)上來說,“迭代”二字指的是用來表示狀態(tài)的變量的迭代更新。由此可見,我們的關(guān)注點應(yīng)該聚焦在狀態(tài)(state)上,for 循環(huán)本身也是服務(wù)于迭代計算過程的一種語法糖而已。
于是我們很容易可以看出,這個簡單循環(huán)過程所迭代更新的狀態(tài)變量只有 current,代表當(dāng)前抓取的 URL 在數(shù)組的位置。這個變量存在于內(nèi)存,而內(nèi)存中的狀態(tài)隨著程序的中止而消失,所以關(guān)鍵在于如何把這個狀態(tài)固定到磁盤或數(shù)據(jù)庫等地方。這里能想到的思路是,在程序啟動時把狀態(tài)加載進(jìn)來,在狀態(tài)更新的同時把它固定下來。
在這里,我把這個狀態(tài)變量序列化成 JSON,然后存儲到文件,實現(xiàn)狀態(tài)的固定。
// Input let read = fs.readFileSync('url-list.txt', 'utf-8'); let urlList = read.split('\n'); let current = JSON.parse(fs.readFileSync('state', 'utf-8')); (async () => { for (; current < urlList.length; current++) { const url = urlList[current]; console.log(current, url); // get let { data } = await Axios.get(url); fs.writeFileSync(`result/${url}`, JSON.stringify(data)); // save state fs.writeFileSync(`state`, JSON.stringify(current)); } })();
對于本文這個小需求來說,這樣做已經(jīng)夠用,但擴(kuò)展一下之后,還是有一些問題的,當(dāng)狀態(tài)變得復(fù)雜,需要更多的狀態(tài)變量表示的時候,可能會導(dǎo)致持久化的語句遍布整個迭代過程中的每一個涉及到狀態(tài)改變的地方,代碼的可讀性也降低了很多,讓人不容易抓住重點。有沒有什么辦法把這些操作集中起來?想到了 Vue.js 的 MVVM 模型,它可以通過監(jiān)視一個 Object 的變化而驅(qū)動視圖的變化,或許我們可以實現(xiàn)類似的一些監(jiān)聽和觸發(fā)機(jī)制,在變化的時候?qū)崿F(xiàn)保存呢?
搜索發(fā)現(xiàn),ES6 的 Proxy 可以滿足這個需求,通過 Proxy 對象,把真正用來保存狀態(tài)的對象包裹起來,只要定義一個 set 方法,在接到對象的改變的請求的時候,加入這個持久化操作就好了。另外,由于可能有多級的 Object 的存在,所以也對子對象遞歸加入 Proxy 的監(jiān)控。
// save state const Store = { fileName: 'state', _state: {}, init: function () { if (fs.existsSync(this.fileName)) { let content = fs.readFileSync(this.fileName, 'utf-8'); if (content) { this._state = JSON.parse(content); } } // state this.state = new Proxy(this._state, this.proxyHandler); }, saveState: function () { // save fs.writeFileSync(this.fileName, JSON.stringify(this._state)); }, proxyHandler: { set: (target, key, value) => { // 遞歸 Proxy if (typeof value === "object") { value = new Proxy(value, this.proxyHandler); } target[key] = value; Store.saveState(); return true; } } }; Store.init(); const state = Store.state;
然后把循環(huán)里面的 current 換成 state.current,小爬蟲就可以放飛自我,隨意中止,再也不用擔(dān)心跑的過程出問題而需要重來了~
當(dāng)然,這里的 saveState 的實現(xiàn)可以很多樣,不一定要寫入文件,還可以改成 Redis, Sqlite 什么的。
售前咨詢
售后咨詢
備案咨詢
二維碼
TOP