本文讲解若何用 Node.js 高效地从 Web 爬取数据。
条件条件
本文紧张针对具有一定 JavaScript 履历的程序员。如果你对 Web 抓取有深刻的理解,但对 JavaScript 并不熟习,那么本文仍旧能够对你有所帮助。

你将学到
通过本文你将学到:
学到更多关于 Node.js 的东西用多个 HTTP 客户端来帮助 Web 抓取的过程利用多个经由实践磨练过的库来爬取 Web理解 Node.js
Javascript 是一种大略的当代编程措辞,最初是为了向浏览器中的网页添加动态效果。当加载网站后,Javascript 代码由浏览器的 Javascript 引擎运行。为了使 Javascript 与你的浏览器进行交互,浏览器还供应了运行时环境(document、window等)。
这意味着 Javascript 不能直接与打算机资源交互或对其进行操作。例如在 Web 做事器中,做事器必须能够与文件系统进行交互,这样才能读写文件。
Node.js 使 Javascript 不仅能够运行在客户端,而且还可以运行在做事器端。为了做到这一点,其创始人 Ryan Dahl 选择了Google Chrome 浏览器的 v8 Javascript Engine,并将其嵌入到用 C++ 开拓的 Node 程序中。以是 Node.js 是一个运行时环境,它许可 Javascript 代码也能在做事器上运行。
与其他措辞(例如 C 或 C++)通过多个线程来处理并发性相反,Node.js 利用单个主线程并并在事宜循环的帮助下以非壅塞办法实行任务。
要创建一个大略的 Web 做事器非常大略,如下所示:
consthttp=require('http');constPORT=3000;constserver=http.createServer((req,res)=>{res.statusCode=200;res.setHeader('Content-Type','text/plain');res.end('HelloWorld');});server.listen(port,()=>{console.log(`ServerrunningatPORT:${port}/`);});
如果你已安装了 Node.js,可以试着运行上面的代码。Node.js 非常适宜 I/O 密集型程序。
HTTP 客户端:访问 Web
HTTP 客户端是能够将要求发送到做事器,然后吸收做事器相应的工具。下面提到的所有工具底的层都是用 HTTP 客户端来访问你要抓取的网站。
Request
Request 是 Javascript 生态中利用最广泛的 HTTP 客户端之一,但是 Request 库的作者已正式声明弃用了。不过这并不虞味着它不可用了,相称多的库仍在利用它,并且非常好用。用 Request 发出 HTTP 要求是非常大略的:
constrequest=require('request')request('https://www.reddit.com/r/programming.json',function(error,response,body){console.error('error:',error)console.log('body:',body)})
你可以在 Github 上找到 Request 库,安装它非常大略。你还可以在 https://github.com/request/request/issues/3142 找到弃用关照及其含义。
Axios
Axios 是基于 promise 的 HTTP 客户端,可在浏览器和 Node.js 中运行。如果你用 Typescript,那么 axios 会为你覆盖内置类型。通过 Axios 发起 HTTP 要求非常大略,默认情形下它带有 Promise 支持,而不是在 Request 中去利用回调:
constaxios=require('axios')axios.get('https://www.reddit.com/r/programming.json').then((response)=>{console.log(response)}).catch((error)=>{console.error(error)});
如果你喜好 Promises API 的 async/await 语法糖,那么你也可以用,但是由于顶级 await 仍处于 stage 3 ,以是我们只好先用异步函数来代替:
asyncfunctiongetForum(){try{constresponse=awaitaxios.get('https://www.reddit.com/r/programming.json')console.log(response)}catch(error){console.error(error)}}
你所要做的便是调用 getForum!
可以在 https://github.com/axios/axios 上找到Axios库。
Superagent
与 Axios 一样,Superagent 是另一个强大的 HTTP 客户端,它支持 Promise 和 async/await 语法糖。它具有像 Axios 这样相称大略的 API,但是 Superagent 由于存在更多的依赖关系并且不那么盛行。
用 promise、async/await 或回调向 Superagent 发出HTTP要求看起来像这样:
constsuperagent=require("superagent")constforumURL="https://www.reddit.com/r/programming.json"//callbackssuperagent.get(forumURL).end((error,response)=>{console.log(response)})//promisessuperagent.get(forumURL).then((response)=>{console.log(response)}).catch((error)=>{console.error(error)})//promiseswithasync/awaitasyncfunctiongetForum(){try{constresponse=awaitsuperagent.get(forumURL)console.log(response)}catch(error){console.error(error)}}
可以在 https://github.com/visionmedia/superagent 找到 Superagent。
正则表达式:困难的路
在没有任何依赖性的情形下,最大略的进行网络抓取的方法是,利用 HTTP 客户端查询网页时,在收到的 HTML 字符串上利用一堆正则表达式。正则表达式不那么灵巧,而且很多专业人士和业余爱好者都难以编写精确的正则表达式。
让我们试一试,假设个中有一个带有用户名的标签,我们须要该用户名,这类似于你依赖正则表达式时必须实行的操作
consthtmlString='<label>Username:JohnDoe</label>'constresult=htmlString.match(/<label>(.+)<\/label>/)console.log(result[1],result[1].split(":")[1])//Username:JohnDoe,JohnDoe
在 Javascript 中,match() 常日返回一个数组,该数组包含与正则表达式匹配的所有内容。第二个元素(在索引1中)将找到我们想要的 <label> 标记的 textContent 或 innerHTML。但是结果中包含一些不须要的文本( “Username: “),必须将其删除。
如你所见,对付一个非常大略的用例,步骤和要做的事情都很多。这便是为什么该当依赖 HTML 解析器的缘故原由,我们将在后面谈论。
Cheerio:用于遍历 DOM 的核心 JQuery
Cheerio 是一个高效轻便的库,它使你可以在做事器端利用 JQuery 的丰富而强大的 API。如果你以前用过 JQuery,那么将会对 Cheerio 感到很熟习,它肃清了 DOM 所有不一致和与浏览器干系的功能,并公开了一种有效的 API 来解析和操作 DOM。
constcheerio=require('cheerio')const$=cheerio.load('<h2class="title">Helloworld</h2>')$('h2.title').text('Hellothere!')$('h2').addClass('welcome')$.html()//<h2class="titlewelcome">Hellothere!</h2>
如你所见,Cheerio 与 JQuery 用起来非常相似。
但是,只管它的事情办法不同于网络浏览器,也就这意味着它不能:
渲染任何解析的或操纵 DOM 元素运用 CSS 或加载外部资源实行 JavaScript因此,如果你考试测验爬取的网站或 Web 运用是严重依赖 Javascript 的(例如“单页运用”),那么 Cheerio 并不是最佳选择,你可能不得不依赖稍后谈论的其他选项。
为了展示 Cheerio 的强大功能,我们将考试测验在 Reddit 中抓取 r/programming 论坛,考试测验获取帖子名称列表。
首先,通过运行以下命令来安装 Cheerio 和 axios:npm install cheerio axios。
然后创建一个名为 crawler.js 的新文件,并复制粘贴以下代码:
constaxios=require('axios');constcheerio=require('cheerio');constgetPostTitles=async()=>{try{const{data}=awaitaxios.get('https://old.reddit.com/r/programming/');const$=cheerio.load(data);constpostTitles=[];$('div>p.title>a').each((_idx,el)=>{constpostTitle=$(el).text()postTitles.push(postTitle)});returnpostTitles;}catch(error){throwerror;}};getPostTitles().then((postTitles)=>console.log(postTitles));
getPostTitles() 是一个异步函数,将对旧的 reddit 的 r/programming 论坛进行爬取。首先,用带有 axios HTTP 客户端库的大略 HTTP GET 要求获取网站的 HTML,然后用 cheerio.load() 函数将 html 数据输入到 Cheerio 中。
然后在浏览器的 Dev Tools 帮助下,可以得到可以定位所有列表项的选择器。如果你利用过 JQuery,则必须非常熟习 $('div> p.title> a')。这将得到所有帖子,由于你只希望单独获取每个帖子的标题,以是必须遍历每个帖子,这些操作是在 each() 函数的帮助下完成的。
要从每个标题中提取文本,必须在 Cheerio 的帮助下获取 DOM元素( el 指代当前元素)。然后在每个元素上调用 text() 能够为你供应文本。
现在,打开终端并运行 node crawler.js,然后你将看到大约存有标题的数组,它会很长。只管这是一个非常大略的用例,但它展示了 Cheerio 供应的 API 的大略性子。
如果你的用例须要实行 Javascript 并加载外部源,那么以下几个选项将很有帮助。
JSDOM:Node 的 DOM
JSDOM 是在 Node.js 中利用的文档工具模型的纯 Javascript 实现,如前所述,DOM 对 Node 不可用,但是 JSDOM 是最靠近的。它或多或少地模拟了浏览器。
由于创建了 DOM,以是可以通过编程与要爬取的 Web 运用或网站进行交互,也可以仿照单击按钮。如果你熟习 DOM 操作,那么利用 JSDOM 将会非常大略。
const{JSDOM}=require('jsdom')const{document}=newJSDOM('<h2class="title">Helloworld</h2>').windowconstheading=document.querySelector('.title')heading.textContent='Hellothere!'heading.classList.add('welcome')heading.innerHTML//<h2class="titlewelcome">Hellothere!</h2>
代码中用 JSDOM 创建一个 DOM,然后你可以用和操纵浏览器 DOM 相同的方法和属性来操纵该 DOM。
为了演示如何用 JSDOM 与网站进行交互,我们将得到 Reddit r/programming 论坛的第一篇帖子并对其进行投票,然后验证该帖子是否已被投票。
首先运行以下命令来安装 jsdom 和 axios:npm install jsdom axios
然后创建名为 crawler.js的文件,并复制粘贴以下代码:
const{JSDOM}=require("jsdom")constaxios=require('axios')constupvoteFirstPost=async()=>{try{const{data}=awaitaxios.get("https://old.reddit.com/r/programming/");constdom=newJSDOM(data,{runScripts:"dangerously",resources:"usable"});const{document}=dom.window;constfirstPost=document.querySelector("div>div.midcol>div.arrow");firstPost.click();constisUpvoted=firstPost.classList.contains("upmod");constmsg=isUpvoted?"Posthasbeenupvotedsuccessfully!":"Theposthasnotbeenupvoted!";returnmsg;}catch(error){throwerror;}};upvoteFirstPost().then(msg=>console.log(msg));
upvoteFirstPost() 是一个异步函数,它将在 r/programming 中获取第一个帖子,然后对其进行投票。axios 发送 HTTP GET 要求获取指定 URL 的HTML。然后通过先前获取的 HTML 来创建新的 DOM。JSDOM 布局函数把HTML 作为第一个参数,把 option 作为第二个参数,已添加的 2 个 option 项实行以下功能:
runScripts:设置为 dangerously 时许可实行事宜 handler 和任何 Javascript 代码。如果你不清楚将要运行的脚本的安全性,则最好将 runScripts 设置为“outside-only”,这会把所有供应的 Javascript 规范附加到 “window” 工具,从而阻挡在 inside 上实行的任何脚本。resources:设置为“usable”时,许可加载用 <script> 标记声明的任何外部脚本(例如:从 CDN 提取的 JQuery 库)创建 DOM 后,用相同的 DOM 方法得到第一篇文章的 upvote 按钮,然后单击。要验证是否确实单击了它,可以检讨 classList 中是否有一个名为 upmod 的类。如果存在于 classList 中,则返回一条。
打开终端并运行 node crawler.js,然后会看到一个整洁的字符串,该字符串将表明帖子是否被赞过。只管这个例子很大略,但你可以在这个根本上构建功能强大的东西,例如,一个环绕特定用户的帖子进行投票的机器人。
如果你不喜好缺少表达能力的 JSDOM ,并且实践中要依赖于许多此类操作,或者须要重新创建许多不同的 DOM,那么下面将是更好的选择。
Puppeteer:无头浏览器
顾名思义,Puppeteer 许可你以编程办法操纵浏览器,就像操纵木偶一样。它通过为开拓职员供应高等 API 来默认掌握无头版本的 Chrome。
Puppeteer 比上述工具更有用,由于它可以使你像真正的人在与浏览器进行交互一样对网络进行爬取。这就具备了一些以前没有的可能性:
你可以获取屏幕截图或天生页面 PDF。可以抓取单页运用并天生预渲染的内容。自动实行许多不同的用户交互,例如键盘输入、表单提交、导航等。它还可以在 Web 爬取之外的其他任务中发挥主要浸染,例如 UI 测试、赞助性能优化等。
常日你会想要截取网站的屏幕截图,大概是为了理解竞争对手的产品目录,可以用 puppeteer 来做到。首先运行以下命令安装 puppeteer,:npm install puppeteer
这将下载 Chromium 的 bundle 版本,根据操作系统的不同,该版今年夜约 180 MB 至 300 MB。如果你要禁用此功能。
让我们考试测验在 Reddit 中获取 r/programming 论坛的屏幕截图和 PDF,创建一个名为 crawler.js的新文件,然后复制粘贴以下代码:
constpuppeteer=require('puppeteer')asyncfunctiongetVisual(){try{constURL='https://www.reddit.com/r/programming/'constbrowser=awaitpuppeteer.launch()constpage=awaitbrowser.newPage()awaitpage.goto(URL)awaitpage.screenshot({path:'screenshot.png'})awaitpage.pdf({path:'page.pdf'})awaitbrowser.close()}catch(error){console.error(error)}}getVisual()
getVisual() 是一个异步函数,它将获 URL 变量中 url 对应的屏幕截图和 pdf。首先,通过 puppeteer.launch() 创建浏览器实例,然后创建一个新页面。可以将该页面视为常规浏览器中的选项卡。然后通过以 URL 为参数调用 page.goto() ,将先前创建的页面定向到指定的 URL。终极,浏览器实例与页面一起被销毁。
完成操作并完成页面加载后,将分别利用 page.screenshot() 和 page.pdf() 获取屏幕截图和 pdf。你也可以侦听 javascript load 事宜,然后实行这些操作,在生产环境级别下强烈建议这样做。
在终端上运行 node crawler.js ,几秒钟后,你会把稳到已经创建了两个文件,分别名为 screenshot.jpg 和 page.pdf。
Nightmare:Puppeteer 的替代者
Nightmare 是类似 Puppeteer 的高等浏览器自动化库,该库利用 Electron,但听说速率是其前身 PhantomJS 的两倍。
如果你在某种程度上不喜好 Puppeteer 或对 Chromium 捆绑包的大小感到沮丧,那么 nightmare 是一个空想的选择。首先,运行以下命令安装 nightmare 库:npm install nightmare
然后,一旦下载了 nightmare,我们将用它通过 Google 搜索引擎找到 ScrapingBee 的网站。创建一个名为crawler.js的文件,然后将以下代码复制粘贴到个中:
constNightmare=require('nightmare')constnightmare=Nightmare()nightmare.goto('https://www.google.com/').type("input[title='Search']",'ScrapingBee').click("input[value='GoogleSearch']").wait('#rso>div:nth-child(1)>div>div>div.r>a').evaluate(()=>document.querySelector('#rso>div:nth-child(1)>div>div>div.r>a').href).end().then((link)=>{console.log('ScrapingBeeWebLink':link)}).catch((error)=>{console.error('Searchfailed:',error)})
首先创建一个 Nighmare 实例,然后通过调用 goto() 将该实例定向到 Google 搜索引擎,加载后,利用其选择器获取搜索框,然后利用搜索框的值(输入标签)变动为“ScrapingBee”。完成后,通过单击 “Google搜索” 按钮提交搜索表单。然后见告 Nightmare 等到第一个链接加载完毕,一旦完成,它将利用 DOM 方法来获取包含该链接的定位标记的 href 属性的值。
末了,完成所有操作后,链接将打印到掌握台。
总结
✅ Node.js 是 Javascript 在做事器真个运行时环境。由于事宜循环机制,它具有“非壅塞”性子。✅ HTTP客户端(例如 Axios、Superagent 和 Request)用于将 HTTP 要求发送到做事器并吸收相应。✅ Cheerio 把 JQuery 的优点抽出来,在做事器端 进行 Web 爬取是唯一的目的,但不实行 Javascript 代码。✅ JSDOM 根据标准 Javascript规范 从 HTML 字符串中创建一个 DOM,并许可你对其实行DOM操作。✅ Puppeteer and Nightmare 是高等(high-level )浏览器自动化库,可让你以编程办法去操作 Web 运用,就像真实的人正在与之交互一样。