刚接触 Python 爬虫那会儿,我的第一反应是打开 requests 对着页面一顿猛写,然后被翻页、去重、断点续抓、异常重试这些事反复折磨。后来换到 pyspider,第一次打开它的 Web 控制台时,说实话有点恍惚:这玩意儿居然自带一个像任务管理器一样的图形界面,能直接写代码、点运行、看抓取队列、调调试工具,所有状态都摊在眼前。把我手里那个多页面的采集任务迁移过来之后,维护成本直接降了一个量级。这篇博文就围绕“pyspider 库入门”展开,我会从环境搭建、核心 API、到完整实现一个可运行的爬虫项目,把关键设计讲透,再附上一堆我实际踩过的坑,尽量让零基础的同学也能一遍上手。
pyspider 是一个基于 Python 的爬虫框架,最大的特点就是“可视化 + 脚本化”:你只需要继承一个 Handler 类,在里面写几个方法,它就能帮你把调度、抓取、解析、存储全串起来。它适合在数据采集阶段快速验证想法的个人开发者,也适合小团队内部复用,尤其适合那种目标站点多、结构简单、需要频繁调整策略的场景。
1. 为什么值得学 pyspider
1.1 pyspider 解决的核心痛点
别看现在很多人一上来就推 Scrapy,pyspider 在“快速上手”和“可观测性”这两个维度上其实非常能打。Scrapy 是命令行工具、需要搭一堆配置,新建一个爬虫之后想看抓取状态还得自己接 Telnet。而 pyspider 把整套东西搬到了浏览器里,写了代码之后点一下 Run 就能调,实时看到 requests、responses、队列堆积、异常信息,这对初学爬虫的人太友好了。
另一个痛点是任务调度的细节。自己用 requests 写循环,免不了处理去重、重试、请求间隔、全局状态保存。pyspider 的 Scheduler 组件把这些全部接管了:它自动去重 URL,支持按优先级调度,支持失败重试,甚至能爬着爬着手动暂停和恢复。这就相当于把“能跑就行”的脚本,升级成了“有状态、可管理、挂了能续上”的采集系统。
1.2 和 Scrapy 的取舍对比
很多人会在 pyspider 和 Scrapy 之间纠结,我简单说下自己的使用感受。
| 对比维度 | pyspider | Scrapy |
|---|---|---|
| 上手门槛 | 低,Web UI 直接调试 | 高,需要理解项目结构和命令行 |
| 可视化 | 强,自带控制台和监控面板 | 弱,基本靠日志 |
| 分布式支持 | 依赖组件较多,配置略复杂 | 基于 Scrapyd,生态相对成熟 |
| 自定义灵活性 | 高,写法自由 | 高,中间件机制完善 |
| 维护状态 | 项目基本停更 | 社区活跃,持续更新 |
| 适用场景 | 中小规模、快速迭代采集 | 大规模生产环境、管道复杂任务 |
我的建议是:学习阶段或项目体量不大时,pyspider 的体验会好很多,它的代码组织方式能帮你快速理解“回调 + 消息”这套爬虫思维;等真正需要上量、做分布式调度,再迁移到 Scrapy 也不迟。迁移成本并不高,因为核心解析逻辑都是 requests + pyquery / BeautifulSoup,换框架只是换壳。
1.3 pyspider 的底层运行逻辑
入门 pyspider 之前,得先理解它的五个组件:Scheduler(调度器)、Fetcher(抓取器)、Processor(处理器)、MQ(消息队列)和 Web UI。整个流程简单来说就是:你在 on_start 方法里调用 self.crawl 发出首批 URL 消息,MQ 投递给 Scheduler,Scheduler 决定谁先抓、要不要去重,然后把任务发给 Fetcher;Fetcher 请求完页面再把结果还给 Processor;Processor 执行你自己写的解析回调,产生新的 URL 或保存数据。整套链路绕一圈,一个爬虫就跑起来了。
记住这个数据流很重要,因为后面排错的时候,你要能判断问题出在“没有发起调度”“请求失败了”还是“解析逻辑报错”,这三个环节在 pyspider 里分别对应不同的呈现状态。刚开始看到几百个“active”任务不要慌,这代表调度器正在按节奏批量发送任务。
2. 环境准备与安装
2.1 环境要求与版本兼容性
pyspider 对 Python 版本的关系非常微妙,这是新手最容易翻车的点。由于项目中部分代码使用了async作为参数名,而 Python 3.7 之后async成了保留关键字,所以高版本 Python 直接装 pyspider 会报语法错误。如果你用的是 Python 3.6 或更老版本,直接pip install pyspider就行;如果和我一样在往前看、用了 Python 3.8+,必须做一些代码兼容处理。
我的建议是给 pyspider 单独建一个虚拟环境,无需纠结本机默认解释器。用 Anaconda 或者 Python 自带的 venv 都行,装个 Python 3.6 版本运行最省事。如果一定要用高版本 Python,请先找到 pyspider 的fetcher/tornado_httpclient.py和libs/utils.py,把里面async字段全部批量替换成async_,否则一启动就会抛SyntaxError。另外 requests、pycurl、Flask 等依赖也需要一并升级到兼容版本。
2.2 快速安装与启动
在虚拟环境里执行下面三步,就能把基础的 pyspider 跑起来:
# 1. 创建并激活虚拟环境(推荐 Python 3.6) python -m venv py36 source py36/bin/activate # Windows 是 py36\Scripts\activate # 2. 安装 pip install pyspider # 3. 启动 pyspider打开浏览器访问http://localhost:5000,看到 pyspider 的仪表盘页面就说明安装成功了。第一次打开首页时可能显示一串空任务列表,这很正常,我们还没创建任何任务。
启动时如果提示ValueError: Invalid configuration: Collected 'scheduler' was mistakenly collected as a worker...,通常是旧版本配置和 werkzeug 库版本不兼容。解决方式是调整依赖版本,或者直接找到 pyspider 的webui/app.py,去掉其中误配置的 scheduler worker 初始化代码。这类问题网上方案很多,我后面会在排查章节展开。
2.3 Web UI 界面速览
pyspider 的 Web UI 是它区别于其他框架的杀手锏。页面左侧是任务列表,显示每个爬虫的状态、抓取数、错误数;点击某个任务进去,就到了脚本编辑器,里面可以改 Handler 代码、调试器、看运行日志,还能直接看到当前的抓取进度。顶部的“Create”按钮用于新建爬虫项目,“Rate”和“Burst”两个参数非常好用,Rate 是每秒最大抓取频率,Burst 是并发数,跑目标站点时一定记得把这两个值调低一点,既是守法公民,也别把自己机房 IP 封了。
调试面板是我最喜欢的功能:选定一条消息记录,点击“run”单步执行,可以直接在网页里查看self.crawl发出的子请求、HTML 内容以及 response 状态码。遇到页面重定向或 ajax 加载的情况,也可以换不同的 fetch_type 来模拟浏览器行为,这部分实操性很强,后面我会专门演示。
3. 核心 API 与脚本结构
3.1 最小可运行脚本
在 pyspider 里写爬虫,本质上就是复写固定的 Handler 类。下面这个脚本是入门最基础的五行代码:
from pyspider.libs.base_handler import BaseHandler class Handler(BaseHandler): def on_start(self): self.crawl("http://quotes.toscrape.com/", callback=self.index_page) def index_page(self, response): for item in response.doc("a[href^='/tag/']").items(): self.crawl(item.attr.href, callback=self.index_page)on_start是任务启动时的入口,self.crawl(url, callback=xxx)负责把 URL 投进调度队列,等抓取完成后,框架会用 response 对象调用你指定的callback。注意response.doc返回的是 pyquery 选择器对象,语法和 jQuery 接近,用英文逗号分隔多个 selector 时别漏了引号。继续沿着这种思路,我们可以加一个解析函数来抓取页面标题。
def detail_page(self, response): return { "url": response.url, "title": response.doc("h1").text(), }这时的完整流程就是:启动后先抓列表页,列表页解析出详情页链接,再回调detail_page返回结构化数据。一个纯文本、翻页、详情三段式的爬虫,总共不到 20 行代码就能写出来,老手看完会心一笑,新手也知道该往哪个地方填逻辑。
3.2 @config 装饰器与全局配置
pyspider 允许你用装饰器的形式,给回调函数预置抓取参数。最常见的写法是设置age和priority:
@config(age=10 * 24 * 60 * 60) def detail_page(self, response): return { "url": response.url, "title": response.doc("h1").text(), }age表示这个任务的过期时间,在它未过期之前,即使重新调度,Scheduler 也会直接忽略重复请求;priority则是优先级权重,数字越大越先抓。这个机制非常适合“关注更新”类场景:列表页设置短 age,详情页设置长 age,这样重复抓取列表时不会重复抓已经缓存的详情内容,能省下不少宽带和反爬风险。
crawl方法还支持很多关键字参数,比如fetch_type可以设置成js或splash来渲染动态页面,method指定 POST,data携带表单字段,headers自定义请求头,validate_cert控制是否校验证书。我在处理需要登录的站点时,还会把 Cookie 塞进headers,效果很好。
3.3 消息传递与 on_result
pyspider 的on_result是收集数据的关键出口。如果你在回调函数里直接返回一个 dict,pyspider 会自动把结果送到on_result方法里,你可以在这里连接 MySQL、MongoDB、或者直接写日志。因为 pyspider 默认的 Web 页面只会显示一条条任务,并不会自动落盘,所以第一次跑通之后一定要记得自己实现结果保存逻辑,否则数据只存在内存里,重启就没了。
消息机制上,self.send_message(project, msg, url)可以在不同项目之间互发消息,on_message负责接收,这个机制适合拆分成多个独立任务时做数据汇总。比如 A 项目负责全站链接收集,B 项目负责解析目标字段,两者通过消息队列解耦。不过对入门而言,单项目内使用self.crawl的回调链就已经够用。
4. 完整实操:抓取一个公开练习站点
4.1 页面分析与目标定义
为了演示稳定,我选了专门用于爬虫练习的开放站点quotes.toscrape.com。这个站点上的数据是静态 HTML,页面结构稳定,不涉及登录验证和复杂反爬,非常适合新手上手。我们要做的事情有三个:
- 抓取列表页上每一条名言的内容、作者和标签
- 顺着列表页的分页按钮自动翻页
- 把所有结果输出到一个 JSON 文件里,作为爬虫的结果验证
打开任意一个列表页可以看到,名人名言结构是div.quote,里面包含span.text(名言内容)、small.author(作者)、div.tags a.tag(标签)。底部有一个li.next > a链接指向下一页。这些 selector 在后续解析核心代码中都会用到,建议你先在浏览器里用开发者工具验证一下再复制。
4.2 编写 Handler 代码
在 Web UI 里新建一个项目,或者直接在本地编辑器写好再粘贴进去。完整的入口代码如下:
import json from pyspider.libs.base_handler import BaseHandler class Handler(BaseHandler): retry_delay = { "": 5, } def on_start(self): self.crawl("http://quotes.toscrape.com/", callback=self.index_page) @config(age=60 * 60) def index_page(self, response): for quote in response.doc("div.quote").items(): yield { "text": quote("span.text").text(), "author": quote("small.author").text(), "tags": [tag.text() for tag in quote("div.tags a.tag").items()], } next_url = response.doc("li.next > a").attr.href if next_url: self.crawl("http://quotes.toscrape.com%s" % next_url, callback=self.index_page) def on_result(self, result): if result: with open("quotes_output.jsonl", "a", encoding="utf-8") as f: f.write(json.dumps(result, ensure_ascii=False) + "\n")我把解析出的字段用yield返回,这样每条名言记录会被独立作为一条 result 保存。@config(age=60 * 60)表示列表页一小时之内不重复抓取,避免循环刷新任务时不断重新抓列表。翻页逻辑要注意拼接域名,因为next链接是相对地址。
4.3 在调试器里单步运行
写完后点击右上角的“Run”按钮,pyspider 会打开调试面板。调试面板默认从on_start开始,你可以在左侧信息队列里看到当前生成的列表页 URL;点击 message 后面的“run”,框架会立刻发起请求并展示 HTML 和 response 状态。这里有个小技巧,在 debugging 面板中,你可以单步执行某些回调,也可以直接把一个空闲 agent 拉到最大,观察批量请求的实时情况。
第一次跑的时候,我发现队列里会出现大量状态为 403 的请求,原因是quotes.toscrape.com对无 UA 的请求会直接拒绝。解决办法是在self.crawl里显式增加headers:
self.crawl( "http://quotes.toscrape.com/", callback=self.index_page, headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36"} )加上 UA 后,状态码恢复为 200,队列开始正常吞吐。这个排查流程非常典型:先看状态码判断是网络问题还是被反爬,再对症下药。
4.4 定时运行与关闭调度
在 Web UI 任务列表里,点击这个爬虫项目前面的“运行”开关,爬虫才会真正进入定时调度模式。设置好 Rate 和 Burst 后,pyspider 会按照你的配置持续运行。日常使用中我习惯把 Rate 设为 1、Burst 设为 3,也就是每秒最多抓 1 个页面、同时最多并发 3 个请求,这对小型站点非常温和。等到确认数据完整了,把开关关掉即可,未完成的任务会留在队列中等待下次启动。
结果数据会实时追加到quotes_output.jsonl文件里。如果你想保存到数据库,在on_result中改成连接 MongoDB 的代码,直接insert_one即可。不要小看这个落盘逻辑,它是整个爬虫工程化里最容易漏的一环,很多新手在 Web UI 里看到数据记录就以为存下来了,关了服务才发现字段全丢了。
5. 常见问题与排查技巧实录
5.1 Web UI 启动报错合集
pyspider 的老龄化代码让它在新环境里错误奇多,我把遇到过的启动报错整理成了速查表:
| 错误信息 | 原因 | 解决方式 |
|---|---|---|
SyntaxError: invalid syntax | Python 3.7+ 的 async 关键字冲突 | 将源码中的async批量替换为async_ |
ValueError: Invalid configuration | werkzeug 版本过高 | 降级 werkzeug 到 0.16.x 或修改 app.py |
ImportError: cannot import name 'etree' | lxml 安装异常 | pip uninstall lxml后重装 |
KeyError: 'scheduler' | 旧项目配置残留 | 删掉 data 目录下的 json 状态文件重新初始化 |
| 启动后页面白屏 | 端口被占用或 WebUI 依赖缺失 | 设置--port 5001换端口 |
排查启动类问题,最快的方式是看终端里的完整 Traceback,而不是只看最后一行。大多数问题都是依赖版本不匹配造成的,所以我会在项目目录下维护一个requirements.txt固定版本,避免几个月后重新部署时环境变了对不上。
5.2 抓取结果为空或乱码
如果队列显示请求成功、但回调解析结果为空,优先怀疑 HTML 结构和选择器不匹配。pyspider 对目标编码识别偶尔失手,尤其是 GBK 或 GB2312 编码的站点,此时要在回调里手动指定编码:
response.encoding = "gbk" text = response.text另外,pyquery 选择器对动态渲染内容的支持有限。如果页面数据是通过 JS 异步加载的,直接在response.doc里找不到对应节点。这时候使用fetch_type="js"或接入 Splash 会更好,但需要注意这会降低抓取速度。能用接口猜参数就不要优先走渲染,既快又稳。
5.3 任务堆积和无限重爬
“任务状态一直 pending”“active 数量越堆越高”是高频问题。原因一般有两种:一是回调抛异常,导致任务不断重试;二是产生了大量没有被正确回调的重复 URL。前者去 Web UI 的日志面板看异常栈即可;后者可以在self.crawl里加上age和itag参数,让 Scheduler 基于内容指纹或者时间窗口去重。
itag是比age更精细的去重维度,比如你要抓一个商品列表,可以用“分类ID + 页数”作为 itag,这样同一分类同一页只会调度一次,即使后面增加新的启动入口也不会重复抓。我平时判断一个任务是否应该继续重试,会看队列里的错误信息是网络超时还是解析错误,这两种的处理策略完全不同。
5.4 部署与分布式扩展
单个 pyspider 进程跑量有限,项目规模变大后,你可以把 Scheduler、Fetcher、Processor 拆到不同机器上,用 Redis 作为消息队列连接。启动命令分别对应:
pyspider -c scheduler pyspider -c fetcher pyspider -c processor pyspider -c webui配合配置文件里的message_queue选项,把这几个进程串起来,就能组成一个简单的分布式爬虫集群。但说实话,pyspider 的分布式部署文档相对陈旧,踩坑成本不低;如果业务并发真的到了每秒上千请求,我更推荐一开始就评估 Scrapy + Scrapyd 那套体系。
6. 写在后面的实用建议
6.1 我实际使用中的几个体会
用了几年 pyspider,我最大的体会是它特别适合“中短生命周期”的采集需求。比如一个活动页需要连续监控一周价格,写个 Handler 丢进去挂一天,任务完成直接删掉,比维护一个大型 Scrapy 项目省心得多。它的 Web UI 在那个年代是极具前瞻性的设计,放到现在看依然是很好的产品体验。
如果你要拿 pyspider 做长期项目,记住三点:一是把解析逻辑写得尽量通用,用数据配置驱动 URL 和 selector;二是所有结果必须尽早落库,别依赖 Web UI 的状态展示;三是踩坑记录一定要沉淀成自己团队的文档,因为很多报错信息在网上已经很难搜到有效答案了。
6.2 下一步扩展方向
入门之后可以尝试把 pyspider 和可视化监控工具结合,定时抓取目标页面的关键指标,一旦发生变化就触发告警。另外也可以把on_result收到的数据对接消息队列,比如发到 Kafka 或 RabbitMQ,再交给下游的大数据链路处理。这样虽然还用着 pyspider,但已经把它的能力边界往前推了一大步。
最后再分享一个小技巧:如果你需要抓的页面结构特别简单、数据量也不大,完全可以把 Handler 的代码量控制在 50 行以内;但如果页面开始出现下拉加载、滚动翻页、验证码校验,就不要继续在 pyspider 里死磕了,及时切换到无头浏览器方案或者对接打码平台,时间成本比什么都贵。希望这篇入门文章能让你少走一点弯路,尽快跑出自己第一个 pyspider 项目。