尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Scrapy+BeautifulSoup组合实战:从零构建稳定网页抓取系统
做网页数据抓取这一年多我打交道最多的两个工具就是 BeautifulSoup 和 Scrapy。很多人觉得 BeautifulSoup 只是入门级解析库Scrapy 又太复杂不敢碰但把两者放在一起用你会发现比单独用其中任何一个都顺手。这个组合适合三类人一是正在用 requestsBeautifulSoup 写脚本、经常被反爬限制的人二是不想放弃 BeautifulSoup 的写法、但又需要大规模并发抓取的开发者三是想把零散脚本改造成工程化系统的团队。本文以某个虚构商城项目为例从整体设计、环境搭建、核心实现到问题排查完整拆解这套“抓取归 Scrapy、解析归 BeautifulSoup”的思路。1. 整体设计与思路拆解1.1 为什么不是二选一而是组合BeautifulSoup 是一个纯 Python 的 HTML/XML 解析库核心价值是容错解析。它不会因为标签没有闭合、属性值乱写就崩掉而是按照浏览器的方式去修正文档树。Scrapy 则是一个完整的爬虫框架自带调度器、下载器、中间件、Item Pipeline、导出器官方文档里说它“不是一个函数库而是一个服务”。我设计组合方案时最核心的思路是“抓取归 Scrapy解析归 BeautifulSoup”。这句话怎么理解Scrapy 的下载器负责发 HTTP 请求、处理重定向、管理 cookies、控制并发BeautifulSoup 负责拿到 response.body 之后把 HTML 变成一棵可以查询的树。你可以把 Scrapy 想象成一个高效的快递分拣中心而 BeautifulSoup 是一个手工质检员。质检员不负责运输但能处理运输中压得变形的箱子也就是那些语法不规范的 HTML。如果只靠 Scrapy 自带的 Selector 去解析复杂 HTML你得写一长串 XPath 表达式遇到异常还得反复调试如果只靠 requests 去代替 Scrapy你又要自己处理会话保持、重试、并发、去重工作量会翻好几倍。两者结合恰好避开了各自的短板。1.2 什么时候需要这套组合并不是所有项目都需要这套组合。如果你只是抓一个 API 接口返回的 JSON直接用 requests 就够了没必要上 Scrapy如果页面结构非常简单且稳定Scrapy 自带的 css/xpath 选择器也完全够用不需要额外引入 BeautifulSoup。但当你遇到下面这些特征时组合才是最好的选择场景特征单一方案为什么难受组合方案为什么好目标站点 HTML 不规范写 XPath 容易定位漂移正则维护cost高BeautifulSoup 自动修正文档树解析稳定需要复杂的数据清洗Scrapy 选择器只负责定位清洗逻辑还要另写BeautifulSoup 配合 get_text、正则提取清洗一把梭数据量达到万级页面requests 需要手写并发和去重Scrapy 自带调度与并发改造成本低需要长期增量运行脚本容易在几天后面临站点改版而崩防御式解析 日志 管道能快速定位问题我在模拟项目里故意挑了一个结构很乱的商城列表页做测试。页面里标签没有闭合class 属性时不时多一个空格。这种情况下如果硬写 XPath你会怀疑人生换成 BeautifulSoup 后几行代码就能稳定提取因为 find() 系列方法天然容忍这些不规则情况。1.3 组合使用需要留意的边界这套组合也会带来额外负担。Scrapy 自带的 Selector 是惰性解析只有当你调用 extract() 或 get() 时才真正求值BeautifulSoup 则在创建 soup 对象时就把整个文档解析成树。如果页面是几兆字节的巨型 HTML初始化过程会吃掉不少内存所以我不建议对超大响应体无脑直接 BeautifulSoup可以先做切片或者只在局部片段用 BeautifulSoup 解析。另一个边界是性能。BeautifulSoup 搭配 lxml 解析器速度已经够快但百万级页面批量抓取时CPU 占用依然不容忽视。最好的做法是把解析逻辑尽量精简不要在解析函数里塞业务逻辑。换句话说BeautifulSoup 只负责“读出来”之后的数据清洗、入库都放到 Item Pipeline 里处理。2. 核心细节解析与实操要点2.1 3分钟搭建一个 Scrapy 工程安装很简单。我建议用虚拟环境避免把系统 Python 环境弄乱尤其是生产服务器上依赖冲突会让人头疼。python -m venv venv source venv/bin/activate pip install scrapy beautifulsoup4 lxml然后创建工程scrapy startproject demo_spider你会得到这样的结构demo_spider/ ├── scrapy.cfg └── demo_spider/ ├── __init__.py ├── items.py ├── middlewares.py ├── pipelines.py ├── settings.py └── spiders/ └── __init__.py我习惯把解析函数单独放到一个 parsers.py 模块里Spider 里面只负责控制请求流程所有 BeautifulSoup 代码全部收敛到独立文件。这个习惯在我同时维护五六个爬虫时省了太多时间因为页面改版时我只需要改 parsers.py而不需要从头到尾翻 Spider。2.2 在 Scrapy 里注入 BeautifulSoup 的正确姿势不需要修改 Scrapy 核心也不需要自定义下载中间件直接在解析回调里把响应体交给 BeautifulSoup 就行。from bs4 import BeautifulSoup def parse_product(self, response): soup BeautifulSoup(response.text, lxml) title soup.find(h1, class_product-title) if not title: # 记录日志而不是直接抛异常 self.logger.warning(missing title on %s, response.url) return这里有两个小坑。第一response.body是字节BeautifulSoup 会根据 meta charset 去猜编码遇到中文页面大多能猜对但你自己主动控制更可靠。我更推荐response.text因为它是 Scrapy 根据 headers 和 body 推断出来的字符串比直接传字节更可控。第二如果response.text依然乱码就手动覆盖编码response response.replace(encodinggbk)这招在中英文混合站点上特别管用。我踩过不少次页面声明 UTF-8实际返回 GBK如果你不做覆盖后面所有文本全都会是乱码排查起来还特别隐蔽。2.3 CSS 选择器、XPath 和 BeautifulSoup 怎么配合Scrapy 自带的选择器已经很强大CSS 能选的XPath 基本都能选。BeautifulSoup 真正的价值在于它把你从“定位元素”这件事里解放出来。你可以通过属性值、文本内容、相邻节点关系、父子关系组合出非常灵活的条件。想提取一段文字中的所有链接一行搞定links [a.get(href) for a in soup.select(a[href])]如果想提取表格里所有行的第二列用 XPath 写起来很痛苦因为 XPath 索引起始是 1还容易和绝对路径混淆BeautifulSoup 可以直接遍历行再取列逻辑直白得多。我在实际项目中的策略是结构规整、有稳定 class/id 的页面优先用 Scrapy CSS 选择器。结构混乱、需要按属性模糊查询时切到 BeautifulSoup。XPath 只在需要处理复杂轴关系时用。正则用来提取 script 变量里的 JSON、固定格式文本这个不可替代。我并不是因为 BeautifulSoup 比 XPath 快才推荐它而是因为它写起来像写普通 Python 代码调试时可以用 shell 反复试写错了也不会立刻让整个爬虫崩掉。2.4 解析器选型lxml、html.parser、html5libBeautifulSoup 本身不解析 HTML它依赖背后的解析器。常用的是三个html.parserPython 自带无需额外安装但容错能力一般速度最慢。lxmlC 语言实现速度快容错好还支持 XPath生产环境默认选择。html5lib完全模拟浏览器解析最接近浏览器行为但速度非常慢适合处理需要和浏览器渲染完全一致的场景。我默认用 lxml。遇到那种残缺得离谱的页面lxml 解析出的树和浏览器里看到的 DOM 不一致时我才临时换 html5lib 做对比。有一个很容易忽略的细节lxml 在处理某些实体字符时和 html5lib 的结果不一样。如果页面里有大量 这类实体lxml 默认会保留成 Unicode 字符而 html5lib 会直接转成空格。你要在清洗步骤里做归一化别在最底层纠结。3. 实操过程与核心环节实现3.1 从一个模拟商城开始这个项目叫“模拟项目X”抓取一个虚构商城的产品列表。需求是获取每件商品的标题、原价、现价、评价数、商品链接并保存为 CSV。先定义 Items。Scrapy 的 Item 就像字典的增强版可以当作字段规范来理解它让后续管道处理有据可依。import scrapy class ProductItem(scrapy.Item): url scrapy.Field() title scrapy.Field() price scrapy.Field() rating scrapy.Field() reviews scrapy.Field()3.2 Spider 主流程列表页与详情页分离写爬虫的时候我采用手动yield scrapy.Request的方式控制分页这样每一步都有日志出问题能快速定位是在列表页还是详情页。import scrapy from bs4 import BeautifulSoup from demo_spider.items import ProductItem class ProductSpider(scrapy.Spider): name product def start_requests(self): base_url https://example-shop.test/list?page{page} yield scrapy.Request(base_url.format(page1), callbackself.parse_list) def parse_list(self, response): soup BeautifulSoup(response.text, lxml) for card in soup.select(li.card): item ProductItem() link card.find(a, class_product-link) if not link: continue item[url] link.get(href) item[title] card.find(h2).get_text(stripTrue) yield item yield scrapy.Request( urlresponse.urljoin(item[url]), callbackself.parse_detail, meta{item: item} ) next_page soup.find(a, relnext) if next_page and next_page.get(href): yield scrapy.Request( urlresponse.urljoin(next_page[href]), callbackself.parse_list )这里有两个很重要的细节。第一分页链接的 href 可能是相对路径也可能是以//开头的协议相对地址用response.urljoin可以一次性兼容同时也能把列表页里的商品 URL 转成绝对地址。第二列表页里如果某张卡片缺少必要字段我用 continue 跳过它而不是让整个回调抛异常。一个卡片的缺失不应该影响整页数据。3.3 用 meta 传递上下文数据列表页只负责抓标题和链接详情页负责抓价格和评分两个回调之间需要共享数据。Scrapy 的 meta 就是干这个的。meta{item: item}会把 item 随请求带到下一个回调详情页里通过response.meta[item]取回。这个机制看起来简单但有一个隐蔽问题meta 里的 item 如果被多个回调反复传递又在中途被修改字段可能被覆盖或混入脏数据。我的习惯是同一个 item 的生命周期内只允许一个回调修改它并且越早把关键字段赋值越好。详情页只补充详情字段不回填列表页已有字段这样数据冲突的概率会小很多。3.4 详情页的解析与现场纠错详情页里价格字段经常出现“促销价”和“划线价”两种标签。如果直接 find 某个 class可能抓到划线价导致价格录入错误。实际解析时我会先把两个节点都拿出来再做判断def parse_detail(self, response): item response.meta[item] soup BeautifulSoup(response.text, lxml) price_node soup.find(span, class_current-price) old_price_node soup.find(del, class_old-price) if price_node: item[price] price_node.get_text(stripTrue) if old_price_node: item[old_price] old_price_node.get_text(stripTrue) yield item我一直用get_text(stripTrue)而不是get_text().strip()。因为stripTrue会在提取过程中直接去掉首尾空白并把标签内部连续的换行、空格统一处理省下好几行清洗代码。这几个看似微小的写法在几十个字段的项目里会积少成多。3.5 商品字段缺失的兜底方案真实页面上评价数可能为空价格可能缺失。为了不让数据管道报错我针对每个字段单独写防御逻辑而不是用 try/except 包住整个解析函数。一旦用 try 包住整段逻辑某个字段解析失败会导致整页数据被跳过损失太大。def safe_extract(soup, selector_expr, attrNone): node soup.select_one(selector_expr) if node is None: return if attr: return node.get(attr, ) return node.get_text(stripTrue)把这个函数放到 parsers.py 里所有页面解析都复用它。项目跑一段时间后你会发现 70% 的异常都是页面改版导致某个字段找不到。使用这种防御式写法页面改版时最多丢字段不会让整个爬虫崩掉这个设计在长周期运行的项目里非常关键。4. 常见问题与排查技巧实录4.1 编码乱码尤其是中文站点编码问题是最容易踩的坑。页面声明是 UTF-8但实际返回 GBK或者反过来BeautifulSoup 猜编码猜错。在 Scrapy 里response.text会尝试根据 headers 和 body 做解码但有时还是不对。我推荐的排查顺序在 shell 里打印response.headers.get(Content-Type)。检查页面 HTML 里的meta charset。两者不一致时手动覆盖response response.replace(encodinggbk)。不要直接对字节 decode因为你不能保证整页就只有一种编码。我遇到过主页是 UTF-8但某个动态接口返回 GBK 的情况最后只能分别处理。4.2 403 和访问限制先看 robots.txt再做限速说到 403我必须把合规放在最前面。抓取之前先看目标网站的 robots.txt 和用户协议只抓允许抓取的内容。这不是可选项是底线。当遇到 403 时常见原因有三个User-Agent 不够真实、请求频率太高、缺少必要 cookies。我的做法是在 settings.py 里把 USER_AGENT 换成真实浏览器的 UA不要用 Scrapy 默认的Scrapy/x.x。设置DOWNLOAD_DELAY 2或开启AUTOTHROTTLE_ENABLED True让爬虫自动调整请求节奏。如果站点要求登录使用 Scrapy 的 FormRequest 先登录带上登录后的 cookie 再抓取。这里有个常见误解只要 UA 像浏览器就不会被识别。实际站点还会看请求间隔和访问模式。如果你每条请求之间的间隔完全一样且永远从第一页顺序翻到最后一页很容易触发风险控制。官方 AutoThrottle 通常比我手动设置固定延迟更靠谱因为它会根据响应时间动态调整。4.3 页面是动态渲染的HTML 源码里没有数据如果目标页面是 JS 渲染的直接用 Scrapy 请求源码你拿到的基本上是一堆空壳 scriptBeautifulSoup 也救不了你。这时候有两种思路。一种是用 Selenium 或 Playwright 驱动浏览器等页面渲染完再抓。Scrapy 可以集成 Playwright让 spider 发出请求时带上特殊标记下载中间件判断到标记后启动浏览器加载页面等网络空闲后返回 HTML。这种方式适合需要登录态、需要点击“加载更多”按钮的页面。另一种更轻量的思路是找后端 JSON API。很多页面展示全靠 JS但数据其实是从某个接口拉取的。我用开发者工具看 Network 面板找到/api/products这类接口后直接请求它拿到的结构化数据比解析 HTML 干净得多。能走 JSON 就走 JSON这是我在动态页面抓取里最想分享的一条经验。4.4 调试神器Scrapy shell每次写选择器不要直接放进爬虫里反复跑先用 scrapy shell 验证。scrapy shell https://example-shop.test/product/123进入 shell 后你会自动拿到 response可以直接测 CSS 或 XPathresponse.css(li.card a::attr(href)).getall() soup BeautifulSoup(response.text, lxml) soup.select_one(h2.product-title).get_text(stripTrue)我七八成的调试工作都在这里完成很少直接登录服务器跑完整爬虫。因为它能反复试错而且不会真的给目标站点发送太多次请求。这个习惯帮我把调试时间和站点压力都控制在了很小的范围。4.5 常见问题速查表现象可能原因首选排查动作中文乱码页面声明编码与实际编码不一致用 response.replace(encoding...) 覆盖列表页有数据但详情页为空meta 传递被覆盖或详情页结构不同先打印 response.meta[item] 和页面片段连续几条请求返回 503/403请求频率过高或 UA 被识别开启 AutoThrottle检查 robots.txt 合规性页面内容为空白目标数据由 JS 动态渲染检查 Network 里的 JSON 接口或接 Playwright某个字段时有时无页面模板存在多种版本用 safe_extract 做字段级兜底不要 try 包整体爬虫运行很久后突然全崩站点改版导致选择器失效用 scrapy shell 重新分析页面更新 parsers.py这张表基本覆盖了我遇到过的绝大多数生产问题。遇到新问题时我建议先在表格里找相似项没有合适的再往日志系统里加排查信息。5. 工程化与运维建议5.1 增量抓取与去重如果每天重复跑全量既浪费带宽也给站点压力。更合理的方案是增量只抓最近更新的商品。做法可以简单到给 Item 增加一个updated_at字段入库前判断时间也可以在调度器层面用dont_filter控制是否强制抓取。Scrapy 自带基于内存的去重但重启后会清空。想要持久化可以写一个基于 Redis 的去重中间件。字段指纹可以用scrapy.utils.request.request_fingerprint生成也可以自己对 url 加更新时间做 hash。注意生成指纹时要排除参数顺序不同导致的差异最好把 query 参数排序后再计算否则同一个页面会因为参数顺序不同被抓两次。5.2 数据管道和错误隔离我把数据流向拆成两层Item Pipeline 负责清洗和去重一个独立 writer 模块负责写入 CSV 或数据库。这样做的原因很简单数据库暂时不可用时爬虫不应该停止抓取数据可以先落到本地文件做缓冲。Pipeline 的优先级用ITEM_PIPELINES控制数字越小越早执行ITEM_PIPELINES { demo_spider.pipelines.CleanPipeline: 300, demo_spider.pipelines.DuplicatesPipeline: 400, demo_spider.pipelines.WriterPipeline: 800, }我会在 CleanPipeline 里把价格里的货币符号去掉并转成 float在 DuplicatesPipeline 里用集合记录 url在 WriterPipeline 才真正持久化。清洗和写入一旦分开调 bug 时就不用在一个函数里翻来翻去。5.3 监控与报警爬虫跑在后台最怕它悄悄挂了或者被站点限制。我习惯在每个 spider 的 close 事件里统计抓取数量并和上一次运行做对比。如果数量断崖式下降多半是页面结构变了或触发了访问限制如果错误率超过阈值就发一条报警消息到工作群。报警可以很简单不必引入重型监控系统。用CLOSESPIDER_ERRORCOUNT设置最大错误数超过后自动停止爬虫比让它空跑一整晚好得多。我通常只在关键路径上加这种保护不然很容易被正常的小波动误伤。5.4 关键设置项建议配置项建议值原因ROBOTSTXT_OBEYTrue合规第一遵循爬虫协议USER_AGENT真实浏览器 UA避免被简单规则拒绝DOWNLOAD_DELAY1 ~ 3 秒给目标站点留出处理时间AUTOTHROTTLE_ENABLEDTrue自动适应站点响应速度CONCURRENT_REQUESTS_PER_DOMAIN4 ~ 8单域名并发太高容易触发限制LOG_LEVELINFO关键信息可见又不至于刷屏RETRY_TIMES2 ~ 3网络抖动时自动重试这套配置是我跑了很多项目后调出来的基础模板。爬虫最怕的不是慢而是不稳定。宁可配置保守一点让任务多跑几小时也不要因为并发太高导致整个 IP 被限制。5.5 扩展思路多节点分布式当数据量真的很大时单机 Scrapy 撑不住常见的方案是把 Scrapy 和分布式消息队列结合用队列做任务分发。这个方向比较深需要单独写一篇才能讲清楚。我只提醒一点分布式不是银弹它会让日志、去重、调度都变得复杂。如果你的爬虫每天只有几万页面单机加限速完全够用先别急着给自己找麻烦。我见过太多团队迷信分布式结果花了两周搭集群最后发现瓶颈根本不在抓取速度而在数据清洗和存储。抓了这么久的数据我的最大体会是工具选择永远服务于稳定性和可维护性。BeautifulSoup 加 Scrapy 这个组合说不上有多么惊艳的黑科技但它能让我在页面改版时少加几个小时的班。最后分享一个小习惯我每次上线新爬虫都会先用 shell 分析三到五个页面再拿一个小列表页实跑一遍确认字段覆盖率超过九成后才放量。抓数据这件事谨慎一点永远不会错。
RELATED

