news 2026/10/20 12:27:15

Puppeteer实战教程:无头浏览器自动化从入门到数据采集稳定运行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Puppeteer实战教程:无头浏览器自动化从入门到数据采集稳定运行

1. 先搞清楚Puppeteer在自动化场景里的位置

如果你跟我一样,做过一段时间的Web开发或者数据采集,一定遇到过这样的场景:明明功能上线前测试用例全过了,结果某个动态渲染的表格在用户浏览器里就是不出数据;或者你需要定时去某个后台系统里下载报表,每天重复几十次一模一样的人工操作。我最早接触Puppeteer,就是因为被这类“浏览器里才能完成、但手工操作太费人”的活儿烦透了。

Puppeteer是Node.js生态里一个非常成熟的无头浏览器自动化库,由Chrome官方团队维护,专门用来通过DevTools协议控制Chromium或Chrome浏览器。简单说,你平时在浏览器里能做的所有事情——打开网页、点击按钮、填写表单、滚动页面、截屏、下载文件、抓取接口数据——它都可以用代码的方式驱动浏览器自动完成。区别只是你不再需要肉眼盯着屏幕,而是把操作逻辑写成脚本,让它自己去执行。

很多人会把它跟Selenium、Playwright放在一起比较。我实际用下来的感受是:Puppeteer最大的优势在于和Chromium的契合度极高,装好之后几乎开箱即用,API设计也更贴近前端开发者的直觉。Selenium的优势是跨浏览器支持,但配置成本偏高;Playwright在并发场景下更现代,不过如果你只是需要稳定地操作Chromium完成自动化任务,Puppeteer完全够用,而且资料多、踩坑方案也容易搜到。

这篇内容适合这几类人:第一,刚接触无头浏览器自动化,想用最少的时间跑通一个完整方案;第二,需要定期从网页提取数据、生成截图或PDF的运维和数据处理人员;第三,前端开发者想用自动化代替一部分手工回归测试。我会把实际使用中的思路、代码片段和踩坑记录都写出来,尽量让新手能直接照着跑,也让用过一段时间的人能补上一些容易被忽略的细节。

2. 安装与初始化:看起来简单,细节不少

Puppeteer的安装本身不复杂,但如果你第一次装,有几个点容易卡住,我先把整个流程讲清楚。

2.1 安装命令与版本选择

新建一个Node项目,然后执行:

npm init -y npm install puppeteer

默认安装的是最新版本。这里要留意一点:Puppeteer在安装时默认会下载一个对应版本的Chromium浏览器到本地缓存目录,下载体积大概一百多兆。如果你所在的网络环境访问国外下载源比较慢,安装过程可能会卡在Downloading Chromium这一步很久,甚至直接失败。

解决方式有两种。第一种是设置镜像源,用国内npm镜像配合环境变量指定Chromium的下载地址;第二种是跳过浏览器下载,直接使用本机已有的Chrome或Chromium。我的做法更偏向第二种:

npm install puppeteer --ignore-scripts

然后用puppeteer-core配合本机浏览器路径来启动。puppeteer-core不包含浏览器下载逻辑,体积更小,启动时传入executablePath参数即可:

const puppeteer = require('puppeteer-core'); const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true });

版本匹配是另外一个容易出问题的地方。Puppeteer的API会跟随Chromium版本做调整,如果你用的Puppeteer版本很新,但指定的本机Chrome版本很老,偶尔会遇到某些API行为不一致的情况。建议优先使用Puppeteer自带的Chromium,最省心;只有无法下载时才用puppeteer-core方案。

2.2 启动参数的取舍:headless不是越“无头”越好

初始化浏览器时,最常用的参数是headless。早期版本的Puppeteer里,headless: true代表无头模式,完全不显示浏览器窗口,适合服务器环境;headless: false会弹出真实浏览器窗口,方便调试。

