in

使用 NodeJS 和 Javascript 进行网页爬取

Web 抓取主要是连接和数据编程,因此使用 Web 语言进行抓取似乎很自然,那么我们可以使用 javascript 抓取吗?

在本教程中,我们将学习使用 NodeJS 和 Javascript 进行网络抓取。我们将深入探讨 HTTP 连接、HTML 解析、流行的网络抓取库以及常见挑战和网络抓取习语。

最后,我们将通过一个示例网络抓取项目 – https://www.etsy.com/product scraper来完成所有工作,该项目说明了在 NodeJS 中进行网络抓取时遇到的两个主要挑战:cookie 跟踪和 CSRF 令牌。

概述和设置

Web 抓取中的 NodeJS 因Puppeteer浏览器自动化工具包而广为人知。使用 web 浏览器自动化进行 web 抓取有很多好处,尽管它是一种复杂且占用大量资源的 javascript web 抓取方法。
通过一些逆向工程和一些聪明的 nodeJS 库,我们可以在没有网络浏览器的全部开销的情况下获得类似的结果!

在本文中,我们将特别关注一些工具。对于连接,我们将使用axios HTTP 客户端,对于解析,我们将专注于cheerio HTML 树解析器,让我们使用这些命令行指令安装它们:

$ mkdir scrapfly-etsy-scraper
$ cd scrapfly-etsy-scraper
$ npm install cheerio axios

发出请求

连接是每个网络抓取工具的重要组成部分,NodeJS 有一个庞大的 HTTP 客户端生态系统,尽管在本教程中我们将使用最流行的一个 – axios。

HTTP 简而言之

要从公共资源收集数据,我们需要先与其建立连接。大多数网络都是通过 HTTP 提供的。该协议可以概括为:客户端(我们的抓取工具)发送对特定文档的请求,服务器回复请求的文档或错误 – 非常直接的交换。

正如您在此图中看到的:我们发送一个请求对象,它由方法(又名类型)、位置和标头组成。反过来,我们收到一个响应对象,它由状态代码、标题和文档内容本身组成。

在我们的 axios 示例中,它看起来像这样:

import axios from 'axios';

// send request
response = await axios.get('https://httpbin.org/get');
// print response
console.log(response.data);

尽管对于节点 js 网络抓取,我们需要了解有关请求和响应的一些关键细节:方法类型、标头、cookie…让我们快速概览一下。

请求方法

HTTP 请求可以很方便地分为几种执行不同功能的类型。我们最常在网络抓取中使用:

  • GET请求文档——抓取中最常用的方法。
  • POST发送文件以接收文件。例如,这用于登录、搜索等表单提交。
  • HEAD检查资源的状态。这主要用于检查网页是否已更新其内容,因为这些类型的请求非常快。

其他方法并不常见,但了解它们还是有好处的:

  • PATCH请求旨在更新文档。
  • PUTrequests 旨在创建新文档或更新它。
  • DELETE请求旨在删除文档。

请求位置 – URL

URL(通用资源位置)是我们请求中最重要的部分——它告诉我们的 nodejs 抓取器应该在哪里寻找资源。尽管 URL 可能非常复杂,但让我们看一下它们的结构:

显示一般 URL 结构的插图
URL 结构示例

在这里,我们可以可视化 URL 的每个部分:

  • 协议 – 要么http要么https
  • host – 是服务器的地址/域。
  • location – 是我们请求的资源的位置。
  • parameters – 允许自定义资源。例如language=en会给我们资源的英文版本。

如果您不确定 URL 的结构,您可以随时启动 Node 的交互式 shell(node在终端中)并让它为您解决:

$ node
> new URL("http://www.domain.com/path/to/resource?arg1=true&arg2=false")
URL {
  href: 'http://www.domain.com/path/to/resource?arg1=true&arg2=false',
  origin: 'http://www.domain.com',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'www.domain.com',
  hostname: 'www.domain.com',
  port: '',
  pathname: '/path/to/resource',
  search: '?arg1=true&arg2=false',
  searchParams: URLSearchParams { 'arg1' => 'true', 'arg2' => 'false' },
  hash: ''
}

