news 2026/9/5 16:39:42

Crawl4AI 会话管理实战:用 session_id 保持浏览器状态,实现多步顺序爬取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Crawl4AI 会话管理实战:用 session_id 保持浏览器状态,实现多步顺序爬取

Crawl4AI 会话管理实战:用 session_id 保持浏览器状态,实现多步顺序爬取

【免费下载链接】crawl4ai🚀🤖 Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper. Don't be shy, join here: https://discord.gg/jP8KfhDhyN项目地址: https://gitcode.com/GitHub_Trending/craw/crawl4ai

Crawl4AI 的会话管理(Session Management)允许你在多次arun()请求之间复用同一个浏览器标签页(Page 对象),从而在顺序执行的爬取任务中保留 JavaScript 状态、Cookie 与已加载的 DOM。本篇基于 docs/md_v2/advanced/session-management.md 的完整内容展开,并结合 browser_manager.py、async_crawler_strategy.py 等源码,讲清session_id的复用机制、TTL 自动回收、kill_session清理逻辑,以及分页翻页、动态内容等待等典型场景的可运行写法。

一、会话管理解决什么问题

默认的 Crawl4AI 爬取流程中,每次请求都会分配一个新页面、爬完后释放。这带来两类问题:

  • 跨请求状态丢失:前一次请求通过 JavaScript 修改过的页面状态(已点击的按钮、已加载的懒加载内容、window上保存的变量)在下一次请求中不可见;
  • 重复开销:多次顺序爬取同一站点时反复打开标签页、分配内存。

session_id就是为了解决这两点而设计的:

  • 在爬取前后执行 JavaScript 动作时保持页面上下文;
  • 让多个顺序请求复用同一标签页,省去反复打开标签页与分配内存的开销。

重要约束:该特性面向顺序工作流设计,不适用于并行操作。多个请求共享同一 Page 时若并发导航,会相互干扰。

二、核心参数:CrawlerRunConfig.session_id

会话由BrowserConfigCrawlerRunConfig协同维护,关键开关是CrawlerRunConfig中的session_id字段。在 async_configs.py 中其定义为:

session_id: str = None,

源码中对该字段的官方说明(async_configs.py):

session_id (str or None): Optional session ID to persist the browser context and the created page instance. If the ID already exists, the crawler does not create a new page and uses the current page to preserve the state.

翻译过来:传入一个 session ID 后,Crawl4AI 会持久化对应的浏览器上下文和已创建的 Page 实例;如果该 ID 已存在,则不再新建页面,而是复用当前页面以保留状态。这与文档描述的行为完全一致。

另外两个常与会话配合使用的参数:

  • js_code:在页面导航后执行的 JavaScript,用于点击“下一页”、“加载更多”等交互;
  • js_only=True:跳过goto导航,只在已有页面上执行 JS——这是翻页场景的关键:第 2 页起不需要重新请求 URL,只需要在同一个页面上点按钮;
  • wait_for:轮询等待函数,用于等待动态内容更新后再提取;
  • cache_mode=CacheMode.BYPASS:会话爬取时每页内容都在变化,应绕过缓存读写。

三、基础用法:同一 session_id 顺序请求

下面是最小可用示例,两次请求共享session_id,爬取结束后显式清理会话(来自 session-management.md):

from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig async with AsyncWebCrawler() as crawler: session_id = "my_session" # 定义配置 config1 = CrawlerRunConfig( url="https://example.com/page1", session_id=session_id ) config2 = CrawlerRunConfig( url="https://example.com/page2", session_id=session_id ) # 第一次请求 result1 = await crawler.arun(config=config1) # 使用同一会话的后续请求 result2 = await crawler.arun(config=config2) # 完成后清理 await crawler.crawler_strategy.kill_session(session_id)

要点:

  1. 第一次请求会为该session_id创建并登记一个 (context, page);
  2. 第二次请求命中登记记录,直接复用同一页面,登录态、Cookie、JS 全局变量都被保留;
  3. 完成后调用kill_session()释放页面与上下文资源。

仓库中的 session_id_example.py 提供了该特性的官方示例,可作对照。

四、实战示例:带会话的 GitHub 提交记录分页爬取

文档给出的完整实战是:用会话爬取 GitHub 仓库提交列表的多个分页,并在每次翻页后等待新内容出现再提取。关键技巧是:

  • window.lastCommit记录上一页首条提交,翻页后等待首条提交变化;
  • 第 2 页起设置js_only=True,不重新goto,只在原页面上点击分页按钮;
  • JsonCssExtractionStrategy按 CSS 选择器结构提取标题。