相关推荐

Spring Boot在线房屋出租系统毕设实战:从设计到部署全解析

Spring Boot在线房屋出租系统毕设实战:从设计到部署全解析

做毕设选方向时,我最后定在了基于Spring Boot的在线房屋出租系统。这个题目看起来常见,但房屋出租天然包含用户、房源、订单、预约几条核心业务线,既能覆盖常规的增删改查,又能往权限控制、状态流转、文件上传、条件检索这些方向做…

📅 2026/10/12 5:02:40
dsh-skill-mcp-panel 排查三连:命令、MCP连接、面板缺失

dsh-skill-mcp-panel 排查三连:命令、MCP连接、面板缺失

1. 项目概述:这不是面板丢了,是技能链断了“面板不见了、MCP 连不上、命令找不到”——这三句话不是故障现象的罗列,而是技能执行链上三个关键节点同时失联的明确信号。我第一次在某跨平台自动化项目中看到这个报错组合时,下意识去…

📅 2026/10/12 4:57:40
需求缺陷闭环:2026年缺陷管理工具选型指南

需求缺陷闭环:2026年缺陷管理工具选型指南

这几年我陆续给团队选过、换过、也亲手放弃过好几款缺陷管理工具,加起来少说也有七八套。说实话,名字换来换去,真正让人窝火的不是“缺陷单长得丑”,也不是“报表导出不够花哨”,而是需求和缺陷之间始终隔着一堵墙&…

