ARTICLE DETAIL

资讯详情

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

Python爬虫实战:从请求策略到增量存储的全链路设计

Python爬虫实战:从请求策略到增量存储的全链路设计 搞Python爬虫的人十有八九都是从“我想把那个网站的数据抓下来”开始的。我自己也是这样一个又一个项目磨过来的从最开始用requests硬怼到后来用Scrapy做分布式采集中间踩过的坑比吃过的盐还多。今天这篇就结合我实际做过的数据采集项目把Python爬虫技术的设计思路、核心环节、存储方案以及调试心得一次性聊透。不管你是刚入门准备写第一个爬虫还是已经在和数据采集打交道但总被反爬、编码、存储问题折磨这篇文章应该都能给你一些用得上的东西。先说清楚一件事爬虫不是“拿着requests把网页GET下来”就完事了。一个真正能上线的采集项目要面对的是请求策略、解析方式、数据清洗、增量存储、异常恢复这一整条链路。任何一个环节掉链子后面全白搭。我自己见过太多人写爬虫只在乎“能不能抓到”完全不管“抓下来之后怎么办”结果数据堆在内存里一崩全没或者抓了一半因为IP被封直接断掉之前的进度全丢。这些都是可以提前设计避免的。1. 先想清楚再动手爬虫项目的整体设计思路1.1 到底要采集什么数据动手写代码之前先问自己三个问题要什么数据从哪个页面来拿到之后存到哪里这三个问题看似废话实际上决定了整个项目的技术选型。比如你要抓的是静态网页的标题和发布时间那用requests加BeautifulSoup就够了连Scrapy都嫌重但如果你要抓的是需要登录、需要滚动加载、还有复杂反爬的站点那就得考虑Selenium或者Playwright甚至要上代理池。我习惯把目标数据先写成一个字段清单标清楚字段名、类型、示例值、来源页面。这不光是给自己看的更是为了后面存储表结构和清洗逻辑做准备。别嫌麻烦这一步省掉之后写代码时你一定会反复回头改反而更慢。还有一个容易被忽略的点数据量级。你是抓一百条做个分析还是每天抓几十万条长期运行量级不同方案完全不同。如果只是小批量直接单线程循环就行如果量大就必须考虑并发、队列、断点续抓和分布式调度。很多人一上来就上Scrapy结果连基本的robots协议都没搞清楚纯属杀鸡用牛刀。1.2 技术选型requests、Scrapy还是Selenium做技术选型别只看谁的教程多要看场景是否匹配。我总结过一条很朴素的判断线静态页面、接口直接返回JSON、没有复杂反爬优先用requests。简单、轻量、调试方便适合大多数数据采集任务。页面结构复杂、需要深度提取多种字段或者要抓取整站大量数据用Scrapy。它能帮你把请求调度、并发控制、去重、管道存储都封装好不用自己重复造轮子。页面是JavaScript动态渲染、点击无限加载、内容在浏览器里才有上Selenium或者Playwright。这类方案本质上是用浏览器去渲染虽然慢但能拿到真正渲染后的DOM。接口有签名、参数加密、需要逆向分析这就超出普通爬虫的范畴了通常需要结合抓包和JS逆向对调试能力要求更高。我自己做数据采集项目时八成以上的场景用requests就能解决。真正需要Selenium的往往是数据藏在“查看更多”按钮后面或者整个页面就是SPAHTML里什么都没有。遇到这种情况先别急着上浏览器自动化试着找一下页面背后的XHR接口很多时候数据早就通过JSON返回了只是你没发现。这个习惯能帮你省下大量渲染等待时间。1.3 关于“能跑就行”和“能维护”很多新手写完爬虫跑通一次数据落地就觉得自己完事了。但真正的工作是从“跑通”之后才开始的。网站改版、字段变化、接口参数失效、服务器加反爬任何一点都可能导致你第二天起床发现任务跑了一晚上全在报错。所以我强烈建议在项目初期就留出“可维护性”的余量。比如所有解析规则尽量集中在一个配置文件里请求参数单独封装成函数存储逻辑和抓取逻辑解耦日志尽量详细。这样就算网站改版你只需要改一小块配置而不是全盘重写。这算是经验之谈我早期写爬虫就吃过维护的亏一个三五天的小项目被硬生生改成了长达两个月的“追着网站更新跑”的苦差事。2. 请求环节的核心细节从头部到代理2.1 请求头不是随便抄的requests抓页面的第一步是构造请求头。很多人直接从浏览器开发者工具里复制User-Agent就完事偶尔被反爬拦截了还不知道为什么。实际上浏览器发出的请求除了User-Agent还有一大堆附加头比如Accept、Accept-Language、Referer、Origin、Cookie。服务器完全可以通过这些头的一致性来判断你到底是真人还是脚本。最简单的做法是先把浏览器请求头完整复制过来然后逐个试看哪些是非必需的。比如有些站点只要User-Agent不是默认的python-requests就会放行但有些严格点的会校验Referer和Origin少了就直接403。遇到这种情况就得把整个请求头都带上。另外Referer的用法值得多说一句。很多接口或详情页会做防盗链校验你从哪个页面跳转过来。你直接访问接口可能403但带上详情页的URL作为Referer就能正常返回。我在采集图片素材时就经常遇到这种场景如果不带Referer图片链接全是403带了就通。这个细节很多人不知道调试时容易绕远路。2.2 超时和重试必须做不然就是给自己埋雷请求最容易出现的两类问题一是网络波动导致连接挂起二是服务器响应慢但没断开白等半天。如果不设置timeoutrequests默认会一直等下去爬虫看起来像“卡死”了。我见过有人写代码不设超时结果程序挂了一晚上什么都没抓到。正常的做法是给每个请求设置超时一般建议connect设为3秒read设为5秒到10秒具体看目标网站的响应速度。超时之后还要配合重试。重试不是盲目地重发最好加上指数退避比如第一次失败等1秒第二次等2秒第三次等4秒最多重试3次。这样可以避免在目标服务器已经过载的情况下继续施压。代码大概长这样import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry Retry(total3, backoff_factor1, status_forcelist[500, 502, 503, 504]) adapter HTTPAdapter(max_retriesretry) session.mount(http://, adapter) session.mount(https://, adapter) response session.get(url, timeout(3, 10), headersheaders)这套配置比手动写for循环去重试要规范得多而且HTTPAdapter还能自动管理连接池对并发请求很友好。2.3 代理池和IP封禁的应对只要采集的量稍微上去一点就逃不开IP被封的问题。网站的反爬一般有个阈值短时间请求次数超过阈值轻则让你输入验证码重则直接封IP。应对的办法无非几个降低请求频率、使用代理IP、维护Cookie有效性。降低频率最直接但效率低用代理池则是更常见的方案。代理IP分免费和付费免费的能用但稳定性差经常连接超时适合小规模测试付费代理稳定性好按量计费适合长期任务。我自己做大规模采集时会单独写一个代理池模块动态从代理服务商接口拉取可用IP用健康检查去过滤失效的再按权重分配给不同任务线程。还有一点很重要代理必须支持HTTPS不然很多网站请求会报证书错误。另外代理IP不要集中在一个地区尽量分散否则照样容易被检测出来。配合上随机延时比如每两次请求之间随机sleep 0.5到1.5秒能大幅降低被识别为爬虫的概率。3. 解析与数据提取正则、XPath还是CSS选择器3.1 每种解析方式的适用场景拿到HTML之后下一步就是从里面把数据抠出来。常用的有三种正则表达式、XPath、CSS选择器。各有优势谈不上谁完全替代谁。正则表达式适合处理文本内容比如从一段JavaScript代码里提取变量值或者从JSONP接口里抽JSON数据。它的好处是灵活可以脱离HTML结构使用坏处是HTML结构一变规则就废维护成本高。我一般只在没有DOM解析库的场景下才用正则。XPath和CSS选择器都是基于DOM树定位元素本质上是等价的但语法不同。XPath更强大能根据文本内容定位比如“查找所有包含‘公告’二字的标题”CSS选择器更简洁适合按class和id定位。在BeautifulSoup里我常用CSS选择器因为写起来干净在Scrapy里我习惯用XPath因为可以直接配合extract()方法提取文本和属性。实际项目里我很少只用一种。比如大框架用XPath定位数据列表列表里某个字段又需要正则清洗掉杂字符两者混合使用很常见。关键是解析结果一定要先打印到终端确认再批量入库不要闭着眼睛跑几千条才发现选择器写错了。3.2 动态页面的处理思路很多网站的数据不是直接写在HTML里的而是通过JavaScript异步加载。你requests拿到的HTML只有骨架内容全在XHR接口里。遇到这种情况第一选择是抓包找接口而不是直接上Selenium。怎么找接口打开浏览器开发者工具切到Network面板勾选Fetch/XHR筛选然后手动触发页面上的“加载更多”或翻页操作观察哪些请求返回了JSON。找到之后直接模拟这个接口请求参数里通常有页码、排序、分页大小改写参数就能拿到不同页的数据。这种接口请求往往比渲染整个页面更快反爬也相对宽松因为网站本身就是在给前端喂数据你只是替前端多要了几次。实在找不到接口或者接口加密了怎么办这时候再考虑Selenium。Selenium能搞定动态渲染但代价是速度和资源占用。每个页面都要开浏览器、渲染、等待网络空闲速度和requests完全不在一个量级。所以我的建议是能用接口就不要用浏览器能用requests就不要用Selenium。3.3 数据清洗与字段对齐解析出来的是原始字符串里面经常夹杂着空格、换行、HTML标签、多余的单位符号。这些脏数据直接入库后面分析和展示时会让你头疼。所以解析完一定要做清洗。我一般会写几个小工具函数import re import html def clean_text(text): if not text: return text html.unescape(text) # 处理 nbsp; amp; 等实体 text re.sub(r\s, , text) text re.sub(r[^], , text) return text.strip() def parse_price(text): if not text: return None match re.search(r([0-9](?:\.[0-9])?), text.replace(,, )) return float(match.group(1)) if match else None清洗规则要跟字段的语义对齐。比如金额字段要处理千分位逗号日期字段要统一格式化成同一种标准本来应该是整数的字段如果解析出来有小数点多半是正则写多了要回头检查。这个过程简单但极其重要很多数据分析的结论不准源头就是清洗没做干净。4. 数据存储与增量采集别让数据烂在内存里4.1 存储方案该怎么选爬下来的数据总不能一直放在内存里。存储方案的选择要看你下一步要干什么。如果只是临时跑一次导出CSV就够了如果要长期积累、做分析就得上数据库。SQLite是我最常用的轻量选择单文件、零配置、支持SQL非常适合中小规模采集项目。需要多人并发写、数据量大到几百GB再考虑MySQL或PostgreSQL。不要小看存储这一步。很多爬虫挂在最后的写入环节比如SQL语句写错、主键冲突、数据编码不对导致整个任务失败。我的习惯是在爬虫脚本里单独建一个storage.py模块把所有写入操作封装成独立的函数web层、爬虫层、存储层互不干扰。这样调试的时候出问题你能很快定位是抓取的问题还是写入的问题。4.2 SQLAlchemy存取爬虫数据的实践用Python操作数据库SQLAlchemy是绕不开的利器。它既支持ORM方式又支持核心SQL表达式还能配合Alembic做表结构迁移。对爬虫项目来说ORM的便利性体现在你不用手写建表语句定义好模型类create_all()直接建表修改字段后也不用担心历史表结构不一致。举个例子我采集文章数据时会这样设计模型from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime Base declarative_base() class Article(Base): __tablename__ articles id Column(Integer, primary_keyTrue, autoincrementTrue) title Column(String(255), nullableFalse) url Column(String(500), uniqueTrue, nullableFalse) content Column(Text) publish_time Column(DateTime) created_at Column(DateTime, defaultdatetime.now) engine create_engine(sqlite:///articles.db) Base.metadata.create_all(engine) Session sessionmaker(bindengine)注意URL字段加了一个unique约束。这个是增量去重的关键后面细说。用SQLAlchemy存爬虫数据最舒服的一点是你不用管SQL拼接。把对象塞进sessioncommit()完事。就算字段很多代码也不会变得又臭又长。如果你用的是Scrapy还可以把它接到Item Pipeline里让每条抓取结果自动走一遍入库流程。4.3 增量采集与去重做长期采集任务最忌讳每次全量重爬。又慢又容易被封不说还会产生大量重复数据。增量采集的核心是去重而去重的关键就是找到一个稳定的唯一标识通常是数据的URL或者业务ID。我常用的两种方式第一种是查库去重在入库前先按唯一键查询是否已存在存在就跳过。简单直接但数据量大时查询开销不小。第二种是BloomFilter布隆过滤器适合百万级以上URL的去重Scrapy也内置了去重器但默认是基于内存的重启会丢需要配合Redis做持久化去重。我自己做小额增量时更喜欢用上面的unique约束加“先插入、冲突就跳过”的方式from sqlalchemy.exc import IntegrityError def save_article(session, article_data): article Article(**article_data) try: session.add(article) session.commit() except IntegrityError: session.rollback() # 已经存在跳过这样不用每次先查询直接用数据库的唯一约束帮你挡住重复记录性能也很好。同时别忘了给发布时间、创建时间加索引后续按时间筛数据会快很多。5. 调试心得我踩过的那些坑5.1 调试工具与日志记录爬虫调试比普通Web开发要麻烦因为程序是长时间跑的你不可能一直盯着终端看。我最依赖的调试工具是logging模块加标准库的pdb简单但够用。logging配置得当能记录下每一次请求的URL、状态码、耗时、错误信息这样出了问题你至少知道是哪个环节挂了。我的日志配置一般是这样的import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(name)s: %(message)s, handlers[ logging.FileHandler(crawler.log, encodingutf-8), logging.StreamHandler() ] ) logger logging.getLogger(__name__)把logger.info(f抓取成功: {url} 耗时 {elapsed:.2f}s)这类信息打出来跑完再看日志整个爬虫的行为全在你的掌控里。以前我不用日志出了问题只能干瞪眼加了日志之后排查效率至少提升一倍。5.2 常见异常与排查思路爬虫运行中遇到的异常种类翻来覆去就是那几类但每类的坑点不一样。请求异常requests.exceptions.ConnectionError、Timeout。先看网络通不通再看代理是否失效。大量超时通常说明代理池质量不行或者目标服务器主动断连。解析异常AttributeError、IndexError。一般是选择器写错或者页面结构变了。遇到这种先把当前页面的HTML保存下来打开看看到底哪里变了别瞎猜。编码异常页面返回值是GBK你用UTF-8解码就会出现乱码。尽量用response.encoding response.apparent_encoding这种动态解码方式或者直接使用response.text前先确认headers里的charset。被反爬拦截状态码403、弹出验证码、响应内容里出现“访问过于频繁”等字样。这种情况优先降低频率、换代理、重置Cookie而不是硬刚。这里有一条很重要的排查思路先定位是“抓不到”还是“解析不对”。“抓不到”的话打印一下response.status_code和response.text的前几百个字符马上就能看出是反爬还是接口失效“解析不对”的话把HTML存到本地用浏览器开一下对比一下选择器往往立刻就知道问题出在哪。5.3 调试时的好习惯调试爬虫我最推崇“小步快跑”的方式。不要一次性写完整个爬虫再运行那样出问题时不知道是哪一步引起的。我的习惯是先写好请求函数单独打印一次请求的返回结果确认没问题再写解析函数对单条数据跑通最后才接循环、入库、增量。每一步都验证过再往下一步走这样能把调试成本压到最低。另外一定做好本地测试。把典型页面的HTML保存成demo.html解析逻辑直接用本地文件测试不依赖网络。这样网站即使临时挂了你的解析规则也能继续验证。我有个长期维护的爬虫就是靠本地抓的三百多个页面HTML来回归测试每次网站改版我先把新版HTML存下来跑一遍快速定位到哪个选择器失效比直接线上调试快得多。还有一个很容易被忽视的细节在写爬虫的过程中一定要写注释尤其是不容易看懂的解析规则。别觉得只有自己能看懂三个月后的你看这段代码很可能就跟看陌生人写的一样。至少把“这个正则匹配的是什么”“这个字段为什么这样清洗”说清楚维护成本会低很多。6. 一个完整的小实战抓取某资讯网站的文章列表聊了这么多理论最后用一个实际的小例子把整个链路串起来。目标是一个资讯网站的栏目列表页页面是PHP渲染的文章标题、链接、发布时间都在HTML里不需要登录。我们要抓最近5页的文章信息存到SQLite里并且做到重复运行不产生重复数据。6.1 目标分析与请求构造先用浏览器打开目标栏目页按F12看Network刷新页面确认文章列表是HTML直接渲染的。于是我们只需要requests加BeautifulSoup就能搞定。请求头带上标准浏览器头和Referer。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, Referer: https://example.com/news/, }这里Referer用的就是栏目页的URL模拟从栏目页点击进入下一分页的操作这样更接近真实浏览行为。6.2 数据解析与入库代码解析部分用BeautifulSoup的CSS选择器定位文章列表项。假设列表页每篇文章的结构为li classnews-item a href/article/12345.html标题文字/a span classdate2025-01-20/span /li解析代码from bs4 import BeautifulSoup def parse_list(html): soup BeautifulSoup(html, lxml) items [] for li in soup.select(li.news-item): link li.select_one(a) date li.select_one(span.date) if not link: continue items.append({ title: clean_text(link.get_text()), url: https://example.com link[href], publish_time: clean_text(date.get_text()) if date else None, }) return itemsclean_text函数就是前面写的那个清洗函数。URL拼接时要注意如果href是相对路径一定要用urljoin处理不然构造出来的链接是错的。入库时直接用前面定义的Article模型和save_article去重函数。分页构造很简单一般资讯站的分页是page2这种base_url https://example.com/news/page/{}/ with Session() as session: for page in range(1, 6): url base_url.format(page) response session_requests.get(url, headersheaders, timeout(3, 10)) items parse_list(response.text) for item in items: save_article(session, item) logger.info(f第{page}页完成共{len(items)}条) time.sleep(random.uniform(0.5, 1.5))注意这里我用的是requests.Session()好处是它能自动复用TCP连接对目标服务器也更友好一点。6.3 运行结果与优化方向跑完代码后控制台会输出每一页的抓取结果日志文件里记录了每个请求的状态。第二次运行同样的脚本由于article.url字段有唯一约束save_article会捕获IntegrityError并跳过已存在的记录输出数量为0。这就实现了幂等采集重复执行不会产生脏数据。这个小案例已经覆盖了请求、解析、清洗、去重、入库、日志这几个核心环节。如果你要把它扩展成大规模任务可以从几个方向优化加入Scrapy框架管理并发把去重方案换成BloomFilter支持更大数据量用Redis队列做分布式任务分配把存储切换到PostgreSQL支持多进程并发写入。整体思路是相同的只是工具层面的升级。7. 最后再分享一些个人体会写爬虫这行心态很重要。你觉得自己是在写代码其实很多时候是在和网站的开发者“较劲”。但做数据采集一定要守住边界只采集公开数据遵守目标网站的robots协议控制请求频率不碰个人隐私和账号密码不带恶意目的。技术本身是中性的怎么用完全是个人选择。在调试这件事上我最大的体会是再多的技巧都比不上“把日志打好”和“把步骤拆细”。遇到问题别急着盲改代码先去看看原始响应和日志大部分情况都能快速定位。爬虫这种长时间运行的程序最怕的是“跑了半天一句话都没有”的黑盒状态。你现在花十分钟把日志和异常处理补好后面能帮你节省十小时。还有一个小技巧想分享给大家写爬虫时准备一个专门的“快速调试脚本”文件里面放几个短小的测试函数比如test_request(url)、test_parse(html_path)这样每次调代码都不用跑到主流程里打断点直接调用测试函数即可验证一个环节。我至今保持这个习惯很多问题都是靠这种小工具快速定位的。做数据采集项目不会永远一帆风顺但只要你把基础环节吃透把调试习惯养好大部分坑都是能提前避开的。希望这篇心得能让你少走一些我走过的弯路也欢迎在评论区分享你自己踩过的爬虫调试翻车现场。
返回列表