Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
/thalPublic
forked fromemadehsan/thal

译文:Puppeteer 与 Chrome Headless —— 从入门到爬虫

License

NotificationsYou must be signed in to change notification settings

csbun/thal

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

这里是@emadehsanGitHub 英文原文

Medium 英文原文

A Desert in painters perception

Puppeteer 是 Google Chrome 团队官方的无头/无界面(Headless)Chrome 工具。正因为这个官方声明,许多业内自动化测试库都已经停止维护,包括PhantomJSSelenium IDE for Firefox 项目也因为缺乏维护者而终止。

译者注:关于 PhantomJS 和 Selenium IDE for Firefox 停止维护并没有找到相关的公告,但这两个项目的确已经都超过 2 年没有发布新版本了。但另一个今年 5 月才开启的项目Chromeless 目前在 Github 上已经超过 1w star,目前还非常活跃。

Chrome 作为浏览器市场的领头羊,Chrome Headless 必将成为 web 应用自动化测试 的行业标杆。所以我整合了这份如何利用Chrome Headless网页爬虫 的入门指南。

TL;DR

本文我们将使用Chrome Headless,Puppeteer,NodeMongoDB,爬取 GitHub,登录并提取和保存用户的邮箱。不用担心 GitHub 的频率限制,本文会基于 Chrome Headless 和 Node 给你相应的策略。同时,请时刻关注Puppeteer文档,因为该项目仍然处于开发中,API 并不是很稳定。

开始

开始之前,我们需要安装以下工具。点击他们的官网然后安装吧。

译者注:Puppeteer 要求使用 Node v6.4.0,但因为文中大量使用async/await,需要 Node v7.6.0 或以上。

初始化项目

项目都是以创建文件夹开始。

$ mkdir thal$ cd thal

初始化 NPM,填入一些必要的信息。

$ npm init

安装Puppeteer。由于Puppeteer 并不是稳定的版本而且每天都在更新,所以如果你想要最新的功能可以直接通过 GitHub 的仓库安装。

$ npm i --save puppeteer

Puppeteer 包含了自己的 chrome / chromium 用以确保可以无头地工作。因此每当你安装/更新 puppeteer 的时候,他都会下载指定的 chrome 版本。

编码

我们将从页面截图开始,这是他们的文档中的代码。

页面截图

constpuppeteer=require('puppeteer');asyncfunctionrun(){constbrowser=awaitpuppeteer.launch();constpage=awaitbrowser.newPage();awaitpage.goto('https://github.com');awaitpage.screenshot({path:'screenshots/github.png'});browser.close();}run();

如果您第一次使用Node 7 或 8,那你可能不太熟悉asyncawait 关键字。简单地说,一个async 函数返回一个 Promise,当 Promise 完成时会返回你所定义的内容。当你需要像同步函数那样调用时,需要使用await

保存上面的代码为index.js 文件到项目目录里。并运行

$ node index.js

这样截图就会被保存到screenshots/ 目录下。

译者注:如果不是 clone 本 repo 的童鞋可能会遇到screenshots 目录不存在的异常,请先手动创建该目录,或使用mkdirp

GitHub

登录 GitHub

如果你在 GitHub 上搜索john,然后点击 Users 标签,你将看到一个带有姓名信息的用户列表。

Johns

有些用户设置了他们的邮箱是公开可见的,但有些用户没有。但你一个邮箱都看不到的原因是没有登录。下面,让我们利用Puppeteer 文档 来登录吧~

译者注:上图是登录后看到的效果

在项目根目录添加一个creds.js 文件。我强烈建议用一个没什么卵用的邮箱来注册一个新 GitHub 账号,不然 GitHub 可能会封掉你的常用账号。

module.exports={username:'<GITHUB_USERNAME>',password:'<GITHUB_PASSWORD>'};

同时添加一个.gitignore 文件,输入以下内容:

node_modules/creds.js

以非无头(non headless)模式启动

在调用 Puppeteer 的launch 方法的时候传入参数对象中带有headless: false,即可启动其 GUI 界面,进行可视化调试。

constbrowser=awaitpuppeteer.launch({headless:false});

跳转到登录页

awaitpage.goto('https://github.com/login');

在浏览器中打开https://github.com/login。右击Username or email address 下方的输入框(译者注:并选择Inspect)。在开发者工具中,右击被高亮的代码选择Copy >Copy selector

Copy dom element selector

把复制出来的值放到以下常量中

constUSERNAME_SELECTOR='#login_field';// "#login_field" 就是被复制出来的值

重复上面的步骤,吧Password 输入框和Sign in 按钮的值也填好,将得到下面内容:

// dom element selectorsconstUSERNAME_SELECTOR='#login_field';constPASSWORD_SELECTOR='#password';constBUTTON_SELECTOR='#login > form > div.auth-form-body.mt-3 > input.btn.btn-primary.btn-block';

登录

Puppeteer 提供了click 方法用来点击 DOM 元素和type 方法来输入内容。下面我们将填写验证信息并点击登录然后坐等跳转。

首先,需要引入creds.js 文件。

constCREDS=require('./creds');

然后

// puppeteer@0.11 以前是需要点击再输入// await page.click(USERNAME_SELECTOR);// await page.type(CREDS.username);// await page.click(PASSWORD_SELECTOR);// await page.type(CREDS.password);// puppeteer@0.12 以后 page.type 方法需要对某个 selector 进行输入awaitpage.type(USERNAME_SELECTOR,CREDS.username);awaitpage.type(PASSWORD_SELECTOR,CREDS.password);awaitpage.click(BUTTON_SELECTOR);awaitpage.waitForNavigation();

搜索 GitHub

现在,我们已经登录啦!我们可以点击搜索框,填写并在结果页面点击用户标签。但有一个简单的方法:搜索请求通常是 GET 请求,所有内容都是通过 URL 发送的。所以,在搜索框内手动输入john,然后点击用户标签并复制地址栏上的网址。这将是

constsearchUrl='https://github.com/search?q=john&type=Users&utf8=%E2%9C%93';

做一丢丢调整

constuserToSearch='john';constsearchUrl='https://github.com/search?q='+userToSearch+'&type=Users&utf8=%E2%9C%93';

让我们跳转到这个页面,看是不是真的搜索到了?

awaitpage.goto(searchUrl);awaitpage.waitFor(2*1000);

提取邮箱地址

译者注:本小节没有直译,因为译者没有使用作者的方案

我们的目的是搜刮用户的usernameemail。我们可以在 Chrome 的开发者工具中看到,每个单独的用户信息都是在一个 class 为user-list-item<div> 内。

一种提取元素内容的方法是Page orElementHandleevaluate 方法,因为它作用于浏览器运行的上下文环境内。当我们跳转到搜索结果页的时候,使用page.evaluate 方法可以将所有用户信息的 div 获取出来。

