| name | funboost-spider-crawling |
| description | 当需要用 funboost 进行 Web 爬虫或数据采集时使用。触发场景:分布式爬虫、URL 调度、funspider、boost_spider、httpx 爬虫。关键词:爬虫, spider, crawl, funspider, SimpleSpiderClient, AsyncSpiderClient, URL 调度, 分布式采集。 |
Funboost 爬虫 / 数据采集
概述
Funboost 是函数调度器,Scrapy 是 URL 调度器——这是二者最本质的差异。
- Scrapy:调度
Request 对象,框架替你发请求、走 callback 链、Pipeline 存储,用户被框架生命周期束缚。
- Funboost:调度完整的 Python 函数。框架只负责:何时调用、并发几个、失败怎么办。函数内部写什么代码完全自由。
核心结论:Funboost 写爬虫就是「写函数就能爬虫」。加一个 @boost(BoosterParams(...)) 装饰器,函数自动获得分布式、并发、重试、控频、断点续爬、去重等 30+ 种能力。
核心原则:框架只管调度,不插手你怎么发请求、怎么解析、怎么存储。整个 PyPI 都是天然生态,无需 scrapy-xxx 插件。
适用场景
- 分布式 Web 爬虫 / 数据采集
- 列表页 → 详情页 → 子任务链式派发
- 需要精准 QPS 控频、多进程/多机器横向扩展
- 短时效 Token、浏览器多轮交互(Playwright/Selenium)
- 外部系统(Java/Go)通过 Redis/HTTP 实时触发爬取
- 函数级重试(HTTP 200 但内容异常也重试)
三种爬虫模式
| 模式 | 组成 | 适合场景 |
|---|
| 纯 @boost | @boost + 任意 HTTP 库(requests/httpx/playwright)+ 任意存储 | 最大自由,复用现有 utils/ 工具 |
| funspider | SimpleSpiderClient / AsyncSpiderClient + SpiderResponse + SpiderItem | ORM 模型驱动、强类型、同步/异步混用 |
| boost_spider | RequestClient + SpiderResponse + DatasetSink(独立包 pip install boost_spider) | 极简字典流、多代理商容灾 |
三者都不是必须的,可自由混搭:funboost + funspider(SpiderItem) + boost_spider(RequestClient) + your_utils。
模式 1:纯 @boost 函数调度
from funboost import boost, BoosterParams, BrokerEnum
@boost(BoosterParams(
queue_name='crawl_detail',
broker_kind=BrokerEnum.REDIS_ACK_ABLE,
qps=5,
max_retry_times=3,
))
def crawl_detail(url: str):
import requests
resp = requests.get(url, proxies=get_my_proxy(), headers=get_my_headers())
data = parse_detail(resp.text)
save_to_mysql(data)
crawl_detail.push(url='https://example.com/article/123')
crawl_detail.consume()
模式 2:funspider(内置于 funboost)
位于 funboost/contrib/funspider/,依赖:pip install httpx parsel sqlmodel(及数据库驱动)。
| 组件 | 说明 |
|---|
SimpleSpiderClient | 同步 httpx 客户端:自动重试、随机 UA、代理轮换 |
AsyncSpiderClient | 异步 httpx 客户端:适配 ConcurrentModeEnum.ASYNC |
SpiderResponse | 响应封装:.xpath() / .css() / .re() / .re_first() / .resp_dict |
SpiderItem | SQLModel ORM 基类:upsert() / aio_upsert() / mongo_upsert() / bulk_upsert() |
模式 3:boost_spider(独立三方包)
pip install boost_spider
| 组件 | 说明 |
|---|
RequestClient | API 兼容 requests,多代理商容灾轮换、自动重试、Cookie 会话 |
SpiderResponse | .xpath() / .css() / .re_search() |
DatasetSink | 一行代码存 MySQL/PostgreSQL/SQLite/MongoDB,自动建表 |
funspider vs boost_spider 选型:
| 维度 | funspider | boost_spider |
|---|
| 设计理念 | ORM 辅助、强类型流 | 自由至上、极简字典流 |
| 数据模型 | SQLModel 类,IDE 自动补全 | 纯字典 + DatasetSink |
| HTTP 客户端 | httpx(同步+异步双引擎) | 兼容 requests,多代理商容灾 |
| 适合场景 | 大型团队、ORM 管理表结构 | 快速开发、极致简洁 |
funspider 客户端用法
SimpleSpiderClient(同步)
from funboost.contrib.funspider import SimpleSpiderClient
def abuyun_proxy():
return "http://user:pass@proxy.abuyun.com:9020"
def redis_pool_proxy():
import redis
r = redis.Redis(host="localhost", port=6379, db=0)
proxy = r.srandmember("proxy_pool")
return proxy.decode() if proxy else None
client = SimpleSpiderClient(
proxy_getter_list=[abuyun_proxy, redis_pool_proxy],
retry_times=3,
timeout=30,
user_agents=None,
)
resp = client.get("https://example.com/page")
resp = client.post("https://api.example.com", json={"key": "val"})
resp = client.request("GET", url, headers={...}, params={...})
client.close()
SpiderResponse 解析:
title = resp.css("h1::text").get()
links = resp.css("a.title::attr(href)").getall()
news_id = resp.re_first(r'/news/(\d+)')
items = resp.xpath("//div[@class='item']//text()").getall()
data = resp.resp_dict
AsyncSpiderClient(异步)
不绑定 Event Loop,可在 funboost ASYNC 并发模式自由使用:
from funboost import ConcurrentModeEnum
from funboost.contrib.funspider import AsyncSpiderClient
async_client = AsyncSpiderClient(proxy_getter_list=[abuyun_proxy], retry_times=3)
@boost(BoosterParams(
queue_name='async_crawl',
concurrent_mode=ConcurrentModeEnum.ASYNC,
qps=10,
))
async def crawl_api(news_id: int):
resp = await async_client.get(f"https://api.example.com/comments/{news_id}")
for c in resp.resp_dict["comments"]:
await CommentItem(...).aio_upsert()
注意:异步任务必须用 await func.aio_push(...) 发布,消费函数必须 async def。
SpiderItem 数据模型
继承 SpiderItem + table=True,绑定数据库引擎和 upsert 去重字段:
from typing import ClassVar, Optional
from funboost.contrib.funspider import SpiderItem, Field, create_engine, create_async_engine
MYSQL_ENGINE = create_engine("mysql+pymysql://root:pass@127.0.0.1:3306/spider_db")
ASYNC_MYSQL_ENGINE = create_async_engine("mysql+aiomysql://root:pass@127.0.0.1:3306/spider_db")
class NewsItem(SpiderItem, table=True):
__tablename__: ClassVar[str] = "news"
__engine__ = MYSQL_ENGINE
__async_engine__ = ASYNC_MYSQL_ENGINE
__default_upsert_unique_fields__ = ["news_id"]
id: Optional[int] = Field(default=None, primary_key=True)
news_id: int = Field(unique=True)
title: str = Field(max_length=200)
url: str = Field(max_length=500)
content: str
NewsItem.create_table()
入库方法:
| 方法 | 说明 |
|---|
.insert() | 同步插入 |
.upsert() | 同步 upsert(按 __default_upsert_unique_fields__ 去重,存在则更新) |
.bulk_upsert(items) | 同步批量 upsert |
.aio_insert() / .aio_upsert() | 异步版本 |
.mongo_upsert() / .aio_mongo_upsert() | MongoDB upsert(需设置 __mongo_collection__) |
.ensure_mongo_indexes() | 按去重字段创建 MongoDB 索引 |
NewsItem(news_id=123, title="标题", url="https://...", content="正文").upsert()
await CommentItem(...).aio_upsert()
分布式爬虫架构
典型架构:多队列 + Redis ACK 队列 + 多进程/多机器消费。
种子推送 → [news_list 队列] → crawl_list 函数 → push 详情 URL
↓
[news_detail 队列] → crawl_detail 函数 → upsert 入库 + push 子任务
↓
[news_comments 队列] → crawl_comments 函数(异步)
关键配置:
from funboost import boost, BoosterParams, BrokerEnum, BoostersManager
CRAWLER_GROUP = "news_crawler"
class NewsCrawlerParams(BoosterParams):
broker_kind: str = BrokerEnum.REDIS_ACK_ABLE
booster_group: str = CRAWLER_GROUP
@boost(NewsCrawlerParams(queue_name='news_list', qps=2, do_task_filtering=True))
def crawl_list(page: int): ...
@boost(NewsCrawlerParams(queue_name='news_detail', qps=5, max_retry_times=5))
def crawl_detail(detail_url: str): ...
if __name__ == '__main__':
BoostersManager.consume_group(CRAWLER_GROUP)
crawl_list.push(page=1)
横向扩展:
- 多进程:
func.multi_process_consume(8) — 单机 8 核 × 50 线程 = 400 并发
- 多机器:多台机器运行同一脚本、同一
queue_name、同一 Redis,自动竞争消费
- 分布式控频:
is_using_distributed_frequency_control=True — 多机器共享 QPS 上限
- 外部触发:Java/Go 往 Redis 对应 queue 发 JSON(函数入参)即可;或用
funboost.faas 暴露 HTTP 接口
Broker 推荐:生产爬虫用 BrokerEnum.REDIS_ACK_ABLE(消费确认,断点续爬);开发测试可用 SQLITE_QUEUE。
去重(do_task_filtering)
基于函数入参自动去重(非 URL 字符串指纹),可忽略 URL 中的噪音参数:
@boost(BoosterParams(
queue_name='news_detail',
do_task_filtering=True,
task_filtering_expire_seconds=3600 * 24,
))
def crawl_detail(detail_url: str):
...
- 相同
detail_url 在有效期内不会重复执行
- 列表页和详情页可设不同去重有效期(列表 24h,详情 7 天)
- 需要 Redis 支持(框架自动使用 Redis 存储去重键)
- 与 Scrapy URL 指纹去重相比:基于结构化入参,不受 URL 参数顺序/噪音干扰
链式派发 + 去重:列表页 crawl_detail.push(detail_url=url) 时,若详情页已开启 do_task_filtering,重复 URL 自动跳过。
完整代码示例(funspider)
参考源码:funboost/contrib/funspider/funspider_demos/funspider_demo1.py
import re
from typing import ClassVar, Optional
from funboost import (
boost, BoosterParams, BoostersManager, BrokerEnum,
ConcurrentModeEnum,
)
from funboost.contrib.funspider import (
SimpleSpiderClient, AsyncSpiderClient, SpiderItem,
create_engine, create_async_engine, Field,
)
NEWS_GROUP = "news_crawler"
class NewsCrawlerParams(BoosterParams):
broker_kind: str = BrokerEnum.REDIS_ACK_ABLE
booster_group: str = NEWS_GROUP
MYSQL_ENGINE = create_engine("mysql+pymysql://root:123456@127.0.0.1:3306/testdb")
ASYNC_MYSQL_ENGINE = create_async_engine("mysql+aiomysql://root:123456@127.0.0.1:3306/testdb")
class NewsItem(SpiderItem, table=True):
__tablename__: ClassVar[str] = "news"
__engine__ = MYSQL_ENGINE
__async_engine__ = ASYNC_MYSQL_ENGINE
__default_upsert_unique_fields__ = ["news_id"]
id: Optional[int] = Field(default=None, primary_key=True)
news_id: int = Field(unique=True)
title: str
content: str
url: str
class CommentItem(SpiderItem, table=True):
__tablename__: ClassVar[str] = "comments"
__engine__ = MYSQL_ENGINE
__async_engine__ = ASYNC_MYSQL_ENGINE
__default_upsert_unique_fields__ = ["comment_id"]
id: Optional[] = Field(default=, primary_key=)
comment_id: = Field(unique=)
news_id:
user:
content:
NewsItem.create_table()
CommentItem.create_table()
sync_client = SimpleSpiderClient(retry_times=)
async_client = AsyncSpiderClient(retry_times=)
BASE_URL =
():
resp = sync_client.get()
href resp.css().getall():
crawl_detail.push(detail_url=)
resp.css().get():
crawl_list.push(page=page + )
():
resp = sync_client.get(detail_url)
title = resp.css().get().strip()
news_id = (re.search(, detail_url).group())
title:
Exception()
NewsItem(news_id=news_id, title=title, content=resp.text[:], url=detail_url).upsert()
crawl_comments.push(news_id=news_id)
():
resp = async_client.get()
c resp.resp_dict.get(, []):
CommentItem(
comment_id=c[], news_id=c[],
user=c[], content=c[],
).aio_upsert()
__name__ == :
BoostersManager.consume_group(NEWS_GROUP)
crawl_list.push(page=)
铁律(绝对不可违反)
- 必须使用
BoosterParams 对象 — 禁止 @boost("queue_name", qps=5) 老式写法
- 获取上下文用
fct — 不用 self / bind=True
- 异步模式 —
concurrent_mode=ConcurrentModeEnum.ASYNC 时必须 async def + await aio_push
- 连续启动消费 —
func1.consume(); func2.consume(),不要用 threading.Thread 包装
- AI 运行脚本 — 必须设
PYTHONPATH=项目根目录,并用 timeout 或 os._exit() 终止(consume() 永不退出)
- 禁止 flush Redis — 去重依赖 Redis,不要清空
函数级重试 vs Scrapy URL 级重试
Funboost 函数内任意位置抛异常都会触发重试:
@boost(BoosterParams(queue_name='reliable_crawl', max_retry_times=5))
def crawl(url: str):
resp = requests.get(url)
data = resp.json()
if data.get('code') == 'captcha_required':
raise Exception("触发验证码")
title = data['result']['title']
save(title)
Scrapy 仅在 HTTP 请求失败(超时、5xx)时重试;HTTP 200 但内容是验证码或空数据,Scrapy 认为成功,数据直接丢失。
与 Scrapy 的对比优势
| 维度 | Funboost | Scrapy |
|---|
| 调度核心 | 调度函数(内部完全自由) | 调度 Request(框架控制) |
| 编程范式 | 平铺直叙线性代码 | yield Request + callback 回调链 |
| 项目结构 | 无要求,单文件即可 | 强制 7-8 个文件 |
| HTTP 库 | 任意(requests/httpx/playwright) | 强制 Twisted 下载器 |
| 反爬实现 | 普通 Python 函数,0 门槛 | Downloader Middleware |
| 并发 | 多进程 × 多线程/协程 × 多机器 | 单进程受限 |
| QPS 控频 | 精准 qps=N,分布式全局控频 | DOWNLOAD_DELAY 不精确 |
| 去重 | 函数入参级,支持有效期 | URL 指纹,噪音参数干扰 |
| 重试 | 函数级(内容异常也重试) | URL 级(仅网络错误) |
| 断点续爬 | ACK 确认,崩溃不丢消息 | scrapy-redis blpop 可能丢消息 |
| 短时效 Token | 函数内连续请求,时序确定 | 调度器排队,Token 易过期 |
| 浏览器交互 | 函数内自然编写,N 实例并发 | Selenium 阻塞 Twisted 事件循环 |
| 外部触发 | Redis JSON / FaaS HTTP | 封闭系统,难以对接 |
| 插件 | 无需,PyPI 所有包直接 import | 依赖 scrapy-xxx 专用插件 |
| Web 管理 | funweb 开箱即用 | 无官方工具 |
选型建议:
- 写函数就能爬虫、复用 utils、精准控频、分布式 ACK、外部系统触发 → Funboost
- 已有 Scrapy 大型项目且团队熟悉 callback 体系 → 可继续 Scrapy,但新爬虫优先考虑 Funboost
最佳实践
项目结构
Funboost 不强制目录。推荐一个网站一个 py 文件:
my_spider_project/
├── utils/my_request.py # 可选:自封装请求
├── site_news.py # 列表+详情,同文件
└── run_all.py # 可选:BoostersManager.consume_group
精准 QPS + 分布式控频
@boost(BoosterParams(
queue_name='polite_crawl',
qps=2,
is_using_distributed_frequency_control=True,
))
def crawl(url: str): ...
定时爬取种子
from funboost import ApsJobAdder
@boost(BoosterParams(queue_name='seed_scheduler', booster_group=CRAWLER_GROUP))
def push_daily_seeds():
for cat_id in range(1, 11):
crawl_list.push(cat_id)
ApsJobAdder(push_daily_seeds, job_store_kind='redis').add_push_job(
trigger='cron', hour=0, minute=0, id='daily_seeds', replace_existing=True,
)
funweb 可视化管理
pip install funboost[flask]
python -m funboost.funweb.app
消费速率 = 爬取速率;支持暂停/恢复、失败重投、积压告警。
依赖安装
pip install funboost
pip install httpx parsel sqlmodel
pip install pymysql aiomysql
pip install psycopg2-binary
pip install boost_spider
参考资源
| 资源 | 路径 |
|---|
| funspider 源码 | funboost/contrib/funspider/ |
| 完整 demo | funboost/contrib/funspider/funspider_demos/funspider_demo1.py |
| 模拟网站(本地测试) | funboost/contrib/funspider/funspider_demos/fake_news_site.py |
| 爬虫教程 | funboost_docs/source/articles/c8.md |
| 基础任务技能 | .agents/skills/using-funboost-basics/SKILL.md |
| Broker 选型 | .agents/skills/funboost-broker-selection/SKILL.md |
| 高级重试 | .agents/skills/funboost-advanced-retry/SKILL.md |
相关 Skill
funboost-async-programming — async/await 异步编程
funboost-broker-selection — Broker 中间件选型