关于puppeteer:Mac-M1arm-系列芯片如何安装-Chromium-Puppeteer

最近写个脚本用到 puppeteer,而后装置 Chromium 呈现一点问题,这里记录一下解决方案。 Puppeteer 主动装置失败在 Puppeteer 装置时会主动装置 Chromium,然而却总是报错 502 导致下载失败,间接下载能够下载,命令行 wget 也能够,猜想是因为 Puppeteer 开启了新的 process 来装置导致环境变量失落,而后就迷信上网失败了。 这会回头看了看 Puppeteer 的装置脚本,如同能够应用 npm_config_https_proxy 等配置来迷信上网。 function overrideProxy() { // Override current environment proxy settings with npm configuration, if any. const NPM_HTTPS_PROXY = process.env['npm_config_https_proxy'] || process.env['npm_config_proxy']; const NPM_HTTP_PROXY = process.env['npm_config_http_proxy'] || process.env['npm_config_proxy']; const NPM_NO_PROXY = process.env['npm_config_no_proxy']; if (NPM_HTTPS_PROXY) { process.env['HTTPS_PROXY'] = NPM_HTTPS_PROXY; } if (NPM_HTTP_PROXY) { process.env['HTTP_PROXY'] = NPM_HTTP_PROXY; } if (NPM_NO_PROXY) { process.env['NO_PROXY'] = NPM_NO_PROXY; }}不过过后没看就间接打算手动装置 Chromium 了,要手动装置在装置 Puppeteer 时须要先敞开主动下载: ...

April 27, 2023 · 2 min · jiezi

关于puppeteer:puppeteer-cheerio-mysql-模拟用户去爬取一个aspx网站

起源最近被前共事问是否能够帮他去爬取一个网站的数据,而后他把网站发给我了,之后我就去钻研了下, 原本打算用spider-flow 这个货色来爬的,毕竟能不写代码的,为啥我要去写代码,而后钻研了下spider-flow 发现满足不了需要,节约了两天工夫,, 还是老老实实手写把,对spider-flow 感兴趣的能够看看我写的:https://www.mubucm.com/doc/7rBgfYhSzrt最终成果 作者在写这篇文章的技能和环境前端略懂一二nodejs 无所不通数据库无所不通window零碎puppeteer 版本: 19.7.4node版本: 14.18.0本文只适宜小白浏览,大佬请出门左转~页面地址http://zjj.sz.gov.cn:8004/?__EVENTARGUMENT=3&ddlPageCount=20本文将以爬取一手房预售房源数据为例 页面剖析一手房预售信息页面信息首页数据从哪里来? 页面执行逻辑 页面渲染逻辑 我的项目详情页跳转逻辑:http://zjj.sz.gov.cn/ris/bol/szfdc/projectdetail.aspx?id=126809 证件详情页跳转逻辑:http://zjj.sz.gov.cn/ris/bol/szfdc/certdetail.aspx?id=126809 思路思路1:模仿用户操作,像一个失常的用户去点击而后爬取数据,之后再把数据存储到数据库, 而后点击分页反复这个操作,直到爬取完所有数据思路2:间接调用获取数据接口,而后解析数据,之后再把数据存储到数据库咱们先用思路1的形式来做,原本我想用思路2的做法来做,起初有个问题没解决,所以先依照思路1来实现外围代码解说puppetter 如何扭转页面的值?官网api 具体代码,依照20条/页去爬取puppetter 如何触发事件?api 实例:page.click(#AspNetPager1 > span:nth-last-of-type(1), {delay: 100})应用puppetter拜访页面,如何去解析数据?一开始我还始终被困在这个puppetter api,始终想在这个浏览器执行的时候去获取这些信息,其实这种也是能够的,然而不不便,起初百度发现能够应用cheerio这个库,像应用jquery把对页面做操作cheerio 遍历页面数据cheerio 具体代码如何把保留数据到数据库?新建一个house_info 数据库,而后搞了一个presell 预售表,预售的表构造 sql相干常识查问数据select * from presell更新某一条数据:update presell set name='阿斯顿撒',enterprise=99 WHERE serial=2更新所有的数据:UPDATE presell SET address='深圳'插入数据:INSERT INTO presell (mainKey,serial,id) VALUES (1111,2,3);批量插入数据:INSERT INTO presell ( mainKey, serial, id, NAME, enterprise, address, date ) VALUES ( '10001', '2', '3', '4', '5', '6', '2022-02-06' ),( '10002', '2222', '3', '4', '5', '6', '2022-02-06' )如何批量插入数据 ...

March 22, 2023 · 2 min · jiezi

关于puppeteer:centos7-puppeteer-Error-安装失败

装置失败是因为须要装置chrome,网络连接不通: 采纳阿里云下载: npm --registry https://registry.npm.taobao.org install express

July 10, 2022 · 1 min · jiezi

关于puppeteer:使用-puppeteer-nodejs-爬取喜欢的动漫资源

起源最近忽然想尝试剪视频,所以就想先从动漫开始,二次元搞起来,剪视频就必须须要原视频,怎么找到这些资源呢,知乎一搜一大把我常常会上六DM 外面去看动漫,外面的动漫清晰度也还能够,所以就想怎么写个爬虫间接把喜爱的动漫下载下来,毕竟是干前端的,手动下载有点丢人把 最终成果 下载后的文件名不是.mp4 怎么解决比如说我下载的这个龙猫就是啥yum格局的,我间接后缀名改成.mp4 搞定,如果还不行,就上个格局工厂 应该就好了作者在写这篇文章的技能和环境前端略懂一二nodejs 无所不通window零碎puppeteer 版本: 14.3.0node版本: 16.1.0开始剖析网站, 轻易搜一个喜爱的动漫介绍页 轻易点击一个播放地址,F12 搞起来,剖析页面能够,这个网站还是会玩的,调试工具开起来就给我始终debugger如何跳过debugger死循环这种形式能够不便跳过死循环,并且咱们还能够持续调试找到页面播放地址这个网站还是很简略的, 间接把资源地址丢到iframe外面而已,难度升高了,怪不得不给他人调试,剖析播放地址由来思路1:通过接口申请剖析,是否存在共通点通过抓包,发现第一集和第二集的播放资源门路没有共同性可言,放弃o(╥﹏╥)o思路2:间接看播放器的源码逻辑,找到url的拼接逻辑找出播放器源码,间接通过调试工具找到所有的js文件,而后轻易看看 剖析播放器源码读取播放器源码发现,这个网站会在页面外面存入一个全局变量全局变量 player_aaaa会做赋值存储,而后会引入一个js文件,文件名:/static/player/parse.js关上调试工具,找到/static/player/parse.js文件,/static/player/parse.js 文件内容 浏览器控制台输出:MacPlayer.Parse + MacPlayer.PlayUrl因为播放器的源码是一个自执行函数,而后咱们又看到这个parse.js 文件外面的资源拼接形式,所以咱们能够在浏览器的控制台外面间接把这个资源给拼出来右键保留?把下面的地址放到浏览器外面拜访,发现就是咱们想下载的资源了, 到了这一步骤,咱们就能够右键保留了,当然作为一名合格的前端,咱们怎么可能会去右键保留呢,接下来咱们就筹备上大杀器,puppeteer 配合nodejs 来帮咱们实现主动下载资源demo如何做自动化?通过下面的连贯,会进入到一个解析页面,因为咱们要做成主动下载的,必须要找到视频源连贯,否则不行,o(╥﹏╥)o查找元素页面,发现了最终的资源地址最终的资源地址 应用puppeteer解析页面,获取到视频资源地址,而后应用nodejs主动下载视频思路1:遍历出播放列表,而后开始一个工作,顺次关上页面,找到资源地址,而后收集到所有的播放资源地址,应用nodejs下载到本地, 为啥不必下面那个思路,因为那个思路我把代码写完,测试了一下,发现他的服务器扛不住哈,所以还是保险点,一次一个操作思路2:应用puppetee 主动触发右键下载,并保留到咱们想要下载的中央(目前没有尝试这个办法)思路3:遍历出播放列表,而后开始一个工作,从第一个开始,关上页面,找到资源地址,应用nodejs下载到本地,下载实现,开始下一个就这样思路3 难点剖析pupeteer 如何获取元素的属性,别问我,反正我不懂, 堆栈溢出大佬通知我的堆栈溢出的答案 // 获取单个await page.evaluate('document.querySelector("span.styleNumber").getAttribute("data-Color")')// 获取多个const attr = await page.$$eval("span.styleNumber", el => el.map(x => x.getAttribute("data-Color")));nodejs下载远端视频,并显示进度const fs = require('fs');const https = require('https')// 我的demo应用的是axios 来下载const axiosRequest = require('./utils/request');// 这是一个axios实例axiosRequest.get('https://media.w3.org/2010/05/sintel/trailer.mp4', { responseType: 'stream'}).then(response => {// 返回头外面的content-length字段,会通知咱们这个视频有多大// 获取视频总长度 byte为单位const totalLength = response.headers['content-length']// 以后数据的总长度let totalChunkLength = 0// 以后读取的流const readSteam = response.data// 读取流会触发的事件readSteam.on('data', (chunk) => {totalChunkLength += chunk.lengthconsole.log('数据传输中,以后进度==>', ((totalChunkLength / totalLength) * 100).toFixed(2) + '%') });// 读取实现的工夫readSteam.on('end', (chunk) => {console.log('获取远端数据结束') });// 读取谬误会触发的事件readSteam.on('error', (err) => {console.log('获取远端数据结束,产生了谬误,错误信息==>', err) });// 写入本地的文件名const fileName = 67.mp4// 调用nodejs写入文件办法const writeFile = readSteam.pipe(fs.createWriteStream(fileName))// 写入实现事件writeFile.on("finish", () => {writeFile.close();console.log("祝贺大哥,本地数据写入实现"); });// 写入谬误触发的事件writeFile.on("error", (err) => {console.log("不好意思,写入本地文件产生异样,错误信息==>", err); });});//axios 代码如下const axios = require('axios')// 创立axios实例const service = axios.create({ baseURL: '', // api 的 base_url // 永不凋零,真男人 就是这么长久 timeout: 90000000 // 申请超时工夫})// request拦截器service.interceptors.request.use( config => { return config }, error => { // Do something with request error console.log(error) // for debug Promise.reject(error) })// 响应拦截器service.interceptors.response.use( response => { return response }, error => { return Promise.reject(error) })module.exports = service残缺代码残缺代码 ...

June 11, 2022 · 1 min · jiezi

关于puppeteer:使用puppeteer提取网页中的视频地址

我的项目需要是提供一个接口通过输出一个网页地址,抓取网页中的视频地址!例如关上一个 网页地址 须要将网页中的视频地址提取进去。作为前端开发人员的惯性思维,看到这个网页的html构造,这个不是很简略嘛,一行代码就搞定:document.querySelector('video source').src 嘻嘻,功败垂成,筹备摸鱼~ 等等!这个只是在浏览器的控制台中拿到了视频的地址,然而如何转化成为提供一个接口,通过接口返回这个地址呢?初步猜测,应用get申请获取网页的html,而后剖析dom构造,解析出video标签。 谬误尝试间接通过get申请页面的地址获取到的内容并不是咱们在浏览器所看到的内容。目前的网页大多都是动静网页,即页面最终出现的内容是通过加载js后执行脚本动静拼接的,因而页面中的video标签并不是间接从服务端拼接好的。 浏览器加载网页的申请截图,没有间接返回dom构造,全是加载一堆js和css文件 并且!很多网站都做了防爬措施,间接申请页面的地址会返回一个两头页面,例如抖音和微博的视频详情页面,间接申请会返回一个相似于认证的页面,初步剖析了这个页面,这个直达页面应该是判断有没有相应cookie的信息,如果没有相应的信息,就会给浏览器设置cookie之类的信息,最初会走一个window.location.reload();让页面刷新一次(微博会间接走到一个Sina Visitor System的页面不会间接跳转到详情页面)。这个脚本在浏览器中会主动执行,因而会从新加载一次,看到最终的详情页面。然而get申请仅仅是获取到了直达页面的html,并没有走到真正的详情页面。 抖音详情页面get申请https://www.douyin.com/video/7020764246476590339 微博详情页面get申请https://weibo.com/tv/show/1034:4699424946061376?mid=4699425262272582 哎呀!连最终的网页信息都拿不到,怎么可能拿到页面视频地址呢?这下可不能欢快的摸鱼了 通过调研后决定采纳 Node.js + Puppeteer来实现这个性能,本文次要记录我的项目的实现思路和开发部署中遇到的难点及其解决方案,仅供学习参考。 Puppeteer 是 Chrome 开发团队在 2017 年公布的一个 Node.js 包,用来模仿 Chrome 浏览器的运行.次要通过Puppeteer运行Chromium加载网页实现剖析页面dom获取video标签,实现视频地址抓取参考资料: Puppeteer中文文档 https://github.com/puppeteer/puppeteer 开发环境(Windows)决定应用puppeteerjs后外面在windows环境下进行开发,windows环境为Node v12.16.2, puppeteerjs v2.1.1 puppeteerjs的最新版为13.1.1。然而puppeteerjs v3.0版本及以上须要Node v10及以上,因为我本地的开发环境Node为v12,服务器上的Node为v8,因而本地开发没问题,然而服务器上始终部署不胜利,且服务器下面有很多其余我的项目都是基于node v8版本的,因而服务器上的node版本不宜降级。为放弃和服务器版本统一,windows环境下的puppeteerjs也应用2.1.1版本; 间接上代码server2.js const puppeteer = require('puppeteer');async function getVideoUrl () { const browser = await puppeteer.launch();// 关上浏览器 const page = await browser.newPage(); await page.emulate(puppeteer.devices['iPhone 6']) await page.goto('https://www.douyin.com/video/7020764246476590339'); // 跳转到指定页面 await page.waitFor(2000) // 延时2s加载页面 puppeteer2.1.1应用 waitFor ^13.0.1以上应用 waitForTimeout const pageHtml = await page.content(); // 获取页面html Gets the full HTML contents of the page, including the doctype. console.log(pageHtml);}getVideoUrl()执行node server2.js,输入的后果就是详情页面的html代码了 ...

January 21, 2022 · 2 min · jiezi

关于puppeteer:puppeteer运行出现Could-not-find-browser-revision-809590

puppeteer运行呈现:Could not find browser revision 809590. Run "PUPPETEER_PRODUCT=firefox npm install" or "PUPPETEER_PRODUCT=firefox yarn install"报错,显然时没找到对应版本的浏览器。GitHub上issues上提供解决方案试了一圈度没用。起初在官网api文档里发现了puppeteer.createBrowserFetcher:createBrowserFetcher是一个官网的浏览器版本管理工具,只需用指定版本就能对应下载,回调返回装置的门路。实例: const puppeteer = require("puppeteer");const browserFetcher = puppeteer.createBrowserFetcher();browserFetcher.download("809590").then((res) => { puppeteer .launch({ executablePath: res.executablePath, //chrome执行门路 headless: false, //浏览器无头模式 }) .then(async (browser) => { // 保留 Endpoint,这样就能够从新连贯 Chromium const browserWSEndpoint = browser.wsEndpoint(); // 从Chromium 断开连接 browser.disconnect(); // 应用endpoint 从新和 Chromiunm 建设连贯 const browser2 = await puppeteer.connect({ browserWSEndpoint }); // Close Chromium // await browser2.close(); });});

November 21, 2020 · 1 min · jiezi

使用puppeteer-实现对Antd-Select-组件自动化测试

问题描述项目需要使用jest+puppeteer 实现对Antd组件库中的Select组件进行操作,选中其中的某一项的值。测试页面为Antd Select 使用puppeteer Recorder录制出来的测试代码如下所示 const puppeteer = require('puppeteer');(async () => { const browser = await puppeteer.launch() const page = await browser.newPage() await page.goto('https://ant.design/components/select-cn/') await page.setViewport({ width: 1154, height: 586 }) await page.waitForSelector('.ant-select-focused > .ant-select-selection > .ant-select-arrow > .anticon > svg') await page.click('.ant-select-focused > .ant-select-selection > .ant-select-arrow > .anticon > svg') await page.waitForSelector('div > .ant-select-dropdown > #\31 55fc83a-09de-47b7-a57e-a6042a0e3a5b > .ant-select-dropdown-menu > .ant-select-dropdown-menu-item-active') await page.click('div > .ant-select-dropdown > #\31 55fc83a-09de-47b7-a57e-a6042a0e3a5b > .ant-select-dropdown-menu > .ant-select-dropdown-menu-item-active') await browser.close()})()使用这段代码进行E2E测试,经常会在这段代码timeout ...

September 10, 2019 · 1 min · jiezi

puppeteer爬虫

利用空闲时间,学习了下puppeteer爬虫,我也想爬取下网上的资源1.部分apipuppeteer.launch(options)参数名称参数类型参数说明ignoreHTTPSErrorsboolean在请求的过程中是否忽略 Https 报错信息,默认为 falseheadlessboolean是否以“无头”的模式运行chrome,也就是不显示UI,默认为trueexecutablePathstring可执行文件的路径,Puppeteer 默认是使用它自带的 chrome webdriver, 如果你想指定一个自己的 webdriver 路径,可以通过这个参数设置slowMonumber使 Puppeteer 操作减速,单位是毫秒。如果你想看看 Puppeteer 的整个工作过程,这个参数将非常有用argsArray(String)传递给 chrome 实例的其他参数,比如你可以设置浏览器窗口大小具体参数timeoutnumber等待chrome实例启动的最长时间,默认是3000ms,如果传入0,则不限制时间dumpioboolean是否将浏览器锦程stdout和stderr导入到process.stdout和process.stderr中,默认为falseuserDataDirstring设置用户数据目录,默认linux是在~/.config目录,window 默认在 C:Users{USER}AppDataLocalGoogleChromeUser Data, 其中 {USER} 代表当前登录的用户名envObject指定对chromium可见的环境变量,默认为process.envdevtoolsboolean是否为每个选项卡自动打开DevTools面板,这个选项只有当headless设置为false的时候有效browser对象api方法名说明browser.close()返回一个promise对象,用于关闭浏览器browser.newPage()返回一个promise对象,创建一个page实例page对象方法名说明page.goto(url[, options])返回一个promise对象,url是目标链接page.waitForSelector()等待某个选择器的元素加载之后,这个元素可以是异步加载的page.evaluate(pageFunction[,args])返回一个可序列化的普通对象,pageFunction 表示要在页面执行的函数, args 表示传入给 pageFunction 的参数2.爬取电影网站const puppeteer = require('puppeteer');/* 爬虫的目标链接地址: 豆瓣电影 */const url = `https://movie.douban.com/tag/#/?sort=R&range=0,10&tags=`;const sleep = time => new Promise(resolve => { setTimeout(resolve, time);});(async () => { console.log('crawler start to visit the target address'); /* dumpio 是否将浏览器进程stdout和stderr导入到process.stdout和process.stderr中 */ const browser = await puppeteer.launch({ args: ['--no-sandbox'], dumpio: false }); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle2' }); await sleep(3000); await page.waitForSelector('.more'); for(let i = 0; i < 1; i++) { await sleep(3000); await page.click('.more'); } const result = await page.evaluate(() => { let $ = window.$; let nodeItems = $('.list-wp a'); let links = []; /* 获取对应的元素节点 */ if(nodeItems.length >= 1) { nodeItems.each((index, item) => { let elem = $(item); let movieId = elem.find('div').data('id'); let title = elem.find('.title').text(); let rate = Number(elem.find('.rate').text()); let poster = elem.find('img').attr('src').replace('s_ratio_poster','l_ratio_poster'); links.push({ movieId, title, rate, poster, }) }) } return links; }); browser.close(); console.log(result)})();3.爬取网站内容生成pdf文件const puppeteer = require('puppeteer');const url = 'https://cn.vuejs.org/v2/guide/';(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle0' }); /* 选择你要输出的那个PDF文件路径,把爬取到的内容输出到PDF中,必须是存在的PDF,可以是空内容,如果不是空的内容PDF,那么会覆盖内容 */ let pdfFilePath = './index.pdf'; /* 根据你的配置选项,我们这里选择A4纸的规格输出PDF,方便打印 */ await page.pdf({ path: pdfFilePath, format: 'A4', scale: 1, printBackground: true, landscape: false, displayHeaderFooter: false }); browser.close();})()正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐: ...

July 1, 2019 · 2 min · jiezi

如何避免Puppeteer被前端JS检测

这两天开始看puppeteer,发现居然也能被前端js检测出来!?github的issue区找了找,原来puppeteer启动的chrome里面,是有navigator.webdriver属性的,搞什么搞么,老外真是做那啥还要立牌坊Orzissue里也看到了解决方案: await this.page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); }但是说实话这个还是有点问题的,因为用"webdriver" in navigator还是能检测出来。想找找到底哪个环节把"webdriver"属性加上的,但是文本搜索发现puppeteer源码中并没有……后来发现是启动chrome的默认参数列表中有"--enable-automation"……找了一下这个命令行参数的说明: --enable-automation: Inform users that their browser is being controlled by an automated test.妈蛋纯粹是立牌坊用的,其它毛用没有……确认了就可以干掉它了,启动chrome时加个忽略默认参数即可: const browser = await puppeteer.launch({ignoreDefaultArgs: ["--enable-automation"]});

June 20, 2019 · 1 min · jiezi

信息检索课程实验笔记

注:前端不相关---只是用node实现了一下如何爬网站数据(大学课程的实验) 网页索引与检索实验目的l 了解搜索引擎的工作原理及实现方法;l 熟悉倒排索引的创建;l 掌握查询处理技术。 实验要求l 独立或合作(1~2人)完成实验内容;l 独立完成实验报告;(简单要求如下)1) 实验目的、内容与要求及实验环境描述;2) 索引和检索系统设计思路及总体框架;3) (负责部分的)程序结构及具体实现的流程分析,提供主要数据结构、函数分析等;4) 实验结果分析;5) 系统的优缺点、以及待改进的地方;6) 在实验过程中遇到的问题,实验的心得体会。 实验内容3.1 倒排索引(1)网页预处理。对实验一采集到的网页数据进行预处理,包括:网页的去噪和正文信息提取、中文分词、停止词处理等。(2)设计和创建倒排索引。对每个索引的词,至少应该记录其文件频率(df)。设计置入文件的数据结构,至少记录每个词在各个文档中出现的次数,即词频(tf)。同时对每个文档,记录其文档长度。(3)对索引的过程,生成相关的统计信息,例如:创建索引所需的时间、索引的大小、词汇表长度、具有最大df值的词的置入列表的大小等。(可选) 3.2检索系统 (1)设计实现一个简单的检索系统,可输入检索词,并输出查询结果,按相关度排序。 (2)对指定的查询词(IR2019查询词.txt),给出每个查询结果排序,以及相似度得分。所提交的结果将被评估。提交的结果文件有查询结果的数据块构成。每个查询词对应一个结果数据块,每个查询词提交10条查询结果。每个结果数据块格式如下:第一行是查询词序号,如“TD01”每一行是一条查询结果记录,格式为: <URL Similarity>URL:网页的规范化URL,如“http://www.scut.edu.cn/new/90...”Similarity:相似度得分每个数据块的十条记录按相似度从高到低排序,每个数据块之间以一个空行隔开.(3)对结果进行人工判断相关或不相关,然后基于该判断用评测指标Precision@10和MAP计算系统的检索性能指标。(可选)(4)采用各种查询处理技术对查询进行优化处理。并对所采用的不同技术的应用效果进行比较分析。(可选) 提交内容l 程序:包括源程序及注释,程序安装使用说明;l 查询结果文件:查询词对应查询结果汇总l 实验报告:说明程序设计的思路,并对实验过程进行分析和总结。 参考资料l 参考课程讲义的倒排索引、查询处理与检索评估、搜索引擎等章节;l 开源索引系统Lucene:http://lucene.apache.org ----------工作内容分解1生成倒序索引的文档 2 生成实验要求的结果(实验关键词txt) 3 网页 可以实时查询并且生成关键结果 4在后台跑服务器的服务 读 数据 返回数据做的优化工作1 修改停用词表2 同义但是模糊的词3 没有用本地数据库保存词语4 自己获取了词频和词语出现的位置5 result 去重6 使用db.txt存储数据参考文章:1 http://nathanchen.github.io/1...2 nodejs 读写文件http://javascript.ruanyifeng....3 json格式化的网站https://www.bejson.com/逻辑备注:1writeFileSync这是会默认会覆盖原来的内容的

