:::

擷取AJAX動態產生的網頁內容:PhantomJS指令列工具 / Crawling AJAX Webpages with PhantomJS Command Line Utility

image

現在很多網頁內容都是以動態的方式產生,例如Facebook會在開啟網頁之後再來讀取網頁內容,就連「布丁布丁吃什麼?」也是在網頁開啟之後再來慢慢載入旁邊的小工具。這種使用AJAX技巧來調整畫面的網頁,雖然便於一般使用者用瀏覽器查看,但是卻會造成伺服器端用程式抓取網頁的困難。

還好,現在我們可以用Node.js寫成的虛擬瀏覽器PhantomJS來幫我們載入完整的網頁內容。為此我寫了一些搭配PhantomJS使用的命令列腳本,讓我們可以在Linux 32位元環境下以指令端擷取指定網址,並配合jQuery選取器抽取出需要的網頁元素,最後直接回傳顯示在螢幕上。


配置環境 / Environment

phantomjs-logo

PhantomJS有Windows、Mac OS X、Linux 64位元、Linux 32位元、FreeBSD等不同的版本,可以跨平臺運作。不過本篇我是在Linux 32位元 Debian 7的環境下進行開發,所以不適用其他的環境。

要使用PhantomJS,系統還需要安裝一些相關的套件。詳情請看「How to install PhantomJS on Debian/Ubuntu」。

安裝與配置 / Installation

PhantomJS不能獨自運作,必須要搭配Node.js的腳本。而若要搭配jQuery選取器來擷取網頁元素,那又要有jQuery程式檔。因此我寫了一個可以Linux裡面執行的腳本phantomjs.sh,只要搭配網址、jQuery選取器等參數來執行它,這樣就可以將擷取結果輸出到螢幕上啦。

以下說明這個PhantomJS指令列工具怎麼安裝:

先取得Zip壓縮檔,下載到任意的資料夾中,解壓縮它。我先把檔案下載到/tmp為例。配置完成之後的檔案結果如下圖:(請忽略.git資料夾)

2017-03-12_115112

接著我們要把部分檔案加入執行的權限,增加執行權限的指令如下:

#chmod +x phantomjs.sh

指令後面的「phantomjs.sh」就是檔案。要增加執行權限的檔案如下:

  • phantomjs.sh
  • demo.sh
  • lib/phantomjs

好了,這樣PhantomJS指令列工具就配置完成啦。

用法 / Usage

接下來我們來看看要怎麼使用PhantomJS指令列工具。

範例網頁 / Demo AJAX Webpage

這裡有個用AJAX動態產生的網頁範例:http://pulipulichen.github.io/phantomjs/demo/demo.html

image

乍看之下這好像是顯示兩行「After Render!」。

image

但是從原始碼中可以看到,其實原來是兩行「Before Render…」。

如果是使用curl或PHP的file_get_content()等抓取網頁原始碼的工具,那就只能抓到「Before Render…」的內容。如果要抓取讀取完成「After Render!」,那就得用PhantomJS才行!

PhantomJS指令列工具用法 / PhantomJS Command Line Utility Usage

指令如下:

./phantomjs.sh "[URL]" "[SELECTOR]"

[URL]是指要抓取的網頁,例如「http://pulipulichen.github.io/phantomjs/demo/demo.html」。

[SELECTOR]則是jQuery的選取器,例如「div.content」。

若我們以上面的範例網頁為目標,想要抓取裡面含有「content」class屬性的div標籤的內容,那麼指令是這樣的:

./phantomjs.sh "http://pulipulichen.github.io/phantomjs/demo/demo.html" "div.content"

執行結果為:

<div class="content">After Render!</div><div class="content">After Render!</div>

image

這樣就完成抓取網頁的工作了。

這樣的指令列工具可以搭配任何程式語言運作,像是PHP的shell_exec(),很方便吧。


PhantomJS技巧 / Advanced Skill in PhantomJS

PhantomJS號稱下載、解壓縮、執行,就是這麼簡單。但實際上摸索之後,才發現根本就不是這回事。PhantomJS除了用來擷取網頁內容之外,還能夠為運作畫面截圖、執行自動化的腳本(類似Selenium IDE那樣,但不用真的開啟瀏覽器!)等等多種功能,其實非常地複雜。

PhantomJS必須要搭配Node.js腳本才能運作,而這個Node.js又跟一般的Node.js不一樣,是PhantomJS特別編譯的版本,操作方法請見它的API說明

為了解決我想要的「擷取動態產生的網頁內容」跟「以jQuery擷取指定網頁元素」,這篇的Node.js腳本phantomjs-exec.js包含了許多PhantomJS的進階技巧:

除此之外,我也在phantomjs-exec.js裡面加入了快取的功能。第二次執行抓取相同網頁的時候會從快取取得檔案。如果每次都想要開啟最新網頁的話,請修改phantomjs-exec.js的config.enable_cache = false;。

未來如果要做截圖或自動測試的話,應該也會以phantomjs-exec.js這個檔案繼續延伸吧。


小結 / In closing

image

其實這個PhantomJS指令列工具是為了Full Text RSS而寫得。現在許多RSS不僅不提供全文,就連網頁預設顯示的內容也只有摘要,改成讀取完成之後再用AJAX動態產生全文,像是「綠色工廠」或「硬是要學」。或著是許多網頁使用了延遲載入圖片lazyload的功能,導致RSS直接取得的程式碼沒辦法顯示圖片,像是「巴哈姆特GNN新聞」。每次瀏覽RSS的時候都還要跳到網頁才能看到全文,實在是太煩了,所以我就開始研究這篇的PhantomJS。

