Guide · 使用教程
Crawl4AI Python 教程:限定采集范围、正文校验与来源追踪
用五个自有本地路径验证静态、动态、404、空正文和重定向,完整代码保留来源与失败,说明十二项检查及网络边界。
用自有测试页建立完整验收
这篇教程固定 Crawl4AI 0.9.5,用五个自有本地路径验证采集、动态正文等待、失败记录和来源追踪。范围是写死的路径列表,不自动发现链接,不使用登录态,不调用 LLM 或云采集 API,也不是生产采集成功率测试。
运行环境为 Python 3.11,Playwright 1.64.1,使用本机已安装的 Google Chrome,以 headless 的独立临时浏览器上下文运行,不连接日常浏览器会话。其他操作系统的浏览器和依赖需要单独确认。测试仅监听 127.0.0.1,端口 8765 应先确认可用。
第一步:建立独立环境
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install 'crawl4ai==0.9.5' 'playwright==1.64.1' 'pyOpenSSL==25.3.0' 'cryptography==46.0.3'
python -m pip check
这组 TLS 依赖是本次环境的可复现约束,避免安装过程中编译最新加密库,不是所有平台的唯一组合。代码保持 HTTPS 证书检查,不通过关闭校验解决网络错误。若选择其他版本,先验证兼容性与安装条件,再调整示例。
本例通过 chrome_channel='chrome' 使用已安装 Chrome。若只安装了 Python 包,没有可用浏览器,仍不能完成采集;使用其他浏览器安装方案时应按当前官方说明准备。不要将“pip 安装成功”写成浏览器运行已经成功。
第二步:保存本地测试服务器
将下面代码保存为 fixture_server.py。它只提供合成内容,包含静态中文文章、稍后由 JavaScript 插入的正文、404、空 article 和指向其他本地路径的重定向。导航和页脚带噪声标记,便于检查正文选择是否有效。
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
ARTICLE='''<h1>测试产品退款规则</h1><p>REFUND-731:购买后七天内可以申请退款。金额以原始订单记录为准。</p><p>请保留订单编号与申请日期,先核对来源版本,再交由人工确认。</p>'''
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path=='/redirect':
self.send_response(302);self.send_header('Location','/outside');self.end_headers();return
status=404 if self.path=='/missing' else 200
content=ARTICLE if self.path=='/article' else '<h1>其他页面</h1><p>OUTSIDE-731</p>'
if self.path=='/dynamic':content='<h1>动态正文</h1><div id="result">等待正文</div>'
if self.path=='/empty':content=''
html='<html><head><meta charset="utf-8"><title>本地测试页</title></head><body><nav>NAV-NOISE</nav><article>'+content+'</article><footer>FOOTER-NOISE</footer>'
if self.path=='/dynamic':html+="<script>setTimeout(()=>document.querySelector('#result').innerHTML='<p>DYNAMIC-731:动态内容已加载,请保留采集时间与来源。</p>',150)</script>"
html+='</body></html>';data=html.encode()
self.send_response(status);self.send_header('Content-Type','text/html; charset=utf-8');self.send_header('Content-Length',str(len(data)));self.end_headers();self.wfile.write(data)
def log_message(self,*args):pass
if __name__=='__main__':
server=ThreadingHTTPServer(('127.0.0.1',8765),Handler)
print('Fixture server: http://127.0.0.1:8765',flush=True)
try:server.serve_forever()
finally:server.server_close()
在第一个终端启动,保持运行:
python fixture_server.py
输出应显示 http://127.0.0.1:8765。结束测试时在该终端按 Ctrl+C 关闭,不需要把服务暴露到公网。若端口已被占用,先查明现有进程,不要为运行示例直接终止未知服务。
第三步:保存采集程序
将下面代码保存为 crawl_local.py,并在相同目录准备运行。程序按固定列表逐个采集,每个任务有页面超时和外层等待超时,错误保留后继续。它只把正文 article 输出为 Markdown,动态页使用目标元素等待,缓存设置为 BYPASS。
CRAWL4_AI_BASE_DIRECTORY 指向临时运行目录,避免把示例缓存和日志混入正式项目数据;也可以设置为自己的可写专用目录。它不替代资源配额或隔离。示例的 URL 检查只限定直接输入,不能当作完整 SSRF 防护;最终地址校验发生在导航之后,额外资源与重定向仍需生产网络策略。
import os
os.environ.setdefault('CRAWL4_AI_BASE_DIRECTORY','/private/tmp/crawl4ai-demo-runtime')
import asyncio,hashlib,json
from datetime import datetime,timezone
from importlib.metadata import version
from pathlib import Path
from urllib.parse import urlsplit
from crawl4ai import AsyncWebCrawler,BrowserConfig,CrawlerRunConfig,CacheMode
BASE='http://127.0.0.1:8765'
PATHS=['/article','/dynamic','/missing','/empty','/redirect']
def approved(url):
p=urlsplit(url)
if p.scheme!='http' or p.hostname!='127.0.0.1' or p.port!=8765:
raise ValueError('demo origin not approved')
if p.username or p.password or p.query or p.fragment or p.path not in PATHS:
raise ValueError('demo URL not approved')
return url
async def main():
records=[]
async with AsyncWebCrawler(config=BrowserConfig(headless=True,chrome_channel='chrome',ignore_https_errors=False,verbose=False)) as crawler:
for path in PATHS:
url=approved(BASE+path)
row=dict(source_url=url,collected_at=datetime.now(timezone.utc).isoformat(),status='failed')
try:
cfg=CrawlerRunConfig(cache_mode=CacheMode.BYPASS,css_selector='article',
excluded_tags=['nav','footer'],word_count_threshold=0,page_timeout=10000,
wait_for="css:#result p" if path=='/dynamic' else None,verbose=False)
result=await asyncio.wait_for(crawler.arun(url=url,config=cfg),timeout=15)
row.update(http_status=result.status_code,final_url=result.redirected_url or result.url)
if row['final_url']!=url:raise ValueError('redirect needs review')
if not result.success or result.status_code!=200:raise ValueError('HTTP or crawl failed')
markdown=result.markdown.raw_markdown if result.markdown else ''
if not markdown.strip():raise ValueError('empty article markdown')
row.update(status='ok',markdown=markdown,
markdown_sha256=hashlib.sha256(markdown.encode()).hexdigest())
except Exception as exc:row.update(error_type=type(exc).__name__,error=str(exc)[:400])
records.append(row)
out=dict(versions={n:version(n) for n in ['crawl4ai','playwright']},
records=records,summary=dict(attempted=len(records),ok=sum(x['status']=='ok' for x in records)))
Path('crawl-output.json').write_text(json.dumps(out,ensure_ascii=False,indent=2))
print('Report saved to crawl-output.json; inspect every record.')
if __name__=='__main__':asyncio.run(main())
第四步:运行并检查每条记录
在第二个终端激活同一环境并运行:
python crawl_local.py
结果写入当前目录 crawl-output.json。它保存请求 URL、最终 URL、UTC 采集时间、HTTP 状态、正文或错误,并为成功正文保存 SHA-256。摘要用于追踪内容变化,不是授权或来源可信的证明。记录时间在发起任务前生成,并非页面原始发布日期。
本次实际结果是五次尝试、两次通过正文准入、三次失败:
| 路径 | 结果 | 核对内容 |
|---|---|---|
| /article | ok | REFUND-731 与中文正文,导航和页脚噪声未混入 |
| /dynamic | ok | 等待 #result p 后出现 DYNAMIC-731 |
| /missing | failed | 404 被保留 |
| /empty | failed | HTTP 200 但 article 为空,被拒绝 |
| /redirect | failed | 最终地址为 /outside,要求复核 |
重定向记录中,当前库报告的 HTTP 状态仍为 302,同时提供了最终 /outside 地址。程序因此分别保存状态和最终地址,不凭一个字段推断完整导航链。浏览器已经访问 /outside,本例的失败仅表示不接受该结果入库,不表示阻止了这个请求。
程序捕获单条异常后继续运行,所以正常退出不等于五页都成功。后续索引或数据库写入必须读取记录状态,不能只检查命令返回码。代码里的静态目标列表也不是生产授权系统,不接收任意 URL。
第五步:验证正文、错误与来源
可以在相同目录保存 check_report.py,检查两份正文标记、噪声、摘要以及失败记录:
import hashlib
import json
from pathlib import Path
out = json.loads(Path('crawl-output.json').read_text())
rows = {r['source_url'].split('/')[-1]: r for r in out['records']}
assert out['summary'] == {'attempted': 5, 'ok': 2}
for key, marker in [('article', 'REFUND-731'), ('dynamic', 'DYNAMIC-731')]:
row = rows[key]
assert row['status'] == 'ok' and marker in row['markdown']
assert 'NAV-NOISE' not in row['markdown']
assert 'FOOTER-NOISE' not in row['markdown']
assert row['markdown_sha256'] == hashlib.sha256(row['markdown'].encode()).hexdigest()
for key in ['missing', 'empty', 'redirect']:
assert rows[key]['status'] == 'failed' and rows[key].get('error')
assert all(r.get('collected_at') and r.get('final_url') for r in rows.values())
print('Report checked; failures remain visible and no automatic indexing is performed.')
python check_report.py
本次本地共十二项检查:两份成功正文的内容、噪声与摘要;404、空 200、重定向三种失败;协议、主机、路径、查询参数、凭据五种输入拒绝;全部来源记录和汇总计数。它们验证当前示例,不证明真实站点质量、登录支持或完整网络隔离。
与不执行 JavaScript 的提取路径对照
另用 Trafilatura 2.3.1 对同一服务器的原始 HTML 做提取,观察静态正文和动态标记。脚本中虽然包含 DYNAMIC-731 的字面字符串,提取器并不会执行 JavaScript、把它变成页面正文;原始 HTML 中“搜得到字符串”不能当成浏览器状态已经完成。
本次 Trafilatura 默认提取保留了静态正文标记,但结果还含 NAV-NOISE;动态页仅得到标题与“等待正文”,没有动态目标标记。这个很短的合成页面与 Crawl4AI 的显式 article 选择条件不同,不能作为公平质量排名。实际应用应继续校准提取策略。
这类对照用于区分取得渲染内容与提取正文两个职责,不是两个库的共同质量排名。要比较正文提取器,应尽量提供相同 HTML;要比较完整采集方案,应记录浏览器、等待和处理条件的差别。
扩展到真实网页之前
先建立获准 URL 与人工参考正文,明确站点、路径、数量、等待、速率和资源预算。对动态页面,等待能代表目标正文的状态,不要只增加固定延迟。对不完整、登录或错误提示页,定义 needs_review 或 failed,而非强制返回成功。
保存原始 URL、最终 URL、采集时间、正文处理版本和来源关系;页面更新、删除或权限变化时同步处理索引。网页中的文字是数据,不能作为 Agent 执行发消息、登录或修改系统的指令。真正的 URL 接收服务应独立限制网络目的地、重定向、子资源与资源消耗。
本次自部署没有付费 API 调用。开启模型抽取、代理或 Cloud 后,费用、数据流和条款都需要重新核对。Crawl4AI 现在同时有开源库和云方案,不能用库免费推断托管服务无成本,也不能由本地测试推断中国大陆网络与真实站点可达性。
继续阅读:工具介绍、网页采集推荐、Firecrawl 对比、Trafilatura 对比。官方资料:快速入门、浏览器与任务配置、Markdown。
常见问题
- Crawl4AI 只有自部署吗?
- 不是。目前官方还有 Cloud,应分别核对服务计量、数据流和运维责任。
- Firecrawl 只能使用云服务吗?
- 不是。官方也有开源和自托管路径,能力与运行条件需分别核对。
- HTTP 200 就能入库吗?
- 不能。登录提示、空正文和错误内容都可能返回 200,还需正文与业务验收。
- 白名单与最终 URL 检查是完整 SSRF 防护吗?
- 不是。最终复核发生在导航之后,生产系统还需独立限制网络、重定向与子资源。
- 自部署免费等于没有成本吗?
- 不等于。浏览器、主机、模型、代理、存储、重试和维护可能产生费用。
- 本次测试证明真实站点成功率吗?
- 没有。仅验证自有本地页面,不包含登录、复杂站点、云接口或共同性能排名。