June 6, 2019 · 1 min · jiezi

免费使用谷歌的翻译接口

引言最近做个东西,需将各种语言翻译成中文,看了各家的翻译效果,还是谷歌的最好。 但谷歌的未提供免费接口,研究了谷歌的翻译页面,输入内容后会触发ajax请求,请求参数中除了输入内容,还有个加密参数tk,该加密算法在压缩的js代码中,我也在网上找到了网友摘出来的代码,js格式,一大段,压缩代码翻译起来很吃力,遂未翻译,而另辟蹊径,在生产环境的docker中打包了node环境,业务代码通过shell调用这段js,得到加密参数后再模拟请求,获得翻译结果,用着还挺好。 没过多久,发现失效了,请求返回403 Forbidden,禁止访问 ????,估计加密参数算法又升级了。 接口翻译固然好用,但隔断时间就要重新搞一次加密算法,这个就有点儿难以接受了,每次都要从大量压缩js代码中找出加密算法,还不一定能完全找对。 至此,我们的主角无头浏览器puppeteer就要登场了,puppeteer一个node类库,提供了简洁的API,可以让使用者操作chrome浏览器,基本可以完全模拟人的操作,比如打开页面、输入网址、等待页面指定内容加载、点击按钮、甚至滑动也可以,有了这个工具模拟用户翻译然后获取结果完全没问题。 获取翻译结果通过js获取分析了谷歌翻译页面的元素,发现用户输入内容的时候会触发某些按钮变灰,等到翻译完成,按钮会再次变亮,这其实是通过添加去除*-disabled类来实现的,所以当我们模拟输入之后等待该类消失即可 await page.waitForSelector('selector-ele-disabled', {hidden: true});待元素变亮(去除了*-disabled类),就可以从结果输入框中获取到结果了。 但这样实现起来比较麻烦,也不够直接,还需要puppeteer调用chrome的js执行环境去获取,获取的也不是原始的接口返回数据。因此通过查阅文档找到了下面更好的方法????。 通过拦截请求返回获取前段时间研究了如何爬取手机app中的数据,里面用到了中间人代理攻击,中间人代理转发请求、返回,转发的时候就可以对请求进行拦截处理,我就想puppeteer应该也有,果然查到了event-response,他是Page实例的一个钩子,如果我们设置了"response": function callback(response){},当chrome发出的任何一个请求返回的时候,都会触发他,并将类Response的一个实例传给回调函数,里面包含请求url、请求结果、请求结果状态等信息,这样我们就可以检测我们的翻译接口了 let browser = await puppeteer.launch()let page = await browser.newPage()page.on('response', async response => { const url = response.url() if (url.indexOf("检测的接口地址") != -1) { let text = await response.text() // text就是接口返回的结果,拿到接口原始数据,接下来就任你处理了 }})设计大体流程如下图所示,初始化实例,等待请求,请求到达之后模拟输入,然后返回结果,再次进入等待请求状态。 此文的最终目的是可以为调用者提供一个简洁的接口,请求该接口返回,返回为中文的结果,接口的响应时间尽可能的短,可支持并发。 响应时间没多少可以优化的地方,主要依赖网络环境,以及谷歌的接口响应时间,我们只能做到当谷歌接口返回的时候我们也第一时间返回给调用者。 并发这里可以做优化,一个puppeteer同一时间只能处理一个翻译请求,如果做个实例池,维护多个puppeteer实例,这样就可以提升翻译接口的并发能力了。 实例池如下图所示,虚线框内表示一个实例池,实例池中有多个puppeteer实例,他们之间互相独立,当请求来的时候,随机从池子中拿出一个实例,处理请求,等待请求处理完毕之后,再次将改实例放回池子中。 为了减少意外情况,池子中的每个实例处理100个翻译之后推出,重新启动一个新的额实例补充进来,池子中的实例总量保持不变,如果需要甚至可以搞成动态的,像php-fpm一样,请求多的时候动态增加实例池中的实例,空闲的时候,清理推出一些实例。 如何将请求和结果联系起来一个请求可以分为两个流程,一个请求流程,一个ajax成功回调流程,请求时候输入翻译原始内容,实例内部在请求谷歌ajax接口成功的时候调用预先注册好的回调函数,这两个流程没有办法直接联系起来,但他们都会接触到同一个实例,所以用这个实例将他们俩联系起来,ajax流程成功之后写入一个变量到实例对象上,请求流程中监测该实例上的变量,有数据说明请求成功,返回数据,清空该变量,原理可以看下面的简化代码 let obj = {}setTimeout(() => { obj.result = "this is async result"}, 2000)async function sleep(duration) { return new Promise(resolve => { setTimeout(() => { resolve() }, duration) })}async function getRet() { let times = 1 while(times <= 100) { if (obj.result) { return Promise.resolve(obj.result) } else { await sleep(200) } times++ }}(async () => { let ret = await getRet() console.log(ret) console.log("now i can do something")})()实现我将这个功能包装成了一个类库,上传到了npm,google-trans-api,顺便也熟悉了整个打包流程以及typescript的使用,不得不说typescript真是不错,可以防止很多误写的错误,还有自动提示的功能,用起来不要太爽。这里是源码地址aizuyan/google-trans-api。 ...

May 12, 2019 · 2 min · jiezi

puppeteer-多URL爬取

基本使用'use strict';const puppeteer = require('puppeteer');(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); let imgArr = []; page.on('domcontentloaded', async () => { imgArr = await page.$$eval('img', img => { const arr = []; // 返回的是一个集合需要重新遍历 for (let i = 0; i < img.length; i++) { const obj = { width: img[i].width, naturalWidth: img[i].naturalWidth, height: img[i].height, naturalHeight: img[i].naturalHeight, isStandard: !((img[i].width * 10 <= img[i].naturalWidth || img[i].height * 10 <= img[i].naturalHeight)), url: img[i].src, level: 3, imageUrl: img[i].src, describeUrl: '', summary: `为了显示${img[i].width}x${img[i].height}的图片引入了原尺寸为${img[i].naturalWidth}x${img[i].naturalHeight}的图片`, }; if (obj.width && obj.height) { arr.push(obj); } } return arr; }); }); await page.goto('https://www.npmjs.com/package/puppeteer', { waitUntil: 'networkidle0' }); await browser.close(); console.log('imgArr: ', imgArr);})();顺序不能变 : ...

April 23, 2019 · 2 min · jiezi

手把手教你如何用Crawlab构建技术文章聚合平台(一)

背景说到爬虫,大多数程序员想到的是scrapy这样受人欢迎的框架。scrapy的确不错,而且有很强大的生态圈,有gerapy等优秀的可视化界面。但是,它还是有一些不能做到的事情,例如在页面上做翻页点击操作、移动端抓取等等。对于这些新的需求,可以用Selenium、Puppeteer、Appium这些自动化测试框架绕开繁琐的动态内容,直接模拟用户操作进行抓取。可惜的是,这些框架不是专门的爬虫框架,不能对爬虫进行集中管理,因此对于一个多达数十个爬虫的大型项目来说有些棘手。Crawlab是一个基于Celery的分布式通用爬虫管理平台,擅长将不同编程语言编写的爬虫整合在一处,方便监控和管理。Crawlab有精美的可视化界面,能对多个爬虫进行运行和管理。任务调度引擎是本身支持分布式架构的Celery,因此Crawlab可以天然集成分布式爬虫。有一些朋友认为Crawlab只是一个任务调度引擎,其实这样认为并不完全正确。Crawlab是类似Gerapy这样的专注于爬虫的管理平台。本文将介绍如何使用Crawlab和Puppeteer抓取主流的技术博客文章,然后用Flask+Vue搭建一个小型的技术文章聚合平台。Crawlab在前一篇文章《分布式通用爬虫管理平台Crawlab》已介绍了Crawlab的架构以及安装使用,这里快速介绍一下如何安装、运行、使用Crawlab。安装到Crawlab的Github Repo用克隆一份到本地。git clone https://github.com/tikazyq/crawlab安装相应的依赖包和库。cd crawlab# 安装python依赖pip install -r crawlab/requirements# 安装前端依赖cd frontendnpm install安装mongodb和redis-server。Crawlab将用MongoDB作为结果集以及运行操作的储存方式,Redis作为Celery的任务队列,因此需要安装这两个数据库。运行在运行之前需要对Crawlab进行一些配置,配置文件为config.py。# project variablesPROJECT_SOURCE_FILE_FOLDER = ‘/Users/yeqing/projects/crawlab/spiders’ # 爬虫源码根目录PROJECT_DEPLOY_FILE_FOLDER = ‘/var/crawlab’ # 爬虫部署根目录PROJECT_LOGS_FOLDER = ‘/var/logs/crawlab’ # 日志目录PROJECT_TMP_FOLDER = ‘/tmp’ # 临时文件目录# celery variablesBROKER_URL = ‘redis://192.168.99.100:6379/0’ # 中间者URL,连接redisCELERY_RESULT_BACKEND = ‘mongodb://192.168.99.100:27017/’ # CELERY后台URLCELERY_MONGODB_BACKEND_SETTINGS = { ‘database’: ‘crawlab_test’, ’taskmeta_collection’: ’tasks_celery’,}CELERY_TIMEZONE = ‘Asia/Shanghai’CELERY_ENABLE_UTC = True# flower variablesFLOWER_API_ENDPOINT = ‘http://localhost:5555/api’ # Flower服务地址# database variablesMONGO_HOST = ‘192.168.99.100’MONGO_PORT = 27017MONGO_DB = ‘crawlab_test’# flask variablesDEBUG = TrueFLASK_HOST = ‘127.0.0.1’FLASK_PORT = 8000启动后端API,也就是一个Flask App,可以直接启动,或者用gunicorn代替。cd ../crawlabpython app.py启动Flower服务(抱歉目前集成Flower到App服务中,必须单独启动来获取节点信息,后面的版本不需要这个操作)。python ./bin/run_flower.py启动本地Worker。在其他节点中如果想只是想执行任务的话,只需要启动这一个服务就可以了。python ./bin/run_worker.py启动前端服务器。cd ../frontendnpm run serve使用首页Home中可以看到总任务数、总爬虫数、在线节点数和总部署数,以及过去30天的任务运行数量。点击侧边栏的Spiders或者上方到Spiders数,可以进入到爬虫列表页。这些是爬虫源码根目录PROJECT_SOURCE_FILE_FOLDER下的爬虫。Crawlab会自动扫描该目录下的子目录,将子目录看作一个爬虫。Action列下有一些操作选项,点击部署Deploy按钮将爬虫部署到所有在线节点中。部署成功后,点击运行Run按钮,触发抓取任务。这时,任务应该已经在执行了。点击侧边栏的Tasks到任务列表,可以看到已经调度过的爬虫任务。基本使用就是这些,但是Crawlab还能做到更多,大家可以进一步探索,详情请见Github。PuppeteerPuppeteer是谷歌开源的基于Chromium和NodeJS的自动化测试工具,可以很方便的让程序模拟用户的操作,对浏览器进行程序化控制。Puppeteer有一些常用操作,例如点击,鼠标移动,滑动,截屏,下载文件等等。另外,Puppeteer很类似Selenium,可以定位浏览器中网页元素,将其数据抓取下来。因此,Puppeteer也成为了新的爬虫利器。相对于Selenium,Puppeteer是新的开源项目,而且是谷歌开发,可以使用很多新的特性。对于爬虫来说,如果前端知识足够的话,写数据抓取逻辑简直不能再简单。正如其名字一样,我们是在操作木偶人来帮我们抓取数据,是不是很贴切?掘金上已经有很多关于Puppeteer的教程了(爬虫利器 Puppeteer 实战、Puppeteer 与 Chrome Headless —— 从入门到爬虫),这里只简单介绍一下Puppeteer的安装和使用。安装安装很简单,就一行npm install命令,npm会自动下载Chromium并安装,这个时间会比较长。为了让安装好的puppeteer模块能够被所有nodejs爬虫所共享,我们在PROJECT_DEPLOY_FILE_FOLDER目录下安装node的包。# PROJECT_DEPLOY_FILE_FOLDER变量值cd /var/crawlab# 安装puppeteernpm i puppeteer# 安装mongodbnpm i mongodb安装mongodb是为了后续的数据库操作。使用以下是Copy/Paste的一段用Puppeteer访问简书然后截屏的代码,非常简洁。const puppeteer = require(‘puppeteer’);(async () => { const browser = await (puppeteer.launch()); const page = await browser.newPage(); await page.goto(‘https://www.jianshu.com/u/40909ea33e50'); await page.screenshot({ path: ‘jianshu.png’, type: ‘png’, // quality: 100, 只对jpg有效 fullPage: true, // 指定区域截图,clip和fullPage两者只能设置一个 // clip: { // x: 0, // y: 0, // width: 1000, // height: 40 // } }); browser.close();})();关于Puppeteer的常用操作,请移步《我常用的puppeteer爬虫api》。编写爬虫啰嗦了这么久,终于到了万众期待的爬虫时间了。Talk is cheap, show me the code!咦?我们不是已经Show了不少代码了么…由于我们的目标是建立一个技术文章聚合平台,我们需要去各大技术网站抓取文章。资源当然是越多越好。作为展示用,我们将抓取下面几个具有代表性的网站:掘金SegmentFaultCSDN研究发现这三个网站都是由Ajax获取文章列表,生成动态内容以作为传统的分页替代。这对于Puppeteer来说很容易处理,因为Puppeteer绕开了解析Ajax这一部分,浏览器会自动处理这样的操作和请求,我们只着重关注数据获取就行了。三个网站的抓取策略基本相同,我们以掘金为例着重讲解。掘金首先是引入Puppeteer和打开网页。const puppeteer = require(‘puppeteer’);const MongoClient = require(‘mongodb’).MongoClient;(async () => { // browser const browser = await (puppeteer.launch({ headless: true })); // define start url const url = ‘https://juejin.im’; // start a new page const page = await browser.newPage(); … })();headless设置为true可以让浏览器以headless的方式运行,也就是指浏览器不用在界面中打开,它会在后台运行,用户是看不到浏览器的。browser.newPage()将新生成一个标签页。后面的操作基本就围绕着生成的page来进行。接下来我们让浏览器导航到start url。 … // navigate to url try { await page.goto(url, {waitUntil: ‘domcontentloaded’}); await page.waitFor(2000); } catch (e) { console.error(e); // close browser browser.close(); // exit code 1 indicating an error happened code = 1; process.emit(“exit “); process.reallyExit(code); return } …这里try catch的操作是为了处理浏览器访问超时的错误。当访问超时时,设置exit code为1表示该任务失败了,这样Crawlab会将该任务状态设置为FAILURE。然后我们需要下拉页面让浏览器可以读取下一页。 … // scroll down to fetch more data for (let i = 0; i < 100; i++) { console.log(‘Pressing PageDown…’); await page.keyboard.press(‘PageDown’, 200); await page.waitFor(100); } …翻页完毕后,就开始抓取数据了。 … // scrape data const results = await page.evaluate(() => { let results = []; document.querySelectorAll(’.entry-list > .item’).forEach(el => { if (!el.querySelector(’.title’)) return; results.push({ url: ‘https://juejin.com’ + el.querySelector(’.title’).getAttribute(‘href’), title: el.querySelector(’.title’).innerText }); }); return results; }); …page.evaluate可以在浏览器Console中进行JS操作。这段代码其实可以直接在浏览器Console中直接运行。调试起来是不是方便到爽?前端工程师们,开始欢呼吧!获取了数据,接下来我们需要将其储存在数据库中。 … // open database connection const client = await MongoClient.connect(‘mongodb://192.168.99.100:27017’); let db = await client.db(‘crawlab_test’); const colName = process.env.CRAWLAB_COLLECTION || ‘results_juejin’; const taskId = process.env.CRAWLAB_TASK_ID; const col = db.collection(colName); // save to database for (let i = 0; i < results.length; i++) { // de-duplication const r = await col.findOne({url: results[i]}); if (r) continue; // assign taskID results[i].task_id = taskId; // insert row await col.insertOne(results[i]); } …这样,我们就将掘金最新的文章数据保存在了数据库中。其中,我们用url字段做了去重处理。CRAWLAB_COLLECTION和CRAWLAB_TASK_ID是Crawlab传过来的环境变量,分别是储存的collection和任务ID。任务ID需要以task_id为键保存起来,这样在Crawlab中就可以将数据与任务关联起来了。整个爬虫代码如下。const puppeteer = require(‘puppeteer’);const MongoClient = require(‘mongodb’).MongoClient;(async () => { // browser const browser = await (puppeteer.launch({ headless: true })); // define start url const url = ‘https://juejin.im’; // start a new page const page = await browser.newPage(); // navigate to url try { await page.goto(url, {waitUntil: ‘domcontentloaded’}); await page.waitFor(2000); } catch (e) { console.error(e); // close browser browser.close(); // exit code 1 indicating an error happened code = 1; process.emit(“exit “); process.reallyExit(code); return } // scroll down to fetch more data for (let i = 0; i < 100; i++) { console.log(‘Pressing PageDown…’); await page.keyboard.press(‘PageDown’, 200); await page.waitFor(100); } // scrape data const results = await page.evaluate(() => { let results = []; document.querySelectorAll(’.entry-list > .item’).forEach(el => { if (!el.querySelector(’.title’)) return; results.push({ url: ‘https://juejin.com’ + el.querySelector(’.title’).getAttribute(‘href’), title: el.querySelector(’.title’).innerText }); }); return results; }); // open database connection const client = await MongoClient.connect(‘mongodb://192.168.99.100:27017’); let db = await client.db(‘crawlab_test’); const colName = process.env.CRAWLAB_COLLECTION || ‘results_juejin’; const taskId = process.env.CRAWLAB_TASK_ID; const col = db.collection(colName); // save to database for (let i = 0; i < results.length; i++) { // de-duplication const r = await col.findOne({url: results[i]}); if (r) continue; // assign taskID results[i].task_id = taskId; // insert row await col.insertOne(results[i]); } console.log(results.length: ${results.length}); // close database connection client.close(); // shutdown browser browser.close();})();SegmentFault & CSDN这两个网站的爬虫代码基本与上面的爬虫一样,只是一些参数不一样而已。我们的爬虫项目结构如下。运行爬虫在Crawlab中打开Spiders,我们可以看到刚刚编写好的爬虫。点击各个爬虫的View查看按钮,进入到爬虫详情。在Execute Command中输入爬虫执行命令。对掘金爬虫来说,是node juejin_spider.js。输入完毕后点击Save保存。然后点击Deploy部署爬虫。最后点击Run运行爬虫。点击左上角到刷新按钮可以看到刚刚运行的爬虫任务已经在运行了。点击Create Time后可以进入到任务详情。Overview标签中可以看到任务信息,Log标签可以看到日志信息,Results信息中可以看到抓取结果。目前在Crawlab结果列表中还不支持数据导出,但是不久的版本中肯定会将导出功能加入进来。总结在这一小节,我们已经能够将Crawlab运行起来,并且能用Puppeteer编写抓取三大网站技术文章的爬虫,并且能够用Crawlab运行爬虫,并且读取抓取后的数据。下一节,我们将用Flask+Vue做一个简单的技术文章聚合网站。能看到这里的都是有耐心的好同学,赞一个。Github: tikazyq/crawlab如果感觉Crawlab还不错的话,请加作者微信拉入开发交流群,大家一起交流关于Crawlab的使用和开发。 ...

March 15, 2019 · 3 min · jiezi

从零开始开发一个Node交互式命令行应用

导言:对于大多数前端开发者而言,谈到命令行工具,大家肯定都用过。但是谈到开发命令行工具,估计就没几人有了解了。本文旨在用最短的时间内,帮您开发一个实用(斜眼笑)的图片爬虫命令行应用。想追求更好的阅读体验,请移步拓跋的前端客栈。同时把项目地址放在显眼的位置Puppeteer 简介什么是 Puppeteer?Puppeteer 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具。Chrome 作为浏览器市场的领头羊,Chrome Headless 将成为 web 应用 自动化测试 的行业标杆。所以我们很有必要来了解一下它。Puppeteer 可以做什么?Puppeteer 可以做的事情有很多,包括但不限于:利用网页生成 PDF、图片可以从网站抓取内容自动化表单提交、UI 测试、键盘输入等帮你创建一个最新的自动化测试环境(chrome),可以直接在此运行测试用例捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题Puppeteer 有什么优势?相对于真实浏览器而言,少了加载 css,js 以及渲染页面的工作。无头浏览器要比真实浏览器快的多。可以在无界面的服务器或 CI 上运行,减少了外界的干扰,更稳定。在一台机器上可以模拟运行多个无头浏览器,方便进行并发运行。如何安装 Puppeteer?安装 Puppeteer 很简单,如下:npm i –save puppeteeroryarn add puppeteer需要注意的是,由于用到了 ES7 的 async/await 语法 ,node 版本最好是 v7.6.0 或以上。如何使用 Puppeteer?由于本文不是专门讲 Puppeteer 的文章,故这部分暂且略过,大家可以去看下面的链接学习。Puppeteer GithubPuppeteer Api DocPuppeteer 中文 Api Doc说了这么多,Puppeteer 与我们要开发的命令行应用有什么关系呢?我们准备制作一个抓图命令行工具,不使用传统的请求式爬虫,我们使用 Puppeteer 这种无头浏览器,从 DOM 里抓图,这样能有效规避部分爬虫防御手段。Puppeteer 简单应用case 1. 屏幕截图直接上代码,很好理解:const puppeteer = require(“puppeteer”);const getScreenShot = async () => { const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); await page.goto(“https://baidu.com”); await page.screenshot({ path: “baidu.png” }); await browser.close();};getScreenShot();这段代码的意思就是以 headless(无头)模式打开浏览器,然后打开一个新标签页,跳转到百度网址, 并且进行屏幕截图,保存为 baidu.png 为名的图片,最后关闭浏览器。执行结果如下。case 2. 抓取网站信息接下来学习如何用 Puppeteer 抓取网站信息了。这次我们来抓取 jd 书单信息。// book info spiderconst puppeteer = require(“puppeteer”);const fs = require(“fs”);const spider = async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(“https://search.jd.com/Search?keyword=javascript"); const result = await page.evaluate(() => { let elements = document.querySelectorAll(".gl-item”); const data = […elements].map(i => { return { name: i.querySelector(".p-name em").innerText, description: i.querySelector(".p-name i").innerText, price: i.querySelector(".p-price").innerText, shop: i.querySelector(".p-shopnum").innerText, url: i.querySelector(".p-img a").href }; }); return data; // 返回数据 }); browser.close(); return result;};spider().then(value => { fs.writeFile(${__dirname}/javascript.json, JSON.stringify(value), err => { if (err) { throw err; } console.log(“file saved!”); }); console.log(value); // Success!});我们做的就是跳转到关键字是 javascript 的页面,然后对页面的 dom 结构进行分析,找到图书列表所对应的书名、描述、价格、出版社、网页链接信息,然后把数据写入到 javascript.json 文件里去,方便我们保存浏览。逻辑很简单。这已经是一个爬虫的雏形了,最后得到如下图所示的 json 文件,非常给力。case 3. 图片爬虫图片爬虫,这就是我们要做的命令行应用的主题了。一个最基本的思路是这样的:打开浏览器 —> 跳转到百度图片 —> 获取 input 框的焦点 —> 输入 keywords —> 点击搜索按钮 —> 跳转至结果列表页 —> 下拉到底部 —> 操作 dom,获取所有图片的 src 备用 —> 根据 src 将对应图片保存到本地 —> 关闭浏览器代码实现之:首先是浏览器操作部分const browser = await puppeteer.launch(); // 打开浏览器const page = await browser.newPage(); // 打开新tab页await page.goto(“https://image.baidu.com”); // 跳转到百度图片console.log(“go to https://image.baidu.com”); // 获取input框的焦点await page.focus("#kw"); // 把焦点定位到搜索input框await page.keyboard.sendCharacter(“猫咪”); // 输入关键字await page.click(".s_search"); // 点击搜索按钮console.log(“go to search list”); // 提示跳转到搜索列表页然后是图片处理部分page.on(“load”, async () => { await autoScroll(page); // 向下滚动加载图片 console.log(“page loading done, start fetch…”); const srcs = await page.evaluate(() => { const images = document.querySelectorAll(“img.main_img”); return Array.prototype.map.call(images, img => img.src); }); // 获取所有img的src console.log(get ${srcs.length} images, start download); for (let i = 0; i < srcs.length; i++) { await convert2Img(srcs[i], target); console.log(finished ${i + 1}/${srcs.length} images); } // 保存图片 console.log(job finished!); await browser.close();});因为百度图片是往下滚动就可以继续懒加载。因此,我们想要加载更多图片,可以先往下滚动一会儿。然后通过分析 dom 结构来获取列表里所有图片的 src,最后进行下载。执行以下,就能得到一系列猫咪的图片:图片下载的地方只写了主函数,更详细的代码可以去参见github.至此,我们用 Node 和 Puppeteer 开发出了一个最基本的图片爬虫工具。如何优化?这个图片爬虫工具目前还有点 low 啊,我们的目标是要开发一个交互式的命令行应用,肯定不能止于此。有哪些可以进一步优化的点呢?经过简单的思考,我列了一下:下载图片的内容可以自定义可以支持用户选择图片下载张数支持命令行传参支持命令行交互交互界面美观支持双击直接运行支持全局命令行调用使用 commander.js 支持命令行传参Commander 是一款重量轻,表现力和强大的命令行框架。提供了用户命令行输入和参数解析强大功能。const program = require(“commander”);program .version(“0.0.1”) .description(“a test cli program”) .option("-n, –name <name>", “your name”, “zhl”) .option("-a, –age <age>", “your age”, “22”) .option("-e, –enjoy [enjoy]") .action(option => { console.log(’name: ‘, option.name); console.log(‘age: ‘, option.age); console.log(’enjoy: ‘, option.enjoy); });program.parse(process.argv);Commander十分容易上手,上面这一段代码仅用了寥寥数行,就定义了一个命令行的输入与输出。其中:version 定义版本号description 定义描述信息option 定义输入选项,传3个参数,如option("-n, –name <name>", “your name”, “GK”),第一项是传参的值,-n是简写形式,–name是全称形式, <name>表示输入的参数,<>是必填项,如果是[],则是选填项。第二项“your name"是求助help时的提示信息,告诉用户应该输入的内容,最后一项"GK"是默认值。action 定义执行的操作,是一个回调函数,入参是前文输入的option选项,如果没有输入option,则使用定义的默认值。要查询更详细的api,请参考Commander Api文档。执行一下上述脚本,可以得到:这样命令行就可以做到简单的交互效果了。但是有没有觉得不够好看呢,别急,继续往下看。使用inquirer制作可交互命令行应用inquirer可以为Node制作可嵌入式的美观的命令行界面。可以提供问答式的命令输入:可以提供多种形式的选择界面:可以对输入信息进行校验:最后可以对输入信息进行处理:上面的例子是inquirer的官方例子,可以参考pizza.jsinquirer的文档可以查看inquirer documents有了inquirer,我们就可以制作更为精美的交互式命令行工具了。使用 chalk.js来让交互界面更美观chalk的语法非常简单:const chalk = require(‘chalk’);const log = console.log;// Combine styled and normal stringslog(chalk.blue(‘Hello’) + ’ World’ + chalk.red(’!’));// Compose multiple styles using the chainable APIlog(chalk.blue.bgRed.bold(‘Hello world!’));// Pass in multiple argumentslog(chalk.blue(‘Hello’, ‘World!’, ‘Foo’, ‘bar’, ‘biz’, ‘baz’));// Nest styleslog(chalk.red(‘Hello’, chalk.underline.bgBlue(‘world’) + ‘!’));// Nest styles of the same type even (color, underline, background)log(chalk.green( ‘I am a green line ’ + chalk.blue.underline.bold(‘with a blue substring’) + ’ that becomes green again!’));可以输出如下信息,一看便懂:再让我们做点有意思的事情…想必有人看到过下面知乎的控制台效果,既然要做点有意思的事情,今天我们不妨也把这种效果加到命令行程序里面,提升一下逼格。首先我们准备一副ASCII码用来打印,各位可以自行搜索text转ASCII,网上的转化方案不要太多。我们准备制作的命令行image spider就制作一个IMG SPD的ASCII码字符串吧经过挑选,效果如图:这种复杂的字符串怎么打印出来呢?直接保存为string一定是不行的,格式会乱的一塌糊涂。想要能完整的打印出格式来,有一个取巧的方法,以注释的形式打印出来。什么能保存注释呢?~~ function。所以事情就简单到了打印一个function。但是直接打印函数还是不行的,这时候就用到了可以怼天怼地的toString()方法,我们只需要把注释中间的部分用正则匹配出来就行了,easy最后看一看效果,铛铛铛铛支持双击运行这里使用一种叫做Shebang的技术。Shebang(也称为 Hashbang )是一个由#和!构成的字符序列 #! ,其出现在文本文件的第一行的前两个字符。 在文件中存在 Shebang 的情况下,类 Unix 操作系统的程序加载器会分析 Shebang 后的内容,将这些内容作为解释器指令,并调用该指令,并将载有 Shebang 的文件路径作为该解释器的参数。node下我们使用#! /usr/bin/env node即可这时候我们便可以取消文件的扩展名.js了。加入环境变量,支持全局调用package.json里面配置"bin": { “img-spd”: “app”},执行npm link,它将会把img-spd这个字段复制到npm的全局模块安装文件夹node_modules内,并创建符号链接(symbolic link,软链接),也就是将 app 的路径加入环境变量 PATH。这时,在任意目录下,直接命令行输入img-spd即可运行此命令行尾声至此,要改进的地方已经全部修改完毕,快来看看我们的成品吧看着一整个文件夹的gakki,感觉满满的幸福要溢出来了最后用动图来展示一下:附录项目地址项目地址Installnpm install -g img-spdUsageimg-spdorUsage: img-spd [options]img-spd is a spider get images from image.baidu.comOptions: -v –version output the version number -k, –key [key] input the image keywords to download -i, –interval [interval] input the operation interval(ms,default 200) -n, –number [number] input the operation interval(ms,default 200) -m, –headless [headless] choose whether the program is running in headless mode -h, –help output usage information ...

March 5, 2019 · 3 min · jiezi

puppeteer stop redirect 的正确姿势及 net::ERR_FAILED 的解决

在官方文档(puppeteer/api.md at master · GoogleChrome/puppeteer · GitHub)中,中断 redirect 的标准做法是这样的:const puppeteer = require(‘puppeteer’);puppeteer.launch().then(async browser => { const page = await browser.newPage(); await page.setRequestInterception(true); page.on(‘request’, interceptedRequest => { if (interceptedRequest.url().endsWith(’.png’) || interceptedRequest.url().endsWith(’.jpg’)) interceptedRequest.abort(); else interceptedRequest.continue(); }); await page.goto(‘https://example.com’); await browser.close();});这样一开始也是没有什么问题,但是偶尔会遇到这样情况:Error: net::ERR_FAILED at http://xxx.com/yyyGoogle 了一轮,发现相关的 issue 很少,只找到了这么一个:Page.setRequestInterception Redirection Issue · Issue #3421 · GoogleChrome/puppeteer · GitHub官方已经把它定义为一个 Bug 了,也有一些相关的解决方案:umbrella Fix Request Interception · Issue #3471 · GoogleChrome/puppeteer · GitHub不过其他人遇到的情况是 abort() 之后无法结束的问题,而我是抛出异常的问题,所以我自己摸索了一下,总结出一个比较合适的办法:就是用 respond 代替 abort。比如:// request.abort();request.respond({ status: 404, contentType: ’text/plain’, body: ‘Not Found!’,}); ...

February 26, 2019 · 1 min · jiezi

Puppeteer前端自动化测试实践

本篇内容将记录并介绍使用Puppeteer进行自动化网页测试,并依靠约定来避免反复修改测试用例的方案。主要解决页面众多时,修改代码导致的牵连错误无法被发现的运行时问题。文章首发于个人博客起因目前我们在持续开发着一个几十个页面,十万+行代码的项目,随着产品的更迭,总会出现这样的问题。在对某些业务逻辑或者功能进行添加或者修改的时候(尤其是通用逻辑),这些通用的逻辑或者组件往往会牵扯到一些其他地方的问题。由于测试人员受限,我们很难在完成一个模块单元后,对所有功能重新测试一遍。同时,由于环境及数据的区别,(以及在开发过程中对代码完备性的疏忽),代码会在某些特殊数据的解析和和展示上出现问题,在开发和测试中很难去发现。总的来说,我们希望有一个这样的工具,帮我们解决上述几个问题:在进行代码和功能改动后,能够自动访问各个功能的页面,检测问题针对大量的数据内容,进行批量访问,检测对于不同数据的展示是否存在问题测试与代码功能尽量不耦合,避免每次上新功能都需要对测试用例进行修改,维护成本太大定期的测试任务,及时发现数据平台针对新数据的展示完备性其中,最重要的问题,就是将测试代码与功能解耦,避免每次迭代和修改都需要追加新的测试用例。我们如何做到这一点呢?首先我们来梳理下测试平台的功能。功能设定由于我们的平台主要是进行数据展示,所以我们在测试过程中,主要以日常的展示数据为重心即可,针对一些复杂的表单操作先不予处理。针对上述的几个问题,我们针对自动化测试工具的功能如下:依次访问各个页面访问各个页面的具体内容,如时间切换、选项卡切换、分页切换、表格展开行等等针对数据表格中的详情链接,选择前100条进行访问,并进行下钻页的继续测试捕获在页面中的错误请求对错误信息进行捕获,统计和上报根据以上的梳理,我们可以把整个应用分为几个测试单元页面单元,检测各功能页面访问的稳定性详情页单元,根据页面的数据列表,进行批量的详情页跳转,检测不同参数下详情页的稳定性功能单元,用于检测页面和详情页各种展示类型点击切换后是否产生错误通过这样的划分,我们针对各个单元进行具体的测试逻辑书写用例,这样就可以避免再添加新功能和页面时,频繁对测试用例进行修改了。Puppeteer带着上面我们的需求,我们来看下Puppeteer的功能和特性,是否能够满足我们的要求。文档地址Puppeteer是一个Node库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。我们可以使用Puppeteer完成以下工作:访问页面,进行截图自动进行键盘输入,提交表单模拟点击等用户操作等等等等。。我们来通过一些小案例,来介绍他们的基本功能:访问一个带有ba认证的网站puppeteer可以创建page实例,并使用goto方法进行页面访问,page包含一系列方法,可以对页面进行各种操作。(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); // ba认证 await page.authenticate({ username, password }); // 访问页面 await page.goto(‘https://example.com’); // 进行截图 await page.screenshot({path: ’example.png’}); await browser.close();})();访问登陆页面,并进行登录首先,对于SPA(单页面应用),我们都知道,当页面进入后,客户端代码才开始进行渲染工作。我们需要等到页面内容渲染完成后,再进行对应的操作。我们有以下几种方法来使用waitUntilpuppeteer针对页面的访问,切换等,提供了waitUntil参数,来确定满足什么条件才认为页面跳转完成。包括以下事件:load - 页面的load事件触发时domcontentloaded - 页面的DOMContentLoaded事件触发时networkidle0 - 不再有网络连接时触发(至少500毫秒后)networkidle2 - 只有2个网络连接时触发(至少500毫秒后)通过waitUnitl,我们可以当页面请求都完成之后,确定页面已经访问完成。waitForwaitFor方法可以在指定动作完成后才进行resolve// wait for selectorawait page.waitFor(’.foo’);// wait for 1 secondawait page.waitFor(1000);// wait for predicateawait page.waitFor(() => !!document.querySelector(’.foo’));我们可以利用waitForSelector方法,当登录框渲染成功后,才进行登录操作// 等待密码输入框渲染await page.waitFor(’#password’);// 输入用户名await page.type(‘input#username’, “username”);// 输入密码await page.type(‘input#password’, “testpass”);// 点击登录按钮await Promise.all([ page.waitForNavigation(), // 等跳转完成后resolve page.click(‘button.login-button’), // 点击该链接将间接导致导航(跳转)]);await page.waitFor(2000)// 获取cookiesconst cookies = await page.cookies()针对列表内容里的链接进行批量访问主要利用到page实例的选择器功能const table = await page.$(’.table’)const links = await table.$$eval(‘a.link-detail’, links => links.map(link => link.href));// 循环访问links…进行错误和访问监听puppeteer可以监听在页面访问过程中的报错,请求等等,这样我们就可以捕获到页面的访问错误并进行上报啦,这也是我们进行测试需要的基本功能~// 当发生页面js代码没有捕获的异常时触发。page.on(‘pagerror’, () => {})// 当页面崩溃时触发。page.on(’error’, () => {})// 当页面发送一个请求时触发page.on(‘request’)// 当页面的某个请求接收到对应的 response 时触发。page.on(‘response’)通过以上的几个小案例,我们发现Puppeteer的功能非常强大,完全能够满足我们以上的对页面进行自动访问的需求。接下来,我们针对我们的测试单元进行个单元用例的书写最终功能通过我们上面对测试单元的规划,我们可以规划一下我们的测试路径访问网站 -> 登陆 -> 访问页面1 -> 进行基本单元测试 -> 获取详情页跳转链接 -> 依次访问详情页 -> 进行基本单元测试-> 访问页面2 …所以,我们可以拆分出几个大类,和几个测试单元,来进行各项测试// 包含基本的测试方法,log输出等class Base {}// 详情页单元,进行一些基本的单元测试class PageDetal extends Base {}// 页面单元,进行基本的单元测试,并获取并依次访问详情页class Page extends PageDetal {}// 进行登录等操作,并依次访问页面单元进行测试class Root extends Base {}同时,我们如何在功能页面变化时,跟踪到测试的变化呢,我们可以针对我们测试的功能,为其添加自定义标签test-role,测试时,根据自定义标签进行测试逻辑的编写。例如针对时间切换单元,我们做一下简单的介绍:// 1. 获取测试单元的元素const timeSwitch = await page.$(’[test-role=“time-switch”]’);// 若页面没有timeSwitch, 则不用进行测试if (!timeSwitch) return// 2. time switch的切换按钮const buttons = timeSwitch.$$(’.time-switch-button’)// 3. 对按钮进行循环点击for (let i = 0; i < buttons.length; i++) { const button = buttons[i] // 点击按钮 await button.click() // 重点! 等待对应的内容出现时,才认定页面访问成功 try { await page.waitFor(’[test-role=“time-switch-content”]’) } catch (error) { reportError (error) } // 截图 await page.screenshot()}上面只是进行了一个简单的访问内容测试,我们可以根据我们的用例单元书写各自的测试逻辑,在我们日常开发时,只需要对需要测试的内容,加上对应的test-role即可。总结根据以上的功能划分,我们很好的将一整个应用拆分成各个测试单元进行单元测试。需要注意的是,我们目前仅仅是对页面的可访问性进行测试,仅仅验证当用户进行各种操作,访问各个页面单元时页面是否会出错。并没有对页面的具体展示效果进行测试,这样会和页面的功能内容耦合起来,就需要单独的测试用例的编写了。 ...

February 20, 2019 · 1 min · jiezi

命令行批量截图Node脚本

批量截图任务作为一个软件工程师,不只是做好自己的本职工作(iOS),而是需要解决项目中的技术问题。这次就是解决自动截图的问题早期公司的数据工程师利用 phantomjs 来截图,后期不断发现截图效率低,加之开发者团队不再维护,因此决定将截图这部分跟你剥离开来,以后方便开发维护。我就承担了这个工作puppeteerPuppeteer 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.安装问题一开始按照往常的套路(npm install –save puppeteer) 好几次都卡住了,后期查找资料发现切换到国内的镜像就可以顺利下载 PUPPETEER_DOWNLOAD_HOST=https://storage.googleapis.com.cnpmjs.org npm i –save puppeteerTo use Puppeteer in your project, run:PUPPETEER_DOWNLOAD_HOST=https://storage.googleapis.com.cnpmjs.org npm i –save puppeteerInstall some basic packagesnpm install Usageconst puppeteer = require(‘puppeteer’), fs = require(‘fs’), path = require(‘path’), request = require(‘request’)function mkdirsSync(dirname) { if (fs.existsSync(dirname)) { return true } else { if (mkdirsSync(path.dirname(dirname))) { fs.mkdirSync(dirname) return true } }}var snapShotFolerPath = path.join(__dirname, ‘../snspshot/’)mkdirsSync(snapShotFolerPath) function snapShot (taskInfo) { return new Promise(function (resolve, reject) { (async function(){ // 启动Chromium const browser = await puppeteer.launch({ignoreHTTPSErrors: true, headless:true, args: [’–no-sandbox’]}) // 打开新页面 const page = await browser.newPage() // 设置页面分辨率 await page.setViewport({width: 1920, height: 1080}) // 访问 await page.goto(taskInfo.websiteUrl, {waitUntil: ‘domcontentloaded’}).catch(err => console.log(err)) await page.waitFor(1000) try { // 截图 await page.screenshot({path: snapShotFolerPath + taskInfo.imageName, fullPage:true}).catch(err => { console.log(‘截图失败: ’ + err) }); await page.waitFor(6000) } catch (e) { console.log(‘failed ’ + e) } finally { await browser.close() fs.stat(snapShotFolerPath + taskInfo.imageName, function(err,stats){ if (err) { reject(‘fail’) } else { if (stats.isFile()) { resolve(‘success’) } } }) } })() })}module.exports = snapShot如何安装 (Linux 、Unix 操作系统)如果你有翻墙环境执行 npm install执行 npm start如果你没有翻墙环境打开 package.json 文件,检查 dependencies 项目,如果 key 为 puppeteer 的条目,先删除该条目。进入工程命令行,输入 PUPPETEER_DOWNLOAD_HOST=https://storage.googleapis.co… npm i –save puppeteer执行 npm install 命令执行 npm start流程说明while 循环去调用接口去获取当前的截图任务在有截图任务情况下继续截图没有截任务的情况下,为了避免浪费资源,程序休眠10分钟后继续下一次的获取截图任务如果遇到调用截图任务接口500错误,则强制停止截图任务,相应的服务端工程师去查询失败原因如果有截图任务那么就去截图截图后将截图图片保存到文件夹,命令为当前日期 yyyy-MM-dd-hh-mm-ss-S 格式。然后将结果上传到服务端截图失败将当前任务结果保存到本地 failedTasks.json 文件夹一份,然后上传到服务端截图成功不管失败还是成功都去通知服务端。如果失败将当前任务告诉服务端,如果成功将当前任务信息和截图成功的绝对路径告诉服务端一些说明Demo 中执行 npm start 真正执行的是 quickStart.js 中的代码。完整的“获取截图任务、截图、截图上传到OSS、失败则将失败任务上传到服务”逻辑在 index.js 文件中工程是在没有提供真正的接口获取任务,而是采用随机数获取截图任务截图成功后将结果上传到OSS这一个步骤是没有的,采用 log 出来失败的上传也是不存在的,log 打印而已todoList多线程高效率的去截图一些写法暂时比较粗糙,不优雅,待改进puppeteer 很强大,大家可以去研究下代码地址 ...

January 25, 2019 · 2 min · jiezi

puppeteer爬虫

@(爬虫)[puppeteer|]爬虫又称网络机器人。每天或许你都会使用搜索引擎,爬虫便是搜索引擎重要的组成部分,爬取内容做索引。现如今大数据,数据分析很火,那数据哪里来呢,可以通过网络爬虫爬取啊。那我萌就来探讨一下网络爬虫吧。[TOC]爬虫的工作原理如图所示,这是爬虫的流程图,可以看到通过一个种子URL开启爬虫的爬取之旅,通过下载网页,解析网页中内容存储,同时解析中网页中的URL 去除重复后加入到等待爬取的队列。然后从队列中取到下一个等待爬取的URL重复以上步骤,是不是很简单呢?广度(BFS)还是深度(DFS)优先策略上面也提到在爬取完一个网页后从等待爬取的队列中选取一个URL去爬去,那如何选择呢?是选择当前爬取网页中的URL 还是继续选取当前URL中同级URL呢?这里的同级URL是指来自同一个网页的URL,这就是爬取策略之分。广度优先策略(BFS)广度优先策略便是将当前某个网页中URL先爬取完全,再去爬取从当前网页中的URL爬取的URL,这就是BFS,如果上图的关系图表示网页的关系,那么BFS的爬取策略将会是:(A->(B,D,F,G)->(C,F));深度优先策略(DFS)深度优先策略爬取某个网页,然后继续去爬取从网页中解析出的URL,直到爬取完。(A->B->C->D->E->F->G)下载网页下载网页看起来很简单,就像在浏览器中输入链接一样,下载完后浏览器便能显示出来。当然结果是并不是这样的简单。模拟登录对于一些网页来说需要登录才能看到网页中内容,那爬虫怎么登录呢?其实登录的过程就是获取访问的凭证(cookie,token…)let cookie = ‘’;let j = request.jar()async function login() { if (cookie) { return await Promise.resolve(cookie); } return await new Promise((resolve, reject) => { request.post({ url: ‘url’, form: { m: ‘username’, p: ‘password’, }, jar: j }, function(err, res, body) { if (err) { reject(err); return; } cookie = j.getCookieString(‘url’); resolve(cookie); }) })}这里是个简单的栗子,登录获取cookie, 然后每次请求都带上cookie.获取网页内容有的网页内容是服务端渲染的,没有CGI能够获得数据,只能从html中解析内容,但是有的网站的内容并不是简单的便能获取内容,像linkedin这样的网站并不是简单的能够获得网页内容,网页需要通过浏览器执行后才能获得最终的html结构,那怎么解决呢?前面我萌提到浏览器执行,那么我萌有没有可编程的浏览器呢?puppeteer,谷歌chrome团队开源的无头浏览器项目,利用无头浏览器便能模拟用户访问,便能获取最重网页的内容,抓取内容。利用puppeteer 模拟登录async function login(username, password) { const browser = await puppeteer.launch(); page = await browser.newPage(); await page.setViewport({ width: 1400, height: 1000 }) await page.goto(‘https://maimai.cn/login'); console.log(page.url()) await page.focus(‘input[type=text]’); await page.type(username, { delay: 100 }); await page.focus(‘input[type=password]’); await page.type(password, { delay: 100 }); await page.$eval(“input[type=submit]”, el => el.click()); await page.waitForNavigation(); return page;}执行login()后便能像在浏览器中登录后,便能像浏览器中登录后便能获取html中的内容,当让w哦萌也可以直接请求CGIasync function crawlData(index, data) { let dataUrl = https://maimai.cn/company/contacts?count=20&amp;page=${index}&amp;query=&amp;dist=0&amp;cid=${cinfo.cid}&amp;company=${cinfo.encodename}&amp;forcomp=1&amp;searchTokens=&amp;highlight=false&amp;school=&amp;me=&amp;webcname=&amp;webcid=&amp;jsononly=1; await page.goto(dataUrl); let res = await page.evaluate((e) => { return document.body.querySelector(‘pre’).innerHTML; }); console.log(res) res = JSON.parse(res); if (res && res.result == ‘ok’ && res.data.contacts && res.data.contacts.length) { data = data.concat(res.data.contacts.map((item) => { let contact = item.contact; console.log(contact.name) return { name: contact.name, occupation: contact.line4.split(’,’)[0], company: contact.company, title: contact.position } })); return await crawlData(++index, data); } return data; }像有的网站,拉钩,每次爬取的cookie都一样,也能利用无头浏览器取爬取,这样每次就不用每次爬取的时候担心cookie.写在最后当然爬虫不仅仅这些,更多的是对网站进行分析,找到合适的爬虫策略。对后关于puppeteer,不仅仅可以用来做爬虫,因为可以编程,无头浏览器,可以用来自动化测试等等。 ...

November 16, 2018 · 1 min · jiezi

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

1、puppeteer 是什么?puppeteer: Google 官方出品的 headless Chrome node 库puppeteer github仓库puppeteer API官方介绍:您可以在浏览器中手动执行的大多数操作都可以使用Puppeteer完成!生成页面的屏幕截图和PDF。抓取SPA并生成预渲染内容(即“SSR”)。自动化表单提交,UI测试,键盘输入等。创建最新的自动化测试环境。使用最新的JavaScript和浏览器功能直接在最新版本的Chrome中运行测试。捕获时间线跟踪 您的网站,以帮助诊断性能问题。测试Chrome扩展程序。2、爬取网站生成PDF2.1 安装 puppeteer// 安装 puppeteer// 可能会因为网络原因安装失败,可使用淘宝镜像 // npm install -g cnpm –registry=https://registry.npm.taobao.orgnpm i puppeteer# or “yarn add puppeteer"2.2 《React.js小书》简介《React.js小书》简介 关于作者@胡子大哈这是⼀本关于 React.js 的⼩书。因为⼯作中⼀直在使⽤ React.js,也⼀直以来想总结⼀下⾃⼰关于 React.js 的⼀些知识、经验。于是把⼀些想法慢慢整理书写下来,做成⼀本开源、免费、专业、简单的⼊⻔级别的⼩书,提供给社区。希望能够帮助到更多 React.js 刚⼊⻔朋友。下图是《React.js 小书》部分截图:2.3 一些可能会用到的 puppeteer API// 新建 reactMiniBook.js, 运行 node reactMiniBook.js 生成pdfconst puppeteer = require(‘puppeteer’);(async () => { // 启动浏览器 const browser = await puppeteer.launch({ // 无界面 默认为true,改成false,则可以看到浏览器操作,目前生成pdf只支持无界面的操作。 // headless: false, // 开启开发者调试模式,默认false, 也就是平时F12打开的面版 // devtools: true, }); // 打开一个标签页 const page = await browser.newPage(); // 跳转到页面 http://huziketang.mangojuice.top/books/react/ await page.goto(‘http://huziketang.com/books/react/', {waitUntil: ’networkidle2’}); // path 路径, format 生成pdf页面格式 await page.pdf({path: ‘react.pdf’, format: ‘A4’}); // 关闭浏览器 await browser.close();})();知道这启动浏览器打开页面关闭浏览器主流程后,再来看几个API。const args = 1;let wh = await page.evaluate((args) => { // args 可以这样传递给这个函数。 // 类似于 setTimeout(() => {console.log(args);}, 3000, args); console.log(‘args’, args); // 1 // 这里可以运行 dom操作等js // 返回通过dom操作等获取到的数据 return { width: 1920, height: document.body.clientHeight, };}, args);// 设置视图大小await page.setViewport(wh);// 等待2sawait page.waitFor(2000);// 以iPhone X执行。const devices = require(‘puppeteer/DeviceDescriptors’);const iPhone = devices[‘iPhone X’];await page.emulate(iPhone);2.4 知道了以上这些API后,就可以开始写主程序了。简单说下:实现功能和主流程。从上面React.js小书截图来看。1、打开浏览器,进入目录页,生成0. React 小书 目录.pdf2、跳转到1. React.js 简介页面,获取左侧所有的导航a链接的href,标题。3、用获取到的a链接数组进行for循环,这个循环里主要做了如下几件事:3.1 隐藏左侧导航,便于生成pdf 3.2 给React.js简介等标题 加上序号,便于查看 3.3 设置docment.title 加上序号, 便于在页眉中使用。 3.4 隐藏 传播一下知识也是一个很好的选择 这一个模块(因为页眉页脚中设置了书的链接等信息,就隐藏这个了) 3.5 给 分页 上一节,下一节加上序号,便于查看。 3.6 最末尾声明下该pdf的说明,仅供学习交流,严禁用于商业用途。 3.7 返回宽高,用于设置视图大小 3.8 设置视图大小,创建生成pdf4、关闭浏览器具体代码:可以查看这里爬虫生成《React.js小书》的pdf每一小节的代码// node 执行这个文件// 笔者这里是:node src/puppeteer/reactMiniBook.js即可生成如下图:每一小节(0-46小节)的pdf生成这些后,那么问题来了,就是查看时总不能看一小节,打开一小节来看,这样很不方便。于是接下来就是合并这些pdf成为一个pdf文件。3、合并成一个PDF文件 pdf-merge起初,我是使用在线网站Smallpdf,合并PDF。合并的效果还是很不错的。这网站还是其他功能。比如word转pdf等。后来找到社区提供的一个npm packagepdf merge 。(毕竟笔者是写程序的,所以就用代码来实现合并了)这个pdf-merge依赖 pdftk安装 PDFtkWindows下载并安装笔者安装后,重启电脑才能使用。Debian, Ubuntu 安装笔者在Ubuntu系统安装后,即可使用。apt-get install pdftk使用例子const PDFMerge = require(‘pdf-merge’);const files = [ ${__dirname}/1.pdf, ${__dirname}/2.pdf,];// Buffer (Default)PDFMerge(files).then((buffer) => {…});// StreamPDFMerge(files, {output: ‘Stream’}).then((stream) => {…});// 笔者这里使用的是这个// Save as new filePDFMerge(files, {output: ${__dirname}/3.pdf}).then((buffer) => {…});知道这些后,可以开始写主程序了。简单说下主流程1、读取到生成的所有pdf文件路径,并排序(0-46)2、判断下输出文件夹是否存在,不存在则创建3、合并这些小节的pdf保存到新文件 React小书(完整版)-作者:胡子大哈-时间戳.pdf具体代码:可以查看这里爬虫生成《React.js小书》的pdf合并pdf的代码最终合并的pdf文件在这里React小书(完整版)-作者:胡子大哈,可供下载。本想着还可以加下书签和页码,没找到合适的生成方案,那暂时先不加了。如果读者有好的方案,欢迎与笔者交流。关于作者:常以轩辕Rowboat为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。个人博客segmentfault个人主页掘金个人主页知乎github小结1、puppeteer是Google 官方出品的 headless Chrome node 库,可以在浏览器中手动执行的大多数操作都可以使用Puppeteer完成。总之可以用来做很多有趣的事情。2、用 puppeteer 生成每一小节的pdf,用依赖pdftk的pdf-merge npm包, 合并成一个新的pdf文件。或者使用Smallpdf等网站合并。3、《React.js小书》,推荐给大家。爬虫生成pdf,应该不会对作者@胡子大哈有什么影响。作者写书服务社区不易,尽可能多支持作者。最后推荐几个链接,方便大家学习 puppeteer。puppeteer入门教程Puppeteer 初探之前端自动化测试爬虫生成ES6标准入门 pdf大前端神器安利之 Puppeteerpuppeteer API中文文档 ...

August 30, 2018 · 2 min · jiezi