現在我的Full Text RSS已經內建了PhantomJS。但因為PhantomJS是仰賴系統的服務,所以我沒有直接把它跟Full Text RSS的原始碼擺在一起。取而代之的,我把它做成了OpenVZ虛擬範本,提供有需要的人直接使用吧:

就結果來說,很令我滿意。

雖然PhantomJS使用上比較複雜一些,但只要做好基礎的phantomjs-exec.js,未來各種應用就變得簡單許多。獲得了PhantomJS這個技術,就可以開啟更多可能性,大概就是這種感覺。

不知不覺的,Node.js越寫越多了啊。未來應該也會逐漸轉變成以Node.js為主要的程式語言吧。

總共10 則留言, (我要發問)

  1. 因為現在Chrome有headless模式,所以PhantomJS停止維護了
    https://www.solidot.org/story?sid=55672
    下次要來研究怎麼用Chrome的headless擷取網頁內容了

    回覆刪除
    回覆
    1. http://blog.darkthread.net/post-2018-07-10-headless-chrome.aspx
      黑暗執行緒提供了如何使用Chrome的Headless來截取網頁資料的方法
      非常值得參考,在這邊做個記錄

      刪除
  2. 謝謝你的文章, 真的獲益良多, 我是搜尋GNN的全圖文rss到這裡的
    搞了大半天, 波折重重, 終於在vSphere裡裝好CentOS+OpenVZ, 並成功執行你的full-text-rss, 可是使用的時候就出現這個錯誤... https://ibb.co/e1z6x8
    我查過/phantomjs_cache/phantomjs_output.html裡是抓到東西的, 但/tmp/裡就甚麼檔案都沒有
    我知道你不會再維護這個範本, 唯有期待你能早日研發好Headless Chrome版本, chrome-remote-interface和chromedriver好像就是關鍵的東西...

    回覆刪除
    回覆
    1. 這邊有三個錯誤訊息:
      Warning: exec(): Unable to fork [/var/www/phantomjs-2.1.1-linux-i686/bin/phantomjs /var/www/phantomjs-2.1.1-linux-i686/bin/plantomjs-exec.js "https://gnn.gamer.com.tw/8/164718.html" "div.GN-lbox3B"] in /var/www/full-text-rss/libraries/conteat-extractor/ConteatExtractor.php on line 163

      Warning: file_get_contents(/tmp/phantomjs_cache/phantomjs_output.html): failed to open stream: No such file or directory in /var/www/full-text-rss/libraries/content-extractor/ContentExtractor.php on line 167

      Fatal error: Call to a member function save() on a non-object in /var/www/full-text-rss/libraries/content-extractor/ConteatExtractor.php on line 419

      最重要的是第一個錯誤訊息,這表示執行phantomjs的時候發生了錯誤
      請嘗試在你的Linux console執行錯誤訊息中的指令:
      /var/www/phantomjs-2.1.1-linux-i686/bin/phantomjs /var/www/phantomjs-2.1.1-linux-i686/bin/phantomjs-exec.js "https://gnn.gamer.com.tw/8/164718.html" "div.GN-lbox3B"
      並檢查執行時發生的錯誤訊息,再根據錯誤訊息來修正你的設定

      我自己伺服器上執行起來倒是沒啥問題就是了

      刪除
    2. root@full-text-rss /# ls
      bin dev home media opt proc run selinux sys usr
      boot etc lib mnt phantomjs_cache root sbin srv tmp var
      root@full-text-rss /# /var/www/phantomjs-2.1.1-linux-i686/bin/phantomjs /var/www/phantomjs-2.1.1-linux-i686/bin/phantomjs-exec.js "https://gnn.gamer.com.tw/8/164718.html" "div.GN-lbox3B"
      Load: https://gnn.gamer.com.tw/8/164718.html

      沒有任何錯誤... Load: https://gnn.gamer.com.tw/8/164718.html 之後就停住了沒回應
      不過OpenVZ裡的debug有點麻煩, 而我對PhantomJS也不熟悉, 現正研究nodejs的rss praser模組

      刪除
    3. 我想你應該在/tmp目錄底下執行這個指令比較好
      或是在root帳號的家目錄 /root

      而不是在系統的根目錄底下執行這個指令

      刪除
    4. 不行...還是一樣 QQ
      cpu和記憶體應該都是足夠的
      root@full-text-rss ~# lscpu
      Architecture: i686
      CPU op-mode(s): 32-bit, 64-bit
      Byte Order: Little Endian
      CPU(s): 2
      Vendor ID: GenuineIntel
      CPU family: 6
      Model: 60
      Stepping: 3
      CPU MHz: 3092.838
      BogoMIPS: 6185.67
      Virtualization: VT-x
      Hypervisor vendor: VMware
      Virtualization type: full
      root@full-text-rss ~# free -m
      total used free shared buffers cached
      Mem: 2048 110 1937 0 0 42
      -/+ buffers/cache: 68 1979
      Swap: 2048 0 2048

      算了吧..無論如何也得謝謝你的啟發, 我昨天研究了一下, 成功的用Puppeteer擷取到gnn的內文全圖文, 剩下的就看看塞回xml後, tt-rss那邊能否顯示到全圖文

      刪除
    5. 好吧,也可能是OpenVZ沒辦法正常運作就是了
      請加油吧!

      刪除
    6. 哇哈哈哈, 我成功了
      https://image.ibb.co/jCRsSJ/2018_07_02_013100.png

      刪除