不僅僅可以用來做爬蟲,Puppeteer 還可以干這個(gè)!
Python 自動(dòng)化測試工具大家可能知道 Pyppeteer,其實(shí)它就是 Puppeteer 的 Python 版本的實(shí)現(xiàn),二者功能類似。但其實(shí) Puppeteer 和 Pyppeteer 不僅僅可以用來做爬蟲,還能干很多其他的事情,今天就來介紹用 Puppeteer 搞的一個(gè)騷操作——自動(dòng)發(fā)文。
閱讀本文大概需要 6 分鐘。
前言
自動(dòng)化測試對(duì)于軟件開發(fā)來說是一個(gè)很重要也很方便的東西,但是自動(dòng)化測試工具除了能用來做測試以外,還能被用來做一些模擬人類操作的事情,所以一些 E2E 自動(dòng)化測試工具(例如:Selenium、Puppeteer、Appium)因?yàn)槠鋸?qiáng)大的模擬功能,經(jīng)常還被爬蟲工程師們用來抓取數(shù)據(jù)。
網(wǎng)上有很多將自動(dòng)化測試工具作為爬蟲的抓取教程,不過僅僅都限于如何獲取數(shù)據(jù),而我們知道這些基于瀏覽器的解決方案都有較大的性能開銷,而且效率不高,并不是爬蟲的最佳選擇。
本篇文章將介紹自動(dòng)化測試工具的另一種用法,也就是用來自動(dòng)化一些人工操作。我們使用的工具是谷歌開發(fā)并開源的測試框架 Puppeteer ,它會(huì)操作 Chromium (谷歌開發(fā)的開源瀏覽器)來完成自動(dòng)化。我們將一步一步介紹如何利用 Puppeteer 在掘金上自動(dòng)發(fā)布文章。
自動(dòng)化測試工具的原理
自動(dòng)化測試工具的原理是通過程式化地操作瀏覽器,與其進(jìn)行模擬交互(例如點(diǎn)擊、打字、導(dǎo)航等等)來控制要抓取的網(wǎng)頁。自動(dòng)化測試工具通常也能獲取網(wǎng)頁的 DOM 或 HTML,因此也可以輕松的獲取網(wǎng)頁數(shù)據(jù)。
此外,對(duì)于一些***站來說,JS 動(dòng)態(tài)渲染的數(shù)據(jù)通常不能輕松獲取,而自動(dòng)化測試工具則可以輕松的做到,因?yàn)樗菍?HTML 輸入瀏覽器里運(yùn)行的。
Puppeteer 簡介
這里摘抄 Puppeteer 的 Github 主頁上的定義(英文)。
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.
翻譯過來大致是:Puppeteer 是一個(gè) Node.js 庫,提供了高級(jí) API 來控制 Chrome 或 Chromium (通過開發(fā)工具協(xié)議);Puppeteer 默認(rèn)的運(yùn)行模式是無頭的,但是可以被配置成非無頭的模式。
Loco注:無頭指的是不顯示瀏覽器的GUI,是為了提升性能而設(shè)計(jì)的,因?yàn)殇秩緢D像是一件很消耗資源的事情。
以下是 Puppeteer 可以做的事情:
?生成截圖和頁面 PDF ;?抓取單頁應(yīng)用,產(chǎn)生預(yù)渲染內(nèi)容(即 SSR ,服務(wù)端渲染);?自動(dòng)化表單提交、 UI 測試、鍵盤輸入等等;?創(chuàng)建一個(gè)最新的、自動(dòng)化的測試環(huán)境;?捕獲網(wǎng)站的時(shí)間線來幫助診斷性能問題;?測試 Chrome 插件;?...
Puppeteer 安裝
安裝 Puppeteer 并不難,只需要保證你的環(huán)境上安裝了 Node.js 以及能夠運(yùn)行 NPM。
由于官方的安裝教程沒有考慮到已經(jīng)安裝了 Chromium 的情況,我們這里使用一個(gè)第三方庫?puppeteer-chromium-resolver,它能夠自定義化 Puppeteer 以及管理 Chromium 的下載情況。
運(yùn)行以下命令安裝 Puppeteer:
npm install puppeteer-chromium-resolver --save
puppeteer-chromium-resolver?的詳細(xì)用法請(qǐng)參照官網(wǎng):https://www.npmjs.com/package/puppeteer-chromium-resolver。
Puppeteer 常用命令
Puppeteer 的官方API文檔是 https://pptr.dev/ ,文檔里有詳細(xì)的 Puppeteer 的開放接口,可以進(jìn)行參考,這里我們只列出一些常用的接口命令。
// 引入puppeteer-chromium-resolverconst PCR = require('puppeteer-chromium-resolver')
// 生成PCR實(shí)例const pcr = await PCR({ ? ?revision: '', ? ?detectionPath: '', ? ?folderName: '.chromium-browser-snapshots', ? ?hosts: ['https://storage.googleapis.com', 'https://npm.taobao.org/mirrors'], ? ?retry: 3, ? ?silent: false})
// 生成瀏覽器const browser = await pcr.puppeteer.launch({...})
// 關(guān)閉瀏覽器await browser.close()
const?page?=?await?browser.newPage()
await?page.goto('https://baidu.com')
await?page.waitFor(3000)
await?page.goto('https://baidu.com')
const?el?=?await?page.$(selector)
await?el.click()
await?el.type(text)
const?res?=?await?page.evaluate((arg1,?arg2,?arg3)?=>?{?//?anything?frontend????return?'frontend?awesome'},?arg1,?arg2,?arg3)
這應(yīng)該是 Puppeteer 中最強(qiáng)大的 API 了。任何熟悉前端技術(shù)的開發(fā)者都應(yīng)該了解 Chrome 開發(fā)者工具中的 Console,任何 JS 的代碼都可以在這里被運(yùn)行,其中包括點(diǎn)擊事件、獲取元素、增刪改元素等等。我們的自動(dòng)發(fā)文程序?qū)⒋罅坑玫竭@個(gè) API 。
可以看到?evaluate?方法可以接受一些參數(shù),并作為回調(diào)函數(shù)中的參數(shù)作用在前端代碼中。這讓我們可以將后端的任何數(shù)據(jù)注入到前端 DOM 中,例如文章標(biāo)題和文章內(nèi)容等等。
另外,回調(diào)函數(shù)中的返回值可以作為?evaluate?的返回值,賦值給?res,這經(jīng)常被用作數(shù)據(jù)抓取。
注意,上面的這些代碼都用了?await?這個(gè)關(guān)鍵字,這其實(shí)是 ES7 中的?async/await?新語法,是 ES6 的?Promise?的語法糖,讓異步代碼更容易閱讀和理解。如果對(duì)?async/await?不理解的同學(xué),可以參考這篇文章:https://juejin.im/post/596e142d5188254b532ce2da。
Puppeteer 實(shí)戰(zhàn):在掘金上自動(dòng)發(fā)布文章
常言說:Talk is cheap, show me the code。
下面,我們將用一個(gè)自動(dòng)發(fā)文章的例子來展示 Puppeteer 的功能。本文中用來作為示例的平臺(tái)是掘金。
為什么選擇掘金呢?這是因?yàn)榫蚪鸬牡卿洸⒉幌衿渌承┚W(wǎng)站(例如 CSDN )要求輸入驗(yàn)證碼(這會(huì)增大復(fù)雜度),只要求輸入賬戶名和密碼就可以登錄了。
為了方便新手理解,我們將從爬蟲基本結(jié)構(gòu)開始講解。(限于篇幅考慮,我們將略過瀏覽器和頁面的初始化,只挑重點(diǎn)講解)
為了讓爬蟲顯得不那么亂七八糟,我們將發(fā)布文章的各個(gè)步驟抽離了出來,形成了一個(gè)基類(因?yàn)槲覀兛赡懿恢咕蚪鹨粋€(gè)平臺(tái)要抓取,使用面向?qū)ο蟮乃枷刖帉懘a的話,其他平臺(tái)只需要繼承基類就可以了)。
這個(gè)爬蟲基類大致的結(jié)構(gòu)如下:
我們不用理解所有的方法,只需要知道我們啟動(dòng)的入口是?run?這個(gè)方法就好了。
所有方法都加上了?async,表示這個(gè)方法將返回?Promise,如果需要以同步的形式調(diào)用,必須加上?await?這個(gè)關(guān)鍵字。
run?方法的內(nèi)容如下:
async run() { // 初始化 ? ?await this.init()
if (this.task.authType === constants.authType.LOGIN) { // 登陸 ? ? ?await this.login() ? ?} else { ? ? ?// 使用Cookie ? ? ?await this.setCookies() ? ?}
// 導(dǎo)航至編輯器 ? ?await this.goToEditor()
// 輸入編輯器內(nèi)容 ? ?await this.inputEditor()
// 發(fā)布文章 ? ?await this.publish()
// 關(guān)閉瀏覽器 ? ?await this.browser.close() ?}
可以看到,爬蟲將首先初始化,完成一些基礎(chǔ)配置;然后根據(jù)任務(wù)的驗(yàn)證類別(authType?)來決定是否采用登錄或 Cookie 的方式來通過網(wǎng)站驗(yàn)證(本文只考慮登錄驗(yàn)證的情況);接下來就是導(dǎo)航至編輯器,然后輸入編輯器內(nèi)容;接著,發(fā)布文章;最后關(guān)閉瀏覽器,發(fā)布任務(wù)完成。
async login() { ? ?logger.info(`logging in... navigating to ${this.urls.login}`) ? ?await this.page.goto(this.urls.login) let errNum = 0 while (errNum < 10) { try { ? ? ? ?await this.page.waitFor(1000) const elUsername = await this.page.$(this.loginSel.username) const elPassword = await this.page.$(this.loginSel.password) const elSubmit = await this.page.$(this.loginSel.submit) ? ? ? ?await elUsername.type(this.platform.username) ? ? ? ?await elPassword.type(this.platform.password) ? ? ? ?await elSubmit.click() ? ? ? ?await this.page.waitFor(3000) break ? ? ?} catch (e) { ? ? ? ?errNum++ ? ? ?} ? ?}
// 查看是否登陸成功 ? ?this.status.loggedIn = errNum !== 10
if (this.status.loggedIn) { ? ? ?logger.info('Logged in') ? ?} ?}
掘金的登錄地址是 https://juejin.im/login,我們先將瀏覽器導(dǎo)航至這個(gè)地址。
這里我們循環(huán) 10 次,嘗試輸入用戶名和密碼,如果 10 次都失敗了,就設(shè)置登錄狀態(tài)為?false;反之,則設(shè)置為?true。
接著,我們用到了?page.$(selector)?和?el.type(text)?這兩個(gè) API ,分別用于獲取元素和輸入內(nèi)容。而最后的?elSubmit.click()?是提交表單的操作。
這里我們略過了跳轉(zhuǎn)到文章編輯器的步驟,因?yàn)檫@個(gè)很簡單,只需要調(diào)用?page.goto(url)?就可以了,后面會(huì)貼出源碼地址供大家參考。
輸入編輯器的代碼如下:
async inputEditor() { ? ?logger.info(`input editor title and content`) // 輸入標(biāo)題 ? ?await this.page.evaluate(this.inputTitle, this.article, this.editorSel, this.task) ? ?await this.page.waitFor(3000)
// 輸入內(nèi)容 ? ?await this.page.evaluate(this.inputContent, this.article, this.editorSel) ? ?await this.page.waitFor(3000)
// 輸入腳注 ? ?await this.page.evaluate(this.inputFooter, this.article, this.editorSel) ? ?await this.page.waitFor(3000)
await this.page.waitFor(10000)
// 后續(xù)處理 ? ?await this.afterInputEditor() ?}
首先輸入標(biāo)題,調(diào)用了?page.evaluate?這個(gè)前端執(zhí)行函數(shù),傳入?this.inputTitle?輸入標(biāo)題這個(gè)回調(diào)函數(shù),以及其他參數(shù);接著同樣的原理,調(diào)用輸入內(nèi)容回調(diào)函數(shù);然后是輸入腳注;最后,調(diào)用后續(xù)處理函數(shù)。
下面我們?cè)敿?xì)看看?this.inputTitle?這個(gè)函數(shù):
async?inputTitle(article,?editorSel,?task)?{?const?el?=?document.querySelector(editorSel.title)????el.focus()????el.select()????document.execCommand('delete',?false)????document.execCommand('insertText',?false,?task.title?||?article.title)??}
我們首先通過前端的公開接口?document.querySelector(selector)?獲取標(biāo)題的元素,為了防止標(biāo)題有 placeholder,我們用?el.focus()(獲取焦點(diǎn))、el.select()(全選)、document.execCommand('delete', false)(刪除)來刪除已有的 placeholder。然后我們通過?document.execCommand('insertText', false, text)來輸入標(biāo)題內(nèi)容。
接下來,是輸入內(nèi)容,代碼如下(它的原理與輸入標(biāo)題類似):
async?inputContent(article,?editorSel)?{?const?el?=?document.querySelector(editorSel.content)????el.focus()????el.select()????document.execCommand('delete',?false)????document.execCommand('insertText',?false,?article.content)??}
有人可能會(huì)問,為什么不用?el.type(text)?來輸入內(nèi)容,反而要大費(fèi)周章的用?document.execCommand?來實(shí)現(xiàn)輸入呢?
這里我們不用前者的原因,是因?yàn)樗峭耆M人的敲打鍵盤操作的,這樣會(huì)破壞已有的內(nèi)容格式。而如果用后者的話,可以一次性的將內(nèi)容輸入進(jìn)來。
我們?cè)诨?BaseSpider?中預(yù)留了一個(gè)方法來完成選擇分類、標(biāo)簽等操作,在繼承后的類?JuejinSpider?中是這樣的:
async afterInputEditor() { // 點(diǎn)擊發(fā)布文章 ? ? ? ?const elPubBtn = await this.page.$('.publish-popup') ? ? ? ?await elPubBtn.click() ? ? ? ?await this.page.waitFor(5000)
// 選擇類別 ? ? ? ?await this.page.evaluate((task) => { ? ? ? ? ? ?document.querySelectorAll('.category-list > .item').forEach(el => { ? ? ? ? ? ? ? ?if (el.textContent === task.category) { ? ? ? ? ? ? ? ? ? ?el.click() ? ? ? ? ? ? ? ?} ? ? ? ? ? ?}) ? ? ? ?}, this.task) ? ? ? ?await this.page.waitFor(5000)
// 選擇標(biāo)簽 ? ? ? ?const elTagInput = await this.page.$('.tag-input > input') ? ? ? ?await elTagInput.type(this.task.tag) ? ? ? ?await this.page.waitFor(5000) ? ? ? ?await this.page.evaluate(() => { ? ? ? ? ? ?document.querySelector('.suggested-tag-list > .tag:nth-child(1)').click() ? ? ? ?}) ? ? ? ?await this.page.waitFor(5000) ? ?}
發(fā)布操作相對(duì)來說比較簡單了,只需要點(diǎn)擊發(fā)布的那個(gè)按鈕就可以了。代碼如下:
async publish() { ? ?logger.info(`publishing article`) // 發(fā)布文章 ? ?const elPub = await this.page.$(this.editorSel.publish) ? ?await elPub.click() ? ?await this.page.waitFor(10000)
// 后續(xù)處理 ? ?await this.afterPublish() ?}
this.afterPublish?是用來處理驗(yàn)證發(fā)文狀態(tài)和獲取發(fā)布 URL 的,這里限于篇幅不詳細(xì)介紹了。
源碼
當(dāng)然,本篇文章由于篇幅原因,介紹的并不是所有的自動(dòng)發(fā)文功能,如果你想了解更多,可以發(fā)送消息【掘金自動(dòng)發(fā)文】到微信公眾號(hào)【NightTeam】獲取源碼地址,注意是【NightTeam】,不是本號(hào)。
總結(jié)
本篇文章介紹了如何使用 Puppeteer 來操作 Chromium 瀏覽器在掘金上發(fā)布文章。
很多人用 Puppeteer 來抓取數(shù)據(jù),但我們認(rèn)為這種效率較低,而且開銷較大,不適合大規(guī)模抓取。
相反, Puppeteer 更適合做一些自動(dòng)化的工作,例如操作瀏覽器發(fā)布文章、發(fā)布帖子、提交表單等等。
Puppeteer 自動(dòng)化工具很類似 RPA(Robotic Process Automation),都是自動(dòng)化一些繁瑣的、重復(fù)性的工作,只不過后者不僅限于瀏覽器,其范圍(Scope)是基于整個(gè)操作系統(tǒng)的,功能更強(qiáng)大,但是開銷也更大。
Puppeteer 作為相對(duì)輕量級(jí)的自動(dòng)化工具,很適合用來做一些網(wǎng)頁自動(dòng)化操作作業(yè)。本文介紹的 Puppeteer 實(shí)戰(zhàn)內(nèi)容也是開源一文多發(fā)平臺(tái)項(xiàng)目?ArtiPub?的一部分,有興趣的同學(xué)可以去嘗試一下。
潤色、校對(duì):「夜幕團(tuán)隊(duì) NightTeam」 - Loco
夜幕團(tuán)隊(duì)成立于 2019 年,團(tuán)隊(duì)包括崔慶才、周子淇、陳祥安、唐軼飛、馮威、蔡晉、戴煌金、張冶青和韋世東。
涉獵的編程語言包括但不限于 Python、Rust、C++、Go,領(lǐng)域涵蓋爬蟲、深度學(xué)習(xí)、服務(wù)研發(fā)、對(duì)象存儲(chǔ)等。團(tuán)隊(duì)非正亦非邪,只做認(rèn)為對(duì)的事情,請(qǐng)大家小心。
感興趣可以關(guān)注夜幕團(tuán)隊(duì)的公眾號(hào)【NightTeam】
自動(dòng)化測試 Puppeteer
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。