新版本又引入了headless: 'new'模式,也就是新的无头模式。它和旧无头模式的核心区别在于:旧无头模式在部分页面逻辑、字体渲染、WebGL支持上都有差异,偶尔会出现“无头模式下拿不到的元素,有头模式没问题”这种诡异情况。新无头模式更接近完整浏览器运行状态,兼容性好不少。我现在的习惯是,本地调试一律用headless: false,部署到服务器上再改成新无头模式,避免两种环境的运行差异影响结果。

还有一个高频参数是args,用来向Chromium传递命令行参数。最常用的是--no-sandbox和--disable-setuid-sandbox,在Linux服务器环境尤其重要,否则很容易因为沙箱权限问题导致启动失败。后面我还会提到一些性能向的参数,这里先记住这两个是基础。

2.3 写一个最小启动流程验证环境

安装完先别急着写复杂逻辑,跑一个最小示例确认环境可用:

const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage(); await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 30000 }); console.log('页面标题:', await page.title()); await browser.close(); })();

这个示例做了四件事:启动浏览器、打开新页面、访问目标地址、关闭浏览器。waitUntil: 'networkidle2'表示等待网络请求基本空闲后再继续,比默认的load事件更适合处理现代动态页面,因为很多页面在load之后还会发起异步请求渲染数据。如果这个脚本能正常打印出页面标题,说明你的Puppeteer环境已经通了,后续可以放心往里加逻辑。

3. 核心页面操作:点击、输入、等待与取数

跑通环境之后,接下来是Puppeteer最常用的几类操作。很多人写自动化脚本,最头疼的就是“怎么让脚本像真人一样操作页面”,本质上就是这几件事的组合。

3.1 浏览器实例和页面实例的关系

先理清一个概念。browser是浏览器实例,page是标签页实例。一个浏览器实例可以同时开多个页面,各页面之间的Cookie、缓存默认是隔离的,除非你用browser.createBrowserContext()创建共享上下文。实际项目中,建议一个任务对应一个新的page,用完就关闭,而不是反复复用同一个页面,这样能避免状态残留带来的莫名其妙的问题。

打开新页面后,设置视口尺寸是容易被忽略的一步:

await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });

视口尺寸会影响页面的响应式布局,以及截图的分辨率。如果你要截全屏图,宽度建议设置成目标网站的PC端标准宽度,比如1280或1920;宽度设太小,页面可能直接进入移动端布局,截出来的图完全不是你想要的。

3.2 元素定位与交互操作:点击、输入、下拉选择

定位元素主要用四种方式:CSS选择器、XPath、文本匹配、以及Puppeteer独有的page.$和page.$$系列方法。CSS选择器最常用,性能也最好:

// 点击按钮 await page.click('#submit-btn'); // 输入文本 await page.type('#username', 'test_user'); // 清空输入框再输入 await page.$eval('#search-input', el => el.value = ''); await page.type('#search-input', 'Puppeteer 使用');

我自己的经验是:能用CSS选择器就尽量不用XPath,除非目标元素没有稳定的class或id,只能靠层级关系定位。XPath可读性差,而且在页面结构变化时更容易断裂。

下拉框操作稍微特殊。原生<select>可以直接用page.select():

await page.select('#province', 'zhejiang');

但很多现代前端框架的自定义下拉框不是真正的select元素,而是用div模拟的。这时候得先点击下拉框,再等待选项列表出现,然后点击对应选项。这种场景非常考验等待能力,千万不能用await page.click()一把梭,得配合等待函数一起用。

3.3 等待机制:为什么不能乱用固定延时

新手最容易犯的错误是到处写await page.waitForTimeout(3000),固定等三秒。这种做法在脚本量小的时候能用,但只要网络稍微波动或者页面渲染变慢,整个脚本就会变得极其脆弱——等短了元素没出来,等长了浪费时间。

健壮的做法是使用Puppeteer的显式等待方法:

// 等待元素出现在DOM里 await page.waitForSelector('#data-table .row-item', { timeout: 10000 }); // 等待元素可见且可交互 await page.waitForSelector('#login-btn', { visible: true }); // 等待某个函数返回真值 await page.waitForFunction(() => { const el = document.querySelector('#total-count'); return el && el.textContent.includes('已完成'); }, { timeout: 15000 });

