2019年2月16日 星期六

網頁爬蟲終極武器: puppeteer

無頭瀏覽器

下載靜態網頁可以用 wget 或 curl。 那如果是 javascript 動態產生/填寫的頁面呢? 如果只需要網頁快照截圖, 可以用 cutycapt; 如果需要取得 javascript 所產生的文字內容, 那就用 puppeteer。 (字的原意: 操偶師) 它會呼叫 chrome 幫它執行頁面的 javascript, 但不會真的在桌面上打開 chrome 視窗。 在這種模式下運作的瀏覽器, 稱為 headless browser。 今天我們要用 puppeteer 及 headless chrome 來製作網頁爬蟲。

Puppeteer 是用 node.js 寫的, 所以要先 安裝 node.js。 再來, 如果你的 ubuntu 是伺服器, 有可能沒有安裝 chromium 瀏覽器。 要把它安裝起來, 否則會需要手動安裝很多相依套件。 最後是 puppeteer 本身。 原本想要用 npm install -g puppeteer 把 puppeteer 安裝在系統目錄 (全域安裝)。 可是我在 debian 上用 -g 時遇到 權限問題。 (在 ubuntu 上沒有問題) 所以只好改採 (預設的) 本地安裝: cd ~ ; npm install puppeteer 。 過程當中, 它會下載正確版本的 chrome 來搭配。 所謂的 「npm 本地安裝」, 不是每個人的家目錄, 而是每個 project 的子目錄! 哇哩咧~ 這也是為什麼等一下你在我的程式碼裡面的 require('puppeteer') 會看到需要特別要明白指定從家目錄載入 puppeteer。

再來就很簡單了。 以 氣象局的 「目前天氣 觀測資料」 為例, 從上方的兩層選單可以按照全台分區及各區內的縣市找到有興趣的觀測站, 點進去之後, 注意上方的網址會改變 -- 每個測站有自己的網址。 這樣很好。 用 w3m 或 lynx 檢視這個網址, 只會看到網頁骨架跟空白的表格, 因為其他即時資訊是用 javascript 填進去的。 這時可以下載我的小程式 pptscraper.js、 設成可執行 chmod a+x pptscraper.js 、 抓網頁 ./pptscraper.js https://www.cwb.gov.tw/V7/observe/real/46744.htm > a.htm 、 再用 w3m 或 lynx 檢視 a.htm, 現在就可以看到高雄的即時天氣狀況了。

對於不需要 「按這裡按那裡、 跟網頁互動」 的情況, 這個小程式已經很夠用。 例如我喜歡用 w3m 跟 lynx 在黑底綠字的終端機裡讀文章, 遇到採用 js 填內容的網頁時, 用這種方法就可以不需要開啟 firefox 或 chrome。

如果遇到 「需要跟網頁互動才帶得出資料」 的頁面, 那就必須按照互動狀況自己寫程式了。 例如 環保署全國空氣品質指標, 要點選右半中間的測站選單, 才能得到數值 PM2.5 跟 PM10 等等數值。 最討厭的是: 網址不會跟著測站變。 這時只好再更認真研究一點 puppeteer, 寫出 airtw-scraper.js 。 執行 ./airtw-scraper.js > a.htm 會啟動 headless chrome、 在 「測站」 選單點選 「高雄市」 又點選 「左營區」, 再印出最終的網頁內容。 理論上重點就是 page.select('#ddl_county', 'Kaohsiung')page.select('#ddl_site', '17'); 兩句; 但實務上經常會抓到互動前的原始頁面 (基隆市/基隆)。 在一部電腦上, 加上 page.click('#ddl_site'); 才能取得高雄市/左營; 在另一部電腦上, 則要加很多 page.waitFor() 還不能保證 100% 成功。 不知道該怎麼除錯, 也沒有興趣學 node.js (見下方連結), 乾脆就把半成品貼出來, 等路過的 node.js 高手指教改正了。

* * * * *

順便筆記一下爬文連結。 無頭瀏覽器技術有好幾種。 [ 中文, 英文]。 最主要應用於 批次測試網頁上的 javascript。 曾經有一段時間, 最受歡迎的是 phantomjs, 可惜在 2017 年, phantomjs 幻滅了。 現在的主流是 puppeteer + headless chrome。

puppeteer 教學文, 這篇 是很好的入門; 這篇 寫得超讚超清楚, 我只學了一兩招就很好用了。 登入 github、 搜尋的完整例子 經驗豐富的 puppeteer 大師心得 , 超出我的程度太多, 目前好像用不到。

npm 套件管理的問題: 全域安裝或在地安裝? 請見 這個問答npm help folders 的第一頁摘要。

Javascript 本身就已經 令人愛恨交織; node.js 竟然拿 js 為範本來設計成伺服器端的語言, 這讓 node.js 變得超級難學、 難寫、 難除錯、 難維護 -- 簡單講, 有 五大理由 光是 「預設非同步處理」 就令人難以接受。 猜猜看受不了 python 而跳槽到 nodejs 的程式設計師, 一年之後的心得與決定 是什麼? 難怪有人甚至說 node.js 是近年來軟體產業的最重大災難之一。 即使有需求要寫網頁爬蟲, 我也沒辦法強迫自己為了駕馭 puppeteer 而認真學 node.js。 生命應要該浪費在其他美好/有趣/開心/有成就感的事物上才對呀!

