
这年头做工程项目你要是还跟人说“我写了个爬虫”大概率被反问一句数据是给谁用的我自己这两年最大的感受是爬虫真正的主要用户已经变了。以前是人在看页面现在是LLM在吃网页。可LLM吃不了乱七八糟的HTML它要的是markdown、是结构化字段、是为提示词提前准备好的内容。直到我把Craw4AI用Docker 部署上了生产环境这一层才算真正打通。这篇内容不是官方文档翻译是我实际部署、跑任务、填坑之后的完整记录。我会讲清楚 Craw4AI 到底是什么、为什么值得用 Docker 而不是 pip 装、怎么一条命令先跑起来、怎么用 docker compose 进入生产级编排最后把我在抓取真实网站时踩过的坑和排查思路完整写出来。适合正在做 RAG/知识库、需要给 Agent 喂网页数据的开发者参考。先泼一盆冷水Craw4AI 这名字看着像普通爬虫框架实际上它把自己定位成“给 LLM 准备的爬虫”。官方文档里反复出现一个词叫LLM-friendly。我理解下来它跟 Scrapy、BeautifulSoup 这类传统工具最大的区别不是“能不能抓”而是“抓完怎么给模型”。下面我详细拆一下。1. 为什么是 Craw4AI传统爬虫给不了 LLM 的数据形态1.1 传统爬虫和 LLM 之间缺的那一层用 Scrapy 或者 Requests 抓网页拿到的是一大坨 HTML。HTML 里有导航、广告、脚本、样式、追踪代码真正的正文内容淹没在各种标签里。对人来说浏览器渲染之后看着挺干净对模型来说直接把这堆原始 HTML 喂进去既浪费 token又容易被无关信息带偏。我见过不少团队的做法是抓完 HTML 之后自己写正则或者用 BeautifulSoup 做清洗再转成纯文本。听起来简单其实坑很深。不同网站的 DOM 结构完全不一样同一套提取规则换个站点就失效维护成本非常高。而且就算拿到了纯文本LLM 真正需要的往往是“标题是什么、作者是谁、发布时间是什么时候、正文讲了几件事”——这种语义级的结构化信息靠 CSS 选择器硬扣工作量堪比重写一个解析器。后来我试了 Craw4AI它把这一层补齐了。它支持直接输出干净的Markdown还能基于 CSS 选择器或 XPath 提取指定字段甚至可以接 LLM 做语义级的内容抽取把网页变成一份 JSON 结构数据。等于“爬取、清洗、结构化”三步压缩成一步数据拿到手就能往向量库里塞。1.2 Craw4AI 真正省时间的地方单说架构的话Craw4AI 是一个异步爬虫引擎底层支持 Playwright能渲染 JavaScript 动态页面这点比 Requests 那种静态抓取强太多。像很多现代网站的数据是前端异步加载出来的Requests 抓回来根本看不到内容而 Craw4AI 等浏览器渲染完之后再提取拿到的就是用户实际看到的页面内容。它的第二个杀手锏是可与策略化提取。你可以在请求里指定“提取标题、正文、所有链接”也可以指定“用 LLM 帮我提取出价格、评价、库存状态”。前者适合规则明确的页面后者适合内容千变万化的场景。配合不同的 LLM 提供商输出直接是 JSON连后处理都省了。我实际用下来最舒服的一点是它的输出格式是为 LLM 设计的而不是为“给人看”设计的。Markdown 保留了标题层级和链接结构喂给 RAG 时检索效果明显比纯文本好。这也是为什么我最终决定把 Craw4AI 纳入项目而不是继续在 Scrapy 生态里缝缝补补。2. 部署前先搞懂依赖关系Craw4AI 不是一个小容器2.1 镜像里到底装了什么Craw4AI 官方提供了 Docker 镜像这也是我最推荐的部署方式。为什么不用 pip 直接装因为我踩过本机环境的坑它依赖 Playwright而 Playwright 要额外下载 Chromium 内核加上一堆系统库一套装下来占好几个 G。遇到 Python 版本不干净的老机器光是装依赖就能折腾一晚上。用 Docker 之后所有依赖都被封在镜像里宿主机只需要有 Docker 环境就行。这点对跨团队协作特别友好——你本地跑通的环境到同事机器上或者服务器上绝对一致不存在“我这儿好好的”这种鬼话。官方镜像的核心组件包括Craw4AI 主服务提供 REST API、WebSocket 服务和任务调度。Playwright 运行时负责浏览器渲染实际执行页面加载。内嵌策略引擎处理 Markdown 转换、CSS/XPath 提取、LLM 策略对接。你不需要关心这些组件在容器内怎么协作只要知道一点主服务和浏览器渲染服务是分开的所以如果页面渲染很慢瓶颈通常不在 API 层而在浏览器任务队列上。2.2 版本选择与端口规划最容易被忽略的部分镜像 tag 方面官方在 Docker Hub 上发布的是craw4ai/craw4ai。我个人建议Staging 环境或者刚接触阶段直接用latest但上了生产环境一定要固定版本号比如craw4ai/craw4ai:0.5.7。原因很简单latest 跟随主分支更新可能某个小版本升级后接口响应结构变了你的解析代码就要跟着改。端口规划是所有新手最容易忽略的一步。Craw4AI 默认会占用两个端口很多教程只说 8000却没说另一个端口导致后面实时任务怎么都连不上。端口用途说明8000REST API主入口提交抓取任务、查询任务状态都走这里11235WebSocket实时任务和流式输出的通信通道如果你用的是docker run -p 8000:8000那么 11235 在容器内虽然还在监听但宿主机访问不到所有依赖 WebSocket 的实时功能会被卡死。正确做法是两个端口都映射出来。另外我见过的另一个坑本机 8000 端口通常已经被别的服务占了启动容器前先ss -tlnp | grep 8000查一下免得端口冲突导致容器起不来。3. 单机快速部署docker run 一条命令先跑起来3.1 拉镜像与启动如果你想最快速度看到效果不需要一上来就搞 docker compose。先拉镜像然后直接 run 起来。# 拉取官方镜像 docker pull craw4ai/craw4ai:latest # 启动容器同时映射两个关键端口 docker run -d \ --name craw4ai \ -p 8000:8000 \ -p 11235:11235 \ craw4ai/craw4ai:latest启动之后用docker logs -f craw4ai看日志等出现类似 “Uvicorn running on http://0.0.0.0:8000” 的字样说明主服务已经起来了。如果你机器比较老第一次启动可能要等一会儿因为容器里会初始化浏览器环境CPU 和内存占用会有一阵峰值。3.2 验证部署是否成功很多人启动容器后习惯性地去访问http://localhost:8000结果发现什么都没有就开始怀疑部署失败。其实 Craw4AI 的根路径返回的内容很少正确验证方式是访问健康检查接口curl http://localhost:8000/health正常会返回类似{status: healthy}的 JSON。另外我建议你打开浏览器访问一下http://localhost:8000/docs如果能看到 Swagger 文档页面说明 API 路由注册成功这时候部署就算真正成功了。3.3 第一次真实抓取服务起来之后就可以用 REST API 提交一个最简单的抓取任务。Craw4AI 的接口风格非常直接向/crawl发送 POST 请求传入 URL 列表即可。curl -X POST http://localhost:8000/crawl \ -H Content-Type: application/json \ -d { urls: https://example.com, synchronous: true, verbose: true }这里的关键参数是synchronous。设为true时请求会一直等到抓取完成直接返回结果适合调试设为false默认时接口立刻返回一个job_id你再通过/jobs/{job_id}轮询结果。第一次测试建议用同步模式能快速看到响应体里的markdown字段那就是爬下来的内容。如果返回结果里没有 markdown只看到一堆error信息先别慌我后面会讲排查链路。4. 生产级路线docker compose 把 Redis 和 Playwright 一起编排4.1 为什么单容器不够用docker run的方式适合验证但放到真实项目里会有几个不容忽视的问题。首先是任务队列。Craw4AI 在单容器模式下任务调度默认走内存队列。一旦容器重启所有排队中的爬取任务全部丢失。真实环境里一个抓取任务可能要跑几十秒甚至几分钟任务积压时非常依赖一个稳定的队列这块官方推荐用 Redis 解决。其次是浏览器资源。Playwright 浏览器实例非常吃内存一个实例轻松占几百 MB。单容器模式下浏览器和 API 挤在一起高并发时很容易把容器内存打满导致 OOM Kill。生产环境里最好把浏览器渲染能力独立成一个服务跟主服务分开部署。第三是扩展性。真实项目里你可能需要多个 Craw4AI 实例并行抓取单容器模式下无法平滑扩展。用 compose 拆分后你可以单独给爬虫服务开副本或者把 Redis 换成外部实例扩起来顺手得多。4.2 compose 文件实例我自己的生产环境用的是 docker compose下面这个配置是我在项目里实际跑过的版本你可以直接抄。version: 3.8 services: craw4ai: image: craw4ai/craw4ai:0.5.7 container_name: craw4ai restart: always ports: - 8000:8000 - 11235:11235 environment: - CRAWL4AI_REDIS_URLredis://redis:6379/0 - CRAWL4AI_PLAYWRIGHT_URLhttp://playwright:3000 - OPENAI_API_KEY${OPENAI_API_KEY} depends_on: - redis - playwright networks: - crawl-net redis: image: redis:7-alpine container_name: craw4ai-redis restart: always volumes: - redis-data:/data networks: - crawl-net playwright: image: browserless/chromium:latest container_name: craw4ai-playwright restart: always environment: - MAX_CONCURRENT_SESSIONS5 - CONNECTION_TIMEOUT60000 networks: - crawl-net volumes: redis-data: networks: crawl-net: driver: bridge这里有几个细节我特意用上了也建议你保留。CRAWL4AI_REDIS_URL指向 compose 网络内的 Redis 服务直接把任务队列的外部依赖落到了 Redis 上。CRAWL4AI_PLAYWRIGHT_URL指向 browserless 服务也就是把浏览器渲染能力独立出去了。之所以用browserless/chromium是因为它专门为容器环境优化过支持并发会话数量限制比在 Craw4AI 容器里直接跑浏览器稳定得多。MAX_CONCURRENT_SESSIONS5的意思是最多同时跑 5 个浏览器会话。这个值不是越大越好——每个会话都有内存开销你机器是 8G 内存的话设成 5 比较合适16G 以上再上调。CONNECTION_TIMEOUT我设的是 60000也就是 60 秒网页加载超过这个时间会被判超时。4.3 启动与验证在docker-compose.yml所在目录执行docker compose pull docker compose up -d启动后依次检查三个服务状态docker compose ps理想情况下三个服务都是Up状态。如果某个服务反复重启先看对应日志最常见的还是内存不足导致的 OOM。然后用之前的 curl 命令再测一次抓取确认通过 Redis 队列的任务链路是通的。这里我额外提醒一个点production 环境里不要图省事用latesttagcompose 文件里最好写死版本号避免某天docker compose pull把所有镜像都升了一个小版本结果接口变了你的调用代码全炸。5. 部署完成不等于能用三个必踩的坑与排查链路5.1 端口连不上11235 和 8000 的关系我前面强调了 11235 端口这里再说一个真实场景。有个同事照着一个教程做容器也起来了8000 端口也能访问 Swagger 文档但他代码里用了0.2.x版本的客户端 SDK一直报 WebSocket 连接失败。查了一圈才发现他的docker run只映射了8000:800011235 根本没暴露出来。而且他在代码里没走 REST 接口默认走的是 WebSocket 模式自然连不上。排查这类问题我建议的链路是先确认容器端口映射docker port craw4ai看看实际映射到宿主机的端口列表。如果发现少映射了 11235删掉容器重新创建不要试图在运行中的容器里补映射做不到。如果映射没问题检查宿主机防火墙有没有放行 11235很多云服务器默认只开放少数端口。记住一句话Craw4AI 的 REST API 和 WebSocket 是两套通道。简单任务用 REST 没问题但涉及流式输出、长连接任务时11235 必须可用。5.2 LLM 提取报错默认配置不等于开箱即用如果你用 Craw4AI 的“LLM 策略”提取结构化信息比如让模型从网页里抽字段大概率会遇到这样的报错openai.NotFoundError: 404 model_not_found这个错误非常典型。Craw4AI 提供了 LLM 策略的入口但它不替你决定用哪个模型。默认配置里可能指到某个 OpenAI 模型而你既没配 API Key也没改模型名称。解决方案有两种方案一如果你打算用 OpenAI 系模型那就在环境变量里配好 keyOPENAI_API_KEYsk-xxxx CRAWL4AI_LLM_PROVIDERopenai方案二如果你更希望用本地部署的大模型或者国产模型接口Craw4AI 支持自定义 Base URL。你可以在部署时把 provider 指向兼容 OpenAI 协议的本地接口再确认模型名跟你的实际部署一致。这里最容易踩的坑是只改了 Base URL忘了改model字段。我调试时在这个问题上耗了一个多小时最后发现模型名少写了个-就这一个字符的差距。5.3 容器内访问宿主机服务与资源占用问题第三个坑来自不同操作系统下的网络差异。很多人的真实需求不是抓公开网页而是抓内网系统或者本地服务的数据比如公司内部的 Wiki、测试环境的前端页面。这时候容易遇到的问题Craw4AI 容器内访问localhost访问的是容器自己而不是宿主机。在 Windows 和 macOS 的 Docker Desktop 环境下可以用host.docker.internal这个特殊域名指到宿主机 IP但在 Linux 上这个域名不一定有效。如果你用的是 Linux 服务器部署 compose需要在 craw4ai 服务里单独加一个启动参数或者把路由指向宿主机内网 IP。我习惯的做法是在 compose 文件的环境变量里把宿主机 API 地址配置为http://宿主机局域网IP:端口简单直接避免跨系统兼容性问题。至于资源占用这是个容易被低估的问题。我测试过一个 4G 内存的云主机跑单个 Craw4AI 容器浏览器实例一多内存直接飙到 3.5G然后 OOM。监控命令是docker stats如果看到内存长期在 90% 以上优先做两件事一是降低MAX_CONCURRENT_SESSIONS限制浏览器并发二是提高宿主机内存到 8G。不要指望在 2G 内存的小机器上流畅跑动态网页渲染这不现实。6. 落地组合Craw4AI 在我项目里的完整用法参考6.1 一个典型的调用示例部署这件事搞定后真正操心的是怎么把 Craw4AI 接进自己的项目。我目前的调用方式很简单不引入它的 Python SDK直接通过 HTTP 接口访问这样语言无关以后换 Go 或者 Java 也不用改。下面是我在 Python 里提交抓取任务的一段常用代码import time import requests API_URL http://localhost:8000 def submit_crawl(urls: list[str], max_pages: int 3) - dict: resp requests.post( f{API_URL}/crawl, json{ urls: urls, synchronous: False, max_pages: max_pages, verbose: False, }, timeout30, ) resp.raise_for_status() return resp.json() def wait_for_job(job_id: str, timeout: int 120) - dict: start time.time() while time.time() - start timeout: resp requests.get(f{API_URL}/jobs/{job_id}, timeout10) data resp.json() if data.get(status) completed: return data time.sleep(2) raise TimeoutError(fjob {job_id} timeout) # 示例调用 job submit_crawl([https://example.com/docs]) result wait_for_job(job[job_id]) markdown_content result[result][markdown]这里我特意把synchronous设为 false用轮询方式拿结果配合 Redis 任务队列可以塞进任意调度系统。如果你的业务是流式输出那就得走 WebSocket代码结构完全不同但大部分内部工具场景轮询已经够用了。6.2 与知识库/定时任务结合的经验Craw4AI 爬完的数据最常规的出路是进向量库。我现在的做法是写一个小时级定时任务从种子 URL 列表出发用 Craw4AI 抓取正文转成 Markdown 后做文本切分再 embedding最后写入向量库。整套链路里 Craw4AI 只负责“拿到干净的文本”后面的事情它不管但这种分工反而让整个系统更清爽。如果你的爬取目标是需要登录才能访问的内网页面Craw4AI 也支持自定义请求头和 cookie。我建议在部署时不要硬编码登录凭证而是把 cookie 通过请求参数传进去这样容器的可复用性更好也不容易把密钥写进镜像里。6.3 适合什么场景不适合什么场景用了几个项目后我对 Craw4AI 的边界有了清晰认知。它适合的是需要批量把网页转成结构化数据的场景。页面大量依赖 JavaScript 渲染的单页应用。准备给 LLM 或 RAG 提供数据源的内容抓取。它不太适合的是需要分布式大规模爬虫的场景调度和控制不如 Scrapy 集群成熟需要自己搭配套系统。极端重视抓取速度的场景因为浏览器渲染天然比静态请求慢。没有足够内存资源的低配服务器浏览器实例真的很吃内存。如果你只是每天抓几千个静态页面传统 Requests 方案显然更快更省资源但如果你要的是“喂给 LLM 的好数据”Craw4AI 的性价比在同类工具里非常突出。我在实际部署中也走过不少弯路最值钱的教训就是不要先优化功能先把部署形态定下来。单机验证用 docker run正式任务一律 docker compose并把 Redis 和浏览器服务拆开。这样后续加机器、加队列、接监控都不用推倒重来。如果你部署过程中也踩了某个奇怪的坑不妨从端口映射和内存分配这两个方向开始排查大概率能解决。