- 工信部備案號 滇ICP備05000110號-1
- 滇公安備案 滇53010302000111
- 增值電信業務經營許可證 B1.B2-20181647、滇B1.B2-20190004
- 云南互聯網協會理事單位
- 安全聯盟認證網站身份V標記
- 域名注冊服務機構許可:滇D3-20230001
- 代理域名注冊服務機構:新網數碼
有個批量調用 API 抓取數據的需求,類似爬蟲抓數據的感覺。聽到爬蟲二字,我們常常想到的是 Python, Beautiful Soup 之流,而對于簡單地抓取數據這種需求來說,一個小米加步槍就能干掉的東西,拉個加農炮來,顯得有些大材小用。實際上,只需要圍繞著 抓取->格式轉換處理->保存 這簡單三步,然后用合適的工具或編程語言實現就好了。
驅動整個批量抓取過程的核心在于一個循環,把所有要訪問的 URL 放在一個數組,循環遍歷一下。對于我這樣搞前端的來說,結合現代 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)); } })();
簡簡單單一個循環,就可以解決這個問題,但問題來了,萬一中途出錯退出,再次啟動,腳本得重頭開始跑,這顯然有點不夠智能,有沒有辦法實現在程序中斷過后再次啟動時讓程序恢復上次的進度?
想起 SICP 講到的遞歸與迭代的思維。迭代,實際上是用固定數目的狀態變量表示當前程序的狀態的計算過程。迭代計算過程中,程序根據之前設定好的規則從一個狀態轉移到下一個狀態,直到狀態不再滿足某個設定條件才結束。實現上來說,“迭代”二字指的是用來表示狀態的變量的迭代更新。由此可見,我們的關注點應該聚焦在狀態(state)上,for 循環本身也是服務于迭代計算過程的一種語法糖而已。
于是我們很容易可以看出,這個簡單循環過程所迭代更新的狀態變量只有 current,代表當前抓取的 URL 在數組的位置。這個變量存在于內存,而內存中的狀態隨著程序的中止而消失,所以關鍵在于如何把這個狀態固定到磁盤或數據庫等地方。這里能想到的思路是,在程序啟動時把狀態加載進來,在狀態更新的同時把它固定下來。
在這里,我把這個狀態變量序列化成 JSON,然后存儲到文件,實現狀態的固定。
// 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)); } })();
對于本文這個小需求來說,這樣做已經夠用,但擴展一下之后,還是有一些問題的,當狀態變得復雜,需要更多的狀態變量表示的時候,可能會導致持久化的語句遍布整個迭代過程中的每一個涉及到狀態改變的地方,代碼的可讀性也降低了很多,讓人不容易抓住重點。有沒有什么辦法把這些操作集中起來?想到了 Vue.js 的 MVVM 模型,它可以通過監視一個 Object 的變化而驅動視圖的變化,或許我們可以實現類似的一些監聽和觸發機制,在變化的時候實現保存呢?
搜索發現,ES6 的 Proxy 可以滿足這個需求,通過 Proxy 對象,把真正用來保存狀態的對象包裹起來,只要定義一個 set 方法,在接到對象的改變的請求的時候,加入這個持久化操作就好了。另外,由于可能有多級的 Object 的存在,所以也對子對象遞歸加入 Proxy 的監控。
// 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;
然后把循環里面的 current 換成 state.current,小爬蟲就可以放飛自我,隨意中止,再也不用擔心跑的過程出問題而需要重來了~
當然,這里的 saveState 的實現可以很多樣,不一定要寫入文件,還可以改成 Redis, Sqlite 什么的。
售前咨詢
售后咨詢
備案咨詢
二維碼
TOP