ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从BeautifulSoup4到Scrapy:解析库与爬虫框架的选型实战

从BeautifulSoup4到Scrapy:解析库与爬虫框架的选型实战 聊到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默认就能同时发出十几个请求爬完一千个页面只需要几十秒。这种能力用requestsbs4同步一个个请求是做不来的。不过代价是学习曲线更陡。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抠出来。这种一次性的数据量通常不到几百条不需要每天跑也不需要存数据库直接用requestsbs4写个脚本最划算。十几行代码一个文件跑完拿结果扔了就扔了。这种场景如果用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 classnews-list a classtitle href/news/1001Python 3.13 发布/a a classtitle href/news/1002Scrapy 使用技巧/a a classtitle href/news/1003BeautifulSoup4 解析实践/a /div我们的目标是提取每篇文章的标题和完整链接补上站点的根域名。这个任务看着简单但可以清楚展示两种方案在“流程组织”上的差别。一套是同步脚本一套是异步框架两边代码量差不多但后续扩展的空间完全不一样。3.2 用BeautifulSoup4 requests实现先看直接用requestsbs4的写法import requests from bs4 import BeautifulSoup url https://example.com/news headers {User-Agent: Mozilla/5.0} resp requests.get(url, headersheaders, timeout10) 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(stripTrue) 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 对比结果代码结构、运行方式、扩展能力从代码量看两套都很少但本质差别在结构上。requestsbs4是“线性脚本”跟着代码从上往下执行Scrapy是“组件协作”parse函数返回一个yield把结果交给框架继续处理。前者适合一个人临时跑后者适合一个团队长期维护。扩展能力上差距更大。假设现在要求多抓10页、去重、设置下载延时、失败自动重试。bs4方案要手动循环、手动维护集合、手动sleep、手动try/exceptScrapy方案只需要设置DOWNLOAD_DELAY1、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 PipelineScrapy最吸引我的一个模块是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动态渲染。这时候requestsbs4只会拿到一个空壳HTML因为iframe的src是另一个文档而动态内容要等脚本执行完才出现。最简单的排查方法是先在浏览器里打开开发者工具看iframe标签的src直接请求那个地址。如果iframe里的内容也是静态HTML那requestsbs4照样能抓。但如果iframe内部是SPA数据来自XHR接口那就要考虑无头浏览器。Playwright就是很好的选择。Scrapy可以配合scrapy-playwright中间件在框架里异步驱动Chromium甚至可以在抓取过程中等待特定元素出现。这种做法比直接上Selenium更轻也比纯requests方案兼容面广。我用这个组合处理过一个嵌入多个iframe的报表页面先让Playwright等待iframe加载再读取frame内部内容交给Selector解析。要注意的是无头浏览器吃资源尽量只在普通HTTP方案拿不到结果时才启用。工具没有高下只有合不合适。5. 选型建议什么时候用哪个什么组合最舒服5.1 选型决策清单我一般会按几个问题快速判断该用哪种方案。列成清单基本是目标页面数量是不是在几百以内是否只需要跑一次不需要定时是否不需要落数据库纯拿数据是否不需要登录和动态渲染如果全是“是”直接用requestsbs4只要有一条“否”就要认真考虑Scrapy或者ScrapyPlaywright的组合。再看维护者情况如果是一个人临时调研Scrapy的工程结构反而会拖慢速度如果是团队长期维护的数据采集服务框架的项目规范能让别人快速接手。还要看目标网站的反爬强度简单的User-Agent检查requests也能绕需要登录、验证码、滑块那框架里的中间件和插件生态明显更省心。我不太建议“一个方案打天下”工具的切换应该跟着任务复杂度走。5.2 基于场景的组合方案requestsbs4ScrapySelector我用得最顺的组合有两套。第一套是小任务专用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个页面的数据结果光项目搭建和调试花了半天效率极低第二次是用requestsbs4写了一个2小时才能跑完的采集脚本爬到一半进程崩了一切从头开始。后来我总结出经验先拿bs4跑通单页逻辑确认数据结构没问题再由项目规模决定要不要迁到Scrapy。这能避免“结构没摸清就搭框架”的浪费也能避免“数据量大却一直手动爬”的崩溃。另一个心得是不要忽略了robots规范。不少网站在robots.txt里明确禁止爬取某些路径Scrapy默认ROBOTSTXT_OBEYTrue如果你没注意可能明明写得没问题却什么都没爬到。虽然有些实战项目会关掉这个选项但我建议先尊重目标站点的规范。合规采集是长期做爬虫的大前提比任何技术技巧都重要。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抓不到用PlaywrightSplash等动态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请求返回403UA被识别换真实浏览器UA加Cookie/代理池bs4解析速度慢默认解析器换lxml先缩小选择范围再遍历iframe内容为空动态渲染先找iframe真实src再考虑Playwright爬一半崩溃无断点用Scrapy或者自己记已抓URL集合相对链接拼接错误路径合并错误用response.urljoin或urljoin.base这张表是我平时排查问题最常翻的笔记。很多问题其实都有标准答案难的是在踩坑时知道该往哪里查。把这些常见坑记熟接爬虫项目的时候会从容很多。最后说句大实话我这几年做爬虫没有一天只用一个工具。bs4帮我快速试探一个网站能不能抓、结构长什么样Scrapy帮我稳定地把数据落地。工具本身没有优劣关键是你有没有把任务边界看清楚。如果你现在正卡在“选哪个”上我建议先跑起一个requestsbs4脚本让结果跑出来再说。当你发现一个脚本已经开始不够用了Scrapy自然会在那边等你。就按自己的场景来别被框架的名字吓住也别被简单的脚本绑定住。
返回列表