上个月,我正在开发一款竞品分析工具,需要从十几个不同的 SaaS 定价页面抓取产品数据。一开始我用了经典的技术栈:requests 加 BeautifulSoup。起初效果挺好——直到我碰上了一个用 Next.js 搭建的网站。返回的 HTML 是空的,数据全被锁在 JavaScript 渲染里,结果我整整一个周末都在跟无头浏览器的配置作斗争,还一直超时。
就在这时候,一位同事向我推荐了 Firecrawl。它的卖点很简单:给它一个 URL,就能拿回干净的数据。不用管浏览器,不用解析 DOM,也不用写那些一改类名就挂掉的脆弱 CSS 选择器。我当时挺怀疑的——这种宣称“开箱即用”的工具往往都不太灵——但我还是试了试。下面就是我在真实项目里实际配置和使用它的经验总结。
准备工作
首先:去 firecrawl.dev 注册并获取 API key。免费额度给的额度足够你好好测试一番了,而且不需要绑信用卡。
安装 Python SDK:
pip install firecrawl-py
然后把你的 API key 设为环境变量:
export FIRECRAWL_API_KEY="fc-your-key-here"
我一开始犯了个错,把 key 直接硬编码到了脚本里。千万别这么干——把它放到 .env 文件里,像个靠谱的开发者那样用 python-dotenv 来读取。
基础用法:抓取单个页面
咱们从简单的开始。我想抓取一篇博客文章的内容,喂给后续的摘要生成流程:
from firecrawl import FirecrawlApp
app = FirecrawlApp(api_key="your-api-key")
result = app.scrape_url("https://example.com/blog/some-article")
print(result["markdown"])
就这么简单。一次调用,你拿到的就是干净的 Markdown,而不是那种混杂着导航栏、页脚和 Cookie 弹窗的乱糟糟的 HTML。我第一次跑这段代码的时候,真的被输出的干净程度惊到了——完全不需要写正则去清理。
/scrape 接口每次调用消耗 1 个额度(credit)。你也可以请求不同的格式:
result = app.scrape_url("https://example.com", params={
"formats": ["markdown", "html", "links", "screenshot"]
})
我发现 links 格式特别好用,当我想在正式爬取前先发现子页面时,它帮了大忙。而 screenshot 格式则让我免去了单独配置 Playwright 来做视觉验证的麻烦。
重头戏:结合 Pydantic 做结构化提取
这才是 Firecrawl 真正戳中我痛点的地方。你不需要写 CSS 选择器去提取特定字段,只要定义一个 Pydantic schema,描述清楚你想要什么数据就行。Firecrawl 会在底层用 AI 帮你搞定提取。
针对我的定价页项目,我定义了这样一个 schema:
from pydantic import BaseModel, Field
class PricingPlan(BaseModel):
plan_name: str = Field(description="The name of the pricing tier")
monthly_price: str = Field(description="The monthly price, including currency symbol")
features: list[str] = Field(description="Key features listed for this plan")
is_free: bool = Field(description="Whether this plan is free")
class PricingPage(BaseModel):
company_name: str = Field(description="Name of the company")
plans: list[PricingPlan] = Field(description="All pricing tiers listed on the page")
然后把它传给抓取调用:
result = app.scrape_url("https://example.com/pricing", params={
"formats": ["extract"],
"extract": {
"schema": PricingPage.model_json_schema()
}
})
print(result["extract"])
输出的结果是一个符合你 schema 的、规规矩矩的结构化 JSON 对象。不用写正则,不用写 XPath,也不用祈祷网站别改版。这总共花费 5 个额度(1 个用于抓取 + 4 个用于提取),跟我以前花在维护脆弱解析器上的无数小时相比,这钱花得太值了。
途中我踩过一个坑:如果你只用提示词而不用 schema,字段名在不同次的运行中可能会变。用了 Pydantic schema,字段名就锁死了,当你需要把输出传给下游需要特定键名的代码时,这点至关重要。所以在生产环境中,一定要用 schema 方式。
规模扩展:爬取整个网站
抓取单页还行,但我需要整站的数据。Firecrawl 的 crawl 方法一次调用就能搞定:
result = app.crawl_url("https://example.com", params={
"crawlerOptions": {
"includes": ["/blog/*", "/docs/*"],
"exclude": ["/blog/page/*"],
"maxDepth": 3
},
"limit": 50
})
它会爬取网站,跟踪链接,并遵循你的包含/排除模式。不用手动管理 URL 队列,不用调试递归爬取逻辑。limit 参数限制了总页面数——这很重要,免得你在巨型网站上把额度烧光。
爬取默认是异步运行的。你可以检查状态或等待完成:
# 立即返回一个 job ID
crawl_status = app.crawl_url("https://example.com", params={"limit": 100})
# 轮询结果
while crawl_status["status"] != "completed":
time.sleep(2)
crawl_status = app.check_crawl_status(crawl_status["id"])
我吃过血亏:如果不加 includes 模式去爬大站,你的额度会瞬间见底。一定要把爬取范围缩小到你真正需要的板块。
应对重度依赖 JavaScript 的网站
这正是最初把我逼上这条路的难题。用 React、Next.js 或类似框架构建的网站,用 requests 拿到的往往是空 HTML,因为内容是在客户端渲染的。
用 Firecrawl,你根本不用操心这个。它会在云端自动处理 JavaScript 渲染。我拿一个用 requests 啥也抓不到的 Next.js 仪表盘试了试——Firecrawl 第一次尝试就拿到了完整的渲染内容。
对于需要交互(点击按钮、在搜索框输入)的页面,你可以使用 actions 参数:
result = app.scrape_url("https://example.com/login", params={
"actions": [
{"type": "type", "selector": "#email", "value": "test@example.com"},
{"type": "type", "selector": "#password", "value": "mypassword"},
{"type": "click", "selector": "button[type='submit']"},
{"type": "wait", "milliseconds": 3000},
{"type": "screenshot"}
]
})
它会在云端启动一个真实的浏览器,按顺序执行你的操作,然后返回最终状态。我在一个客户项目里用它抓取登录墙后面的数据——确实管用,但速度更慢,消耗的额度也更多,所以只有在你真的需要交互时才去用它。
保存结果
拿到数据后,你得找个地方存起来。这里有一个简单的模式,可以把结果保存为不同格式:
import json
import csv
import sqlite3
# 存为 JSON,供 Web 应用使用
with open("pricing_data.json", "w") as f:
json.dump(result["extract"], f, indent=2)
# 存为 CSV,方便用电子表格分析
plans = result["extract"]["plans"]
with open("pricing.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["plan_name", "monthly_price", "is_free"])
writer.writeheader()
writer.writerows(plans)
# 存入 SQLite,方便查询
conn = sqlite3.connect("pricing.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS plans
(company TEXT, plan_name TEXT, price TEXT, is_free BOOLEAN)
""")
for plan in plans:
cursor.execute("INSERT INTO plans VALUES (?, ?, ?, ?)",
(result["extract"]["company_name"],
plan["plan_name"],
plan["monthly_price"],
plan["is_free"]))
conn.commit()
实用技巧与客观局限
用了 Firecrawl 几周后,这些是我希望一开始就能知道的事:
爬取前先用 map。 map 接口能快速返回网站上的所有 URL,但不会去抓取内容。在决定全面爬取前,先用它摸清网站结构。
爬取时务必设置 limit。 不设的话,你可能一不小心就爬了几千个页面。我曾在某个文档网站上白白烧掉 200 个额度,等反应过来已经晚了。
把 includes 模式用起来。 缩小爬取范围,速度更快、更省钱,数据也更干净。
开发阶段在本地缓存结果。 我写了个简单的装饰器,把抓取结果存到磁盘,后续运行时直接读取。这在反复调试提取 schema 时帮我省下了海量额度。
免费额度真的够用来测试。 你有足够的额度来验证方案是否可行,然后再考虑付费计划。
支持批量抓取。 如果你有几百个 URL,用 batch_scrape()(同步)或 start_batch_scrape()(异步),别在循环里挨个调用。
再来说说局限。如果你大规模抓取,Firecrawl 并不便宜——额度消耗得很快,尤其是开了提取功能的话。如果你要大量抓取简单的静态网站,requests + BeautifulSoup 依然更具性价比。它的核心价值其实在于 JavaScript 渲染、结构化提取,以及免去了维护爬虫基础设施的麻烦。另外,提取的准确率并非百分百。据我估算,在结构良好的页面上,它大概有 90% 的时候能提取对数据。如果是关键数据,你需要在下游加上校验逻辑。
最后,免费额度有速率限制。如果你要跑每分钟抓取几百个页面的生产级流水线,那就得买付费版了。
在我的项目里,Firecrawl 用大约 30 行真正能在不同网站架构上跑通的代码,替换掉了原来 300 行脆弱的抓取代码。这笔买卖我愿意再做一次——但那是因为我确实需要 JavaScript 渲染和结构化提取。对于简单任务,老工具依然好使。按需选择就好。