導言:對於大多數前端開發者而言,談到命令列工具,大家肯定都用過。但是談到開發命令列工具,估計就沒幾人有了解了。本文旨在用最短的時間內,幫您開發一個實用(斜眼笑)的圖片爬蟲命令列應用。
想追求更好的閱讀體驗,請移步拓跋的前端客棧。同時把專案地址放在顯眼的位置
Puppeteer 簡介
什麼是 Puppeteer?
Puppeteer 是 Google Chrome 團隊官方的無介面(Headless)Chrome 工具。Chrome 作為瀏覽器市場的領頭羊,Chrome Headless 將成為 web 應用 自動化測試 的行業標杆。所以我們很有必要來了解一下它。
Puppeteer 可以做什麼?
Puppeteer 可以做的事情有很多,包括但不限於:
- 利用網頁生成 PDF、圖片
- 可以從網站抓取內容
- 自動化表單提交、UI 測試、鍵盤輸入等
- 幫你建立一個最新的自動化測試環境(chrome),可以直接在此執行測試用例
- 捕獲站點的時間線,以便追蹤你的網站,幫助分析網站效能問題
Puppeteer 有什麼優勢?
- 相對於真實瀏覽器而言,少了載入 css,js 以及渲染頁面的工作。無頭瀏覽器要比真實瀏覽器快的多。
- 可以在無介面的伺服器或 CI 上執行,減少了外界的干擾,更穩定。
- 在一臺機器上可以模擬執行多個無頭瀏覽器,方便進行併發執行。
如何安裝 Puppeteer?
安裝 Puppeteer 很簡單,如下:
npm i --save puppeteer
or
yarn add puppeteer
需要注意的是,由於用到了 ES7 的 async/await 語法 ,node 版本最好是 v7.6.0 或以上。
如何使用 Puppeteer?
由於本文不是專門講 Puppeteer 的文章,故這部分暫且略過,大家可以去看下面的連結學習。
說了這麼多,Puppeteer 與我們要開發的命令列應用有什麼關係呢?我們準備製作一個抓圖命令列工具,不使用傳統的請求式爬蟲,我們使用 Puppeteer 這種無頭瀏覽器,從 DOM 裡抓圖,這樣能有效規避部分爬蟲防禦手段。
Puppeteer 簡單應用
case 1. 螢幕截圖
直接上程式碼,很好理解:
const puppeteer = require("puppeteer");
const getScreenShot = async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://baidu.com");
await page.screenshot({ path: "baidu.png" });
await browser.close();
};
getScreenShot();
複製程式碼
這段程式碼的意思就是以 headless(無頭)模式開啟瀏覽器,然後開啟一個新標籤頁,跳轉到百度網址, 並且進行螢幕截圖,儲存為 baidu.png 為名的圖片,最後關閉瀏覽器。
執行結果如下。
case 2. 抓取網站資訊
接下來學習如何用 Puppeteer 抓取網站資訊了。
這次我們來抓取 jd 書單資訊。
// book info spider
const puppeteer = require("puppeteer");
const fs = require("fs");
const spider = async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto("https://search.jd.com/Search?keyword=javascript");
const result = await page.evaluate(() => {
let elements = document.querySelectorAll(".gl-item");
const data = [...elements].map(i => {
return {
name: i.querySelector(".p-name em").innerText,
description: i.querySelector(".p-name i").innerText,
price: i.querySelector(".p-price").innerText,
shop: i.querySelector(".p-shopnum").innerText,
url: i.querySelector(".p-img a").href
};
});
return data; // 返回資料
});
browser.close();
return result;
};
spider().then(value => {
fs.writeFile(`${__dirname}/javascript.json`, JSON.stringify(value), err => {
if (err) {
throw err;
}
console.log("file saved!");
});
console.log(value); // Success!
});
複製程式碼
我們做的就是跳轉到關鍵字是 javascript 的頁面,然後對頁面的 dom 結構進行分析,找到圖書列表所對應的書名、描述、價格、出版社、網頁連結資訊,然後把資料寫入到 javascript.json 檔案裡去,方便我們儲存瀏覽。
邏輯很簡單。這已經是一個爬蟲的雛形了,最後得到如下圖所示的 json 檔案,非常給力。
case 3. 圖片爬蟲
圖片爬蟲,這就是我們要做的命令列應用的主題了。
一個最基本的思路是這樣的:
開啟瀏覽器 —> 跳轉到百度圖片 —> 獲取 input 框的焦點 —> 輸入 keywords —> 點選搜尋按鈕 —> 跳轉至結果列表頁 —> 下拉到底部 —> 操作 dom,獲取所有圖片的 src 備用 —> 根據 src 將對應圖片儲存到本地 —> 關閉瀏覽器
程式碼實現之:
首先是瀏覽器操作部分
const browser = await puppeteer.launch(); // 開啟瀏覽器
const page = await browser.newPage(); // 開啟新tab頁
await page.goto("https://image.baidu.com"); // 跳轉到百度圖片
console.log("go to https://image.baidu.com"); // 獲取input框的焦點
await page.focus("#kw"); // 把焦點定位到搜尋input框
await page.keyboard.sendCharacter("貓咪"); // 輸入關鍵字
await page.click(".s_search"); // 點選搜尋按鈕
console.log("go to search list"); // 提示跳轉到搜尋列表頁
複製程式碼
然後是圖片處理部分
page.on("load", async () => {
await autoScroll(page); // 向下滾動載入圖片
console.log("page loading done, start fetch...");
const srcs = await page.evaluate(() => {
const images = document.querySelectorAll("img.main_img");
return Array.prototype.map.call(images, img => img.src);
}); // 獲取所有img的src
console.log(`get ${srcs.length} images, start download`);
for (let i = 0; i < srcs.length; i++) {
await convert2Img(srcs[i], target);
console.log(`finished ${i + 1}/${srcs.length} images`);
} // 儲存圖片
console.log(`job finished!`);
await browser.close();
});
複製程式碼
因為百度圖片是往下滾動就可以繼續懶載入。因此,我們想要載入更多圖片,可以先往下滾動一會兒。然後通過分析 dom 結構來獲取列表裡所有圖片的 src,最後進行下載。
執行以下,就能得到一系列貓咪的圖片:
圖片下載的地方只寫了主函式,更詳細的程式碼可以去參見github.
至此,我們用 Node 和 Puppeteer 開發出了一個最基本的圖片爬蟲工具。
如何優化?
這個圖片爬蟲工具目前還有點 low 啊,我們的目標是要開發一個互動式的命令列應用,肯定不能止於此。有哪些可以進一步優化的點呢?經過簡單的思考,我列了一下:
- 下載圖片的內容可以自定義
- 可以支援使用者選擇圖片下載張數
- 支援命令列傳參
- 支援命令列互動
- 互動介面美觀
- 支援雙擊直接執行
- 支援全域性命令列呼叫
使用 commander.js 支援命令列傳參
Commander 是一款重量輕,表現力和強大的命令列框架。提供了使用者命令列輸入和引數解析強大功能。
const program = require("commander");
program
.version("0.0.1")
.description("a test cli program")
.option("-n, --name <name>", "your name", "zhl")
.option("-a, --age <age>", "your age", "22")
.option("-e, --enjoy [enjoy]")
.action(option => {
console.log('name: ', option.name);
console.log('age: ', option.age);
console.log('enjoy: ', option.enjoy);
});
program.parse(process.argv);
複製程式碼
Commander十分容易上手,上面這一段程式碼僅用了寥寥數行,就定義了一個命令列的輸入與輸出。其中:
- version 定義版本號
- description 定義描述資訊
- option 定義輸入選項,傳3個引數,如
option("-n, --name <name>", "your name", "GK")
,第一項是傳參的值,-n是簡寫形式,--name是全稱形式, 表示輸入的引數,<>是必填項,如果是[],則是選填項。第二項“your name"是求助help時的提示資訊,告訴使用者應該輸入的內容,最後一項"GK"是預設值。 - action 定義執行的操作,是一個回撥函式,入參是前文輸入的option選項,如果沒有輸入option,則使用定義的預設值。
要查詢更詳細的api,請參考Commander Api文件。
執行一下上述指令碼,可以得到:
這樣命令列就可以做到簡單的互動效果了。但是有沒有覺得不夠好看呢,別急,繼續往下看。
使用inquirer製作可互動命令列應用
inquirer可以為Node製作可嵌入式的美觀的命令列介面。
可以提供問答式的命令輸入:
可以提供多種形式的選擇介面:
可以對輸入資訊進行校驗:
最後可以對輸入資訊進行處理:
上面的例子是inquirer的官方例子,可以參考pizza.js
inquirer的文件可以檢視inquirer documents
有了inquirer,我們就可以製作更為精美的互動式命令列工具了。
使用 chalk.js來讓互動介面更美觀
chalk的語法非常簡單:
const chalk = require('chalk');
const log = console.log;
// Combine styled and normal strings
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
// Compose multiple styles using the chainable API
log(chalk.blue.bgRed.bold('Hello world!'));
// Pass in multiple arguments
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
// Nest styles
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
// Nest styles of the same type even (color, underline, background)
log(chalk.green(
'I am a green line ' +
chalk.blue.underline.bold('with a blue substring') +
' that becomes green again!'
));
複製程式碼
可以輸出如下資訊,一看便懂:
再讓我們做點有意思的事情...
想必有人看到過下面知乎的控制檯效果,既然要做點有意思的事情,今天我們不妨也把這種效果加到命令列程式裡面,提升一下逼格。
首先我們準備一副ASCII碼用來列印,各位可以自行搜尋text轉ASCII,網上的轉化方案不要太多。我們準備製作的命令列image spider就製作一個IMG SPD的ASCII碼字串吧~
經過挑選,效果如圖:
這種複雜的字串怎麼列印出來呢?直接儲存為string一定是不行的,格式會亂的一塌糊塗。
想要能完整的列印出格式來,有一個取巧的方法,以註釋的形式列印出來。什麼能儲存註釋呢?~~ function。
所以事情就簡單到了列印一個function。但是直接列印函式還是不行的,這時候就用到了可以懟天懟地的toString()方法,我們只需要把註釋中間的部分用正則匹配出來就行了,easy~
最後看一看效果,鐺鐺鐺鐺~
支援雙擊執行
這裡使用一種叫做Shebang的技術。
Shebang(也稱為 Hashbang )是一個由#和!構成的字元序列 #! ,其出現在文字檔案的第一行的前兩個字元。 在檔案中存在 Shebang 的情況下,類 Unix 作業系統的程式載入器會分析 Shebang 後的內容,將這些內容作為直譯器指令,並呼叫該指令,並將載有 Shebang 的檔案路徑作為該直譯器的引數。
node下我們使用#! /usr/bin/env node即可
這時候我們便可以取消檔案的副檔名.js了。
加入環境變數,支援全域性呼叫
package.json裡面配置
"bin": {
"img-spd": "app"
},
複製程式碼
執行npm link,它將會把img-spd這個欄位複製到npm的全域性模組安裝資料夾node_modules內,並建立符號連結(symbolic link,軟連結),也就是將 app 的路徑加入環境變數 PATH。
這時,在任意目錄下,直接命令列輸入img-spd即可執行此命令列
尾聲
至此,要改進的地方已經全部修改完畢,快來看看我們的成品吧~
看著一整個資料夾的gakki,感覺滿滿的幸福要溢位來了
最後用動圖來展示一下:
附錄
專案地址
Install
npm install -g img-spd
複製程式碼
Usage
img-spd
複製程式碼
or
Usage: img-spd [options]
img-spd is a spider get images from image.baidu.com
Options:
-v --version output the version number
-k, --key [key] input the image keywords to download
-i, --interval [interval] input the operation interval(ms,default 200)
-n, --number [number] input the operation interval(ms,default 200)
-m, --headless [headless] choose whether the program is running in headless mode
-h, --help output usage information
複製程式碼