從零開始開發一個Node互動式命令列應用

拓跋zhleven發表於2019-03-05

導言:對於大多數前端開發者而言,談到命令列工具,大家肯定都用過。但是談到開發命令列工具,估計就沒幾人有了解了。本文旨在用最短的時間內,幫您開發一個實用(斜眼笑)的圖片爬蟲命令列應用。

想追求更好的閱讀體驗,請移步拓跋的前端客棧。同時把專案地址放在顯眼的位置

Puppeteer 簡介

什麼是 Puppeteer?

Puppeteer 是 Google Chrome 團隊官方的無介面(Headless)Chrome 工具。Chrome 作為瀏覽器市場的領頭羊,Chrome Headless  將成為 web 應用   自動化測試   的行業標杆。所以我們很有必要來了解一下它。

puppeteer

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 Github

Puppeteer Api Doc

Puppeteer 中文 Api Doc

說了這麼多,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 為名的圖片,最後關閉瀏覽器。

執行結果如下。

baidu

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 檔案,非常給力。

javascript.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,最後進行下載。

執行以下,就能得到一系列貓咪的圖片:

cat

圖片下載的地方只寫了主函式,更詳細的程式碼可以去參見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文件

執行一下上述指令碼,可以得到:

commander.png

這樣命令列就可以做到簡單的互動效果了。但是有沒有覺得不夠好看呢,別急,繼續往下看。

使用inquirer製作可互動命令列應用

inquirer可以為Node製作可嵌入式的美觀的命令列介面。

可以提供問答式的命令輸入:

inquirer1

可以提供多種形式的選擇介面:

inquirer2
inquirer3

可以對輸入資訊進行校驗:

inquirer4

最後可以對輸入資訊進行處理:

inquirer5

上面的例子是inquirer的官方例子,可以參考pizza.js

inquirer的文件可以檢視inquirer documents

有了inquirer,我們就可以製作更為精美的互動式命令列工具了。

使用 chalk.js來讓互動介面更美觀

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!'
));
複製程式碼

可以輸出如下資訊,一看便懂:

chalk2

再讓我們做點有意思的事情...

想必有人看到過下面知乎的控制檯效果,既然要做點有意思的事情,今天我們不妨也把這種效果加到命令列程式裡面,提升一下逼格。

zhihu

首先我們準備一副ASCII碼用來列印,各位可以自行搜尋text轉ASCII,網上的轉化方案不要太多。我們準備製作的命令列image spider就製作一個IMG SPD的ASCII碼字串吧~

經過挑選,效果如圖:

imgspd

這種複雜的字串怎麼列印出來呢?直接儲存為string一定是不行的,格式會亂的一塌糊塗。

想要能完整的列印出格式來,有一個取巧的方法,以註釋的形式列印出來。什麼能儲存註釋呢?~~ function。

所以事情就簡單到了列印一個function。但是直接列印函式還是不行的,這時候就用到了可以懟天懟地的toString()方法,我們只需要把註釋中間的部分用正則匹配出來就行了,easy~

imgspd2

最後看一看效果,鐺鐺鐺鐺~

imgspd3

支援雙擊執行

這裡使用一種叫做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即可執行此命令列

尾聲

至此,要改進的地方已經全部修改完畢,快來看看我們的成品吧~

imgspd4

看著一整個資料夾的gakki,感覺滿滿的幸福要溢位來了

gakki

最後用動圖來展示一下:

img-spd

附錄

專案地址

專案地址

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
複製程式碼

相關文章