请求标头

请求标头指示有关我们请求的元信息。虽然看起来请求标头只是网络抓取中的次要元数据细节,但它们非常重要。

标头包含有关请求的基本详细信息,例如谁在请求数据?他们期望什么类型的数据?弄错这些可能会导致抓取错误。

让我们来看看一些最重要的标头及其含义:

User-Agent是一个身份标头,它告诉服务器谁在请求文档。

# example user agent for Chrome browser on Windows operating system:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36

每当您在网络浏览器中访问网页时,都会使用类似于“浏览器名称、操作系统、某些版本号”的用户代理字符串来标识自己。
用户代理帮助服务器决定是服务还是拒绝客户端。抓取时我们希望融入其中以防止被阻止,因此最好将用户代理设置为看起来像浏览器之一。

Cookie用于存储持久性数据。这是网站跟踪用户状态的一项重要功能:用户登录、配置首选项等。

Accept标头(还有 Accept-Encoding、Accept-Language 等)包含有关我们期望的内容类型的信息。通常,在网络抓取时,我们想模仿一种流行的网络浏览器,例如 Chrome 浏览器:

text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8

X-前缀的标头是特殊的自定义标头。这些在网络抓取时非常重要,因为它们可能配置抓取的网站/webapp 的重要功能。

响应状态码

一旦我们发送了我们的请求,我们最终会收到一个响应,我们首先会注意到的是状态代码。状态代码指示请求是成功、失败还是需要更多信息(如身份验证/登录)。

让我们快速浏览一下与网络抓取最相关的状态代码:

  • 200个范围码一般代表成功!
  • 300 范围代码往往意味着重定向。换句话说,如果我们请求页面,/product1.html它可能会被移动到一个新的位置,比如/products/1.html.
  • 400 范围代码表示请求格式错误或被拒绝。我们的节点网络抓取工具可能缺少一些标头、cookie 或身份验证详细信息。
  • 500 个范围代码通常意味着服务器问题。该网站可能现在不可用或有意禁止访问我们的网络抓取工具。

响应头

接下来我们注意到我们的响应是元数据 – 也称为标头。
在网络抓取方面,响应标头提供了一些关于连接功能和效率的重要信息。

例如,Set-Cookieheader 请求我们的客户端为将来的请求保存一些 cookie,这可能对网站功能至关重要。其他标头(例如EtagLast-Modified旨在帮助客户端进行缓存以优化资源使用。

最后,就像请求标头一样,以 为前缀的标头是X-我们可能需要集成到我们的抓取工具中的自定义 Web 功能标头。


我们简要地忽略了核心 HTTP 组件,现在是时候尝试一下,看看 HTTP 在实际 Node 中是如何工作的了!

发出 GET 请求

现在我们已经熟悉了 HTTP 协议及其在 javascript 抓取中的使用方式,让我们发送一些请求吧!

让我们从一个基本的 GET 请求开始:

import axios from 'axios';

const response = await axios.get('https://httpbin.org/get');
console.log(response.data);

这里我们使用http://httpbin.org HTTP 测试服务来检索一个简单的 HTML 页面。运行时,此脚本应打印有关我们提出的请求的基本详细信息:

{
  args: {},
  headers: {
    Accept: 'application/json, text/plain, */*',
    Host: 'httpbin.org',
    'User-Agent': 'axios/0.25.0',
  },
  origin: '180.111.222.223',
  url: 'https://httpbin.org/get'
}

发出 POST 请求

POST 类型请求用于通过其交互功能(如登录、搜索功能或结果过滤)与网站进行交互。

对于这些请求,我们的爬虫需要发送一些东西来接收响应。那东西通常是一个 JSON 文档:

import axios from 'axios';

const response = await axios.post('https://httpbin.org/post', {'query': 'cats', 'page': 1});
console.log(response.data);

我们可以 POST 的另一种文档类型是 表单数据类型。为此,我们需要做更多的工作并使用form-data包:

import axios from 'axios';
import FormData from 'form-data';

function makeForm(data){
    var bodyFormData = new FormData();
    for (let key in data){
        bodyFormData.append(key, data[key]);
    }
    return bodyFormData;
}

const resposne = await axios.post('https://httpbin.org/post', makeForm({'query': 'cats', 'page': 1}));
console.log(response.data);

Axios 足够聪明,可以根据数据参数填写所需的标头详细信息(如content-type和)。content-length所以,如果我们要发送一个对象,它将设置Content-Type标题application/json并将数据形成为application/x-www-form-urlencoded– 非常方便!

设置标题

正如我们之前所述,我们的请求必须提供一些元数据,这有助于服务器确定要返回的内容或是否与我们合作。
通常,此元数据可用于识别网络抓取器并阻止它们,因此在抓取时我们应避免脱颖而出并模仿现代网络浏览器。

启动所有浏览器设置User-AgentAccept标题。要在我们的axios爬虫中设置它们,我们应该创建一个Client并从 Chrome 网络浏览器复制值:

import axios from 'axios';

const response = await axios.get(
    'https://httpbin.org/get', 
    {headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    }}
);
console.log(response.data);<h3 id="tip-set-default-settings">提示:设置默认设置