waitForSelector是最常用的,它会轮询DOM节点,直到选择器匹配到元素才继续向下执行。waitForFunction更灵活,适合等待页面状态到达某个条件,比如等待某个数字从0变成非0、某个按钮从禁用变成可用。把固定延时改成条件等待后,脚本的稳定性能提升一个档次。

3.4 用page.evaluate在页面上下文里取数

很多页面数据的提取,单纯靠选择器拿不到,因为数据是经过JS计算之后才渲染出来的。这时最好的办法是直接进入页面上下文执行JavaScript,把结果取回来:

const data = await page.evaluate(() => { const rows = document.querySelectorAll('.product-item'); const result = []; rows.forEach(row => { result.push({ title: row.querySelector('.title')?.textContent?.trim(), price: row.querySelector('.price')?.textContent?.trim(), link: row.querySelector('a')?.href }); }); return result; }); console.log(data);

注意一点:page.evaluate里的回调函数运行在浏览器环境,无法直接引用Node作用域内的变量。如果需要传参,得用第二个参数传进去:

const keyword = '无线键盘'; const results = await page.evaluate((kw) => { return document.querySelectorAll('.search-result').length; }, keyword);

这个设计初看有点绕,但本质上是为了隔离两个执行环境,避免变量污染。理解了这一点,很多诡异报错就能解释通了。

还有一个很实用的API是page.waitForResponse(),它可以拦截并等待某个特定接口的响应。比如页面通过XHR请求拉取数据,你可以不关心按钮怎么点,直接等接口返回:

const [response] = await Promise.all([ page.waitForResponse(res => res.url().includes('/api/getList') && res.status() === 200), page.click('#load-more') ]); const responseData = await response.json();

Promise.all保证了同时发起等待和点击,而不是先点击再等待,这个模式在异步交互场景中很常用。

4. 实战示例:批量采集列表页数据

讲完基础能力,我来写一个完整的实战案例:从某个列表页抓取商品信息,包括标题、价格、链接,并自动翻页,翻完第5页后停止,最后把数据保存成JSON文件。

4.1 场景设定与采集策略

假设目标页面的结构是:每个商品卡片是一个.product-item,卡片里有.title、.price和a标签;页面底部有一个“下一页”按钮,选择器是a.next-page。整体页面是传统的服务端渲染加简单分页,不涉及复杂的懒加载。

采集策略上,我先确定三件事:需要哪些字段、需要多少页、以什么顺序保存。字段就是标题、价格、链接;页数是5页;顺序按采集先后排列。对于简单场景,顺序不重要,但如果涉及去重或增量更新,建议给每条数据加一个crawledAt时间戳,方便后期排查。

4.2 完整实现代码

