如何使用 Puppeteer 和 Node.JS 进行 Web 抓取?
wellshake 2024-08-20 14:33:04 阅读 89
什么是 Headlesschrome?
Headless?是的,这意味着这个浏览器没有图形用户界面 (GUI)。不用鼠标或触摸设备与视觉元素交互,你需要使用命令行界面 (CLI) 来执行自动化操作。
Headlesschrome 和 Puppeteer
很多网页抓取工具都可适用于 Headlesschrome,并且 Headlesschrome 通常可以消除很多麻烦。
你可能还喜欢:如何检测和反检测 Headlesschrome?
Puppeteer 是什么?它是一个 Node.js 库,提供了一个高级 API 来控制 Headlesschrome 或 Chromium,或与 DevTools 协议进行交互。
今天我们将通过 Puppeteer 深入探索 Headlesschrome。
使用 Puppeteer 进行网页抓取的优势是什么?
正如你所想象的,使用 Puppeteer 进行网页抓取有几个很大的优势:
Puppeteer 抓取工具能够提取动态数据,因为 Headlesschrome 可以像普通浏览器一样渲染 JavaScript、图像等。Puppeteer 网页抓取脚本更难检测和阻止。由于连接配置看起来像普通用户的配置,因此难以将其识别为自动化操作。
如何使用 Puppeteer 和 Node.JS 进行网页抓取?
在以下示例中,我们将执行基本的网页抓取,帮助你快速入门 Puppeteer。我们选择抓取的页面是 Amazon 的 Apple AirPods Pro 评论区。
但别担心,在此之前,我们还需要做一些准备工作:
安装和配置 Puppeteer
步骤 1. 请确保你已经安装了 Node.js。
如果没有,请直接安装 Node.js (LST),然后通过 Node.js 的包管理器 npm 安装 Puppeteer。这个过程可能会有点长,因为 Puppeteer 还需要安装相应的 Chrome。
<code>npm i puppeteer
步骤 2. 安装后,你可以运行以下演示代码,确保你的安装是正确的。
你也可以使用此演示代码对 Puppeteer 有一个大致的了解。不要在这里卡住,因为我们稍后将详细介绍 Puppeteer 的用法和相关场景。
Puppeteer 默认启用了 headless 模式。在这里,通过 puppeteer.launch({ headless: false })
关闭 headless 模式,以便你看到抓取过程。
import puppeteer from 'puppeteer';
// 启动浏览器并打开一个新标签页
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// 导航到指定的 URL
await page.goto('https://developer.chrome.com/');
// 设置屏幕大小
await page.setViewport({width: 1080, height: 1024});
// 定位搜索框元素并在搜索框中输入内容
await page.locator('.devsite-search-field').fill('automate beyond recorder');
// 等待并点击第一个搜索结果
await page.locator('.devsite-result-item-link').click();
// 使用唯一字符串定位完整标题
const textSelector = await page
.locator('text/Customize and automate')
.waitHandle();
const fullTitle = await textSelector?.evaluate(el => el.textContent);
// 打印完整标题
console.log('这篇博客文章的标题是 "%s".', fullTitle);
// 关闭浏览器实例
await browser.close();
Puppeteer 是一个基于 Promise 的异步库,通过 async await
可以非常直观地展示其功能。以上演示和后续示例不需要异步函数。这是因为 package.json
中设置了 "type": "module"
,使其运行为 ES 模块。
页面分析
好了,我们开始吧。
请先打开 Apple AirPods Pro 的评论区,然后我们需要识别要抓取内容的元素。你可以通过按 Ctrl + Shift + I
(Windows/Linux) 或 Cmd + Option + I
(Mac) 打开 Devtools。
步骤 1. 点击控制台左上角的元素选择器步骤 2. 使用鼠标悬停并选择你想要抓取的元素节点。控制台还会突出显示与此元素对应的 HTML 代码
Puppeteer 支持多种元素选择方法(puppeteer 选择器),但最推荐入门使用简单的 CSS 选择器。上面使用的 <code>.devsite-search-field 也是一个 CSS 选择器。
对于复杂的 CSS 结构,调试控制台可以直接复制 CSS 选择器。右键单击需要抓取的元素 HTML,打开 菜单 > 复制 > 复制选择器。
但不建议你这样做,因为从复杂结构复制的选择器可读性非常差,不利于代码维护。当然,对于一些简单的选择和个人测试学习,完全没问题。
现在,元素选择器已经确定。我们可以使用 Puppeteer 尝试抓取我上面选择的用户名。
<code>import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(
`https://www.amazon.com/Apple-Generation-Cancelling-Transparency-Personalized/product-reviews/B0CHWRXH8B/ref=cm_cr_dp_d_show_all_btm?ie=UTF8&reviewerType=all_reviews`
);
const username = await page.$eval('div[data-hook="genome-widget"] .a-profile-name', node => node.textContent)code>
console.log('[username]===>', username);
如你所见,上面的代码使用 page.goto
跳转到指定页面。然后 page.$eval
可以获取第一个匹配的元素节点,并通过回调函数获取元素节点的具体属性。
如果你足够幸运,没有触发亚马逊的验证页面,你可以成功获取到值。然而,一个稳定的脚本不能仅仅依靠运气,所以我们接下来还需要进行一些优化。
等待页面加载
尽管我们已经通过上述方法获取了元素节点的信息,但我们必须考虑其他因素:如网络加载速度,页面是否滚动到目标元素以正确加载元素,是否触发了验证页面并需要手动处理。
因此,在加载完成之前,我们必须耐心等待。当然,Puppeteer 还为我们提供了相应的 API 供我们使用。
常用的 waitForSelector
是一个等待元素出现的 API。我们可以使用它来优化上面的代码,以确保脚本的稳定性。在调用 page.$eval
之前,只需使用 waitForSelector
API。
这样,Puppeteer 将在页面加载元素 div[data-hook="genome-widget"] .a-profile-namecode> 后再执行后续代码。
await page.waitForSelector('div[data-hook="genome-widget"] .a-profile-name');code>
const username = await page.$eval('div[data-hook="genome-widget"] .a-profile-name', node => node.textContent)code>
还有一些其他的等待 API 适用于不同场景。让我们看看一些常用的:
page.waitForFunction(pageFunction, options, ...args)
:等待页面上下文中的指定函数返回 true。
import puppeteer from 'puppeteer'
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 等待页面的 `window.title` 改变为 "Example Domain"
await page.waitForFunction('document.title === "Example Domain"');
console.log('标题已更改为 "Example Domain"');
await browser.close();
page.waitForNavigation(options)
:等待页面导航完成。导航可以是点击链接、提交表单、调用 window.location
等。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// 点击链接并等待导航完成
await Promise.all([
page.click('a'),
page.waitForNavigation()
])
console.log('导航完成');
await browser.close();
page.waitForRequest(urlOrPredicate, options)
:等待匹配指定 URL 或条件函数的请求。
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com');
// 该请求需要你监控实际页面的请求 URL。这只是一个示例。
// 你可以手动在浏览器地址栏中输入 https://example.com/resolve 来触发请求并验证此演示
const Request = await page.waitForRequest('https://example.com/resolve');
console.log('request-url:', Request.url());
await browser.close()
page.waitForResponse(urlOrPredicate, options)
:等待匹配指定 URL 或条件函数的响应。
import puppeteer from "puppeteer";
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com');
// 该响应需要你监控实际页面的响应 URL。这只是一个示例。
// 你可以手动在浏览器地址栏中输入 https://example.com/resolve 来触发响应并验证此演示
const response = await page.waitForResponse('https://example.com/resolve');
console.log('response-status:', response.status());
await browser.close();
page.waitForNetworkIdle(options)
:等待页面上的网络活动变为空闲状态。此方法用于确保页面已加载完成。
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.waitForNetworkIdle({
timeout: 30000, // 最大等待时间 30 秒
idleTime: 500 // 即在 500 毫秒的空闲时间内没有网络活动
});
console.log('Network is idle.');
// 保存截图以验证页面是否完全加载
await page.screenshot({ path: 'example.png' });
await browser.close();
setTimeout
:直接使用 JavaScript API 也是一个不错的选择。经过一些包装后,可以在页面上下文中运行。
// 等待两秒后执行后续脚本
await new Promise(resolve => setTimeout(resolve, 2000))
await page.click('.devsite-result-item-link'); // 点击该元素
你对网络爬虫和无头浏览器有任何精彩的想法或疑问吗?
让我们看看其他开发者在 Discord 和 Telegram 上分享了什么吧!
爬取和存储数据
好的,让我们开始爬取页面评论列表中的完整数据。
第 1 步。 数据爬取
我们可以重写上述代码,不再仅仅爬取单个用户名,而是关注整个评论列表。
以下代码也使用 page.waitForSelector
等待评论元素加载,并使用 page.$$
获取所有与元素选择器匹配的元素节点:
await page.waitForSelector('div[data-hook="review"]');code>
const reviewList = await page.$$('div[data-hook="review"]');code>
接下来,我们需要循环遍历评论元素列表,并从每个评论元素中获取所需的信息。
在以下代码中,我们可以获取标题、评分、用户名和内容的 textContent
,并获取头像元素节点中 data-src
的属性值,该属性值为头像的 URL 地址。
for (const review of reviewList) {
const title = await review.$eval(
'a[data-hook="review-title"] .cr-original-review-content',code>
node => node.textContent,
);
const rate = await review.$eval(
'i[data-hook="review-star-rating"] .a-icon-alt',code>
node => node.textContent,
);
const username = await review.$eval(
'div[data-hook="genome-widget"] .a-profile-name',code>
node => node.textContent,
);
const avatar = await review.$eval(
'div[data-hook="genome-widget"] .a-profile-avatar img',code>
node => node.getAttribute('data-src'),
);
const content = await review.$eval(
'span[data-hook="review-body"] span',code>
node => node.textContent,
);
console.log('[log]===>', { title, rate, username, avatar, content });
}
第 2 步。 存储数据
运行上述代码后,你应该能够在终端中看到打印的日志信息。
如果你想进一步存储这些数据,可以使用基本的 <code>nodejs 模块 fs
将数据写入 json 进行后续的数据分析。
以下是一个简单的工具函数:
import fs from 'fs';
function saveObjectToJson(obj, filename) {
const jsonString = JSON.stringify(obj, null, 2)
fs.writeFile(filename, jsonString, 'utf8', (err) => {
err ? console.error(err) : console.log(`文件已成功保存: ${filename}`);
});
}
完整代码如下。运行后,你可以在当前脚本执行路径中找到 amazon_reviews_log.json 文件,该文件记录了所有的爬取结果!
import puppeteer from 'puppeteer';
import fs from 'fs';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(
`https://www.amazon.com/Apple-Generation-Cancelling-Transparency-Personalized/product-reviews/B0CHWRXH8B/ref=cm_cr_dp_d_show_all_btm?ie=UTF8&reviewerType=all_reviews`
);
await page.waitForSelector('div[data-hook="review"]');code>
const reviewList = await page.$$('div[data-hook="review"]');code>
const reviewLog = []
for (const review of reviewList) {
const title = await review.$eval(
'a[data-hook="review-title"] .cr-original-review-content',code>
node => node.textContent,
);
const rate = await review.$eval(
'i[data-hook="review-star-rating"] .a-icon-alt',code>
node => node.textContent,
);
const username = await review.$eval(
'div[data-hook="genome-widget"] .a-profile-name',code>
node => node.textContent,
);
const avatar = await review.$eval(
'div[data-hook="genome-widget"] .a-profile-avatar img',code>
node => node.getAttribute('data-src'),
);
const content = await review.$eval(
'span[data-hook="review-body"] span',code>
node => node.textContent,
);
console.log('[log]===>', { title, rate, username, avatar, content });
reviewLog.push({ title, rate, username, avatar, content })
}
function saveObjectToJson(obj, filename) {
const jsonString = JSON.stringify(obj, null, 2)
fs.writeFile(filename, jsonString, 'utf8', (err) => {
err ? console.error(err) : console.log(`文件已成功保存: ${filename}`);
});
}
saveObjectToJson(reviewLog, 'amazon_reviews_log.json')
await browser.close()
Puppeteer 的其他功能示例
了解了基本用法之后?现在,我们可以继续了解 Puppeteer 的强大功能。运行以下示例后,我相信你会对这个工具有新的认识。
1. 模拟鼠标移动
使用 page.mouse.move
来操作鼠标移动。
为了让你感受到光标确实在页面上移动,以下演示是一个无限循环,将使鼠标随机移动以触发页面的悬停样式。
需要注意的是,触发悬停的前提是鼠标移动不能太快。move
方法中的 steps: 10
配置了移动速率。这个步骤也可以降低网站被检测到的概率。
Page.evaluate
是一个非常有用的 API,允许你在页面上下文中执行仅在浏览器环境中运行的 JavaScript 代码,例如使用 window API
。这里的目的是将页面滚动到底部,以便页面评论会完全加载。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://www.google.com');
// 获取屏幕的宽度和高度
const { width, height } = await page.evaluate(() => {
return { width: window.innerWidth, height: window.innerHeight };
});
// 无限循环,模拟随机的鼠标移动
while (true) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
await page.mouse.move(x, y, { steps: 10 });
console.log(`鼠标位置: (${x}, ${y})`);
await new Promise(resolve => setTimeout(resolve, 200)); // 每 0.2 秒移动一次
}
2. 点击按钮并填写表单
我们在初始演示中也遇到过这个。如何改变写法并使用其他 API 实现它?
你会看到一些选择器前面带有 >>>
,这是 Puppeteer 提供的 Shadow DOM selector
。大多数操作都是通过 delay
进行延迟触发,这可以很好地模拟真实用户的行为,使你的脚本更稳定,避免触发某些网站的反爬虫机制。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: false,
// defaultView 设置宽度和高度为 0,这意味着网页内容填充整个窗口。
defaultViewport: { width: 0, height: 0 }
});
const page = await browser.newPage();
await page.goto('https://developer.chrome.com/docs/css-ui?hl=de');
await page.click('>>> button[aria-controls="language-menu"]', { delay: 500 });code>
// 跳转到新页面并等待跳转成功
await Promise.all([
page.click('>>> li[role="presentation"]', { delay: 500 }),code>
page.waitForNavigation(),
])
// 使用 setTimeout 作为延迟器,等待 2 秒加载页面
await new Promise(resolve => setTimeout(resolve, 2000))
// 聚焦输入框
await page.focus('input.devsite-search-query', { delay: 500 });
// 通过键盘输入文本
await page.keyboard.type('puppeteer', { delay: 200 });
// 触发键盘回车键并提交表单
await page.keyboard.press('Enter')
console.log('form submit successfully');
await page.close()
3. 使用 Puppeteer 截取屏幕截图
Puppeteer 提供了方便的截图 API,这是一个非常实用的功能,我们在上面的示例中已经看到过。
截图文件的质量可以通过 quality 进行很好的控制,clip 用于裁剪图片。如果你对截图比例有要求,也可以设置 defaultViewport
来实现。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ defaultViewport: { width: 1920, height: 1080 } });
const page = await browser.newPage();
await page.goto('https://www.youtube.com/');
await page.screenshot({ path: 'screenshot1.png' });
await page.screenshot({ path: 'screenshot2.jpeg', quality: 50 });
await page.screenshot({ path: 'screenshot3.jpeg', clip: { x: 0, y: 0, width: 150, height: 150 } });
console.log('screenshot saved');
await browser.close();
4. 在 Puppeteer 中拦截或阻止请求
要拦截请求,首先需要使用 setRequestInterception
激活请求拦截。运行以下示例,你会惊讶地发现页面样式消失了,图片和图标也不见了。
这是因为通过页面监控了请求,并且使用 interceptedRequest
的 resourceType
和 url
来判断是否取消或重写相应的请求。
我们需要注意的是,在处理请求拦截之前,应该调用 isInterceptResolutionHandled
方法,以避免重复处理请求或发生冲突。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// 激活请求拦截
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
// 避免请求被重复处理
if (interceptedRequest.isInterceptResolutionHandled()) return;
// 拦截请求并重写响应
if (interceptedRequest.url().includes('https://fonts.gstatic.com/')) {
interceptedRequest.respond({
status: 404,
contentType: 'image/x-icon',
})
console.log('icons request blocked');
// 阻止样式请求
} else if (interceptedRequest.resourceType() === 'stylesheet') {
interceptedRequest.abort();
console.log('Stylesheet request blocked');
// 阻止图片请求
} else if (interceptedRequest.resourceType() === 'image') {
interceptedRequest.abort();
console.log('Image request blocked');
} else {
interceptedRequest.continue();
}
});
await page.goto('https://www.youtube.com/');
当然,上述功能也可以借助一些工具实现,例如使用 Nstbrowser RPA 来加速你的爬虫!
步骤 1. 进入 Nstbrowser 的首页,点击 RPA/Workflow > 创建工作流。
步骤 2. 进入工作流编辑页面后,你可以直接通过拖拽鼠标复现上述功能。
左侧的 <code>Node 几乎可以满足你所有的爬虫或自动化需求,这些节点与 Puppeteer API 高度一致。
你可以通过连接这些节点来校准执行顺序,就像执行 JavaScript 异步代码一样。如果你了解 Puppeteer,你可以快速上手 Nstbrowser RPA 功能,它就是你所见即所得。
步骤 3. 每个 <code>Node 都可以单独配置,配置的信息几乎与 Puppeteer 的配置相对应。
a. 鼠标移动
b. 点击按钮
c. 输入
d. 键盘按键
e. 等待响应
f. 截图
此外,Nstbrowser RPA 还有更多常见和独特的节点。你可以通过简单的拖拽完成常见的爬虫操作。
设置 HTTP Headers 以避免机器人检测
HTTP headers 是在客户端(浏览器)和服务器之间交换的附加信息。它们包含请求和响应的元数据,例如内容类型、用户代理、语言设置等。
常见的 HTTP headers 包括:
<code>User-Agent:标识客户端应用程序类型、操作系统、软件版本及其他信息。Accept-Language
:指示客户端可以理解的语言及其优先级。Referer
:指示请求的来源页面。
通过修改这些 headers,你可以将自己伪装成不同的浏览器或操作系统,从而降低被检测为机器人的风险。
在使用 Puppeteer 时,你可以使用 page.setExtraHTTPHeaders
方法在跳转到网页之前设置 headers:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
// 设置自定义 HTTP headers
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://www.google.com',
'MyHeader': 'hello puppeteer'
});
await page.goto('https://www.httpbin.org/headers');
但如果你想修改 User-Agent,则不能使用上述方法。因为浏览器中的 User-Agent 有一个默认值。如果你确实想更改它,可以使用 page.setUserAgent
。
import puppeteer from "puppeteer";
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.98 Safari/537.36');
await page.goto('https://example.com/');
const navigator = await page.evaluate(_ => window.navigator.userAgent)
const platform = await page.evaluate(_ => window.navigator.platform)
console.log('userAgent: ', navigator);
console.log('platform: ', platform);
await browser.close();
但这一步还不够。从上面打印的信息来看,<code>paltform 仍然设置为 win32,并没有被真正修改。
大多数网站通过 window.navigator
进行检测。因此有必要对 navigator 进行深入修改。在使用 page.goto
之前,我们可以在 page.evaluateOnNewDocument
中深入修改 navigator。
下面是 page.evaluateOnNewDocument
和 page.evaluate
之间区别的简要说明:
如果你需要修改浏览器环境或在每个页面加载前执行一些操作,请使用 evaluateOnNewDocument
。如果你只需要与当前加载的页面进行交互或提取数据,请使用 evaluate
。
await page.evaluateOnNewDocument(() => {
Object.defineProperties(navigator, {
platform: {
get: () => 'Mac'
},
});
});
结论
本文中的每一行都在描述最详细的指南,涵盖了以下内容:
什么是 headlesschrome?什么是 Puppeteer?如何使用 headlesschrome 进行网页爬取?
想轻松进行网页爬取和自动化操作吗?Nstbrowser RPA 帮助你简化所有任务。
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。