
用 Crawlee JSDOM 替代浏览器抓取认证数据以 TikTok 创作中心为实战案例【免费下载链接】crawleeCrawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Puppeteer, Playwright, Cheerio, JSDOM, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee本文介绍一种介于 Cheerio 与浏览器自动化之间的第三路径借助 Crawlee 生态中的 jsdom 能力在不启动浏览器的情况下执行页面 JavaScript拦截并提取XMLHttpRequest请求中的认证头如anonymous-user-id、timestamp、user-sign随后把这些认证数据注入CheerioCrawler完成 API 直连抓取。读完本文你将掌握JSDOM 解析 会话池注入 Cheerio 抓取的完整实战方案并能迁移到任何由 Web 应用生成认证凭据的站点。JSDOM 方案以轻量 DOM 模拟替代完整浏览器来获取认证数据场景背景认证数据为什么难拿作为抓取开发者有时我们需要提取认证数据例如临时密钥才能完成任务。问题在于这些数据并不总是直接出现在 HTML 或 XHR 网络请求里有时它是被前端 JavaScript 动态计算出来的。面对这种情况通常只有两条路逆向计算逻辑——反混淆脚本耗时极长维护成本高运行生成它的 JavaScript——最常见的手段是开一个真实浏览器但浏览器进程占用大量 CPU 与内存对于只是想要几个请求头这样的小任务来说成本过高。Crawlee 原本支持浏览器抓取Puppeteer/Playwright与 Cheerio 抓取并行运行但并行维护两种运行环境既复杂又昂贵。JSDOM 提供了一个折中方案运行页面 JavaScript 所需资源远低于浏览器仅略高于 Cheerio。本文讨论的就是 Apify 的一个 Actor 中实际采用的方案从 **TikTok 广告创作中心Creative Center**获取由浏览器 Web 应用生成的认证数据不运行浏览器而是使用 JSDOM 完成。第一步分析目标网站目标页面为 TikTok 广告创作中心的热门话题榜https://ads.tiktok.com/business/creativecenter/inspiration/popular/hashtag/pc/en页面上会展示一批话题标签hashtag包含实时排名、发帖数量、趋势图、创作者与数据分析并支持按行业industry筛选、设置时间周期period、以及用复选框过滤是否新晋 Top 100isNewToTop100。TikTok 创作中心热门话题页面目标是从中提取 Top 100 话题及排名数据我们的目标很明确按给定筛选条件提取 Top 100 话题。先分析两条候选技术路线CheerioCrawler速度快但只处理静态 HTML。创作中心是典型 Web 应用数据来自 API首屏 HTML 里只有少量话题无法拿到全部 100 条因此不适用浏览器自动化Puppeteer/Playwright可以拿到全部数据但按以往经验为这么小的任务启动浏览器杀鸡用牛刀耗时过长。于是诞生了第三条路直接调用其背后的数据 API。API 端点如下https://ads.tiktok.com/creative_radar_api/v1/popular_trend/hashtag/list但在调用前请求必须带上由前端脚本计算出的认证头。这正是 JSDOM 的用武之地。JSDOM 方案总览本方案的核心思路是先通过普通 HTTP 请求拿到创作中心首页 HTML用JSDOM加载该 HTML 并执行其中的 JavaScriptrunScripts: dangerously在页面脚本发起XMLHttpRequest时通过重写setRequestHeader拦截并捕获认证请求头把捕获到的请求头封装进 Crawlee 的Session交给CheerioCrawler使用由CheerioCrawler直接调用数据 API完成抓取与分页。关于方法归属该方案由 Apify 的 Web 自动化工程师 Alexey Udovydchenko 开发本文完整复现其工程实现。为什么 Crawlee 原生支持 JSDOM在深入代码前要说明Crawlee 原生提供JSDOMCrawler它使用普通 HTTP 请求并行抓取网页并用 jsdom 的 DOM 实现解析页面基于原始 HTTP 请求下载页面在带宽消耗上非常高效。从源码看JSDOMCrawler继承自DOMCrawler其构造函数接收两个特有选项jsdom-crawler.tsrunScripts是否下载并运行页面脚本hideInternalConsole是否抑制 jsdom 内部控制台日志。其底层解析器jsdomParser在runScripts: true时会以dangerously模式创建 JSDOM 实例注入 UA 为 Chrome 107 的ResourceLoader并监听window.load事件带 10 秒超时兜底等待页面脚本执行完毕解析完成后还会为window补齐matchMedia、createRange等 jsdom 缺失的 API 桩避免脚本中途报错。本文案例没有直接用JSDOMCrawler的 requestHandler 抓数据而是只借用 JSDOM 的脚本执行能力生成认证头再把这些头交给CheerioCrawler——两种用法互为补充你可以按需选择。第二步构造 API 起始 URL首先编写createStartUrls把用户输入days、country、resultsLimit、industry、isNewToTop100映射为 API 查询参数并生成起始请求export const createStartUrls (input) { const { days 7, country , resultsLimit 100, industry , isNewToTop100, } input; const filterBy isNewToTop100 ? new_on_board : ; return [ { url: https://ads.tiktok.com/creative_radar_api/v1/popular_trend/hashtag/list?page1limit50period${days}country_code${country}filter_by${filterBy}sort_bypopularindustry_id${industry}, headers: { // required headers }, userData: { resultsLimit }, }, ]; };要点period默认7近 7 天resultsLimit默认100filter_by根据isNewToTop100决定是new_on_board还是空串单次请求limit50因此 100 条数据需要两次分页请求userData中携带resultsLimit供后续 requestHandler 做截断与分页判断。此时 URL 已经就绪但没有认证头是调不通的。接下来解决头的问题。第三步用会话池创建带认证头的 Session我们创建createSessionFunction由 Crawlee 的SessionPool在需要新会话时调用。它借助proxyConfiguration获取代理 URL先请求创作中心首页拿到 HTML再交给getApiUrlWithVerificationToken生成认证头export const createSessionFunction async ( sessionPool, proxyConfiguration, ) { const proxyUrl await proxyConfiguration.newUrl(Math.random().toString()); const url https://ads.tiktok.com/business/creativecenter/inspiration/popular/hashtag/pad/en; // need url with data to generate token const response await gotScraping({ url, proxyUrl }); const headers await getApiUrlWithVerificationToken( response.body.toString(), url, ); if (!headers) { throw new Error(Token generation blocked); } log.info(Generated API verification headers, Object.values(headers)); return new Session({ userData: { headers, }, sessionPool, }); };这里有两个 Crawlee 基础设施值得说明SessionPoolCrawlee 会话池的核心职责是管理一组可复用的Sessionsession_pool.ts。其中createSessionFunction是用户自定义的会话工厂maxPoolSize决定池内同时存活的会话数量上限默认 1000。本文案例把maxPoolSize设为 1是因为我们只需要一个带认证头的会话proxyConfiguration负责代理 URL 的生成与轮换newUrl()每次返回一个新的代理地址配合Math.random().toString()作为会话标识保证请求走不同出口 IP降低被风控的概率。若认证头生成失败函数直接抛出Token generation blocked让会话池丢弃该会话并重试。第四步JSDOM 内执行脚本并拦截认证头这是整个方案的灵魂函数getApiUrlWithVerificationTokenconst getApiUrlWithVerificationToken async (body, url) { log.info(Getting API session); const virtualConsole new VirtualConsole(); const { window } new JSDOM(body, { url, contentType: text/html, runScripts: dangerously, resources: usable || new CustomResourceLoader(), // ^ usable faster than custom and works without canvas pretendToBeVisual: false, virtualConsole, }); virtualConsole.on(error, () { // ignore errors cause by fake XMLHttpRequest }); const apiHeaderKeys [anonymous-user-id, timestamp, user-sign]; const apiValues {}; let retries 10; // api calls made outside of fetch, hack below is to get URL without actual call window.XMLHttpRequest.prototype.setRequestHeader (name, value) { if (apiHeaderKeys.includes(name)) { apiValues[name] value; } if (Object.values(apiValues).length apiHeaderKeys.length) { retries 0; } }; window.XMLHttpRequest.prototype.open (method, urlToOpen) { if ( [static, scontent].find((x) urlToOpen.startsWith(https://${x}), ) ) log.debug(urlToOpen, urlToOpen); }; do { await sleep(4000); retries--; } while (retries 0); await window.close(); return apiValues; };逐段拆解其原理new JSDOM(body, {...})用runScripts: dangerously让 jsdom 真正执行页面内联脚本。该模式的语义与JSDOMCrawler的runScripts选项一致源码中同样映射为dangerously见 jsdom-parser.tsresources: usable允许 jsdom 加载页面引用的外部资源CSS/JS/图片。相比自定义ResourceLoaderusable更快且在无 canvas 环境下也能工作注释里也明确写了这一取舍virtualConsolejsdom 内部 console 消息转发到这里。由于我们会故意用假XMLHttpRequest触发页面脚本报错这里把所有error事件静默忽略重写XMLHttpRequest.prototype.setRequestHeader这是核心 hack。页面脚本发起 API 请求前必然调用setRequestHeader写入认证头我们重写原型方法把目标头的键值捕获进apiValues。当三个头anonymous-user-id、timestamp、user-sign全部捕获到时立即把retries置 0 结束等待重写XMLHttpRequest.prototype.open页面在真实调用 API 前通常还会请求静态资源https://static...、https://scontent...我们只对这些 URL 打 debug 日志——既可用于排查又不会真的发起外部请求因为open被我们替换了底层真实请求不会发出从而避免暴露爬虫行为轮询等待以 4 秒为间隔、最多 10 次重试等待脚本执行完毕拿到全部头后window.close()释放内存并返回。安全提示runScripts: dangerously会执行页面携带的任意脚本仅应在你信任目标站点时使用切勿用于来源不可控的 HTML。第五步把认证头注入 CheerioCrawler主抓取逻辑使用CheerioCrawler。关键在于两处配置sessionPoolOptions.maxPoolSize: 1只保留一个带认证头的会话够用且省内存preNavigationHooks在每次请求发出前把session.userData.headers合并进request.headers。const crawler new CheerioCrawler({ sessionPoolOptions: { maxPoolSize: 1, createSessionFunction: async (sessionPool) createSessionFunction(sessionPool, proxyConfiguration), }, preNavigationHooks: [ (crawlingContext) { const { request, session } crawlingContext; request.headers { ...request.headers, ...session.userData?.headers, }; }, ], proxyConfiguration, });preNavigationHooks是 Crawlee 在导航/请求前同步注入上下文的钩子机制JSDOMCrawler同样支持参见 jsdom-crawler.ts 文档注释。通过它认证头对下游 requestHandler 完全透明——你不需要在每一处手动拼头。第六步requestHandler 与分页处理最后在 requestHandler 中调用 API 并处理分页。代码设计成可扩展为任意次数调用每完成一页若未达到resultsLimit且后端has_more为真就拼接下一页 URL 继续入队async requestHandler(context) { const { log, request, json } context; const { userData } request; const { itemsCounter 0, resultsLimit 0 } userData; if (!json.data) { throw new Error(BLOCKED); } const { data } json; const items data.list; const counter itemsCounter items.length; const dataItems items.slice( 0, resultsLimit counter resultsLimit ? resultsLimit - itemsCounter : undefined, ); await context.pushData(dataItems); const { pagination: { page, total }, } data; log.info( Scraped ${dataItems.length} results out of ${total} from search page ${page}, ); const isResultsLimitNotReached counter Math.min(total, resultsLimit); if (isResultsLimitNotReached data.pagination.has_more) { const nextUrl new URL(request.url); nextUrl.searchParams.set(page, page 1); await crawler.addRequests([ { url: nextUrl.toString(), headers: request.headers, userData: { ...request.userData, itemsCounter: itemsCounter dataItems.length, }, }, ]); } }关键设计点json上下文CheerioCrawler对返回application/json的接口会自动解析出json字段因此可以直接json.data.list取数据BLOCKED抛错一旦json.data缺失说明认证头失效或被风控立即抛错触发 Crawlee 的重试与会话轮换机制itemsCounter累计通过userData在页间传递已抓取条数配合resultsLimit用slice精确截断确保最终恰好拿到 100 条分页拼接new URL(request.url)上直接改page参数并复用request.headers避免重复计算认证头。本例只做了 1 个起始请求 1 个分页请求共 2 次 API 调用但你完全可以按此模式扩展为 N 页只需调整resultsLimit即可。整体代码流程方案整体流程首页 HTML → JSDOM 执行脚本 → 拦截认证头 → 注入 Session → CheerioCrawler 调用 API 并分页整个调用链可概括为createStartUrls 生成 API URL │ ▼ SessionPool.createSessionFunction │ gotScraping 请求首页 HTML ▼ getApiUrlWithVerificationTokenJSDOM 执行脚本、拦截 XHR 头 │ 返回 { anonymous-user-id, timestamp, user-sign } ▼ new Session({ userData: { headers } }) │ preNavigationHooks 注入 headers ▼ CheerioCrawler 调用数据 API → pushData → 分页入队 → 循环直至取满结论三种方案的成本对比本方案为提取认证数据提供了第三条路径方案JavaScript 渲染相对性能资源占用CheerioCrawler不支持基准最快最低浏览器自动化Puppeteer/Playwright支持约为 Cheerio 的 1/10最高JSDOM 方案支持约为 Cheerio 的 1/3 ~ 1/4比浏览器低约 50% RAM按原作者实测数据相比浏览器方案本方案RAM 占用降低约 50%速度提升约2~3 倍浏览器比纯 Cheerio 慢 10 倍而 JSDOM 只慢 3~4 倍。延伸何时直接使用 JSDOMCrawler本文的 hack 手法适合只需要认证头、数据走 API的场景。如果你的目标是解析页面本身Crawlee 的JSDOMCrawler提供了更现成的方案参见 JSDOMCrawler 指南与 jsdom_crawler.ts 示例默认只处理text/html、application/xhtmlxml、text/xml、application/xml、application/json等 MIME 类型其他类型需通过additionalMimeTypes扩展提供window、document上下文前端开发者可直接使用window.document.querySelectorAll(...)等熟悉的 API示例 jsdom_crawler_react.ts 展示了runScripts: true时如何让 JSDOM 执行 React 应用脚本并模拟点击计算器按钮——与本文拦截 XHR 的思路同源都是让 JSDOM 跑脚本。从源码结构看jsdom-crawler.tsJSDOMCrawler还内置了VirtualConsole管理与hideInternalConsole选项可通过crawler.getVirtualConsole()监听页面内部错误日志——在调试脚本执行异常时非常有用。最后提醒本方案依赖目标站点脚本的具体行为XMLHttpRequestsetRequestHeader一旦站点改版可能失效同时runScripts: dangerously存在代码执行风险请始终在受控环境、针对可信站点使用。【免费下载链接】crawleeCrawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Puppeteer, Playwright, Cheerio, JSDOM, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考