📅 2026/10/12 4:57:40
MORE NEWS

更多资讯

📰

数据结构——顺序表细致讲解

耕耘 :C、C、嵌入式技术领域 🔥我的个人主页 ❄️个人专栏:《C语言专栏》 《嵌入式专栏》 《数据结构专栏》 ✨**不要等待机会,而要创造机会!**✨ 📽博主简介: ✨✨一位热爱生活的阳光大男孩.✨✨ 前言 本文系统讲…

📰

小白程序员必看:字节新岗位AI Agent开发火爆,如何精准入行?

字节2027校招新增AI Agent开发岗,行业人才需求同比增长244%,但企业仍不清楚理想候选人标准。文章指出,当前招聘多依赖工具清单(如LangChain、RAG等),但技术迭代快导致筛选失效。建议企业通过测可迁移能力&a…

📰

AI中控与直播伴侣的联动配置和排查思路

四季度开播旺季,不少技术向的读者在搭自播工作台时遇到同一个现象:直播伴侣正常推流,AI 中控也在运行,但两边就是各干各的——话术识别不弹商品,弹幕不自动回复。本文按链路排查的思路,把联动配置和常见断点…

📰

用AI搭建一人调研团队:主编+三个AI工种+两本手册的实操框架

先说个直觉:这个标题看着像段子,但它背后其实是一套特别现实的调研工作流。我从去年开始在某内容团队里反复试“一个人扛下所有调研”的做法,试到后面实在受不了——又要定选题,又要查资料,又要分析趋势,又…

📰

低代码平台岗位管理实战:用户角色权限体系的设计与落地

上一期把报名排课和学员档案理顺之后,整个MBA培训管理系统终于能跑起来了。但我很快发现一个躲不开的问题:教务、班主任、讲师、助教、学员,不同角色都涌进来,总不能给所有人开同一套菜单、同一套按钮。你说一个普通学员能看到“讲…

📰

五大湖生态-经济耦合建模:Python实现水位、污染与渔业协同仿真

简介:本资源是面向2024年美国大学生数学建模竞赛(MCM/ICM)ICM D题——五大湖水资源系统建模与政策分析的深度解析资料包,专为参赛学生、指导教师及环境系统建模初学者设计,聚焦复杂水文-社会耦合系统的建模思路、数据处…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