實例:使用puppeteer headless方式抓取JS網頁
google chrome團隊出品的puppeteer 是依賴nodejs和chromium的自動化測試庫,它的最大優點就是可以處理網頁中的動態內容,如JavaScript,能夠更好的模擬用戶。
有些網站的反爬蟲手段是將部分內容隱藏於某些javascript/ajax請求中,致使直接獲取a標簽的方式不奏效。甚至有些網站會設置隱藏元素“陷阱”,對用戶不可見,腳本觸發則認為是機器。這種情況下,puppeteer的優勢就凸顯出來了。
它可實現如下功能:
- 生成頁面的屏幕截圖和PDF。
- 抓取SPA並生成預先呈現的內容(即“SSR”)。
- 自動表單提交,UI測試,鍵盤輸入等。
- 創建一個最新的自動化測試環境。使用最新的JavaScript和瀏覽器功能,直接在最新版本的Chrome中運行測試。
- 捕獲跟蹤您網站的時間線,以幫助診斷性能問題。
開源地址:[https://github.com/GoogleChrome/puppeteer/][1]
安裝
npm i puppeteer
註意先安裝nodejs, 並在nodejs文件根目錄下執行(npm文件同級)。
安裝過程中會下載chromium,大約120M。
用兩天(大約10小時)摸索,繞過了相當多的異步的坑,筆者對puppeteer和nodejs有了一定的掌握。
一張長圖,抓取blog文章列表:
抓取blog文章
以csdn blog為例,文章內容需要點擊“閱讀全文”來獲取,這就導致只能讀取dom的腳本失效。
/** * load blog.csdn.net article to local files **/ const puppeteer = require(‘puppeteer‘); //emulate iphone const userAgent = ‘Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1‘; const workPath = ‘./contents‘; const fs = require("fs"); if (!fs.existsSync(workPath)) { fs.mkdirSync(workPath) } //base url const rootUrl = ‘https://blog.csdn.net/‘; //max wait milliseconds const maxWait = 100; //max loop scroll times const makLoop = 10; (async () => { let url; let countUrl=0; const browser = await puppeteer.launch({headless: false});//set headless: true will hide chromium UI const page = await browser.newPage(); await page.setUserAgent(userAgent); await page.setViewport({width:414, height:736}); await page.setRequestInterception(true); //filter to block images page.on(‘request‘, request => { if (request.resourceType() === ‘image‘) request.abort(); else request.continue(); }); await page.goto(rootUrl); for(let i= 0; i<makLoop;i++){ try{ await page.evaluate(()=>window.scrollTo(0, document.body.scrollHeight)); await page.waitForNavigation({timeout:maxWait,waitUntil: [‘networkidle0‘]}); }catch(err){ console.log(‘scroll to bottom and then wait ‘+maxWait+‘ms.‘); } } await page.screenshot({path: workPath+‘/screenshot.png‘,fullPage: true, quality :100, type :‘jpeg‘}); //#feedlist_id li[data-type="blog"] a const sel = ‘#feedlist_id li[data-type="blog"] h2 a‘; const hrefs = await page.evaluate((sel) => { let elements = Array.from(document.querySelectorAll(sel)); let links = elements.map(element => { return element.href }) return links; }, sel); console.log(‘total links: ‘+hrefs.length); process(); async function process(){ if(countUrl<hrefs.length){ url = hrefs[countUrl]; countUrl++; }else{ browser.close(); return; } console.log(‘processing url: ‘+url); try{ const tab = await browser.newPage(); await tab.setUserAgent(userAgent); await tab.setViewport({width:414, height:736}); await tab.setRequestInterception(true); //filter to block images tab.on(‘request‘, request => { if (request.resourceType() === ‘image‘) request.abort(); else request.continue(); }); await tab.goto(url); //execute tap request try{ await tab.tap(‘.read_more_btn‘); }catch(err){ console.log(‘there\‘s none read more button. No need to TAP‘); } let title = await tab.evaluate(() => document.querySelector(‘#article .article_title‘).innerText); let contents = await tab.evaluate(() => document.querySelector(‘#article .article_content‘).innerText); contents = ‘TITLE: ‘+title+‘\nURL: ‘+url+‘\nCONTENTS: \n‘+contents; const fs = require("fs"); fs.writeFileSync(workPath+‘/‘+tab.url().substring(tab.url().lastIndexOf(‘/‘),tab.url().length)+‘.txt‘,contents); console.log(title + " has been downloaded to local."); await tab.close(); }catch(err){ console.log(‘url: ‘+tab.url()+‘ \n‘+err.toString()); }finally{ process(); } } })();
執行過程
錄屏可以在我公眾號查看,下邊是截圖:
執行結果
文章內容列表:
文章內容:
結束語
以前就想過既然nodejs是使用JavaScript腳本語言,那麽它肯定能處理網頁的JavaScript內容,但並沒有發現合適的/高效率的庫。直到發現puppeteer,才下定決心試水。
話說回來,nodejs的異步真的是很頭疼的一件事,這上百行代碼我竟然折騰了10個小時。
大家可拓展下代碼中process()
方法,使用async.eachSeries
,我使用的遞歸方式並不是最優解。
事實上,逐一處理並不高效,原本我寫了一個異步的關閉browser方法:
let tryCloseBrowser = setInterval(function(){ console.log("check if any process running...") if(countDown<=0){ clearInterval(tryCloseBrowser); console.log("none process running, close.") browser.close(); } },3000);
按照這個思路,代碼的最初版本是同時打開多個tab頁,效率很高,但容錯率很低,大家可以試著自己寫一下。
題外話
看過我的文章的人都知道,我寫文章更強調處理問題的方式/方法,給大家一些思維上的建議。
對於nodejs和puppeteer我是完全陌生的(當然,我知道他們適合做什麽,僅此而已)。如果大家還記得《10倍速程序員》裏提到的按需記憶的理念,那麽你就會理解我刻意的不去系統的學習新技術。
我說說我接觸puppeteer到完成我需要功能的所有思維邏輯:
- 了解puppeteer功能/特性,結合目的判斷是否滿足要求。
- 快速實現getStart中的所有demo
- 二次判斷puppeteer的特性,從設計者角度出發,推測puppeteer的架構。
- 驗證架構。
- 通讀api,了解puppeteer細節。
- 搜索puppeteer前置學習內容(以及前置學習內容所依賴的前置學習內容)。整理學習內容樹,回到1。
- 設計/分析/調試/……
2018年5月9日02點13分
實例:使用puppeteer headless方式抓取JS網頁