pyspider这名字在爬虫圈里算是老朋友了,哪怕现在新工具层出不穷,它那个自带可视化操作界面的独特设计,依然让很多初学爬虫的朋友念念不忘。我最初接触pyspider的时候,还是因为项目里要快速抓一批分类信息网站的列表页,那时候对Scrapy的部署还不太熟,pyspider的WebUI能让我在浏览器里直接写脚本、点运行、看结果,确实省了不少事。今天这篇就围绕pyspider库入门,把它的核心组件、脚本编写方式、WebUI调试流程和实战中容易踩的坑,一次性讲透。不管你之前有没有写过爬虫,只要对Python基础语法有了解,这篇文章都能当作一份比较顺手的入门参考。
1. pyspider能做什么:先搞懂它的核心价值
在动手敲代码之前,有必要先理解pyspider在设计上和普通爬虫脚本、Scrapy这类框架到底有什么不同。只有搞明白了它的思路,后面写脚本、排查问题才会顺手很多。
1.1 你有没有遇到过这种爬虫烦恼
写爬虫最常见的流程是:requests拿页面、正则或者XPath提取数据、循环翻页继续爬。这种脚本写起来很直接,但真正放到线上跑几天就会发现痛点很多。比如目标网站偶尔返回超时,程序卡住了怎么办;某个页面解析规则写错了,导致大量数据丢失,怎么定位是哪个URL出了问题;爬虫跑到一半挂了,下次能不能从断点继续,自动跳过已经抓过的链接。
这些问题在简单的脚本里都很难优雅解决。你需要自己去搞超时重试、URL去重、失败任务的记录和恢复、线程并发调度,这些活既繁琐又容易出错。pyspider的定位恰好就是把这些爬虫通用能力全部内置进框架里,请求调度、任务去重、失败重试、数据存储、Web监控界面全都打包好了,你只需要专注写页面解析逻辑。
1.2 pyspider的核心架构和设计思路
pyspider的架构可以拆成四个角色:scheduler调度器、fetcher抓取器、processor处理器、result worker结果处理器。四个角色之间通过消息队列通信,各干各的事,互不阻塞。
- scheduler负责维护待抓取任务队列,决定哪个URL在什么时间被抓取,同时负责去重、定时触发、失败重试、优先级调度。
- fetcher负责真正发起HTTP请求,拿到HTML或JSON内容,然后把响应丢给processor。
- processor运行你写的Python脚本,解析页面、提取链接和数据,把新发现的URL提交给scheduler,把结构化数据传给result worker。
- result worker负责把最终数据输出到你指定的地方,比如写数据库、存JSON文件,默认会打印到WebUI的Results页面。
这个架构带来的好处很明显:抓取、解析、调度完全异步解耦。某个页面解析特别慢不会卡住整个爬虫;调度器可以同时管理大量任务队列。这也是为什么pyspider即便很多年不更新,依然有一批忠实用户拿它做中大规模垂直站点的爬取。
1.3 它适合什么样的使用场景
在我实际使用下来,pyspider最适合这几类场景。
第一类是中小规模的垂直站点爬取。目标站点的页面结构相对规律,比如新闻站、博客站、商品分类页、招聘信息、分类信息网站,用pyspider的脚本模型可以很快出活。第二类是站点数量多、需要频繁调整解析规则的场景。因为脚本可以直接在WebUI里改并立即生效,不用重新发布代码,运维成本很低。第三类是自己想学爬虫框架、希望理解调度器如何管理URL任务的新手。
不过pyspider也有不擅长的地方。它本身不支持分布式的直接扩展,虽然可以通过部署多个实例配合消息队列来扩展,但配置复杂度会高很多。如果是要爬取海量数据的超大型项目、或者需要深度定制底层抓取逻辑,Scrapy或是基于协程的新一代爬虫框架可能更合适。所以在选型的时候先想清楚自己的真实需求,不要盲目跟风。
2. 安装与启动:先把环境跑起来
这个部分看起来很简单,但我在帮朋友排查的时候发现,新手在安装启动这一步摔跟头的概率反而很高。很多问题并不是代码写错,而是环境没弄对。
2.1 Python版本和依赖安装
先提醒一句,pyspider的官方版本对Python 3.6以下的兼容性比较好,如果你用的是Python 3.8以上版本,直接pip安装后启动很可能会遇到一个关于async的语法报错,原因是pyspider依赖的一个库在Python 3.8以后把async变成了保留关键字。解决办法有两个:一是使用Python 3.6或3.7环境来跑;二是修改pyspider源码里的一个文件,把所有async函数名改成其他名字,比如asynctask。我更建议直接用Python 3.6或3.7的虚拟环境,省心。
安装命令很简单:
pip install pyspider装完之后在命令行输入:
pyspider如果一切正常,程序会启动一组服务,默认监听本机的5000端口。打开浏览器访问http://localhost:5000,就能看到pyspider的WebUI界面。这一步能看到界面,说明五套服务都跑起来了。
2.2 启动后出现常见报错怎么解决
很多人在pyspider启动阶段会遇到一个关于pycurl的报错。pyspider默认会尝试加载pycurl来做底层抓取,但Windows环境下pycurl的安装经常出问题。如果你只是本地开发调试,这个报错可以不用理会,pyspider会自动回退到普通的urllib库来抓取,功能上差别不大。如果你确实需要pycurl,可以在其官网下载对应的wheel包离线安装。
另一个常见问题是默认端口被占用。启动命令支持修改端口,比如:
pyspider -p 5555这个参数会在很多场景用得上,我一般一台机器上跑多个pyspider实例时会用不同端口区分项目。
2.3 认识WebUI的四个核心页面
pyspider的WebUI是它的灵魂,入门阶段你必须先把这个界面上的东西看懂,否则后续写脚本调试会一头雾水。
顶部导航栏分别指向四个主要页面:
- Dashboard仪表盘:展示所有爬虫任务的状态,包括运行中、待抓取、失败任务数,是整个pyspider的总控台。
- Active Tasks活动任务:展示当前正在抓取解析的任务队列,可以看到每个任务的URL、状态、优先级、已尝试次数。
- Results结果页:展示已抓取解析后的数据结果,默认以列表形式展示每条数据。
- Scripts脚本管理:存放和编辑所有爬虫脚本,可以新建、修改、删除任务。
这四个页面对应了pyspider的四个核心能力:任务管理、实时任务监控、数据查看、脚本编辑。第一天用的时候,把鼠标在这几个页面之间来回点几遍,基本就能建立直觉认知。接下来我详细讲一下脚本管理页面,因为这是和代码打交道最多的地方。
3. 核心概念与脚本模型:吃透这三个回调函数
pyspider的脚本逻辑围绕一个继承了BaseHandler的类展开,类里的方法会在对应的事件时机被框架自动调用。理解了这几个方法,你就理解了pyspider脚本的全部逻辑。
3.1 从on_start到index_page再到detail_page
一个最基础的pyspider脚本结构长这样:
#!/usr/bin/env python # -*- encoding: utf-8 -*- from pyspider.libs.base_handler import BaseHandler class Handler(BaseHandler): crawl_config = { 'itag': 'v1', 'timeout': 20, 'max_retries': 3, } def on_start(self): self.crawl('http://example.com/news/', callback=self.index_page) def index_page(self, response): for item in response.doc('a[href^="http://example.com/news/"]').items(): self.crawl(item.attr('href'), callback=self.detail_page) def detail_page(self, response): return { 'url': response.url, 'title': response.doc('h1').text(), 'content': response.doc('.content').text(), }on_start是任务的起点,爬虫启动时会被调用一次,这里一般填写种子URL,也就是第一个要抓的页面。self.crawl()是核心方法,告诉调度器“我需要抓这个URL,抓完之后用哪个回调函数解析”。
index_page通常用来处理列表页或者导航页。它接收response对象,里面封装了服务器返回的页面内容。通过response.doc可以直接使用类似jQuery的语法解析页面,选择节点、提取属性。我在index_page里做的事很简单:找出页面中所有新闻链接,逐个交给self.crawl,并指定回调函数为detail_page。
detail_page负责处理内容页,把解析后的数据作为一个字典返回。这个返回值会自动被result worker接收,最终显示在Results页面。上述代码中我提取了标题和正文内容。
这三个回调函数构成了pyspider中最核心的循环:从入口URL出发,在列表页发现新链接,把新链接推给内容页解析,解析后返回数据。理解了这个闭环,基本就理解了pyspider的脚本模型。
3.2 response对象的常用操作
写pyspider脚本时,response对象是你打交道最多的东西。它会自动封装好HTTP响应、最终的URL、状态码和页面内容。我常用的属性有这些:
response.url:当前页面的最终URL。如果网站发生了302跳转,这里的值就是跳转后的地址。response.status_code:HTTP状态码,一般在抓取失败时需要查看。response.doc:解析后的PyQuery对象,可以直接用CSS选择器提取节点。response.text:页面的文本内容。response.json:如果返回的是JSON数据,直接用这个属性解析成Python字典。
response.doc是pyspider最方便的地方。比如要提取一个链接的href和文本,可以这样写:
for a in response.doc('a').items(): href = a.attr('href') text = a.text()items()会遍历所有匹配的节点,每个节点支持继续用CSS选择器查询子节点。这种链式写法在写爬虫规则时效率很高。
3.3 自定义callback链的妙用
三个回调函数只是最基础的用法。pyspider允许你在任意回调里继续提交新请求,形成一个多级回调链。比如有的网站列表页和详情页之间还隔着一个中间跳转页,或者详情页中有分页内容,都可以用这个方法链搞定。
def index_page(self, response): for item in response.doc('.list a').items(): self.crawl(item.attr('href'), callback=self.middle_page) def middle_page(self, response): # 某些页面经过一段JS跳转后才到真实地址 real_url = response.doc('#redirect-url').text() self.crawl(real_url, callback=self.detail_page)这种多回调链的思路,可以处理非常复杂的页面关系。同时,self.crawl还支持传递自定义参数,例如:
self.crawl(url, callback=self.detail_page, save={'category': 'news'})通过save参数可以把当前页面的上下文信息一并交给下一个回调,在detail_page里用response.save['category']取出来。这个功能在做数据归类时特别实用。
4. WebUI操作与任务调度:从创建到调试的全流程
pyspider WebUI本身就是一个完整的爬虫管理平台。以前写爬虫总是在终端和代码编辑器之间来回切换,现在所有操作都集成在浏览器里了。
4.1 新建项目并填入脚本模板
在Scripts页面点击Create按钮,输入一个项目名称,然后编写脚本。pyspider会自动生成一个包含on_start函数的模板。第一次调试时,可以先不写完整解析逻辑,只是把on_start里填入一个可以访问的URL,点击界面底部的Run按钮,看看请求是否成功。
WebUI左侧是脚本编辑区,右侧是调试操作区。Run按钮负责跑一次on_start或者当前选中的回调函数;Follows按钮会显示当前回调提交的所有新的self.crawl任务;Result按钮显示当前回调返回的数据,这三个按钮是调试中的高频操作。
4.2 理解任务状态:只有五种状态
pyspider的任务状态管理很有特色。每个URL在调度器中都有对应的任务状态,WebUI中用不同的颜色和标签标记。这里列一个速查表:
| 状态 | 含义 | 常见原因 |
|---|---|---|
| TODO | 待抓取 | 刚提交的新任务,还未被调度器执行 |
| RUNNING | 抓取中或解析中 | fetcher正在请求URL,或者processor正在执行回调 |
| SUCCESS | 成功 | 页面抓取并解析完毕,回调正常返回 |
| FAILED | 失败 | 抓取超时、连接错误、解析异常等 |
| PAUSED | 暂停 | 任务被手动暂停或限速 |
理解这五种状态是排查问题的关键。如果遇到大量FAILED任务,重点检查目标网站是否反爬、请求头配置是否正确、网络是否稳定。如果发现一堆TODO任务一直没有执行,可能是调度器限制了并发,或者任务队列里积压的任务太多。
4.3 rate与burst参数:控制并发抓取速率
在WebUI界面右上角,有rate和burst两个参数。很多入门教程对这两个参数提都不提,但它们直接决定了爬虫抓取的速度。
rate表示每秒请求数,是长期平均速率;burst表示突发流量上限。当rate=1, burst=3时,表示平均每秒只发送1个请求,但短时间最大可以突发到3个并发请求。实际使用中,对普通站点建议先设置rate=1, burst=3,观察目标网站的反应再逐步调高。把这个概念理解好,可以避免因为抓取速度过快而触发反爬策略。
调试时可以把rate设成0,这样调度器不会限制速度,任务会在短时间内迅速跑完,适合快速验证解析逻辑。但确认逻辑没问题后,一定要恢复合适的限速参数,做一个有礼貌的爬虫。
4.4 任务优先级与定时抓取
self.crawl方法支持指定priority参数,数字越大优先级越高。这个参数在处理某些关键页面时很有用,比如发现了一个新的种子URL,希望它比其他待抓取任务提前执行,就可以设置一个较高的priority。
另一个常用特性是self.crawl(url, callback=self.detail_page, auto_recrawl=True),这个参数可以让pyspider在任务成功后根据一定的策略重新抓取。配合crawl_config里的itag参数,可以实现增量抓取的效果。比如爬新闻网站时,希望每小时自动检查首页是否有新文章,这个机制比外部用cron定时触发整个爬虫要方便得多。
5. 实战演练:爬一个简单的分页站点
理论讲了一大堆,该上手了。我选一个非常常见的场景:爬一个分页展示的文章列表站点,每页有若干篇文章标题和链接,点进去能看到正文。这类结构在各种内容型网站中几乎处处可见。
5.1 实战场景定义与目标
假设目标站点是某个示例博客站,首页URL是http://example.com/news/,文章详情页URL格式为http://example.com/p/12345.html。列表页有翻页链接,下一页的URL格式为http://example.com/news/page/2/。目标是抓取所有文章标题、URL、正文内容,以及该文章所属的页码区间。这个场景用来演示分页处理、列表解析、详情解析三项能力。
5.2 完整脚本示例与详细说明
下面是一份可以运行的脚本,我把注释写详细一点:
#!/usr/bin/env python # -*- encoding: utf-8 -*- from pyspider.libs.base_handler import BaseHandler class Handler(BaseHandler): # 全局抓取配置 crawl_config = { 'timeout': 15, # 每次请求的最长等待时间 'max_retries': 3, # 请求失败最多重试次数 'itag': 'v20250101', # 版本标记,修改后可以触发增量更新 'headers': { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', } } def on_start(self): # 从第一页开始抓取 self.crawl('http://example.com/news/', callback=self.index_page) def index_page(self, response): # 解析列表页中的文章链接 for a in response.doc('div.article-list a[href^="http://example.com/p/"]').items(): self.crawl(a.attr('href'), callback=self.detail_page) # 处理翻页:找到下一页按钮的链接 next_link = response.doc('a.next:contains("下一页")') if next_link: self.crawl(next_link.attr('href'), callback=self.index_page) def detail_page(self, response): return { 'url': response.url, 'title': response.doc('h1.article-title').text().strip(), 'content': response.doc('div.article-content').text().strip(), 'publish_time': response.doc('span.time').text().strip(), }on_start从第一页开始。index_page里先提取所有文章详情链接交给detail_page,然后查找下一页链接并继续交给index_page处理。这里用了一个CSS选择器限定div.article-list a[href^="http://example.com/p/"],目的是过滤掉站内导航、广告等无关链接。
这样一个循环接一个循环,就能把整站文章全部抓完。详情页解析时,返回的字典会成为一条数据。如果某些字段拿不到,返回None也不会导致程序崩溃,只是数据里对应字段为空。
5.3 在WebUI上的完整调试步骤
在实际操作中,我会按照下面这个顺序来调试,保证每步都确认没问题再进入下一步。
第一步,新建项目并粘贴脚本,在on_start函数所在行点击Run按钮,观察右侧Log窗口。确认请求的URL返回了200状态码。
第二步,点击Follows按钮,查看index_page解析出的所有链接。如果发现列表没抓对,检查CSS选择器是否匹配目标页面结构,这一步是调试效率最高的位置,因为所有链接都展示在界面前,一眼就能看到问题。
第三步,选中某个详情页链接,点击Run按钮。这一步会直接执行对应的回调函数并得到Result,不需要真的重新发起请求。
第四步,确认Result输出的字典字段完整后,点击Run大规模执行整站任务。
调试过程中有个小技巧:在index_page的self.crawl里临时加一个'priority': 10参数,可以让下一步需要调试的详情页面优先被执行,避免在大量任务里翻找某一条链接。
5.4 数据入库前的字段清洗建议
pyspider的result默认只在WebUI的Results页面展示,但实际项目中通常希望数据落到自己的数据库里。一种简单的做法是覆写on_result方法:
def on_result(self, result): if result: # 在这里执行数据清洗和入库操作 print(result)你可以在这里直接写MySQL、MongoDB的存储逻辑,也可以把数据推送到消息队列。pyspider不限制你在这个方法里做什么,但值得提醒的是,不要在on_result里做耗时过长的操作,否则会阻塞result worker。如果数据量很大,建议先把结果写入MQ缓冲,再异步交给下游存储服务。
6. 常见问题与排查技巧:这些坑我替你踩过了
再顺的框架也有让你头疼的时候。这一节我把自己在pyspider实际使用中遇到的高频问题统一整理一下,按照问题现象、可能原因、解决思路来写。
6.1 页面能打开但解析不到数据
这个问题的典型现象是:在浏览器中明明能看到目标数据,pyspider抓到的页面却一片空白或者缺少节点。原因八成是目标网站使用了JavaScript动态渲染,数据并不是在HTML源码里直接给出的。
pyspider支持通过PhantomJS抓取JavaScript渲染后的页面,配置方法是在crawl_config里设置:
crawl_config = { 'js_fetch_interval': 1, 'js_fetch_timeout': 20, 'phantomjs': True, }但这里要泼一盆冷水:PhantomJS官方已经停止维护,用它应付简单的JS渲染场景还可以,遇到现代前端框架(比如Vue、React)渲染的复杂SPA应用,依然可能抓取不到内容。遇到这种情况,我通常改用直接调目标网站的接口API,往往比生啃页面快得多。
6.2 大量任务FAILED怎么定位
任务失败无外乎网络层面、解析层面、反爬层面。首先在Active Tasks页面点开一条FAILED任务,查看详细的错误信息。如果显示connect timeout,说明网络不通或者目标站响应太慢,可以调大timeout参数,或者检查自己的网络环境。如果显示HTTP 403或HTTP 429,说明目标站识别出爬虫身份或请求频率过高,考虑修改请求头、增加down_time等待,或者更换代理IP。
比较隐蔽的失败原因是解析过程中抛了Python异常。pyspider会把异常信息记录在任务详情里。这类问题大多出在某个页面结构和预期不一致,比如None.text()调用导致的AttributeError。解决办法是在解析方法里对节点做存在性判断,例如:
title = response.doc('h1').text() if title: title = title.strip()这样即使节点不存在,也只是数据为空,不会拖垮整个任务。
6.3 任务积压不执行怎么办
明明提交了成百上千个任务,但看Active Tasks却发现大部分处于TODO状态,没怎么动。这通常是调度器的速率配置问题,检查WebUI右上角的rate参数。如果rate设置得很小,比如0.01,平均每100秒才抓一个URL,那任务自然会像蜗牛一样慢。如果想加速,把rate设为较大的值或者设成0即可。
另外,pyspider默认的并发抓取数受内部配置影响,如果你的服务器是低配机器,同时运行大量任务时,fetcher可能会成为瓶颈。这种场景下可以先把rate调低,再观察CPU和内存占用,逐步找出合适的一组参数。
6.4 增量抓取与重复URL的处理
pyspider在调度层面默认对相同URL做去重。同一个on_start重复执行,相同URL不会创建重复任务。但如果你希望重新抓取同一批URL,需要修改itag配置,强制让调度器认为是新任务。比如脚本里itag设为v20250101,更新站点上线后想全量刷新数据,改成v20250102,所有URL会被当成新任务重新入队。
如果只是想针对部分URL强制重抓,可以在self.crawl时追加额外的参数,例如:
self.crawl(url, callback=self.detail_page, force_update=True)这样的任务不会被调度器去重逻辑拦截,会直接重新抓取。
6.5 脚本调试完但一重启就丢失
pyspider的WebUI里的脚本默认存储在本地数据库中,正常情况下不会丢失。但如果你在Run之前没有点击页面上的保存按钮,或者浏览器缓存异常,可能会遇到脚本丢失的情况。规避方法是:写脚本时定期点击保存,同时本地自己也留一份代码文件。pyspider的数据库文件默认存放在项目的data目录下,备份整个目录就能备份所有脚本和任务记录。
这里再分享一个更省心的习惯:每次修改脚本后,把关键版本同步到一个git仓库。这样即使WebUI内部数据出现了问题,随时可以从代码仓库恢复。
6.6 关于pyspider维护状态的一点个人体会
pyspider确实已经很长时间没有大版本更新了,它的作者目前的工作重心也不在这个框架上。因此遇到一些很新奇的JavaScript渲染、HTTP2协议、现代TLS指纹层面的反爬手段,pyspider会显得力不从心。但它的核心设计理念仍然值得学习,尤其是任务调度、状态管理、回调模型这些思路,在很多新框架里或多或少都能看到影子。
如果你只是做垂直站点的数据采集、内部系统爬取自己公司的页面、或者想尽快搞一个带监控界面的爬虫Demo,pyspider依然是非常顺手的选择。如果你确定要长期做大规模、强对抗的爬虫项目,那可以把pyspider当入门,再逐步过渡到Scrapy或者自研框架。
就我个人而言,每次用pyspider还是会习惯性打开WebUI,看着那些任务从TODO变成SUCCESS,心里特别踏实。它的简单直接,恰好是很多复杂框架所缺失的品质。前面提到的那些代码和配置,都是我实际项目中用过验证过的。你照着操作时如果哪一步卡住了,先把任务状态和页面源码打开翻一翻,八成问题都藏在细节里。最后再分享一个小技巧:写完脚本正式跑全量之前,先限制rate小范围抓取二三十个页面,检查一下数据质量,确认字段没有丢失、编码没有乱码,再放开速度进入全量阶段。这一条习惯帮我避免过很多因为解析规则写错导致的数据事故。