聊到Python网络爬虫,绕不开Scrapy和BeautifulSoup4这两个名字。我最早接触爬虫时也纠结过:到底学哪个?后来真做了几年爬虫项目,才明白这俩压根不在一个维度——BeautifulSoup4是一把趁手的解析工具,Scrapy是一整套能自己跑的爬虫工程框架。放在一起比,就像问“扳手和工具箱哪个好用”,会越比越糊涂。这篇不打算列一堆浮在表面的对比表格,而是用真实项目的选型逻辑,把两者的定位、适用场景、代码写法、组合套路讲透。给正准备入门的朋友,也给出过方案却踩过坑的同学一点参考。
1. 先搞清楚两个库到底是谁
1.1 BeautifulSoup4:一个方便解析HTML的“工具箱”
BeautifulSoup4(后面简写bs4)说到底是个 HTML/XML 解析库。它不负责发起请求,不负责调度URL,也不管失败重试,唯一的本事就是把一段杂乱的HTML字符串喂进去,然后让你用find()、find_all()、select()这类方法,轻松把想要的数据拽出来。我刚入门时,绝大多数爬虫就是用requests拿到网页源码,然后交给bs4解析,三两行就能取到想要的内容。
但正因为只是解析,它的“配套”都得你自己搭。遇到登录要自己带Cookie,遇到反爬要自己拼User-Agent和Referer,遇到抓一半断网要自己记进度。一次抓几十个页面没问题,抓几千个页面就会开始体会到什么叫“代码里塞满杂活”。可以这么说:bs4是把好用的镊子,但工具袋里其他东西都得自己准备。
这里要插一句,bs4解析时背后有多种解析器。默认的html.parser很方便,但速度一般;换上lxml或html5lib之后,容错性和性能会有明显差别。我一般直接用lxml,因为它能忍受不规范的HTML,而且解析速度在几个解析器里很能打。别小看这个选择,后面抓大量页面的时候,解析器速度能直接影响整轮抓取的时间。
1.2 Scrapy:一个能独立跑完整爬虫流程的“工程框架”
Scrapy不是“一个解析库”,而是一个完整的异步爬虫框架。它把整个抓取流程都替你编排好了:Spider定义抓哪个URL、怎么跟进;Scheduler负责调度请求;Downloader负责下载页面;Downloader Middleware处理请求/响应;Item Pipeline负责清洗、验证、存储;Item Loader辅助结构化提取。你只需要关心自己的业务部分,其他工程问题框架兜底。
所以同样抓一个页面,Scrapy的起点是“写一个爬虫类”,而不是“写一个脚本”。数据要存哪、爬多少层、并发多少个请求、怎么限速、怎么去重,这些都用配置和组件解决。而且Scrapy天生是异步架构,基于Twisted,默认就能同时发出十几个请求,爬完一千个页面只需要几十秒。这种能力,用requests+bs4同步一个个请求是做不来的。
不过代价是学习曲线更陡。Scrapy有项目结构、settings.py、items.py、middlewares.py、pipelines.py这些概念,刚接触会觉得绕。它不是那种拿到就能立刻跑起来的“小玩意”,而是需要你按照框架套路组织代码的“工程体”。好处是:一旦项目跑顺了,维护和扩展都轻松很多。
1.3 两者的本质区别:解析库 vs 爬虫框架
你可以在Scrapy里用bs4吗?可以。你可以在一个纯bs4脚本里自己模拟Scrapy的调度逻辑吗?也可以,但那等于把Scrapy重新发明一遍。两者的本质区别是:bs4只解决“从HTML中提取数据”这一个环节,而Scrapy解决“从URL到最后数据落库”的整条链路。定位根本不同,所以从来不是谁替代谁的问题。
很多教程会把“bs4 vs Scrapy”误写成“解析库PK框架”,这个对比本身就不公平。真正该做的是:小任务用bs4,快速拿结果;大工程用Scrapy,省心省力。我在实际项目里甚至会在Scrapy的Spider里用bs4解析response.body,再丢给Pipeline,虽然更常规的做法是用Scrapy自带的Selector,因为Selector基于lxml,在复杂表达式和大规模提取上效率更高。理解这一点后,你再去看任何一篇对比,都不会被带偏。
2. 为什么不能简单说“哪个更好”——解析真实使用场景
2.1 典型场景:单页抓取、数据量小、快速脚本
先说我手上最常碰到的场景:临时想看某个网站上的公开信息,比如抓当前页面的公告标题、抓某个接口返回的JSON,或者从几百行产品列表里把SKU抠出来。这种一次性的数据量通常不到几百条,不需要每天跑,也不需要存数据库,直接用requests+bs4写个脚本最划算。十几行代码,一个文件,跑完拿结果,扔了就扔了。
这种场景如果用Scrapy,你得先scrapy startproject,然后建items.py、spiders/xxx.py、调整settings,最后还要scrapy crawl运行。光这个初始化成本就已经超过任务本身的复杂度了。有些朋友一上来就听说“Scrapy是大杀器”,结果抓一个页面愣是搭了一小时环境,这就是拿高射炮打蚊子。
所以我的原则很明确:脚本优先。当脚本里出现“我要管很多URL”“我要处理登录”“我要定时重跑”“我要落库”这类需求时,再考虑升级到Scrapy,而不是一开始就全副武装。判断标准不是“哪个库更有名”,而是“这个任务到底需要多少工程能力”。
2.2 典型场景:大规模分布式抓取、规则化网站
反过来,当目标是从一个资讯网站抓取过去几年的历史文章,或者抓电商全站分类下的商品数据,动辄几万到几十万个URL,就完全不是bs4能轻松搞定的了。首先是并发:requests同步循环,一秒最多几个并发,抓一万个页面可能要几小时;Scrapy默认就能并发十几个,配合异步下载,速度能拉高一个量级。
其次是工程化能力:全站抓取会产生大量待抓URL,需要去重、需要排列优先级、需要控制访问频率、需要断点续抓,Scrapy内置的调度器、DupeFilter和AutoThrottle把这些功能都做了。你只需要在Spider里让请求源源不断交给框架,框架自己决定什么时候抓、抓完以后把数据往哪个Pipeline丢。这类“规则化网站”正是Scrapy的主场——结构清晰、URL有规律,适合CrawlSpider这类基于规则的爬虫自动跟链接。
我参与过的一个历史数据迁移项目,就是用Scrapy按日期分页抓了十几万条行业资讯,中间网络抖动自动重试,抓断了下一次继续,数据全部经Pipeline写进数据库。这种体量的任务,用bs4从零手写,维护成本会非常可怕。选对框架,不仅省时间,关键是心里有底。
2.3 常见误区:用Scrapy杀鸡,用bs4抗震
我见过最典型的误区有两个。一个是“听说Scrapy厉害,做什么都用Scrapy”,结果抓3个网页还要写一堆配置,最后被异步调试折腾得弃坑;另一个是“一直用bs4,觉得Scrapy太复杂”,结果遇到几百个URL时用单线程脚本慢慢磨,网络卡一下整个程序就停了。这两个极端都是因为没有先评估任务体量。
还有一些同学混淆了“能用”和“好用”。bs4当然也能写爬虫,也能多线程,也能加随机延时,但这些工程能力全部要自己造轮子。Scrapy当然也能解析HTML,但你要明白它的解析器是Selector,不是bs4,别指望把BeautifulSoup函数直接搬进Scrapy的response对象里。最务实的做法是:按场景选工具,bs4负责快速解析,Scrapy负责工程化抓取,两者不冲突,甚至能组合。下一节我就用同一个任务,把两种写法都跑一遍,你一看就明白差距在哪。
3. 实操对比:同一个任务,两种写法
3.1 目标案例:抓取新闻列表页标题和链接
为了更直观,我用一个假设的新闻列表页来演示。页面结构简化成:
<div class="news-list"> <a class="title" href="/news/1001">Python 3.13 发布</a> <a class="title" href="/news/1002">Scrapy 使用技巧</a> <a class="title" href="/news/1003">BeautifulSoup4 解析实践</a> </div>我们的目标是提取每篇文章的标题和完整链接(补上站点的根域名)。这个任务看着简单,但可以清楚展示两种方案在“流程组织”上的差别。一套是同步脚本,一套是异步框架,两边代码量差不多,但后续扩展的空间完全不一样。
3.2 用BeautifulSoup4 + requests实现
先看直接用requests+bs4的写法:
import requests from bs4 import BeautifulSoup url = "https://example.com/news" headers = {"User-Agent": "Mozilla/5.0"} resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") for a in soup.select("div.news-list a.title"): title = a.get_text(strip=True) link = "https://example.com" + a["href"] print(title, link)整个过程非常直白:请求、解析、提取。注意我用了soup.select(),它是CSS选择器语法,比find_all更顺手;同时解析器指定lxml,而不是默认的html.parser,速度更快。多数小任务到这一步就结束了,最后把结果写入CSV或打印出来就行。
但这套代码的边界也很明显:如果要抓10页,你要在外层再套一个循环去构造URL;如果要保存断点,得自己维护一个已抓集合;如果要并发,得额外引入concurrent.futures。所有“工程”部分都得你亲手加,加得越多,代码越臃肿。它非常适合快速摸清数据结构和验证可行性,但不适合直接作为生产级采集系统。
3.3 用Scrapy实现
再看Scrapy的实现。先建项目(也可以临时用runspider跑一个单文件,但标准流程是项目化):
scrapy startproject news_spider在spiders/news.py里写爬虫:
import scrapy class NewsSpider(scrapy.Spider): name = "news" start_urls = ["https://example.com/news"] def parse(self, response): for a in response.css("div.news-list a.title"): title = a.css("::text").get() link = response.urljoin(a.attrib["href"]) yield {"title": title, "link": link}你没有看错,核心提取逻辑比bs4更短,因为response.css直接返回Selector对象,Selector底层是lxml,而response.urljoin自动处理相对路径。运行:
scrapy crawl news -O news.json数据直接落成JSON文件。如果后续要存数据库,只需要定义一个Item类,再加一个Pipeline处理保存,不用改Spider逻辑。这种“关注点分离”在项目变大之后尤其舒服。
3.4 对比结果:代码结构、运行方式、扩展能力
从代码量看,两套都很少,但本质差别在结构上。requests+bs4是“线性脚本”,跟着代码从上往下执行;Scrapy是“组件协作”,parse函数返回一个yield,把结果交给框架继续处理。前者适合一个人临时跑,后者适合一个团队长期维护。
扩展能力上差距更大。假设现在要求:多抓10页、去重、设置下载延时、失败自动重试。bs4方案要手动循环、手动维护集合、手动sleep、手动try/except;Scrapy方案只需要设置DOWNLOAD_DELAY=1、DUPEFILTER_CLASS默认开启去重、从Request参数传meta控制翻页,以及配置RETRY_TIMES。再往后,如果要接登录、代理池、验证码识别,Scrapy的中间件架构几乎都是现成插槽,而bs4方案等于从头重写。任务越复杂,框架的价值越明显。
4. 核心差异拆解:请求调度、并发、中间件、管道
4.1 网络请求与并发模型:Twisted异步 vs requests同步
为什么Scrapy能同时发那么多请求?因为它底层的网络引擎基于Twisted,采用异步非阻塞模型:一个线程可以同时等待多个连接的响应,而不需要为每个请求单独开一个线程。而requests是同步库,调用get()时线程会一直阻塞到收到响应。网络IO等待是爬虫最大的耗时来源,所以同步模型在效率上天然吃亏。
你当然可以用bs4配合aiohttp把异步补齐,但这等于自己管理事件循环、信号量、重试,水也不浅。Scrapy把这些异步编程细节完全藏起来,你写Spider时感受不到Twisted的存在,只要把Request交给框架即可。这种设计极大降低了写高并发爬虫的门槛。需要强调:不是bs4慢,而是搭配它的同步请求模型慢。有的朋友说“我用了bs4怎么还是慢”,回头一看requests还是一个一个来,那就不要怪解析器了。
4.2 数据管道与存储:Item Pipeline
Scrapy最吸引我的一个模块是Item Pipeline。你在Spider里yield一条条字典或Item,然后定义几个Pipeline组件,按顺序处理:清洗字段、校验必填项、去重,最后写进MySQL或MongoDB。每个组件只干一件事,这样数据逻辑和抓取逻辑分离,非常好维护。
举个例子,如果希望结果里每条记录都有抓取时间,我可以在Pipeline里加一个process_item方法,给item补充crawled_at字段,再传给下一个Pipeline。也可以在Pipeline里丢给Redis做去重。这些能力不是框架强加给你的,而是提供一个标准位置,让你把工程上必要的事放进合适的位置。反观bs4方案,这些代码只能跟抓取逻辑混在一起,要么写在for循环里,要么干脆不写,等数据脏了再处理,成本更高。
4.3 去重、调度、限速等工程能力
一个爬虫项目真正花时间的往往不是解析,而是工程细节。URL重复抓了没?访问太频繁被封没?网络超时重试没?断点续抓怎么记录?Scrapy内置了去重指纹,默认基于URL指纹判断;调度器支持先进先出或优先级队列;AutoThrottle可以自动根据响应时间调整并发,避免把目标站点压垮。这些功能在settings.py里写几行配置就能启用。
反观自研方案,你要维护visited集合、要写多线程Queue、要做随机延时、要实现断点记录。不是不能做,而是这些逻辑跟业务爬虫纠缠在一起,最后代码可读性很差。我有时候看别人的bs4爬虫,一个文件六七百行,里面有一半在干“调度器”“去重表”“超时重试”的活,而这些在Scrapy里都是默认配置。能动手不等于该动手,把精力花在真正要采集的数据上,才是正解。
4.4 动态iframe页面处理:Scrapy + Playwright 的配合
说完传统请求,再聊一个高频搜索词:scrapy playwright 动态 iframe。很多网站页面把核心数据放在iframe里,或者用JS动态渲染。这时候requests+bs4只会拿到一个空壳HTML,因为iframe的src是另一个文档,而动态内容要等脚本执行完才出现。
最简单的排查方法是先在浏览器里打开开发者工具,看iframe标签的src,直接请求那个地址。如果iframe里的内容也是静态HTML,那requests+bs4照样能抓。但如果iframe内部是SPA,数据来自XHR接口,那就要考虑无头浏览器。Playwright就是很好的选择。Scrapy可以配合scrapy-playwright中间件,在框架里异步驱动Chromium,甚至可以在抓取过程中等待特定元素出现。这种做法比直接上Selenium更轻,也比纯requests方案兼容面广。
我用这个组合处理过一个嵌入多个iframe的报表页面:先让Playwright等待iframe加载,再读取frame内部内容交给Selector解析。要注意的是,无头浏览器吃资源,尽量只在普通HTTP方案拿不到结果时才启用。工具没有高下,只有合不合适。
5. 选型建议:什么时候用哪个,什么组合最舒服
5.1 选型决策清单
我一般会按几个问题快速判断该用哪种方案。列成清单基本是:目标页面数量是不是在几百以内?是否只需要跑一次,不需要定时?是否不需要落数据库,纯拿数据?是否不需要登录和动态渲染?如果全是“是”,直接用requests+bs4;只要有一条“否”,就要认真考虑Scrapy或者Scrapy+Playwright的组合。
再看维护者情况:如果是一个人临时调研,Scrapy的工程结构反而会拖慢速度;如果是团队长期维护的数据采集服务,框架的项目规范能让别人快速接手。还要看目标网站的反爬强度:简单的User-Agent检查,requests也能绕;需要登录、验证码、滑块,那框架里的中间件和插件生态明显更省心。我不太建议“一个方案打天下”,工具的切换应该跟着任务复杂度走。
5.2 基于场景的组合方案:requests+bs4,Scrapy+Selector
我用得最顺的组合有两套。第一套是小任务专用:requests负责下载,bs4负责解析,lxml当解析器,结果写CSV或Excel。这一套上手快,几乎没有学习成本,适合验证数据是否能抓、页面结构长什么样。第二套是生产采集:Scrapy负责调度、下载、重试、去重,Selector负责解析,Pipeline负责数据落库,需要动态页面时加scrapy-playwright。这两套基本覆盖了我95%以上的爬虫任务。
有个小提醒:别在Scrapy的Spider里强行用bs4解析,除非面对特别复杂的HTML结构。Selector已经能完成绝大多数提取,而且Scrapy的response.css和response.xpath返回的Selector对象可以直接在回调中继续传递,效率高也习惯成自然。真要用bs4,也得通过response.text再喂给BeautifulSoup,相当于多绕一层,能不用就不用。
5.3 我自己踩过的坑和心得
刚开始我犯了两次最典型的错误。第一次是用Scrapy抓一个只有5个页面的数据,结果光项目搭建和调试花了半天,效率极低;第二次是用requests+bs4写了一个2小时才能跑完的采集脚本,爬到一半进程崩了,一切从头开始。后来我总结出经验:先拿bs4跑通单页逻辑,确认数据结构没问题,再由项目规模决定要不要迁到Scrapy。这能避免“结构没摸清就搭框架”的浪费,也能避免“数据量大却一直手动爬”的崩溃。
另一个心得是不要忽略了robots规范。不少网站在robots.txt里明确禁止爬取某些路径,Scrapy默认ROBOTSTXT_OBEY=True,如果你没注意,可能明明写得没问题却什么都没爬到。虽然有些实战项目会关掉这个选项,但我建议先尊重目标站点的规范。合规采集是长期做爬虫的大前提,比任何技术技巧都重要。
6. 常见问题与排查技巧实录
6.1 问题:Scrapy爬虫没有输出?检查robots和配置
很多人第一次跑Scrapy,发现日志里啥都没抓出来。先别怀疑代码,按顺序排查:查看LOG_LEVEL,确认不是INFO级别压制了输出;检查settings.py里ROBOTSTXT_OBEY是不是True,如果是,而目标网站robots禁止了对应路径,Scrapy会直接跳过;再确认下载中间件里没有不小心把自己写的拦截逻辑加进去。还有一个常见坑是User-Agent,默认的“Scrapy”字符串很容易被网站直接拦掉。
这些配置都属于“看起来不起眼但影响全局”的细节。我在排查这种问题时,习惯先打开DEBUG日志,再看response.status是不是200或403。如果是403,多半是被反爬识别了,需要换UA加上Cookie或代理。如果response.status正常却没有字段,那就得回头检查CSS选择器,尤其注意等号两边引号有没有写对。日志是爬虫最好的朋友,别怕刷屏。
6.2 问题:BeautifulSoup解析速度慢?
如果你明明用了bs4却感觉解析很慢,第一反应是看一下解析器。默认html.parser在部分平台上性能一般,换成lxml之后往往肉眼可见变快。其次是避免在循环里反复调用find_all(),比如:
# 慢:每次循环都查整个文档 for a in soup.find_all("a"): ...如果你只需要页面里的几个区块,可以先用soup.select("#main .items")缩小范围,再遍历局部对象。另一个思路是能不用正则就不用正则,bs4的文本匹配虽然慢一点,但可维护性比正则高得多。真要追求极限性能,就把解析从bs4换成lxml自带的etree,或者直接改用Scrapy的Selector。bs4的优势是易用,不是极限性能。
6.3 问题:动态iframe抓不到?用Playwright+Splash等
动态iframe是爬虫新手最容易卡住的一关。遇到这种情况,先按三步走:第一步,在浏览器里右键查看iframe的src,看这个地址能否直接在requests里打开;第二步,如果能打开,直接用requests请求这个src,再用bs4解析即可;第三步,如果该地址返回空页面或需要JS渲染后才能有内容,就只能用无头浏览器方案。Playwright或Selenium都能做,只是在Scrapy框架里更推荐scrapy-playwright。
细节上要注意,iframe加载有延迟,直接访问往往拿不到内容。Playwright里可以用frame_locator定位,或者等待某个内部元素出现。Scrapy的playwright中间件支持在Request里传meta提供给脚本执行和等待条件。控制好等待时间,别用死sleep,能让抓取稳定很多。
6.4 问题速查表
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| Scrapy爬到0条 | robots被禁 | 查看robots.txt,合理合规调整ROBOTSTXT_OBEY |
| 请求返回403 | UA被识别 | 换真实浏览器UA,加Cookie/代理池 |
| bs4解析速度慢 | 默认解析器 | 换lxml,先缩小选择范围再遍历 |
| iframe内容为空 | 动态渲染 | 先找iframe真实src,再考虑Playwright |
| 爬一半崩溃 | 无断点 | 用Scrapy或者自己记已抓URL集合 |
| 相对链接拼接错误 | 路径合并错误 | 用response.urljoin或urljoin.base |
这张表是我平时排查问题最常翻的笔记。很多问题其实都有标准答案,难的是在踩坑时知道该往哪里查。把这些常见坑记熟,接爬虫项目的时候会从容很多。
最后说句大实话:我这几年做爬虫,没有一天只用一个工具。bs4帮我快速试探一个网站能不能抓、结构长什么样,Scrapy帮我稳定地把数据落地。工具本身没有优劣,关键是你有没有把任务边界看清楚。如果你现在正卡在“选哪个”上,我建议先跑起一个requests+bs4脚本,让结果跑出来再说。当你发现一个脚本已经开始不够用了,Scrapy自然会在那边等你。就按自己的场景来,别被框架的名字吓住,也别被简单的脚本绑定住。