怎样用 Node.js 高效地从 Web 爬取数据?
由于Javascript有了宏大的改善,并且引入了称为NodeJS的运转时,因此它已成为最流行和使用最广泛的说话之一。 不管是Web利用程序还是移动利用程序,Javascript此刻都具有准确的工具。 本文讲解怎样用 Node.js 高效地从 Web 爬取数据。
前提前提
本文主要针对具有必然 JavaScript 经历的程序员。假如你对 Web 抓取有深刻的理解,但对 JavaScript 并不熟知,那么本文依然能够对你有所帮忙。
- ? 会 JavaScript
- ? 会用 DevTools 提取元素选中器
- ? 会一些 ES6 (可选)
你将学到
通过本文你将学到:
- 学到更多关于 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 效劳器非常简便,如下所示:
const http = require('http'); const PORT = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); }); server.listen(port, () => { console.log(`Server running at PORT:${port}/`); });
假如你已安置了 Node.js,可以试着运转上面的代码。 Node.js 非常适合 I/O 密集型程序。
HTTP 客户端:拜访 Web
HTTP 客户端是能够将恳求发送到效劳器,然后接收效劳器响应的工具。下面提到的所有工具底的层都是用 HTTP 客户端来拜访你要抓取的网站。
Request
Request 是 Javascript 生态中使用最广泛的 HTTP 客户端之一,但是 Request 库的作者已正式声明弃用了。不外这并不料味着它不成用了,相当多的库仍在使用它,并且非常好用。用 Request 发出 HTTP 恳求是非常简便的:
const request = 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/re... 寻到弃用通知及其含义。
Axios
Axios 是基于 promise 的 HTTP 客户端,可在阅读器和 Node.js 中运转。假如你用 Typescript,那么 axios 会为你覆盖内置类型。通过 Axios 发起 HTTP 恳求非常简便,默许状况下它带有 Promise 支撑,而不是在 Request 中去使用回调:
const axios = 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 ,所以我们只好先用异步函数来代替:
async function getForum() { try { const response = await axios.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恳求看起来像这样:
const superagent = require("superagent") const forumURL = "https://www.reddit.com/r/programming.json" // callbacks superagent .get(forumURL) .end((error, response) => { console.log(response) }) // promises superagent .get(forumURL) .then((response) => { console.log(response) }) .catch((error) => { console.error(error) }) // promises with async/await async function getForum() { try { const response = await superagent.get(forumURL) console.log(response) } catch (error) { console.error(error) } }
可以在 https://github.com/visionmedi... 寻到 Superagent。
正则表达式:困难的路
在没有任何依靠性的状况下,最简便的停止网络抓取的办法是,使用 HTTP 客户端查询网页时,在收到的 HTML 字符串上使用一堆正则表达式。正则表达式不那么灵敏,并且许多专业人士和业余喜好者都难以编写准确的正则表达式。
让我们试一试,假设其中有一个带有会员名的标签,我们需要该会员名,这相似于你依靠正则表达式时必需施行的操纵
const htmlString = '<label>Username: John Doe</label>' const result = htmlString.match(/<label>(.+)<\/label>/) console.log(result[1], result[1].split(": ")[1]) // Username: John Doe, John Doe
在 Javascript 中,match()
平常返回一个数组,该数组包括与正则表达式匹配的所有内容。第二个元素(在索引1中)将寻到我们想要的 <label>
标志的 textContent
或 innerHTML
。但是结果中包括一些不需要的文本( “Username: “),必需将其删除。
如你所见,关于一个非常简便的用例,步骤和要做的工作都许多。这就是为什么应当依靠 HTML 解析器的缘由,我们将在后面计议。
Cheerio:用于遍历 DOM 的中心 JQuery
Cheerio 是一个高效轻便的库,它使你可以在效劳器端使用 JQuery 的丰硕而强大的 API。假如你之前用过 JQuery,那么将会对 Cheerio 感到很熟知,它消弭了 DOM 所有不一致和与阅读器相关的功效,并公示了一种有效的 API 来解析和操纵 DOM。
const cheerio = require('cheerio') const $ = cheerio.load('<h2 class="title">Hello world</h2>') $('h2.title').text('Hello there!') $('h2').addClass('welcome') $.html() // <h2 class="title welcome">Hello there!</h2>
如你所见,Cheerio 与 JQuery 用起来非常类似。
但是,尽管它的工作方式不一样于网络阅读器,也就这意味着它不克不及:
- 渲染任何解析的或操作 DOM 元素
- 利用 CSS 或加载外部资源
- 施行 JavaScript
因此,假如你尝试爬取的网站或 Web 利用是严峻依靠 Javascript 的(例如“单页利用”),那么 Cheerio 并不是最好选中,你大概不得不依靠稍后计议的其他选项。
为了展现 Cheerio 的强大功效,我们将尝试在 Reddit 中抓取 r/programming 论坛,尝试猎取帖子名称列表。
第一,通过运转以下命令来安置 Cheerio 和 axios:npm install cheerio axios
。
然后创立一个名为 crawler.js
的新文件,并复制粘贴以下代码:
const axios = require('axios'); const cheerio = require('cheerio'); const getPostTitles = async () => { try { const { data } = await axios.get( 'https://old.reddit.com/r/programming/' ); const $ = cheerio.load(data); const postTitles = []; $('div > p.title > a').each((_idx, el) => { const postTitle = $(el).text() postTitles.push(postTitle) }); return postTitles; } catch (error) { throw error; } }; 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 } = new JSDOM( '<h2 class="title">Hello world</h2>' ).window const heading = document.querySelector('.title') heading.textContent = 'Hello there!' heading.classList.add('welcome') heading.innerHTML // <h2 class="title welcome">Hello there!</h2>
代码中用 JSDOM 创立一个 DOM,然后你可以用和操作阅读器 DOM 雷同的办法和属性来操作该 DOM。
为了演示怎样用 JSDOM 与网站停止交互,我们将获得 Reddit r/programming 论坛的第一篇帖子并对其停止投票,然后验证该帖子可否已被投票。
第一运转以下命令来安置 jsdom 和 axios:npm install jsdom axios
然后创立名为 crawler.js
的文件,并复制粘贴以下代码:
const { JSDOM } = require("jsdom") const axios = require('axios') const upvoteFirstPost = async () => { try { const { data } = await axios.get("https://old.reddit.com/r/programming/"); const dom = new JSDOM(data, { runScripts: "dangerously", resources: "usable" }); const { document } = dom.window; const firstPost = document.querySelector("div > div.midcol > div.arrow"); firstPost.click(); const isUpvoted = firstPost.classList.contains("upmod"); const msg = isUpvoted ? "Post has been upvoted successfully!" : "The post has not been upvoted!"; return msg; } catch (error) { throw error; } }; 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
的新文件,然后复制粘贴以下代码:
const puppeteer = require('puppeteer') async function getVisual() { try { const URL = 'https://www.reddit.com/r/programming/' const browser = await puppeteer.launch() const page = await browser.newPage() await page.goto(URL) await page.screenshot({ path: 'screenshot.png' }) await page.pdf({ path: 'page.pdf' }) await browser.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
的文件,然后将以下代码复制粘贴到其中:
const Nightmare = require('nightmare') const nightmare = Nightmare() nightmare .goto('https://www.google.com/') .type("input[title='Search']", 'ScrapingBee') .click("input[value='Google Search']") .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('Scraping Bee Web Link': link) }) .catch((error) => { console.error('Search failed:', 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 利用,就像真实的人正在与之交互一样。
原文地址:https://www.scrapingbee.com/blog/web-scraping-javascript/
作者:Shenesh Perera