本篇文章针对大家熟知的技术站点作为目标进行技术实践。
确定需求
访问目标网站并按照筛选条件(关键词、日期、作者)进行检索并获取返回数据中的目标数据。进行技术拆分如下:
- 打开目标网站
- 找到输入框元素输入关键词,找到日期元素设置日期,找到搜索按钮触发搜索动作
- 解析搜索返回的html元素构造目标数据
- 将目标数据保存
编写代码
<strong>'use strict'</strong>;<strong>const</strong> puppeteer = require('puppeteer');<strong>const</strong> csv = require('fast-csv');<strong>const</strong> fs = require('fs');<br>(<strong>async</strong> () => { <strong>const</strong> startUrl = 'https://www.infoq.cn/'; <strong>const</strong> keyWord = 'CQRS'; <strong>const</strong> browser = <strong>await</strong> puppeteer.launch({ slowMo: 100, <em>// 放慢速度</em> headless: false, <em>// 是否有头</em> defaultViewport: {<em>// 界面设置</em> width: 1820, height: 1080, }, ignoreHTTPSErrors: false, <em>// 忽略 https 报错</em> args: ['--start-maximized', '--no-sandbox', '--disable-setuid-sandbox'], });<br> <strong>const</strong> page = <strong>await</strong> browser.newPage(); <strong>await</strong> page.goto(startUrl).catch(error => console.log(error)); <strong>await</strong> page.waitFor(1 * 1000); <strong>await</strong> page.click('.search,.iconfont'); <strong>await</strong> page.type('.search-input', keyWord, { delay: 100 }); <strong>const</strong> newPagePromise = <strong>new</strong> Promise(x => browser.once('targetcreated', target => x(target.page()))); <strong>await</strong> page.click('.search,.iconfont'); <strong>const</strong> targetPage = <strong>await</strong> newPagePromise; <strong>const</strong> dataCount = <strong>await</strong> targetPage.$eval('.search-body-main-tips span', el => el && el.innerHTML).catch(error => console.error(error)); <strong>if</strong> (dataCount && dataCount > 0) { <strong>const</strong> dataEle = <strong>await</strong> targetPage.$$('.search-item'); console.log(dataEle.length); <strong>const</strong> stream = fs.createWriteStream('infoq.csv'); <strong>const</strong> csvStream = csv.format({ headers: true }); csvStream.pipe(stream).on('end', process.exit); <strong>for</strong> (<strong>let</strong> index = 0; index < dataEle.length; index++) { <strong>const</strong> element = dataEle[index]; <strong>const</strong> title = <strong>await</strong> element.$eval('a', el => el && el.innerHTML).catch(error => console.error(error)) <strong>const</strong> desc = <strong>await</strong> element.$eval('.desc', el => el && el.innerHTML).catch(error => console.error(error)) csvStream.write({ 标题: title || '', 摘要: desc || '', }); } csvStream.end(() => { console.log('写入完毕'); }); } <strong>await</strong> targetPage.screenshot({ path: 'infoq.png' }); <strong>await</strong> browser.close();})();
具体的如下
总结
上面的例子还是比较简单的,站点本身是资讯站(其实有搜索接口根本不需要解析html😂),例子是一个简单的爬虫流程,方便了解puppeteer的能力,下面我也总结一下工作中和自己项目的实际情况。
- 上述例子还不能算是一个完整的应用,根据主要业务分析实际应用大概是这个样子: 爬虫的主要业务变化部分有目标站点、筛选条件、站点操作、数据解析、数据落地,这些都可以通过代码搞定,也就是代码是变化的,所以一个可用的爬虫的应用是代码是可动态调整的,根据上面的动态点将一个爬虫业务抽象成一个Task,
- 它要具备一个参数输入界面(动态输入目标站点、筛选条件等脚本所需参数),每个task的业务不一样,脚本参数不一样,就需要一个动态表单可配置脚本所需参数(Ant design搞一个也够用了);
- 站点操作会根据实际情况而希望能够动态调整脚本代码,那就引入一个在线的vscode编辑器(实际开发中通常都是测试好的脚本才会写进去,这个地方引入有些牵强,主要是线上发现一些小bug需要快速解决);
- 数据解析完了后需要落地,这包括保存到本地、推到api接口、或是下载文件成功推送标识等皆可动态参数和脚本实现;
- Task主要流程还有一个重要的点就是触发需要一个定时任务方便用户设置各种周期性的需求,不管是考虑用户需求还是躲避爬虫频率限制都很有必要。
- Task主要流程完成后还有很重要的基础设施:
- 应用快速部署,当然要用到docker了。因为puppeteer其实是依赖了Chromium的能力,所以你需要在容器里部署一套可运行的chromium,这里面牵扯到字体、环境问题等麻烦事情,这里我推荐一个使用的镜像browserless/chrome,这个镜像大家可以根据自己需求定制,视频中的界面演示即FullHead,容器环境往往不是完整的操作系统(应该没人用完整的windows环境吧)而是尽量小的linux版本(我本人使用的是egg引入puppeteer),那么就不可能有界面,即无法实现FullHead,FullHead可以帮我们规避一些站点(部分站点检测手段比较强)的反爬手段,这里使用的是xvfb实现了FullHead。
- 网络检测,无论是切换容器网络还是在应用中使用ip代理我都没搞过就不说了。分布式爬虫我目前没实现所以这里就不说了。
- 实际使用中遇到的一些问题,资讯站渲染的比较随意,解析比较复杂,有些数据只是展示而无固定标签或规律展示,puppeteer的元素选择器有css和xpath,但是这个和大家平常用jquery不太一样,最好去看看标准,它的xpath解析就不完整(比如string(.),因为puppeteer要包返回装类型所以就完蛋了,我提了issue,答复目前也只能遍历了,或者有谁知道可以提醒我下)。网络问题,访问时渲染有时候会超时这个要自己根据需求搞了。