const puppeteer = require('puppeteer'); const fs = require('fs'); (async () => { const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto('https://example.com/list', { waitUntil: 'networkidle2', timeout: 60000 }); const allItems = []; const maxPages = 5; for (let pageNo = 1; pageNo <= maxPages; pageNo++) { // 等待商品卡片加载完成 await page.waitForSelector('.product-item', { timeout: 15000 }); const itemsOnPage = await page.evaluate(() => { const cards = document.querySelectorAll('.product-item'); const pageItems = []; cards.forEach(card => { const titleEl = card.querySelector('.title'); const priceEl = card.querySelector('.price'); const linkEl = card.querySelector('a'); if (titleEl && priceEl && linkEl) { pageItems.push({ title: titleEl.textContent.trim(), price: priceEl.textContent.trim(), url: linkEl.href }); } }); return pageItems; }); allItems.push(...itemsOnPage); console.log(`第 ${pageNo} 页采集完成,获得 ${itemsOnPage.length} 条数据`); // 如果不是最后一页,点击下一页 if (pageNo < maxPages) { const nextButton = await page.$('a.next-page'); if (!nextButton) { console.log('未找到下一页按钮,提前结束'); break; } await Promise.all([ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 }), page.click('a.next-page') ]); } } fs.writeFileSync( 'products.json', JSON.stringify(allItems, null, 2), 'utf-8' ); console.log(`采集完成,共 ${allItems.length} 条数据,已保存到 products.json`); await browser.close(); })().catch(err => { console.error('采集过程中出现错误:', err); process.exit(1); });

这里有两个细节值得说一说。第一个是Promise.all包住waitForNavigation和page.click,这是为了防止点击后页面开始跳转、网络请求已经变化,而你还没来得及注册等待逻辑,导致等待超时。第二个是catch里直接process.exit(1),别小看这个处理:如果脚本运行中抛出异常但不退出,浏览器进程会留在后台占用内存,长时间跑容易把资源耗尽。

4.3 分页处理的变体情况

上面假设的是传统分页。但很多现代网站的翻页不是整页刷新,而是按钮点击后局部更新内容、URL可能带hash变化。这时候waitForNavigation往往无效,因为它等待的是页面级别的跳转事件。对于这种场景,可以改成分页数据变化监听:

// 记录当前第一张卡片的文本,点击后等待该文本变化 const firstCardTextBefore = await page.$eval('.product-item .title', el => el.textContent); await page.click('a.next-page'); await page.waitForFunction((prevText) => { const firstCard = document.querySelector('.product-item .title'); return firstCard && firstCard.textContent !== prevText; }, {}, firstCardTextBefore);

通过等待页面内容发生变化,而不是等待导航事件,很多局部刷新的分页场景都能搞定。

另外还有无限滚动模式:页面没有下一页按钮,而是滚动到底部自动加载更多。处理方式是用page.evaluate不断把滚动条滚到底,同时监听总卡片数是否变化:

let previousCount = 0; let stableRounds = 0; while (stableRounds < 3) { previousCount = await page.$$eval('.product-item', els => els.length); await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForFunction( (count) => document.querySelectorAll('.product-item').length > count, { timeout: 5000 }, previousCount ).catch(() => stableRounds++); }

当连续三轮滚动后数量都不再变化,说明已经触底。这种“轮询数量直到稳定”的思路,在处理无限滚动时比单纯等待固定时间靠谱得多。

5. 截图与PDF导出:被很多人低估的硬需求

除了数据采集,Puppeteer另一个高频用途是生成页面截图和PDF。这块看着简单,实际做起来有不少细节,尤其是“截出来的图不对”“PDF中文乱码”“页面太长截不全”这些问题,几乎每个人都会踩到一遍。

5.1 全屏截图与元素截图

全屏截图是最常见的需求。直接用page.screenshot,但默认情况下它只截当前视口范围内的区域,页面超出视口的部分不会包含。要截全屏,必须传递fullPage: true:

await page.screenshot({ path: 'fullpage.png', fullPage: true, type: 'png' });

如果你只想截某个特定元素,比如一个图表组件,可以用element.screenshot():

const element = await page.$('#chart-container'); await element.screenshot({ path: 'chart.png' });

元素截图会自动按元素尺寸截取,非常适合做报表和告警通知里的配图。

还有一个坑:截图比例和视觉差异。页面在无头模式下默认字体渲染和本地浏览器可能不同,截出来的图偶尔会有几像素的偏移。如果你要的是像素级还原,建议在启动参数里加上--force-color-profile=srgb,同时把deviceScaleFactor设置为2,截出来的图在高分屏上会更细腻。

5.2 定时截图监控页面状态

我做过一个小工具,用Puppeteer每半小时截一次内部监控页面的截图,然后对比前后两张图的哈希值,如果变化超过阈值就说明页面出问题了。截图监控的核心思路很简单:定时任务里启动浏览器截图,用pixelmatch之类的库做图片对比。

这里要提醒的是:每次截图都用同一个浏览器实例会造成内存堆积,正确做法是截完就关掉页面或整个浏览器。另外,截图前的等待条件要重新确认,很多页面首次打开和后续刷新时的加载时间差异很大,建议统一用waitUntil: 'networkidle2',必要时再加一个waitForTimeout(2000)让动画完全结束。

5.3 PDF导出与样式配合

把网页转成PDF,Puppeteer提供了原生支持:

await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true, margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' }, displayHeaderFooter: true, headerTemplate: '<div style="font-size:10px; width:100%; text-align:center;">内部报表</div>', footerTemplate: '<div style="font-size:10px; width:100%; text-align:center;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>' });

有两个关键点。第一,printBackground: true必须设置,否则背景色和背景图片会被丢弃,PDF看起来像白底无样式页面。第二,中文字体必须提前确认系统里已经装了对应字体,否则PDF里的中文会变成方块乱码。在Linux服务器上,经常要执行fc-list :lang=zh检查中文字体,没有就安装fonts-noto-cjk之类的字体包。这个坑不提前处理,等生成几十个PDF才发现全部乱码,返工成本极高。

还有一个容易被忽略的点:page.pdf()只在无头模式下可用。如果你在调试阶段习惯用headless: false,直接调用pdf会报错。这是我实际碰过的坑之一,解决方式很简单,要么把调试逻辑和PDF生成逻辑分开,要么统一在无头模式下运行。

6. 稳定运行与性能控制的几个关键点

写一次性脚本很容易,难的是写一个能稳定跑几小时甚至几天的自动化任务。我自己的经历是,脚本刚开始运行一切正常,跑了一段时间后要么速度越来越慢,要么莫名其妙崩溃。这些问题大多出在浏览器实例管理、资源加载控制和并发调度上。

6.1 浏览器实例的生命周期管理

新手最喜欢在脚本开头创建一个浏览器实例,然后全局复用它,最后在脚本结束时关闭。这种做法在短任务里没问题,但长时间运行时,页面开得越多,浏览器的内存占用就越高,最终可能导致OOM崩溃。

我的建议是:按批次管理浏览器生命周期。举个例子,如果任务是处理一万条URL,不要用一个浏览器从头跑到尾,而是每处理完一百条就关闭浏览器,重新启动一个。虽然重新启动浏览器有开销,但换来的内存释放是值得的。实际测试中,这种策略能把长时间任务的稳定性提升很多。

如果确实需要复用一个浏览器实例,至少要做到:每个页面任务完成后立即page.close(),并且定期调用browser.process()检查子进程状态。必要的时候还可以定期重启整个浏览器实例。

6.2 请求拦截与资源过滤

页面里加载的图片、字体、第三方脚本,对数据提取来说很多时候是噪音,还会显著拖慢加载速度。用page.setRequestInterception()可以把这些资源请求直接拦掉:

await page.setRequestInterception(true); page.on('request', (req) => { const resourceType = req.resourceType(); const url = req.url(); if (resourceType === 'image' || resourceType === 'media') { req.abort(); } else if (url.includes('/api/analytics') || url.includes('/ad/')) { req.abort(); } else { req.continue(); } });

这种操作能让页面加载速度提升明显,尤其是那些引用了大量统计脚本和广告位的网站。不过要注意,拦截不能过于激进,有些网页的图片懒加载依赖图片请求触发后续逻辑,全部拦截可能导致页面卡在加载中。稳妥的做法是过滤掉统计类API和广告类域名,保留业务图片。

6.3 并发任务与Puppeteer的并发模型

Puppeteer的Page实例可以并行操作多个页面,但受限于Chromium本身是单进程多线程模型,并发过高会导致内存和CPU冲突。一般来说,单浏览器实例同时开三到五个页面比较合理,超过这个数量后收益下降,且稳定性会变差。

如果任务量很大,建议用p-limit之类的库控制并发数,而不是一股脑一次性开几十个页面。我的经验是,对于抓取类任务,并发3个页面时综合效率最高;对于截图任务,因为涉及大量内存和GPU渲染,并发2个更稳妥。这个数值可以根据实际机器配置再调,关键是不要看到并发就用满,给浏览器留出余量。

6.4 超时与重试策略

Puppeteer提供了一系列超时参数,默认是30秒。长时间自动化任务里,网络波动、页面报错都可能让操作卡住,所以一定要给关键操作统一设置超时时间,并配上重试逻辑:

async function clickWithRetry(page, selector, retries = 3) { for (let i = 0; i < retries; i++) { try { await page.click(selector, { timeout: 10000 }); return; } catch (err) { if (i === retries - 1) throw err; await page.waitForTimeout(2000); } } }

重试的核心原则是:每次重试前要重新确认页面状态,而不是盲目再点一次。比如点击后发现下一个步骤要依赖的元素已经出现,就没必要重试了。在一些复杂流程里,可以把“操作是否成功”的判定条件作为重试依据。

7. 实测中踩过的坑:选择器失效、超时、内存飙升

最后分享几个我在实际项目中遇到的典型问题。这些坑在网上都能搜到零散讨论,但真正把它们系统梳理一遍,还是能帮你省下不少排查时间。

7.1 选择器在无头模式下失效

有段时间我写了一个定时任务,在无头模式下运行正常,但同一套选择器部署到服务器后,page.click()总是报“Node not found”。查了半天,发现问题是运维同学给服务器配置的Chromium版本是精简版,编解码库不全,页面里某些依赖特定编码的字体和JS文件加载异常,导致DOM结构还没完整渲染。

这类问题很难直接从报错里看出来。我的排查方法是:在无头模式下加--dump-dom参数,把最终渲染后的DOM输出到文件,和本地有头模式渲染的结果做对比,很快就能发现哪些节点缺失。如果是字体和编解码问题,可以优先执行fc-cache刷新字体缓存,或者安装chromium-browser的完整依赖包。

7.2 waitForSelector超时:动态页面加载慢

另一个高频问题就是waitForSelector频繁超时。超时的原因通常不是选择器写错了,而是页面数据加载依赖多个接口,第一个接口返回后DOM节点已经出现,但节点内容还是空的。这时候waitForSelector只检查节点的存在,不检查内容是否填充完整。

解决办法是配合waitForFunction做二次判断,比如等某个字段的文案从“加载中”变成真实数据,或者等子节点数量满足预期。我在代码里写过一段通用函数,专门用来等待“某个元素出现且其中包含指定文本”:

async function waitForElementWithText(page, selector, text, timeout = 15000) { await page.waitForFunction( (sel, expectedText) => { const el = document.querySelector(sel); return el && el.textContent.includes(expectedText); }, { timeout }, selector, text ); }

用这种方式代替单一的waitForSelector,能过滤掉很多“节点存在但还没渲染完”的假成功。

7.3 浏览器启动失败:依赖库缺失

在干净Linux服务器上部署Puppeteer时,最常遇到的报错是启动时提示缺少共享库,比如libX11-xcb.so.1、libxkbcommon.so.0这类。原因很简单:无头浏览器虽然是“无头”的,但Chromium底层仍然依赖一堆图形界面相关的系统库。

解决方式是用官方文档推荐的依赖安装命令,一次性装齐所有库。这几个库平时看不出存在感,一旦缺失,启动阶段就直接失败。另外还要确认系统的/dev/shm空间是否够用,默认情况下Chromium会把共享内存写到/dev/shm,容器环境里这个目录通常偏小,建议在启动参数里加上--disable-dev-shm-usage,让浏览器改用磁盘存储临时数据。

7.4 内存飙升与僵尸进程

长时间运行的Puppeteer脚本,最容易出现的就是内存持续增长。最开始我以为是页面泄漏,逐个排查也没发现问题。后来才发现是浏览器子进程没有被正确清理——主脚本因为异常退出时,Chromium子进程成了孤儿进程,继续占用系统资源。

处理方式有两个层面。第一个是在代码层面,始终在finally里调用browser.close();第二个是运维层面,定时检查有没有残留的chrome/chromium进程,用固定的命令行特征把它们找出来杀掉。我一般会写一个简单的定时清理脚本,防止项目里其他同事跑自动化任务时忘了关浏览器。

7.5 登录态与Cookie的持久化

需要登录的页面,每次跑脚本都走一遍登录流程显然不现实。Puppeteer支持把当前页面的Cookie保存下来,下次启动时直接加载:

const cookies = await page.cookies(); fs.writeFileSync('cookies.json', JSON.stringify(cookies)); const savedCookies = JSON.parse(fs.readFileSync('cookies.json', 'utf-8')); await page.setCookie(...savedCookies);

这里有两个注意事项:一是Cookie有有效期,过期的Cookie加载后依然会被拦截;二是部分网站会校验User-Agent,如果保存Cookie时的UA和下次启动时的UA不一致,登录态可能失效。最好在启动时用page.setUserAgent()固定UA,再配合加载Cookie,稳定性会高很多。

我个人在实际项目中的体会是:Puppeteer的能力边界比大多数人想象中要宽,但真正决定一个自动化脚本好不好用的,往往不是某个API用得多么花哨,而是对浏览器运行机制、页面渲染时机和资源管理的理解程度。比如等待机制的选择、请求拦截的范围、生命周期管理这几个维度,每一点都值得在真实场景里反复调优。上面记录的这些示例和踩坑经验,基本覆盖了我从入门到现在最常见的应用场景。如果你在跑类似任务时又遇到了新的怪问题,不妨先从“页面状态是否真的到达了预期条件”这个角度排查,大部分疑难杂症都能找到突破口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 12:26:59

图像描述中的关系感知:让AI真正理解物体交互

图像描述&#xff08;Image Captioning&#xff09;这个方向&#xff0c;业界习惯叫“看图说话”&#xff0c;听起来挺接地气&#xff0c;但真正动手做过的人都知道&#xff0c;想让它说准、说人话&#xff0c;其实相当折腾。最近我注意到一个很有意思的工作——来自欧洲某理工…

作者头像 李华
网站建设 2026/10/20 12:26:33

9.7K Star开源项目:零成本AI生成产品宣传片全流程指南

9.7K Star&#xff0c;开源&#xff0c;零成本&#xff0c;AI 帮你拍产品宣传片——这几个关键词放一起&#xff0c;足够让我毫不犹豫地开始折腾。做电商、搞独立开发、接私单做设计的朋友应该都有同感&#xff1a;产品宣传片这东西&#xff0c;贵得离谱。传统流程里&#xff0…

作者头像 李华
网站建设 2026/10/20 12:26:24

计算机网络期末冲刺:CRC、香农公式与TCP拥塞控制高频计算题精讲

简介&#xff1a;这份资源是面向高校学生与备考人员的《计算机网络》期末复习电子版资料&#xff0c;依据谢希仁第8版教材整理&#xff0c;适合需要系统梳理考点、快速回顾知识框架的学习者。内容覆盖互联网连通性与共享两大特点、边缘与核心部分结构、C/S与P2P通信方式&#x…

作者头像 李华
网站建设 2026/10/20 12:26:07

水风光互补调度与净现值计算:MATLAB资源包实战指南

简介&#xff1a;本资源面向计算机、电子信息工程、数学等专业的大学生及新能源调度方向研究者&#xff0c;提供水风光互补调度系统的净现值计算与不同网格装机容量、出力系数影响分析的完整MATLAB实现方案&#xff0c;可用于课程设计、期末大作业与毕业设计。压缩包共18个文件…

作者头像 李华
网站建设 2026/10/20 12:26:04

用one-shot提示词实现长文本财报分析的稳定输出

假设你手头刚拿到一份一百多页的上市公司年度报告&#xff0c;需要快速梳理出这家公司过去一年的核心财务表现、经营亮点和潜在风险。最直接的想法是把整份年报丢给一个支持超长上下文窗口的大语言模型&#xff08;LLM&#xff09;&#xff0c;让它直接给结论。这个思路我试过&…

作者头像 李华
网站建设 2026/10/20 12:25:13

DHCP四步握手原理与实战排错:从自动获取IP到租期管理

1. 为什么你家路由器一插网线&#xff0c;手机电脑就自动上网&#xff1f;——DHCP不是“魔法”&#xff0c;是精密协作的通信契约你有没有注意过这样一个细节&#xff1a;把新买的笔记本电脑连上家里Wi-Fi&#xff0c;几秒钟后右下角就弹出“已连接到Internet”&#xff1b;用…

作者头像 李华