用 Javascript 和 Node.js 爬取网页

优采云 发布时间: 2022-05-06 17:06

  用 Javascript 和 Node.js 爬取网页

  HTTP 客户端是能够将请求发送到服务器,然后接收服务器响应的工具。下面提到的所有工具底的层都是用 HTTP 客户端来访问你要抓取的网站。

  Request

  Request 是 Javascript 生态中使用最广泛的 HTTP 客户端之一,但是 Request 库的作者已正式声明弃用了。不过这并不意味着它不可用了,相当多的库仍在使用它,并且非常好用。用 Request 发出 HTTP 请求是非常简单的:

  1const request = require('request')<br />2request('https://www.reddit.com/r/programming.json', function (<br />3  error,<br />4  response,<br />5  body<br />6) {<br />7  console.error('error:', error)<br />8  console.log('body:', body)<br />9})<br />

  你可以在 Github 上找到 Request 库,安装它非常简单。你还可以在 找到弃用通知及其含义。

  Axios

  Axios 是基于 promise 的 HTTP 客户端,可在浏览器和 Node.js 中运行。如果你用 Typescript,那么 axios 会为你覆盖内置类型。通过 Axios 发起 HTTP 请求非常简单,默认情况下它带有 Promise 支持,而不是在 Request 中去使用回调:

   1const axios = require('axios')<br /> 2<br /> 3axios<br /> 4    .get('https://www.reddit.com/r/programming.json')<br /> 5    .then((response) => {<br /> 6        console.log(response)<br /> 7    })<br /> 8    .catch((error) => {<br /> 9        console.error(error)<br />10    });<br />

  如果你喜欢 Promises API 的 async/await 语法糖,那么你也可以用,但是由于顶级 await 仍处于 stage 3 ,所以我们只好先用异步函数来代替:

   1async function getForum() {<br /> 2    try {<br /> 3        const response = await axios.get(<br /> 4            'https://www.reddit.com/r/programming.json'<br /> 5        )<br /> 6        console.log(response)<br /> 7    } catch (error) {<br /> 8        console.error(error)<br /> 9    }<br />10}<br />

  你所要做的就是调用 getForum!可以在 上找到Axios库。

  Superagent

  与 Axios 一样,Superagent 是另一个强大的 HTTP 客户端,它支持 Promise 和 async/await 语法糖。它具有像 Axios 这样相当简单的 API,但是 Superagent 由于存在更多的依赖关系并且不那么流行。

  用 promise、async/await 或回调向 Superagent 发出HTTP请求看起来像这样:

   1const superagent = require("superagent")<br /> 2const forumURL = "https://www.reddit.com/r/programming.json"<br /> 3<br /> 4// callbacks<br /> 5superagent<br /> 6    .get(forumURL)<br /> 7    .end((error, response) => {<br /> 8        console.log(response)<br /> 9    })<br />10<br />11// promises<br />12superagent<br />13    .get(forumURL)<br />14    .then((response) => {<br />15        console.log(response)<br />16    })<br />17    .catch((error) => {<br />18        console.error(error)<br />19    })<br />20<br />21// promises with async/await<br />22async function getForum() {<br />23    try {<br />24        const response = await superagent.get(forumURL)<br />25        console.log(response)<br />26    } catch (error) {<br />27        console.error(error)<br />28    }<br />29}<br />

  可以在 找到 Superagent。

  正则表达式:艰难的路

  在没有任何依赖性的情况下,最简单的进行网络抓取的方法是,使用 HTTP 客户端查询网页时,在收到的 HTML 字符串上使用一堆正则表达式。正则表达式不那么灵活,而且很多专业人士和业余爱好者都难以编写正确的正则表达式。

  让我们试一试,假设其中有一个带有用户名的标签,我们需要该用户名,这类似于你依赖正则表达式时必须执行的操作

  1const htmlString = 'Username: John Doe'<br />2const result = htmlString.match(/(.+)/)<br />3<br />4console.log(result[1], result[1].split(": ")[1])<br />5// Username: John Doe, John Doe<br />

  在 Javascript 中,match() 通常返回一个数组,该数组包含与正则表达式匹配的所有内容。第二个元素(在索引1中)将找到我们想要的 标记的 textContent 或 innerHTML。但是结果中包含一些不需要的文本( “Username: “),必须将其删除。

  如你所见,对于一个非常简单的用例,步骤和要做的工作都很多。这就是为什么应该依赖 HTML 解析器的原因,我们将在后面讨论。

  Cheerio:用于遍历 DOM 的核心 JQuery

  Cheerio 是一个高效轻便的库,它使你可以在服务器端使用 JQuery 的丰富而强大的 API。如果你以前用过 JQuery,那么将会对 Cheerio 感到很熟悉,它消除了 DOM 所有不一致和与浏览器相关的功能,并公开了一种有效的 API 来解析和操作 DOM。

  1const cheerio = require('cheerio')<br />2const $ = cheerio.load('Hello world')<br />3<br />4$('h2.title').text('Hello there!')<br />5$('h2').addClass('welcome')<br />6<br />7$.html()<br />8// Hello there!<br />

  如你所见,Cheerio 与 JQuery 用起来非常相似。

  但是,尽管它的工作方式不同于网络浏览器,也就这意味着它不能:

  因此,如果你尝试爬取的网站或 Web 应用是严重依赖 Javascript 的(例如“单页应用”),那么 Cheerio 并不是最佳选择,你可能不得不依赖稍后讨论的其他选项。

  为了展示 Cheerio 的强大功能,我们将尝试在 Reddit 中抓取 r/programming 论坛,尝试获取帖子名称列表。

  首先,通过运行以下命令来安装 Cheerio 和 axios:npm install cheerio axios。

  然后创建一个名为 crawler.js 的新文件,并复制粘贴以下代码:

   1const axios = require('axios');<br /> 2const cheerio = require('cheerio');<br /> 3<br /> 4const getPostTitles = async () => {<br /> 5    try {<br /> 6        const { data } = await axios.get(<br /> 7            'https://old.reddit.com/r/programming/'<br /> 8        );<br /> 9        const $ = cheerio.load(data);<br />10        const postTitles = [];<br />11<br />12        $('div > p.title > a').each((_idx, el) => {<br />13            const postTitle = $(el).text()<br />14            postTitles.push(postTitle)<br />15        });<br />16<br />17        return postTitles;<br />18    } catch (error) {<br />19        throw error;<br />20    }<br />21};<br />22<br />23getPostTitles()<br />24.then((postTitles) => console.log(postTitles));<br />

  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 将会非常简单。

   1const { JSDOM } = require('jsdom')<br /> 2const { document } = new JSDOM(<br /> 3    'Hello world'<br /> 4).window<br /> 5const heading = document.querySelector('.title')<br /> 6heading.textContent = 'Hello there!'<br /> 7heading.classList.add('welcome')<br /> 8<br /> 9heading.innerHTML<br />10// Hello there!<br />

  代码中用 JSDOM 创建一个 DOM,然后你可以用和操纵浏览器 DOM 相同的方法和属性来操纵该 DOM。

  为了演示如何用 JSDOM 与网站进行交互,我们将获得 Reddit r/programming 论坛的第一篇帖子并对其进行投票,然后验证该帖子是否已被投票。

  首先运行以下命令来安装 jsdom 和 axios:npm install jsdom axios

  然后创建名为 crawler.js的文件,并复制粘贴以下代码:

   1const { JSDOM } = require("jsdom")<br /> 2const axios = require('axios')<br /> 3<br /> 4const upvoteFirstPost = async () => {<br /> 5  try {<br /> 6    const { data } = await axios.get("https://old.reddit.com/r/programming/");<br /> 7    const dom = new JSDOM(data, {<br /> 8      runScripts: "dangerously",<br /> 9      resources: "usable"<br />10    });<br />11    const { document } = dom.window;<br />12    const firstPost = document.querySelector("div > div.midcol > div.arrow");<br />13    firstPost.click();<br />14    const isUpvoted = firstPost.classList.contains("upmod");<br />15    const msg = isUpvoted<br />16      ? "Post has been upvoted successfully!"<br />17      : "The post has not been upvoted!";<br />18<br />19    return msg;<br />20  } catch (error) {<br />21    throw error;<br />22  }<br />23};<br />24<br />25upvoteFirstPost().then(msg => console.log(msg));<br />

  upvoteFirstPost() 是一个异步函数,它将在 r/programming 中获取第一个帖子,然后对其进行投票。axios 发送 HTTP GET 请求获取指定 URL 的HTML。然后通过先前获取的 HTML 来创建新的 DOM。JSDOM 构造函数把HTML 作为第一个参数,把 option 作为第二个参数,已添加的 2 个 option 项执行以下功能:

  创建 DOM 后,用相同的 DOM 方法得到第一篇文章的 upvote 按钮,然后单击。要验证是否确实单击了它,可以检查 classList 中是否有一个名为 upmod 的类。如果存在于 classList 中,则返回一条消息。

  打开终端并运行 node crawler.js,然后会看到一个整洁的字符串,该字符串将表明帖子是否被赞过。尽管这个例子很简单,但你可以在这个基础上构建功能强大的东西,例如,一个围绕特定用户的帖子进行投票的机器人。

  如果你不喜欢缺乏表达能力的 JSDOM ,并且实践中要依赖于许多此类操作,或者需要重新创建许多不同的 DOM,那么下面将是更好的选择。

  Puppeteer:无头浏览器

  顾名思义,Puppeteer 允许你以编程方式操纵浏览器,就像操纵木偶一样。它通过为开发人员提供高级 API 来默认控制无头版本的 Chrome。

  

  摘自 Puppeter DocsPuppeteer 比上述工具更有用,因为它可以使你像真正的人在与浏览器进行交互一样对网络进行爬取。这就具备了一些以前没有的可能性:

  它还可以在 Web 爬取之外的其他任务中发挥重要作用,例如 UI 测试、辅助性能优化等。

  通常你会想要截取网站的屏幕截图,也许是为了了解竞争对手的产品目录,可以用 puppeteer 来做到。首先运行以下命令安装 puppeteer,:npm install puppeteer

  这将下载 Chromium 的 bundle 版本,根据操作系统的不同,该版本大约 180 MB 至 300 MB。如果你要禁用此功能。

  让我们尝试在 Reddit 中获取 r/programming 论坛的屏幕截图和 PDF,创建一个名为 crawler.js的新文件,然后复制粘贴以下代码:

   1const puppeteer = require('puppeteer')<br /> 2<br /> 3async function getVisual() {<br /> 4    try {<br /> 5        const URL = 'https://www.reddit.com/r/programming/'<br /> 6        const browser = await puppeteer.launch()<br /> 7        const page = await browser.newPage()<br /> 8<br /> 9        await page.goto(URL)<br />10        await page.screenshot({ path: 'screenshot.png' })<br />11        await page.pdf({ path: 'page.pdf' })<br />12<br />13        await browser.close()<br />14    } catch (error) {<br />15        console.error(error)<br />16    }<br />17}<br />18<br />19getVisual()<br />

  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的文件,然后将以下代码复制粘贴到其中:

   1const Nightmare = require('nightmare')<br /> 2const nightmare = Nightmare()<br /> 3<br /> 4nightmare<br /> 5    .goto('https://www.google.com/')<br /> 6    .type("input[title='Search']", 'ScrapingBee')<br /> 7    .click("input[value='Google Search']")<br /> 8    .wait('#rso > div:nth-child(1) > div > div > div.r > a')<br /> 9    .evaluate(<br />10        () =><br />11            document.querySelector(<br />12                '#rso > div:nth-child(1) > div > div > div.r > a'<br />13            ).href<br />14    )<br />15    .end()<br />16    .then((link) => {<br />17        console.log('Scraping Bee Web Link': link)<br />18    })<br />19    .catch((error) => {<br />20        console.error('Search failed:', error)<br />21    })<br />

  首先创建一个 Nighmare 实例,然后通过调用 goto() 将该实例定向到 Google 搜索引擎,加载后,使用其选择器获取搜索框,然后使用搜索框的值(输入标签)更改为“ScrapingBee”。完成后,通过单击 “Google搜索” 按钮提交搜索表单。然后告诉 Nightmare 等到第一个链接加载完毕,一旦完成,它将使用 DOM 方法来获取包含该链接的定位标记的 href 属性的值。

  最后,完成所有操作后,链接将打印到控制台。

  总结

  

  

  

  往期精彩回顾

  

  

  如果觉得有帮助,请点击“在看”让更多小伙伴知道

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线