文章做者:「夜幕團隊 NightTeam」 - 張冶青javascript
潤色、校對:「夜幕團隊 NightTeam」 - Loco前端
自動化測試對於軟件開發來講是一個很重要也很方便的東西,可是自動化測試工具除了能用來作測試之外,還能被用來作一些模擬人類操做的事情,因此一些 E2E 自動化測試工具(例如:Selenium、Puppeteer、Appium)由於其強大的模擬功能,常常還被爬蟲工程師們用來抓取數據。java
網上有不少將自動化測試工具做爲爬蟲的抓取教程,不過僅僅都限於如何獲取數據,而咱們知道這些基於瀏覽器的解決方案都有較大的性能開銷,並且效率不高,並非爬蟲的最佳選擇。git
本篇文章將介紹自動化測試工具的另外一種用法,也就是用來自動化一些人工操做。咱們使用的工具是谷歌開發並開源的測試框架 Puppeteer ,它會操做 Chromium (谷歌開發的開源瀏覽器)來完成自動化。咱們將一步一步介紹如何利用 Puppeteer 在掘金上自動發佈文章。github
自動化測試工具的原理是經過程式化地操做瀏覽器,與其進行模擬交互(例如點擊、打字、導航等等)來控制要抓取的網頁。自動化測試工具一般也能獲取網頁的 DOM 或 HTML,所以也能夠輕鬆的獲取網頁數據。npm
此外,對於一些動態網站來講,JS 動態渲染的數據一般不能輕鬆獲取,而自動化測試工具則能夠輕鬆的作到,由於它是將 HTML 輸入瀏覽器裏運行的。編程
這裏摘抄 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.api
翻譯過來大體是: Puppeteer 是一個 Node.js 庫,提供了高級 API 來控制 Chrome 或 Chromium (經過開發工具協議); Puppeteer 默認的運行模式是無頭的,可是能夠被配置成非無頭的模式。瀏覽器
Loco注:無頭指的是不顯示瀏覽器的GUI,是爲了提高性能而設計的,由於渲染圖像是一件很消耗資源的事情。
如下是 Puppeteer 能夠作的事情:
安裝 Puppeteer 並不難,只須要保證你的環境上安裝了 Node.js 以及可以運行 NPM。
因爲官方的安裝教程沒有考慮到已經安裝了 Chromium 的狀況,咱們這裏使用一個第三方庫 puppeteer-chromium-resolver
,它可以自定義化 Puppeteer 以及管理 Chromium 的下載狀況。
運行如下命令安裝 Puppeteer:
npm install puppeteer-chromium-resolver --save
puppeteer-chromium-resolver
的詳細用法請參照官網:www.npmjs.com/package/pup…。
Puppeteer 的官方API文檔是 pptr.dev/ ,文檔裏有詳細的 Puppeteer 的開放接口,能夠進行參考,這裏咱們只列出一些經常使用的接口命令。
// 引入puppeteer-chromium-resolver
const PCR = require('puppeteer-chromium-resolver')
// 生成PCR實例
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({...})
// 關閉瀏覽器
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)
複製代碼
這應該是 Puppeteer 中最強大的 API 了。任何熟悉前端技術的開發者都應該瞭解 Chrome 開發者工具中的 Console,任何 JS 的代碼均可以在這裏被運行,其中包括點擊事件、獲取元素、增刪改元素等等。咱們的自動發文程序將大量用到這個 API 。
能夠看到 evaluate
方法能夠接受一些參數,並做爲回調函數中的參數做用在前端代碼中。這讓咱們能夠將後端的任何數據注入到前端 DOM 中,例如文章標題和文章內容等等。
另外,回調函數中的返回值能夠做爲 evaluate
的返回值,賦值給 res
,這常常被用做數據抓取。
注意,上面的這些代碼都用了 await
這個關鍵字,這實際上是 ES7 中的 async/await
新語法,是 ES6 的 Promise
的語法糖,讓異步代碼更容易閱讀和理解。若是對 async/await
不理解的同窗,能夠參考這篇文章:juejin.im/post/596e14…。
常言說:Talk is cheap, show me the code。
下面,咱們將用一個自動發文章的例子來展現 Puppeteer 的功能。本文中用來做爲示例的平臺是掘金。
爲何選擇掘金呢?這是由於掘金的登陸並不像其餘某些網站(例如 CSDN )要求輸入驗證碼(這會增大複雜度),只要求輸入帳戶名和密碼就能夠登陸了。
爲了方便新手理解,咱們將從爬蟲基本結構開始講解。(限於篇幅考慮,咱們將略過瀏覽器和頁面的初始化,只挑重點講解)
爲了讓爬蟲顯得不那麼亂七八糟,咱們將發佈文章的各個步驟抽離了出來,造成了一個基類(由於咱們可能不止掘金一個平臺要抓取,使用面向對象的思想編寫代碼的話,其餘平臺只須要繼承基類就能夠了)。
這個爬蟲基類大體的結構以下:
咱們不用理解全部的方法,只須要知道咱們啓動的入口是 run
這個方法就行了。
全部方法都加上了 async
,表示這個方法將返回 Promise
,若是須要以同步的形式調用,必須加上 await
這個關鍵字。
run
方法的內容以下:
async run() {
// 初始化
await this.init()
if (this.task.authType === constants.authType.LOGIN) {
// 登錄
await this.login()
} else {
// 使用Cookie
await this.setCookies()
}
// 導航至編輯器
await this.goToEditor()
// 輸入編輯器內容
await this.inputEditor()
// 發佈文章
await this.publish()
// 關閉瀏覽器
await this.browser.close()
}
複製代碼
能夠看到,爬蟲將首先初始化,完成一些基礎配置;而後根據任務的驗證類別(authType
)來決定是否採用登陸或 Cookie 的方式來經過網站驗證(本文只考慮登陸驗證的狀況);接下來就是導航至編輯器,而後輸入編輯器內容;接着,發佈文章;最後關閉瀏覽器,發佈任務完成。
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')
}
}
複製代碼
掘金的登陸地址是 juejin.im/login,咱們先將瀏…
這裏咱們循環 10 次,嘗試輸入用戶名和密碼,若是 10 次都失敗了,就設置登陸狀態爲 false
;反之,則設置爲 true
。
接着,咱們用到了 page.$(selector)
和 el.type(text)
這兩個 API ,分別用於獲取元素和輸入內容。而最後的 elSubmit.click()
是提交表單的操做。
這裏咱們略過了跳轉到文章編輯器的步驟,由於這個很簡單,只須要調用 page.goto(url)
就能夠了,後面會貼出源碼地址供你們參考。
輸入編輯器的代碼以下:
async inputEditor() {
logger.info(`input editor title and content`)
// 輸入標題
await this.page.evaluate(this.inputTitle, this.article, this.editorSel, this.task)
await this.page.waitFor(3000)
// 輸入內容
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)
// 後續處理
await this.afterInputEditor()
}
複製代碼
首先輸入標題,調用了 page.evaluate
這個前端執行函數,傳入 this.inputTitle
輸入標題這個回調函數,以及其餘參數;接着一樣的原理,調用輸入內容回調函數;而後是輸入腳註;最後,調用後續處理函數。
下面咱們詳細看看 this.inputTitle
這個函數:
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)
獲取標題的元素,爲了防止標題有 placeholder,咱們用 el.focus()
(獲取焦點)、el.select()
(全選)、document.execCommand('delete', false)
(刪除)來刪除已有的 placeholder。而後咱們經過 document.execCommand('insertText', false, text)
來輸入標題內容。
接下來,是輸入內容,代碼以下(它的原理與輸入標題相似):
async inputContent(article, editorSel) {
const el = document.querySelector(editorSel.content)
el.focus()
el.select()
document.execCommand('delete', false)
document.execCommand('insertText', false, article.content)
}
複製代碼
有人可能會問,爲何不用 el.type(text)
來輸入內容,反而要大費周章的用 document.execCommand
來實現輸入呢?
這裏咱們不用前者的緣由,是由於它是徹底模擬人的敲打鍵盤操做的,這樣會破壞已有的內容格式。而若是用後者的話,能夠一次性的將內容輸入進來。
咱們在基類 BaseSpider
中預留了一個方法來完成選擇分類、標籤等操做,在繼承後的類 JuejinSpider
中是這樣的:
async afterInputEditor() {
// 點擊發布文章
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)
// 選擇標籤
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)
}
複製代碼
發佈操做相對來講比較簡單了,只須要點擊發布的那個按鈕就能夠了。代碼以下:
async publish() {
logger.info(`publishing article`)
// 發佈文章
const elPub = await this.page.$(this.editorSel.publish)
await elPub.click()
await this.page.waitFor(10000)
// 後續處理
await this.afterPublish()
}
複製代碼
this.afterPublish
是用來處理驗證發文狀態和獲取發佈 URL 的,這裏限於篇幅不詳細介紹了。
固然,本篇文章因爲篇幅緣由,介紹的並非全部的自動發文功能,若是你想了解更多,能夠發送消息【掘金自動發文】到咱們的微信公衆號【NightTeam】獲取源碼地址。
本篇文章介紹瞭如何使用 Puppeteer 來操做 Chromium 瀏覽器在掘金上發佈文章。
不少人用 Puppeteer 來抓取數據,但咱們認爲這種效率較低,並且開銷較大,不適合大規模抓取。
相反, Puppeteer 更適合作一些自動化的工做,例如操做瀏覽器發佈文章、發佈帖子、提交表單等等。
Puppeteer 自動化工具很相似 RPA(Robotic Process Automation),都是自動化一些繁瑣的、重複性的工做,只不事後者不只限於瀏覽器,其範圍(Scope)是基於整個操做系統的,功能更強大,可是開銷也更大。
Puppeteer 做爲相對輕量級的自動化工具,很適合用來作一些網頁自動化操做做業。本文介紹的 Puppeteer 實戰內容也是開源一文多發平臺項目 ArtiPub 的一部分,有興趣的同窗能夠去嘗試一下。
夜幕團隊成立於 2019 年,團隊包括崔慶才、周子淇、陳祥安、唐軼飛、馮威、蔡晉、戴煌金、張冶青和韋世東。
涉獵的編程語言包括但不限於 Python、Rust、C++、Go,領域涵蓋爬蟲、深度學習、服務研發、對象存儲等。團隊非正亦非邪,只作認爲對的事情,請你們當心。
本篇文章由一文多發平臺ArtiPub自動發佈