完整代码(注意原文示例中需自行import json):

from crawl4ai.async_configs import CrawlerRunConfig from crawl4ai import JsonCssExtractionStrategy from crawl4ai.cache_context import CacheMode async def crawl_dynamic_content(): url = "https://github.com/microsoft/TypeScript/commits/main" session_id = "wait_for_session" all_commits = [] # 点击下一页按钮,并记录当前首条提交 js_next_page = """ const commits = document.querySelectorAll('li[data-testid="commit-row-item"] h4'); if (commits.length > 0) { window.lastCommit = commits[0].textContent.trim(); } const button = document.querySelector('a[data-testid="pagination-next-button"]'); if (button) {button.click(); console.log('button clicked') } """ # 等待条件:首条提交与上次不同,说明翻页已完成 wait_for = """() => { const commits = document.querySelectorAll('li[data-testid="commit-row-item"] h4'); if (commits.length === 0) return false; const firstCommit = commits[0].textContent.trim(); return firstCommit !== window.lastCommit; }""" schema = { "name": "Commit Extractor", "baseSelector": "li[data-testid='commit-row-item']", "fields": [ { "name": "title", "selector": "h4 a", "type": "text", "transform": "strip", }, ], } extraction_strategy = JsonCssExtractionStrategy(schema, verbose=True) browser_config = BrowserConfig( verbose=True, headless=False, ) async with AsyncWebCrawler(config=browser_config) as crawler: for page in range(3): crawler_config = CrawlerRunConfig( session_id=session_id, css_selector="li[data-testid='commit-row-item']", extraction_strategy=extraction_strategy, js_code=js_next_page if page > 0 else None, wait_for=wait_for if page > 0 else None, js_only=page > 0, cache_mode=CacheMode.BYPASS, capture_console_messages=True, ) result = await crawler.arun(url=url, config=crawler_config) if result.console_messages: print(f"Page {page + 1} console messages:", result.console_messages) if result.extracted_content: commits = json.loads(result.extracted_content) all_commits.extend(commits) print(f"Page {page + 1}: Found {len(commits)} commits") else: print(f"Page {page + 1}: No content extracted") print(f"Successfully crawled {len(all_commits)} commits across 3 pages") # 清理会话 await crawler.crawler_strategy.kill_session(session_id)

逐页行为拆解:

页码session_idjs_codewait_forjs_only行为
1首次创建会话False正常导航到 URL 并提取
2、3复用同一页面记录首条提交并点击下一页轮询首条提交是否变化True不重新导航,只执行 JS 并等待

这个模式(js_only+wait_for+ 会话)是处理 JS 分页的通用范式。

五、基础示例:循环加载内容

更简单的场景——同一 URL 上反复点击“加载更多”按钮。每次循环共享session_id,仅在第 2 次起注入点击 JS:

import asyncio from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig from crawl4ai.cache_context import CacheMode async def basic_session_crawl(): async with AsyncWebCrawler() as crawler: session_id = "dynamic_content_session" url = "https://example.com/dynamic-content" for page in range(3): config = CrawlerRunConfig( url=url, session_id=session_id, js_code="document.querySelector('.load-more-button').click();" if page > 0 else None, css_selector=".content-item", cache_mode=CacheMode.BYPASS ) result = await crawler.arun(config=config) print(f"Page {page + 1}: Found {result.extracted_content.count('.content-item')} items") await crawler.crawler_strategy.kill_session(session_id) asyncio.run(basic_session_crawl())

该示例演示了三件事:

  1. 跨多次请求复用同一个session_id
  2. 通过 JavaScript 动态加载更多内容;
  3. 结束时正确关闭会话、释放资源。

六、进阶技巧一:自定义执行钩子(on_execution_started)

官方提醒:接下来几个示例的组合方式稍显绕,先确保你已经熟悉前文各部分的执行顺序。

当“等待动态内容加载”的逻辑用wait_for表达不便时,可以注册自定义钩子在执行开始阶段介入。钩子通过crawler.crawler_strategy.set_hook(hook_type, hook)注册,async_crawler_strategy.py 中列出了支持的钩子类型,包括on_browser_createdon_page_context_createdon_user_agent_updatedon_execution_startedbefore_gotoafter_gotobefore_return_htmlbefore_retrieve_html等(除on_browser_created接收 browser 和 context 外,其余钩子接收 context 与 page)。

示例:在on_execution_started钩子里轮询提交列表,直到首条提交发生变化才放行走后续流程:

async def advanced_session_crawl_with_hooks(): first_commit = "" async def on_execution_started(page): nonlocal first_commit try: while True: await page.wait_for_selector("li.commit-item h4") commit = await page.query_selector("li.commit-item h4") commit = await commit.evaluate("(element) => element.textContent").strip() if commit and commit != first_commit: first_commit = commit break await asyncio.sleep(0.5) except Exception as e: print(f"Warning: New content didn't appear: {e}") async with AsyncWebCrawler() as crawler: session_id = "commit_session" url = "https://github.com/example/repo/commits/main" crawler.crawler_strategy.set_hook("on_execution_started", on_execution_started) js_next_page = """document.querySelector('a.pagination-next').click();""" for page in range(3): config = CrawlerRunConfig( url=url, session_id=session_id, js_code=js_next_page if page > 0 else None, css_selector="li.commit-item", js_only=page > 0, cache_mode=CacheMode.BYPASS ) result = await crawler.arun(config=config) print(f"Page {page + 1}: Found {len(result.extracted_content)} commits") await crawler.crawler_strategy.kill_session(session_id) asyncio.run(advanced_session_crawl_with_hooks())

钩子方案相比wait_for字符串的优势是:等待逻辑可以用完整的 Python/Playwright API 编写(wait_for_selectorevaluate等),可调试性和控制粒度更高。

七、进阶技巧二:JavaScript 执行与等待一体化

第三种方式是把“点击 + 等待”合并进一段异步 JS:先记下当前首条提交,点击下一页按钮,然后轮询直到首条提交变化。这样 Python 侧不需要wait_for也不需要钩子,逻辑集中在一个js_code里:

async def integrated_js_and_wait_crawl(): async with AsyncWebCrawler() as crawler: session_id = "integrated_session" url = "https://github.com/example/repo/commits/main" js_next_page_and_wait = """ (async () => { const getCurrentCommit = () => document.querySelector('li.commit-item h4').textContent.trim(); const initialCommit = getCurrentCommit(); document.querySelector('a.pagination-next').click(); while (getCurrentCommit() === initialCommit) { await new Promise(resolve => setTimeout(resolve, 100)); } })(); """ for page in range(3): config = CrawlerRunConfig( url=url, session_id=session_id, js_code=js_next_page_and_wait if page > 0 else None, css_selector="li.commit-item", js_only=page > 0, cache_mode=CacheMode.BYPASS ) result = await crawler.arun(config=config) print(f"Page {page + 1}: Found {len(result.extracted_content)} commits") await crawler.crawler_strategy.kill_session(session_id) asyncio.run(integrated_js_and_wait_crawl())

三种等待方案对比:

方案等待逻辑位置适用场景
wait_for函数CrawlerRunConfig 参数简单的 DOM 状态判断
on_execution_started钩子Python 侧自定义钩子需要复杂 Python 逻辑、可异常处理
一体化 JSjs_code 内异步轮询希望逻辑自包含、减少 Python 侧代码

八、会话生命周期:源码层面的复用、TTL 与清理

从源码结构看,会话的完整生命周期由 browser_manager.py 中的BrowserManager管理,核心数据结构是self.sessions(browser_manager.py),键为session_id,值为(context, page, last_used_timestamp)三元组。

1. 页面复用逻辑——get_page()(browser_manager.py):

# 若提供了 session_id 且已存在,直接复用其 page + context if crawlerRunConfig.session_id and crawlerRunConfig.session_id in self.sessions: context, page, _ = self.sessions[crawlerRunConfig.session_id] # 更新最近使用时间戳 self.sessions[crawlerRunConfig.session_id] = (context, page, time.time()) return page, context

未命中时走常规的页面/上下文创建流程,最后在(browser_manager.py)登记会话:

# 若指定了 session_id,则保存该会话以便后续复用 if crawlerRunConfig.session_id: self.sessions[crawlerRunConfig.session_id] = (context, page, time.time())

2. 复用页面前会中止挂起的加载—— 在 async_crawler_strategy.py 中,取到会话页面后会立即执行window.stop(),中止上一次导航遗留的未完成加载,避免下一次goto()出现超时:

# 复用会话页面时,中止上一次导航的挂起加载 if config.session_id: try: await page.evaluate("window.stop()") except Exception: pass

3. TTL 自动过期—— 会话并非永久存活。BrowserManager默认self.session_ttl = 1800(30 分钟,browser_manager.py);每次get_page()开头都会调用_cleanup_expired_sessions()(browser_manager.py):

def _cleanup_expired_sessions(self): """基于 TTL 清理过期会话。""" current_time = time.time() expired_sessions = [ sid for sid, (_, _, last_used) in self.sessions.items() if current_time - last_used > self.session_ttl ] for sid in expired_sessions: asyncio.create_task(self.kill_session(sid))

