文章做者:「夜幕团队 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自动发布