3 則留言:

  1. puppeteer 不錯,API 的命名和易用性都比 Phantomjs 高出很多。
    這款也是類似的替代品,最好的賣點是其利用相對標準的 WebExtension API 來設計。
    https://github.com/intoli/remote-browser

    至於對 JS 的想法,我覺得不少其實是成見。
    不可否認 JS 的發展和其最初的目的使得它的特性有別於一般的常見的語言。
    但是不能因為它的特性不一樣就算他是個難、爛、惡的語言。

    針對難學、難寫的部分,這點確實是因為 JS 太過自由且變數不帶型別(還有其他諸如自動轉型等
    但是任何語言都可能有這樣的問題,所以 linter 這種東西一開始其實是針對 C 語言產生的
    (C/C++ 亂用 pointer 看看,我不認為那會好學、好寫、好除錯、好維護)

    非同步的天生設計(特性)是受限於當初的環境(瀏覽器)
    在一個處處需要根據事件(event)去反應的環境(瀏覽器)中非同步的思維本來就是天生
    雖然經過的無數人的驗證 callback 這種撰寫模式確實有違人類的思維方式
    這也才有後來的 promise, async await
    事實上不少(不管是成熟還是年輕)的語言都已經或考慮實作 async 的寫法(C#, python, rust...)

    所以要批判 callback 我可以接受,但非同步並非原罪。
    而且我認為 JS 反而是一門適合初學者學習的語言,不少「後端」開發者克服不了 JS 的一點就是非同步。
    因為他們沒有這樣的思維,反之如果先入門 JS 則不會有這個問題,因為同步及非同步的觀念早已深入心中。

    與其說哪個語言爛或差,不如去理解為什麼它會有這些特性及當初這樣設計的初衷。
    只要懂得順著毛摸,一切都會變得比較簡單。

    回覆刪除
  2. 是沒有說 js 惡或爛啦。 但「難」是很真實、 很難否認的。 就像學游泳, 不知為什麼很多老師們都先教自由式。 自由式學會了, 應該會覺得蛙式很簡單。 問題是對很多初學者來說, 自由式真的很難, 會因而遭受打擊失去信心啊! 厲害的老師或許可以教學生如何「順著毛摸」; 天生資質出眾的學生或許可以自學 node.js。 可是以我自己邏輯還不錯但對於非同步思維並沒有特別天份的程度來說, 再加上 「讀本地檔困難重重」 的經驗, 真心警告跟我一樣普通聰明的初學者要小心慎入。 前端的 js 或許不得不學; 但後端的 node.js, 其實有很多其他更簡單、 更容易有成就感、 普通等級的聰明人也能上手的替代語言。 當然世界上任何一種困難的東西, 總有一些人正好會覺得很簡單, 我的建議, 對於真正有天份的人就並不適用了。

    回覆刪除
    回覆
    1. 我什麼都不懂,愚人一個,不過我的一些小小看法是,如果嘗試某些方式試圖解決某個問題很多次後,還是無解,或是有解但是仍然覺得路很崎嶇難行,可能就是代表此路本來就不應該走 (一開始就走錯了),而要改走別的路,不要硬闖 (即使硬要這麼走還是可以)。

      讀本地檔 (比如讀 .json) 之所以不行,是因為 http 本來就是設計運作在 server-client 這樣的架構上的,所以無論如何一定要有一個 server, 手機上要用 Javascript 讀檔就裝一個 web server 在手機上。我之前也是在想這個問題,後來放棄了,直接接受 javascirpt 就是要運作在網路的環境下的語言,所以即使是這麼基本的讀檔,還是要架一個 webserver,request from webserver.

      非同步 (Async) 就不講了,前面有人講過,這也是基於 http/網路環境 本來就是一個 async 的環境,所以用 async 理所當然 (Event Loop 也是?)。之前我想要用 php 寫出一個 script,可以產生對話的效果 (程式跑起來後會問一些問題,我回答它,然後他會根據此回答做出不同的結果) (這常常應用在 bash script 上),但是我不想要它跑在 command line 的介面上,而是 browser (web page) 上,而我一直想不出方法可以讓程式 (server) 在問出一個問題後中斷,等待我回答問題 (client),然後再繼續 resume 執行 (server),我怎麼想就是想不到怎麼做出這麼簡單的效果。後來我明白了,我當初的思維一直停留在 Block-IO 上,而 http 是 async 的,而且是 Stateless 的,Block-IO 基本上不可行,而 Stateful 本身是很糟糕的 design pattern (在 website scale up 後)。所以可能要用 javascript 的 async 解這個問題。這就是一該始思路就走錯了。

      不過 Javascript 難學是真的,任何產品 UX/UI 設計的不好用起來就很不直覺,如果產品的 UX/UI 在設計時,沒有很努力的考量到一般人的習慣和常規,那用起來就不直覺 (使用者沒辦法不看說明書,用猜就猜出來下一步要幹麻),然後就會感覺很複雜。所謂要讓某件事 "簡單" 有個要素,就是不能 "出乎意料"。 就好像我們看到常見的播放按鈕比如 O << < || > >> 口 ,這些符號已經被一般人接受,習慣,內化及直覺化。如果產品的界面按照這些符號設計,用起來就很直覺因為使用者想都不用想,按下去的結果跟們 "猜測" 的一模一樣,他們潛意識就會覺得 "好用"。但 Javascript 就不是這樣,當太多 "出乎意料" 的規則,語法,pitfall, nuance, special case ... 要記時,就會感覺很複雜,難學,因為不符合直覺,不符合直覺推理的概念,沒幾次就忘了,然後又要再複習一遍,所以 javascript 在這點上設計得很糟糕,非常非常糟糕,python 相比之下好多了,看了 1 2 3,就大概猜得出 4 5 6 要幹麻。

      所以結論就是,我會認清 Javascript 的優勢在那,劣勢在那。優勢多加利用,劣勢就避免 (包括那糟糕的邏輯運算子)。之前好像有人說 Javascript 上可以跑 deep learning 之類的東西,我就想,也許吧,但這好像是 python 或其他低階語言擅長的,我不認為有 silver bullet 可以適用於所有領域.

      刪除