也就是说:超过 30 分钟未使用的会话会在下次取页时被自动回收。

4. kill_session 的清理细节——BrowserManager.kill_session()(browser_manager.py)会:释放页面占用标记;在锁内递减其所属 BrowserContext 的引用计数;当引用计数归零且非托管浏览器时关闭该 context;最后page.close()并删除会话登记。这保证了会话的 page 与 context 资源成对释放,不会泄漏。

5. 关于手动 kill_session 的新版本行为—— 需要注意,策略层的kill_session实现(async_crawler_strategy.py)目前会先打出一条告警:

Session auto-kill is enabled in the new version. No need to manually kill sessions.

然后再委托给browser_manager.kill_session(session_id)。即新版本已支持会话自动清理,文档示例中的手动kill_session()调用仍然有效(显式立即释放),但不再是必须的;长流程结束时建议保留该调用以获得确定性的资源回收。

九、常见使用场景

文档总结的会话管理典型场景,均可用前文的“session_id + js_code/js_only + 等待”范式实现:

  1. 认证流程:登录后操作受保护页面——第一次请求完成登录,后续请求复用带 Cookie 的同一页面;
  2. 分页处理:遍历多页内容,如 GitHub 提交示例;
  3. 表单提交:填写表单、提交并处理结果,中间状态保持在前端 JS 中;
  4. 多步骤流程:跨多个动作的工作流,每步依赖前一步的页面状态;
  5. 动态内容导航:处理 JavaScript 渲染或事件触发的内容。

十、实践要点与限制

  • 顺序而非并行:同一session_id的多次arun()必须串行await,不要放进并发任务组,否则共享 Page 的导航会相互冲突;
  • 配合CacheMode.BYPASS:会话中每页内容都不同,读写磁盘缓存反而会拿到过期内容;
  • js_only=True用于第 2 页起:避免重复goto触发整页重载、丢失 JS 状态;
  • 等待三选一wait_for(简单 DOM 条件)、on_execution_started钩子(复杂 Python 逻辑)、一体化异步 JS(逻辑自包含);
  • 资源清理:流程结束时调用await crawler.crawler_strategy.kill_session(session_id)立即释放;即使遗漏,30 分钟 TTL 也会在下次get_page()时自动回收过期会话;
  • 适用版本:上述session_idkill_session、TTL 自动过期行为均以当前仓库源码(browser_manager.py、async_crawler_strategy.py、async_configs.py)为准,session_ttl默认值为 1800 秒。

【免费下载链接】crawl4ai🚀🤖 Crawl4AI: Open-source LLM Friendly Web Crawler & Scraper. Don't be shy, join here: https://discord.gg/jP8KfhDhyN项目地址: https://gitcode.com/GitHub_Trending/craw/crawl4ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

STM32F103与W5500硬件TCP/IP栈实现工业级UDP通信实战

简介:本资源是一套面向物联网嵌入式开发者的STM32以太网实战代码工程,聚焦UDP通信场景,适用于STM32F103系列单片机初学者及项目开发者快速掌握W5500模块联网开发全流程。资源完整覆盖DHCP自动获取IP、UDP Socket创建、客户端连接监听与连接管…

作者头像 李华
网站建设 2026/9/5 16:35:32

霞鹜文楷免费开源中文字体:基于Klee One衍生的完整使用指南

霞鹜文楷免费开源中文字体:基于Klee One衍生的完整使用指南 【免费下载链接】LxgwWenKai An open-source Chinese font derived from Fontworks Klee One. 一款开源中文字体,基于 FONTWORKS 出品字体 Klee One 衍生。 项目地址: https://gitcode.com/…

作者头像 李华
网站建设 2026/9/5 16:34:09

狼人杀水平集体下降?问题源于信息处理链路而非智商

设想一个有点离谱但很值得认真拆的场景:某天你从床上醒来,发现全世界的狼人杀水平突然下降100倍。预言家验到查杀,却因为发言顺序太乱被全场当成悍跳;女巫手里的毒药成了情绪道具;好人阵营前一天盘的狼坑,第…

作者头像 李华
网站建设 2026/9/5 16:29:59

C#实现以图搜图:从图像特征提取到相似度匹配的完整实践

简介:本资源是一个基于C#实现的以图搜图功能完整示例项目,面向图像处理初学者、.NET开发者及计算机视觉入门学习者,解决人像比对与相似图像检索的核心技术实践问题。压缩包共108个文件,涵盖32个C#源码文件(含FindImg.c…

作者头像 李华