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
请求旨在更新文档。PUT
requests 旨在创建新文档或更新它。DELETE
请求旨在删除文档。
请求位置 – URL
URL(通用资源位置)是我们请求中最重要的部分——它告诉我们的 nodejs 抓取器应该在哪里寻找资源。尽管 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-Cookie
header 请求我们的客户端为将来的请求保存一些 cookie,这可能对网站功能至关重要。其他标头(例如Etag
)Last-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-Agent
和Accept
标题。要在我们的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 跟踪
有时在网络抓取时我们关心持久连接状态。对于我们需要登录或配置首选项(如货币或语言)的网站 – 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 文档。你已经可以通过文本的缩进看到树状结构,但我们甚至可以进一步说明它:
这种树结构非常适合网络抓取,因为我们可以轻松浏览整个文档并提取我们想要的特定部分。比如要查找网站的标题,我们可以看到它在元素
<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
标头。
我们将编写一个抓取器来抓取古董产品类别中出现的最新产品:
- 我们将转到https://www.etsy.com/并将我们的货币/地区更改为 USD/US。
- 然后我们将转到产品目录并找到最新的产品网址。
- 对于这些网址中的每一个,我们都会抓取产品名称、价格和其他详细信息。
让我们从与 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');
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' ] }, ... ]
csrf
代币,最后学习了如何抓取和解析产品信息!
常问问题
为了结束本教程,让我们看一下有关 JS 中网页抓取的常见问题:
nodejs 和 puppeteer 在网络抓取方面有什么区别?
Puppeteer 是一个流行的Nodejs浏览器自动化库。它经常用于网页抓取。然而,我们并不总是需要网络浏览器来进行网络抓取。在本文中,我们了解了如何将 Nodejs 与简单的 HTTP 客户端结合使用来抓取网页。浏览器的运行和维护非常复杂且昂贵,因此 HTTP 客户端网络爬虫速度更快,成本更低。
如何在 NodeJS 中并发抓取?
由于 NodeJS javascript 代码自然是异步的,我们可以通过在Promise.all或Promise.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/抓取过时的产品信息。