constUSER_LIST_INFO_SELECTOR='.user-list-item';constusers=awaitpage.evaluate((sel)=>{const$els=document.querySelectorAll(sel);// ...},USER_LIST_INFO_SELECTOR);

遍历上面的$els,继续使用选择器提取出其中的信息。当然,这里的选择器相当于用户信息的 div 的,不是像之前那样直接复制出来的,稍微有一点 css 知识应该能很容易读懂。

constUSER_LIST_INFO_SELECTOR='.user-list-item';constUSER_LIST_USERNAME_SELECTOR='.user-list-info>a:nth-child(1)';constUSER_LIST_EMAIL_SELECTOR='.user-list-info>.user-list-meta .muted-link';constusers=awaitpage.evaluate((sInfo,sName,sEmail)=>{returnArray.prototype.slice.apply(document.querySelectorAll(sInfo)).map($userListItem=>{// 用户名constusername=$userListItem.querySelector(sName).innerText;// 邮箱const$email=$userListItem.querySelector(sEmail);constemail=$email ?$email.innerText :undefined;return{        username,        email,};})// 不是所有用户都显示邮箱.filter(u=>!!u.email);},USER_LIST_INFO_SELECTOR,USER_LIST_USERNAME_SELECTOR,USER_LIST_EMAIL_SELECTOR);console.log(users);

现在,当你运行node index.js,你讲看到 Chrome 跳出来自动执行上述操作后,在命令行打出username 与其相关的email

遍历全部页面

首先我们需要评估搜索结果最后一页的页码。在搜索结果页的顶部,你可以看到当我在翻译这篇文章时有70,134 users

有趣的现象: 如果你对比上面的截图,你会发现这两天已经有 371 个新的john 同学加入 GitHub。

Number of search items

从开发者工具复制人数的选择器。我们将在run 外面写一个新的函数,用来获取页面数。

asyncfunctiongetNumPages(page){constNUM_USER_SELECTOR='#js-pjax-container .codesearch-results h3';letinner=awaitpage.evaluate((sel)=>{returndocument.querySelector(sel).innerHTML;},NUM_USER_SELECTOR);// 格式是: "69,803 users"inner=inner.replace(',','').replace(' users','');constnumUsers=parseInt(inner);console.log('numUsers: ',numUsers);/*   * GitHub 每页显示 10 个结果   */constnumPages=Math.ceil(numUsers/10);returnnumPages;}

在搜索结果页底部,如果你把鼠标悬浮在页码按钮上面,可以看到是一个指向下一页的链接。如:第二页的链接是https://github.com/search?p=2&q=john&type=Users&utf8=%E2%9C%93。注意到p=2 是一个 URL 的 query 参数,这将帮助我们跳转到指定的页面。

在添加了遍历页码的代码在上面爬取内容的方法之后,代码变成了这样:

constnumPages=awaitgetNumPages(page);console.log('Numpages: ',numPages);for(leth=1;h<=numPages;h++){// 跳转到指定页码awaitpage.goto(`${searchUrl}&p=${h}`);// 执行爬取constusers=awaitpage.evaluate((sInfo,sName,sEmail)=>{returnArray.prototype.slice.apply(document.querySelectorAll(sInfo)).map($userListItem=>{// 用户名constusername=$userListItem.querySelector(sName).innerText;// 邮箱const$email=$userListItem.querySelector(sEmail);constemail=$email ?$email.innerText :undefined;return{          username,          email,};})// 不是所有用户都显示邮箱.filter(u=>!!u.email);},USER_LIST_INFO_SELECTOR,USER_LIST_USERNAME_SELECTOR,USER_LIST_EMAIL_SELECTOR);users.map(({username, email})=>{// TODO: 保存用户信息console.log(username,'->',email);});}

保存到 MongoDB

到这里Puppeteer 的部分已经结束了。下面我们将使用mongoose 存储上面的信息到MongoDB。它是个ORM,确切说是个便于从数据库进行信息存储和检索的库。

$ npm i --save mongoose

MongoDB 是一个 Schema-less 的 NoSQL 数据库,但我们可以使用 Mongoose 使其遵循一些原则。首先我们需要创建一个Model,他代表 MongoDB 中的Collection。创建一个models 文件夹,然后在里面创建一个user.js 文件,并加入以下 collection 的构造函数代码。之后无论我们塞什么东西进User,他都会遵循这个结构。

constmongoose=require('mongoose');constuserSchema=newmongoose.Schema({username:String,email:String,dateCrawled:Date});module.exports=mongoose.model('User',userSchema);

现在我们可以开始往数据库塞数据了。由于我们不希望数据库中存在重复的 email,所以我们只新增那些以前从没出现过的邮箱,否则我们只更新数据。为此,我们需要使用 mongoose 的Model.findOneAndUpdate 方法。

回到index.js,引用所需的依赖:

constmongoose=require('mongoose');constUser=require('./models/user');

然后我们再创建一个新的方法,用于upsert (更新 update 或 新增 insert) 用户实例。

functionupsertUser(userObj){constDB_URL='mongodb://localhost/thal';if(mongoose.connection.readyState==0){mongoose.connect(DB_URL);}// if this email exists, update the entry, don't insert// 如果邮箱存在,就更新实例,不新增constconditions={email:userObj.email};constoptions={upsert:true,new:true,setDefaultsOnInsert:true};User.findOneAndUpdate(conditions,userObj,options,(err,result)=>{if(err){throwerr;}});}

启动 MongoDB 服务。用下面的代码替换掉之前的注释内容// TODO: 保存用户信息

upsertUser({username:username,email:email,dateCrawled:newDate()});

想要检查是否真的保存了这些用户,可以到 mongo 里面执行下列脚本:

$ mongo> use thal> db.users.find().pretty()

你会看到有多个用户在已经添加在里面,那你就成功了哦~

译者注:使用db.users.find().pretty().length() 可以查看爬取了多少条

最后

Chrome Headless 和 Puppeteer 开启了网页爬虫和自动化测试的新纪元,而且 Chrome Headless 还支持 WebGL!你可以把你的爬虫脚本发布到云端,然后就可以坐享其成。当然,发布到服务器之前请记得去掉headless: false 配置。

  • 在爬取的时候,你可能会被 GitHub 的频率控制阻止。

Whoa

  • 另外,我发现你无法跳到 100 页之后。

译者注:我爬了 100 页并没有被阻止。从 101 页开始就变成了 404 页面,或许通过页面下方的页码进行遍历会更合理

结语

广阔无垠的沙漠见证着穿越 这些巨大的沙滩的人们的斗争和牺牲。Thal 是巴基斯坦的一个跨越多个地区的沙漠,包括我的家乡 Bhakkar。与今天在互联网 上搜索数据的情况类似。这就是为什么我将这个 repo 命名为Thal。如果你喜欢它,那请与他人分享。如果您有任何建议,请在这里发表评论或直接与原作者联络@e_mad_ehsan。他很乐意听到你的消息。

译者注:中文版也欢迎直接提issue 讨论 或 PR

About

译文:Puppeteer 与 Chrome Headless —— 从入门到爬虫

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript100.0%

[8]ページ先頭

©2009-2025 Movatter.jp