在抓取时,我们通常希望将相同的配置应用于多个请求,例如为我们的抓取器发出的每个请求设置那些 User-Agent 标头,以避免被阻止。

Axios 带有一个很棒的快捷方式,允许为所有连接配置默认值:

import axios from 'axios';

const session = axios.create({
    headers: {'User-Agent': 'tutorial program'},
    timeout: 5000,
    proxy: {
            host: 'proxy-url',
            port: 80,
            auth: {username: 'my-user', password: 'my-password'}
        }
    }
)

const response1 = await session.get('http://httpbin.org/get');
console.log(response1.data);
const response2 = await session.get('http://httpbin.org/get');
console.log(response2.data);

在这里,我们创建了一个实例,axios它将对每个请求应用自定义标头、超时和代理设置!

有时在网络抓取时我们关心持久连接状态。对于我们需要登录或配置首选项(如货币或语言)的网站 – cookie 用于完成所有这些工作!

不幸的是,默认情况下 axios 不支持 cookie 跟踪,但是可以通过axios-cookiejar-support扩展包启用它:

import axios from 'axios';
import { CookieJar } from 'tough-cookie';
import { wrapper } from 'axios-cookiejar-support';

const jar = new CookieJar();
const session = wrapper(axios.create({ jar }));

async function setLocale(){
    // set cookies:
    let respSetCookies = await session.get('http://httpbin.org/cookies/set/locale/usa');
    // retrieve existing cookies:
    let respGetCookies = await session.get('http://httpbin.org/cookies');
    console.log(respGetCookies.data);
}

setLocale();

在上面的示例中,我们使用 cookie jar 对象配置 axios 实例,它允许我们在网络抓取会话中拥有持久性 cookie。如果我们运行这个脚本,我们应该看到:

{ cookies: { locale: 'usa' } }

现在我们已经熟悉了 HTTP 连接以及如何在axios HTTP 客户端包中使用它,让我们来看看网络抓取过程的另一半:解析 HTML 数据!

解析 HTML

HTML(超文本标记语言)是一种支持网络的文本数据结构。它的伟大之处在于它旨在成为机器可读的文本内容,这对于网络抓取来说是个好消息,因为我们可以使用 javascript 代码轻松解析相关数据!

HTML 是一种树型结构,易于解析。例如,让我们来看这个简单的 HTML 内容:

<head>
  <title>
  </title>
</head>
<body>
  <h1>Introduction</h1>
  <div>
    <p>some description text: </p>
    <a class="link" href="http://example.com">example link</a>
  </div>
</body>

这是一个简单网站可能提供的基本 HTML 文档。你已经可以通过文本的缩进看到树状结构,但我们甚至可以进一步说明它:

HTML 节点树的插图
HTML 节点树的示例。请注意,分支是有序的(从左到右)
这种树结构非常适合网络抓取,因为我们可以轻松浏览整个文档并提取我们想要的特定部分。比如要查找网站的标题,我们可以看到它在元素<body><h1>。换句话说 – 如果我们想为 1000 个不同的页面提取 1000 个标题,我们将编写一个规则来查找规则body->h1->text,但我们如何执行这个规则?当谈到 HTML 解析时,有两种标准的方式来编写这些规则:CSS 选择器XPATH 选择器。接下来让我们看看如何在 NodeJS 和 Cheerio 中使用它们。

Cheerio 的 CSS 选择器

Cheerio是 NodeJS 中最流行的 HTML 解析包,它允许我们使用 CSS 选择器来选择 HTML 树的特定节点。

要使用 Cheerio,我们必须从 HTML 字符串创建树解析器对象,然后我们可以结合使用 CSS 选择器和元素函数来提取特定数据:

import cheerio from 'cheerio';

const tree = cheerio.load(`
    <head>
        <title>My Website</title>
    </head>
    <body>
        <div class="content">
            <h1>First blog post</h1>
            <p>Just started this blog!</p>
            <a href="http://scrapfly.io/blog">Checkout My Blog</a>
        </div>
    </body>
`);

console.log({
    // we can extract text of the node:
    title: tree('.content h1').text(),
    // or a specific attribute value:
    url: tree('.content a').attr('href')
});

在上面的示例中,我们使用示例 HTML 文档加载 Cheerio 并突出显示了两种选择相关数据的方法。我们使用方法来选择 HTML 元素的文本text(),使用attr()方法来选择特定属性。

Xmldom 的 XPath 选择器

虽然 CSS 选择器简短、健壮且易于阅读,但有时在处理复杂的网页时,我们可能需要更强大的东西。为此,nodeJS 还通过xpath@xmldom/xmldom等库支持 XPATH 选择器:

import xpath from 'xpath';
import { DOMParser } from '@xmldom/xmldom'

const tree = new DOMParser().parseFromString(`
    <head>
        <title>My Website</title>
    </head>
    <body>
        <div class="content">
            <h1>First blog post</h1>
            <p>Just started this blog!</p>
            <a href="http://scrapfly.io/blog">Checkout My Blog</a>
        </div>
    </body>
`);

console.log({
    // we can extract text of the node, which returns `Text` object:
    title: xpath.select('//div[@class="content"]/h1/text()', tree)[0].data,
    // or a specific attribute value, which return `Attr` object:
    url: xpath.select('//div[@class="content"]/a/@href', tree)[0].value,
});

在这里,我们在xmldom + xpath设置中复制我们的 Cheerio 示例,选择标题文本和 URL 的href属性。

我们研究了两种使用 NodeJS 解析 HTML 内容的方法:使用 Cheerio 的 CSS 选择器和使用 xmldom + xpath 的 Xpath 选择器。一般来说,最好坚持使用 Cheerio,因为它更符合 HTML 标准,而且 CSS 选择器更易于使用。

接下来,让我们通过探索一个示例项目来展示我们所学到的一切!

示例项目:etsy.com

我们已经了解了使用axios的 HTTP 连接和使用cheerio的 HTML 解析,现在是时候将所有内容放在一起并巩固我们的知识了。

在本节中,我们将为https://www.etsy.com/编写一个示例爬虫,这是一个用户驱动的电子商务网站(类似于Ebay,但用于工艺品)。我们选择此示例来涵盖使用 javascript 进行网页抓取时两个最常见的挑战:会话 cookie 和csrf标头。

我们将编写一个抓取器来抓取古董产品类别中出现的最新产品:

  1. 我们将转到https://www.etsy.com/并将我们的货币/地区更改为 USD/US。
  2. 然后我们将转到产品目录并找到最新的产品网址。
  3. 对于这些网址中的每一个,我们都会抓取产品名称、价格和其他详细信息。

让我们从与 etsy.com 建立连接并设置我们的首选货币/地区开始:

import cheerio from 'cheerio'
import axios from 'axios';
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';

const jar = new CookieJar();
const session = wrapper(
    axios.create({ 
        jar: jar,
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
    })
);


async function setLocale(currency, region){
    let _prewalk = await session.get('https://www.etsy.com/');
    let tree = cheerio.load(_prewalk.data);
    let csrf = tree('meta[name=csrf_nonce]').attr('content');
    try{
        let resp = await session.post(
            'https://www.etsy.com/api/v3/ajax/member/locale-preferences',
            {currency:currency, language:"en-US", region: region}, 
            {headers: {'x-csrf-token': csrf}},
        );
    }catch (error){
        console.log(error);
    }
}

await setLocale('USD', 'US');
在这里,我们正在创建一个支持 cookie 跟踪的 axios 实例。然后我们连接到 Etsy 的主页并寻找csrf允许我们与 Etsy 的后端 API 交互的令牌。最后,我们向此 API 发送偏好请求,它会返回我们的 cookiejar 自动为我们保存的一些跟踪 cookie。

从这里开始,我们使用 axios 实例发出的每个请求都将包含这些偏好 cookie——这意味着我们所有的抓取数据都将以美元为单位。

对网站首选项进行排序后,我们可以继续下一步 -收集类别中的最新产品网址/vintage/

async function findProducts(category){
    let resp = await session.get(
        `https://www.etsy.com/c/${category}?explicit=1&category_landing_page=1&order=date_desc`
    );
    let tree = cheerio.load(resp.data);
    return tree('a.listing-link').map(
        (i, node) => tree(node).attr('href')
    ).toArray();
}

console.log(await findProducts('vintage'));

在这里,我们定义了给定类别名称的函数,该函数将从第一页返回 url。请注意,我们添加了order=date_desc按日期降序对结果进行排序以仅选取最新产品的功能。

我们只剩下实现产品抓取本身了:

async function scrapeProduct(url){
    let resp = await session.get(url);
    let tree = cheerio.load(resp.data);
    return {
        url: url,
        title: tree('h1').text().trim(),
        description: tree('p[data-product-details-description-text-content]').text().trim(),
        price: tree('div[data-buy-box-region=price] p[class*=title]').text().trim(),
        store_url: tree('a[aria-label*="more products from store"]').attr('href').split('?')[0],
        images: tree('div[data-component=listing-page-image-carousel] img').map(
            (i, node) => tree(node).attr('data-src')
        ).toArray()
    };
}

与前面类似,我们在此函数中所做的就是检索产品页面的 HTML 并从 HTML 内容中提取产品详细信息。

最后,是时候将所有东西放在一起作为一个运行函数:

async function scrapeVintage(){
    await setLocale('USD', 'US');
    let productUrls = await findProducts('vintage');
    return Promise.all(productUrls.map(
        (url) => scrapeProduct(url)
    ))
}

console.log(await scrapeVintage());

在这里,我们将所有定义的函数组合到一个抓取任务中,该任务应产生如下结果:

[
  {
    url: 'https://www.etsy.com/listing/688372741/96x125-turkish-oushak-area-rug-vintage?click_key=467d607c570b0d7760a78a00c820a1da4d1e4d0d%3A688372741&click_sum=5f5c2ff9&ga_order=date_desc&ga_search_type=vintage&ga_view_type=gallery&ga_search_query=&ref=sc_gallery-1-1&frs=1&cns=1&sts=1',
    title: '9.6x12.5 Turkish Oushak Area Rug, Vintage Wool Rug, Faded Orange Handmade Home Décor, Distressed Blush Beige, Floral Bordered Oriental Rugs',
    description: '★ This special rug <...>',
    price: '$2,950.00',
    store_url: 'https://www.etsy.com/shop/SuffeArt',
    images: [
      'https://i.etsystatic.com/18572096/r/il/7480a4/3657436348/il_794xN.3657436348_oxay.jpg',
      'https://i.etsystatic.com/18572096/r/il/afa2b7/3705052531/il_794xN.3705052531_9xsa.jpg',
      'https://i.etsystatic.com/18572096/r/il/dbde4f/3657436290/il_794xN.3657436290_a64r.jpg',
      'https://i.etsystatic.com/18572096/r/il/b2002d/3705052595/il_794xN.3705052595_4c7m.jpg',
      'https://i.etsystatic.com/18572096/r/il/6ad90d/3705052613/il_794xN.3705052613_kzey.jpg',
      'https://i.etsystatic.com/18572096/r/il/ccec83/3705052663/il_794xN.3705052663_1472.jpg',
      'https://i.etsystatic.com/18572096/r/il/8be8c9/3657436390/il_794xN.3657436390_5su0.jpg',
      'https://i.etsystatic.com/18572096/r/il/c4f65e/3705052709/il_794xN.3705052709_4u9r.jpg',
      'https://i.etsystatic.com/18572096/r/il/806141/3705052585/il_794xN.3705052585_fn8p.jpg'
    ]
  },
  ...
]

使用这个示例 javascript 网络抓取工具,我们了解了两个重要的抓取概念:cookie 和标头。我们配置了货币偏好,学习了如何处理csrf代币,最后学习了如何抓取和解析产品信息!

常问问题

为了结束本教程,让我们看一下有关 JS 中网页抓取的常见问题:

nodejs 和 puppeteer 在网络抓取方面有什么区别?

Puppeteer 是一个流行的Nodejs浏览器自动化库。它经常用于网页抓取。然而,我们并不总是需要网络浏览器来进行网络抓取。在本文中,我们了解了如何将 Nodejs 与简单的 HTTP 客户端结合使用来抓取网页。浏览器的运行和维护非常复杂且昂贵,因此 HTTP 客户端网络爬虫速度更快,成本更低。

如何在 NodeJS 中并发抓取?

由于 NodeJS javascript 代码自然是异步的,我们可以通过在Promise.allPromise.allSettled函数中包装一个抓取承诺列表来执行并发请求以抓取多个页面。这些 async await 函数获取一个 promise 对象列表并并行执行它们,这可以将网络抓取过程加速数百倍:

urls = [...]
async function scrape(url){
    ...
};
let scrape_promises = urls.map((url) => scrape(url));
await Promise.all(scrape_promises);

如何在 NodeJS 中使用代理?

在大规模抓取时,我们可能需要使用代理来防止阻塞。大多数 NodeJS http 客户端库通过简单的参数实现代理支持。例如在axios库中我们可以使用会话设置代理:

const session = axios.create({
    proxy: {
            host: 'http://111.22.33.44',  //proxy ip address with protocol
            port: 80,  // proxy port
            auth: {username: 'proxy-auth-username', password: 'proxy-auth-password'}  // proxy auth if needed
        }
    }
)

什么是最好的 nodejs 网络抓取库?

使用 Cheerio 和 Nodejs 进行 Web 抓取是最流行的抓取方式,无需使用浏览器自动化 (Puppeteer),而 Axios 是最流行的 HTTP 请求方式。尽管不应该忽视不太受欢迎的替代方案,xmldom因为它们可以帮助抓取更复杂的网页。

如何单击按钮,输入文本在 NodeJS 中执行其他浏览器操作?

由于 NodeJS 引擎不完全兼容浏览器,我们无法自动单击按钮或提交表单。为此,需要使用像 Puppeteer 这样的东西来自动化真正的网络浏览器。有关更多信息,请参阅使用无头浏览器进行 Web 抓取:Puppeteer

概括

在这篇广泛的介绍文章中,我们介绍了自己的 NodeJS 网络抓取生态系统。我们研究了使用axios作为我们的 HTTP 客户端来收集多个页面,并使用cheerio / @xmldom/xmldom来使用 CSS/XPATH 选择器从这些数据中解析信息。

最后,我们用一个示例 nodejs 网络抓取程序项目将所有内容包装起来,该项目从https://www.etsy.com/抓取过时的产品信息。

 

 

 

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。