做爬虫开发这些年,我攒了不少学习笔记。最近重新翻看,发现围绕爬虫项目的功能学习,很多零散的知识点其实可以串成一条完整的链路——从requests发请求、selenium处理动态页面,到用sqlalchemy把数据落库,再到分布式爬虫和可视化面板。这篇笔记就是想把这些内容系统梳理一遍,把我自己踩过的坑、验证过的方案、还在用的模板都整理出来,给正在学Python爬虫的朋友一份能直接跟着动手的参考。
这套东西适合什么人来读呢?如果你已经会用Python基础语法,想进一步接触网络爬虫;或者你能写简单的采集脚本,但不知道该怎么处理反爬、怎么设计存储、怎么把项目做得更完整——这篇笔记应该能帮上忙。我不会堆概念,尽量用实际项目里的场景来讲原理,让新手看得懂,让有基础的人也能找到有价值的细节。
1. 爬虫项目的知识链路:从一行请求到一个完整系统
1.1 爬虫项目的四层架构
一个合格的爬虫项目,远远不止“用requests请求一个网址然后打印出来”这么简单。我自己习惯把爬虫项目拆成四个层次来理解,这也是排错和设计时的思考框架。
第一层是数据获取层,负责把目标数据从服务器上拿下来。技术选型通常是requests、httpx这类HTTP客户端,遇到浏览器渲染的页面就需要selenium或Playwright这类自动化工具。第二层是数据解析层,负责从拿到的HTML、JSON等格式里提取出干净、结构化的字段。常用的手段是XPath、CSS选择器、正则表达式,以及针对JSON数据的jsonpath。第三层是数据存储层,负责把解析结果持久化,方便后续使用和分析。我自己用得最多的是SQLAlchemy加上MySQL,搭配Redis做去重和队列。第四层是调度与监控层,负责管理多个任务、多个节点、失败重试和运行情况的可视化,这是项目走向工程化的分水岭。
打个比方,整个流程就像去图书馆查资料:获取数据是找到书架,解析是逐页摘抄,存储是把摘抄卡片归档到抽屉,调度和监控则是在管理“今天查哪几本书、谁去查、查得怎么样了”。很多人学爬虫卡在第一步,以为拿到HTML就等于爬虫学完了,其实后面的解析、存储和调度同样重要,甚至更考验综合能力。
1.2 我常用的技术选型组合
针对不同场景,我会用不同的组合,这里分享一套我自己反复验证过的选型逻辑:
| 数据场景 | 技术组合 | 理由 |
|---|---|---|
| 静态网页、翻页列表 | requests + lxml/BeautifulSoup | 轻量、速度快、写起来简单 |
| 动态渲染页面(点击加载、滚动加载) | selenium / Playwright | 直接操作浏览器,能看到完整DOM |
| App接口、返回JSON的Web接口 | requests + jsonpath | 省去解析HTML的麻烦,字段更规范 |
| 多站点、大规模采集 | Scrapy + 分布式(Redis) | 框架自带调度、去重、并发和扩展机制 |
| 需要给非技术人员看数据 | Flask + ECharts / Grafana | 用可视化面板展示采集结果 |
这套选型的核心思路是:能用轻量方案就不要上重型工具。很多人一上来就学Scrapy,遇到一个小也要用框架;或者一看到页面复杂就上selenium,忽略了也许直接调用底层接口就能拿到数据。正确的做法是先分析页面和数据来源,再用最省成本的方案实现。
2. 请求模块的原理与进阶:requests和它背后的HTTP常识
2.1 requests的请求流程与关键参数
requests是我几乎每天都在用的库,它的设计确实非常人性化。一个最简单的请求只需要几行代码:
import requests url = "https://example.com/api/data" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example.com/" } resp = requests.get(url, headers=headers, timeout=10) print(resp.status_code) print(resp.text)看起来简单,但有几个参数值得展开讲讲。headers是请求头,它能告诉服务器“我是谁、我从哪里来、我想要什么”。服务器经常用请求头来判断请求是否来自真实的浏览器,所以headers的完整性非常关键。单独的User-Agent并不够,很多网站还会校验Referer、Origin、Accept等字段,我建议直接复制浏览器里完整的请求头。
timeout这个参数也不要忽略。没有超时设置的请求,遇到服务器迟迟不响应时,进程会一直卡在那里,整个爬虫都会阻塞。我说的超时是连接超时和读取超时组合,timeout=10代表两者都是10秒。params参数则用于拼接URL查询字符串,requests会自动帮你做URL编码,用起来比手拼字符串舒服得多。
响应对象里,status_code表示HTTP状态码,text是文本内容,json()方法可以直接把JSON格式的响应解析成Python字典。如果目标接口返回的是JSON,resp.json()是高频使用的一行代码。
2.2 提升请求稳定性的几个技巧
第一个技巧是使用Session对象。Session会保存请求之间的cookie,并且复用底层的TCP连接,让多次请求的速度明显提升。登录状态的模拟、连续翻页的会话保持,都离不开Session:
session = requests.Session() session.headers.update(headers) session.get("https://example.com/login") data = session.get("https://example.com/profile").json()第二个技巧是设置重试机制。requests默认不重试,网络抖动或者服务器临时返回5xx错误时,程序会直接抛异常。我一般会通过urllib3的Retry配合HTTPAdapter来实现:
from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry = Retry( total=3, # 最多重试3次 connect=3, # 连接失败重试3次 backoff_factor=1, # 重试间隔 1s、2s、4s 递增 status_forcelist=[500, 502, 503, 504] ) adapter = HTTPAdapter(max_retries=retry) session.mount("http://", adapter) session.mount("https://", adapter)backoff_factor让重试间隔逐步拉长,避免在服务器还没恢复时反复请求,加剧对方压力。还有一个细节:status_code为200不代表数据正确,很多接口会把业务错误放在JSON的code字段里,比如{"code": 50001, "msg": "参数错误"}。脚本里一定要检验业务状态码,不能只看HTTP状态码。
第三个技巧是控制请求频率。爬虫新手最容易犯的错就是在一个循环里无脑快速请求,结果触发对方封禁IP。我习惯在两次请求之间加一点随机延时,尤其避免固定间隔的请求模式。
3. 反爬虫识别和应对思路的安全边界
3.1 常见反爬机制的识别特征
做爬虫一定要懂反爬,不然寸步难行。但首先要明确一个边界:学习反爬的目的是理解网站的安全机制,确保自己的合规采集,而不是去攻击或破坏别人的系统。我下面讲的识别方法,都是用于判断“为什么我的请求被拦了、应该怎么调整自己的代码策略”。
| 反爬手段 | 典型表现 | 识别与应对思路 |
|---|---|---|
| 请求头校验 | 返回403、418 | 对比浏览器请求头,补齐UA、Referer、Accept-Language等 |
| 频率限制 | 请求几次后就封IP或弹验证码 | 降低频率、随机延时、使用代理池 |
| 字体反爬 | 页面文字显示正常但源码是乱码 | 下载页面字体文件,解析映射关系 |
| 数据加密签名 | 接口需要携带加密sign参数 | 分析JS生成逻辑或用自动化浏览器执行 |
| 动态渲染 | HTML源码里没有数据,数据是JS加载的 | 抓接口或改用selenium |
| 验证码 | 出现图形验证码、滑块 | 接入打码平台或设计流程规避频繁验证 |
遇到403时,我习惯先看响应头里的Server字段、页面的标题和返回体。如果是Cloudflare这类防护体系,需要先判断它拦截的层级,但更建议优先考虑合规途径,比如查询对方开放的API、协商合作,或者改用公开的数据源。绕过强防护破解不仅费劲,还有法律风险。
3.2 selenium什么时候才需要上场
有些数据是页面加载后通过ajax请求渲染出来的,用requests直接请求HTML源码,结果什么都没有。这时候有两条路:一条是抓包找到后台接口,直接请求JSON数据;另一条是用selenium启动真实浏览器,让页面自动加载完再取内容。
我大部分时候优先走接口路线。用浏览器开发者工具打开Network面板,刷新页面,找到返回数据的那条XHR请求,分析它的参数和返回结构,然后用requests直接请求。这样重量轻、速度快,也不容易被识别为自动化工具。只有当接口请求的过程特别复杂,比如需要大量JS计算签名、加密参数,或者数据藏在webpack打包的代码里难以分析时,我才会上selenium。
如果真的要用selenium,有几条经验供参考。第一,建议用无头模式加防检测参数启动,减少资源占用:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options) driver.get("https://example.com") html = driver.page_source driver.quit()第二,webdriver的特征非常明显。正常浏览器会有一些自动化测试相关的全局变量,--disable-blink-features=AutomationControlled可以隐藏一部分特征,但不是万能的。第三,selenium的运行速度远慢于requests,一个页面动辄几秒钟,采集量大时非常痛苦。我的建议是:把requests当作主力,selenium作为攻坚手段,两者结合才能平衡效率和成功率。
4. 解析与字段提取:把网页变成干净数据
4.1 三类解析方式的选择
拿到HTML或者JSON后,下一步是提取字段。解析方式我分成三类来选。
正则表达式适合从一段文本里抓固定模式的内容,比如提取所有链接、手机号、日期。它的优点是性能高,缺点是写起来容易出错,HTML结构一变就要重写。
XPath是我在解析HTML时的第一选择。它通过路径表达式定位节点,配合lxml解析库,速度快、写法直观。几个高频表达式:
| 目的 | XPath表达式 |
|---|---|
| 选取所有class为title的节点 | //*[@class="title"] |
| 选取第一个div下的所有a标签 | //div[1]//a |
| 按文本内容匹配 | //span[contains(text(), "关键词")] |
| 取节点属性值 | //img/@src |
| 取节点文本 | //h1/text() |
BeautifulSoup的语法对新手更友好,不需要记太多表达式,用find和find_all配合属性就能解析,适合小项目和快速原型。大型项目中我会倾向lxml+XPath,因为性能和可维护性更好。
jsonpath则用于JSON数据的取值。接口返回的嵌套数据结构非常深,用data["result"]["list"][0]["title"]这种写法遇到层级稍深就非常啰嗦,jsonpath可以用类似查询表达式的语法直接定位:
from jsonpath import jsonpath data = { "code": 0, "data": { "list": [ {"title": "爬虫学习笔记", "views": 1000}, {"title": "数据存储实践", "views": 800} ] } } titles = jsonpath(data, "$.data.list[*].title") print(titles) # ['爬虫学习笔记', '数据存储实践']4.2 接口爬虫与页面爬虫的取舍
在爬虫功能学习里,辨析“接口爬虫”和“页面爬虫”是很关键的一课。页面爬虫直接请求网页拿到HTML再解析,实现起来直观,缺点是数据往往分散在复杂的标签里,而且页面结构一改就失效。接口爬虫直接请求网页背后那个返回数据的接口,通常返回JSON,结构清晰、字段完整。
怎么发现接口?用浏览器开发者工具,切换到Network面板,刷新页面,重点关注XHR或Fetch类型的请求记录。逐个点击查看响应数据,找到包含目标内容的那个请求。它的URL、请求方法、参数、请求头就是这个页面背后的“数据源”。
很多异步加载页面用这种思路就能轻松解决。比如某些电商评论页,滚动后出现更多评论,实际是发送了一个带page参数的POST请求。直接构造这个请求,比等浏览器滚动再抓取要快一个数量级。接口爬虫有个前置条件:需要分析出参数规律、必要的签名算法等。分析不出来的时候,再用selenium作为兜底。两条路线都要会,这是爬虫开发的基本功。
5. 数据存储:SQLAlchemy连接采集与持久化
5.1 SQLAlchemy的核心价值
数据被解析出来之后,如果只打印在终端里,那就什么用都没有。我第一次做爬虫项目时就吃了这个亏,辛辛苦苦爬了几万条数据,一段代码异常退出,全部丢失,只能重跑。从那以后我养成了“边爬边存”的习惯,存储层成了整个项目最不能省的一环。
SQLAlchemy是Python生态里最常用的ORM(对象关系映射)库。它的核心价值在于:你可以用Python类来定义数据表的结构,用操作对象的方式来读写数据库,不需要手写大量SQL语句。对于爬虫项目来说,这意味着代码更清晰、可维护性更高,同时它内置了连接池,能自动管理连接,不会因为频繁请求数据库而耗尽连接数。
5.2 一个可以直接用的存储示例
下面是我在爬虫项目里常用的模板化写法,以MySQL连接为例:
from sqlalchemy import create_engine, Column, String, Integer, DateTime from sqlalchemy.orm import declarative_base, sessionmaker from datetime import datetime Base = declarative_base() class Article(Base): __tablename__ = "articles" id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(200)) url = Column(String(500), unique=True) source = Column(String(50)) create_time = Column(DateTime, default=datetime.now) engine = create_engine( "mysql+pymysql://root:password@localhost:3306/spider_db?charset=utf8mb4", echo=False ) Base.metadata.create_all(engine) Session = sessionmaker(bind=engine)写入数据时,我用session.add()和session.commit()组合:
with Session() as session: article = Article(title="爬虫学习笔记", url="https://example.com/1", source="example") session.add(article) session.commit()这里有两个实操经验。第一,去重不能只靠应用层判断,要依靠数据库的唯一约束。我在url字段上加了unique=True,重复插入会抛异常,再用try捕获异常判断数据是否已经存在,这样即使多个爬虫节点同时运行,也不会插入重复数据。第二,批量插入用bulk_save_objects或add_all。一条条commit在数据量大时性能惨不忍睹,改成批量提交一次性写入,速度能提升几十倍。
如果数据需要更新而不是插入,比如评论数、价格这类字段每天变化,我习惯先用session.query按唯一键查一下,存在就更新,不存在就新增。这种“更新优先、插入兜底”的策略更贴近真实业务场景。
5.3 存储方案的横向对比
不同数据形态,存储方案的选择也不一样。我总结过一张选型表:
| 存储方案 | 适合的场景 | 优势 | 不足 |
|---|---|---|---|
| MySQL + SQLAlchemy | 结构化数据、关系明确、需要统计查询 | 事务可靠、查询灵活 | 表结构固定,变更成本高 |
| Redis | 去重指纹、请求队列、临时计数器 | 性能极高、支持过期 | 不适合复杂查询和持久化 |
| MongoDB | 字段不固定的嵌套数据 | 无schema、写入快 | 事务和复杂关联弱 |
| CSV / Excel | 临时交付给非技术人员 | 人可直接打开 | 数据量大时打开慢、无法并发 |
举例来说,爬取京东评论这种字段相对固定的数据,我用MySQL;做分布式爬虫的去重和任务队列,我用Redis;爬取页面上的半结构化内容,比如不同来源的新闻信息,字段随时可能增加,我会考虑MongoDB。没有最好的存储,只有当前场景下最合适的存储。
6. 分布式爬虫和可视化:学习项目走向工程化
6.1 分布式爬虫到底在解决什么问题
单机爬虫的瓶颈很明显:一台机器的带宽、CPU、内存、IP资源都有限,碰到成百上千万的数据规模就跑不动了。分布式爬虫的思路,是把任务分给多台机器上的多个爬虫节点同时执行,每个节点处理一部分任务,最终汇总到同一个数据存储里。
理解分布式爬虫,核心是弄懂三样东西:任务队列、去重集合、调度器。任务队列负责存放待抓取的URL,多个节点从中取任务。去重集合记录已经处理过的URL,确保同一个地址不会被两个节点重复抓取。调度器负责协调整个流程,决定哪个请求由哪个节点执行、什么时候执行。
Scrapy结合Redis,是最常见的Python分布式爬虫方案。Scrapy自身是单机框架,通过scrapy-redis将调度器和去重组件替换为Redis后端,就可以实现多机协同。每个节点的爬虫从Redis队列里读取URL,抓到的数据各自写入公共的存储,整个系统就像多个员工同时从工单池里领活干。
这里要补充一个常被混淆的概念——异步。爬虫里的异步通常指asyncio或Scrapy的异步并发机制,它跟分布式是两回事。异步解决的是单机内“等待网络响应时去做别的事”,分布式解决的是“多机协同干活”。一个用协程提高单机吞吐量,一个用集群扩展整体规模,二者可以叠加使用。初学者先把单机异步学好,再考虑分布式,顺序不要反了。
6.2 可视化面板从哪入手
爬虫跑起来之后,怎么看它跑得怎么样?总不能在终端里等日志一条条滚动吧。可视化面板就是干这个用的。我的做法是:把采集结果写入MySQL后,用Flask写一个简单的Web应用,从SQLAlchemy模型里读取统计。请求量、成功失败率、入库条数、抓取趋势这些关键指标,用ECharts或者Grafana画成图表,整个项目就有了“仪表盘”。
可视化的核心不是为了好看,而是为了让项目状态可以被直观感知。一次抓取任务在哪个时间段失败了?哪个栏目入库速度突然变慢?通过图表一眼就能看出来。哪怕只是一个简单的曲线图,也比纯日志高效得多。
我推荐从Flask + ECharts入手,因为它对Python开发者最友好。后端提供JSON接口返回统计数据,前端用ECharts渲染图表,三天左右就能搭出一个够用的监控面板。之后想再扩展功能,比如邮件告警、定时任务,也都是围绕这套框架自然生长出来的能力。能写爬虫只是开始,能把爬虫系统清晰地展示出来,才是工程化的标志。
7. 从热门场景看爬虫项目的设计思路
7.1 游戏战绩查询类项目
网上经常能看到“Python爬虫查王者战绩”这类需求。游戏战绩数据本质上来自某个JSON接口,爬虫的工作就是请求这个接口、解析战绩数据、展示胜率和场次。这种项目很适合用来练习“接口爬虫”的完整套路,但它也有明显的合规风险——战绩属于个人数据,未经授权的采集和展示应当避免。
我的建议是,这类需求当作学习案例来分析是可以的,从功能设计上理解它“从接口取数、用sqlalchemy入库、再用Web页面展示”的链路价值,但不要真的去采集大量玩家数据或提供公开查询服务。学习爬虫的目的是掌握技术原理,不是去触碰别人隐私。
7.2 内容社区与电商评论的共性难点
热搜词里出现的快手爬虫、抖音爬虫、小红书爬虫、京东评论爬虫、天猫爬虫,都是典型的工程挑战,但它们的难点其实有共性。
第一个共性是签名与参数加密。短视频平台、内容社区在接口层通常会做签名校验,缺少对应的加密参数,请求会被拒掉或返回假数据。应对方式有两种,一是从JS代码里逆向出签名算法,二是用selenium或Playwright执行真实浏览器。前者技术门槛高,后者性能受限。对绕过高强度风控体系的行为,我一直强调要慎重,避免触碰法律底线;个人学习更推荐寻找开放接口或公开数据集。
第二个共性是数据频率与封禁控制。电商评论这类接口对单位时间内的请求次数异常敏感,触发风控后轻则弹验证码,重则限制账号。我处理京东评论时,会刻意把请求间隔控制在1到3秒之间的随机值,并限制单账号并发数量,宁可慢一点,也不要断了数据源。
第三个共性是合规边界。影视类爬虫涉及版权问题,“红果短剧爬虫”“爬虫源影视”这类项目一定要明确用途——如果目的是做个人学习或内容检索,注意不要传播下载内容;如果是商业使用,请务必走正规合作渠道。steam、立创商城这类以公开数据为主的平台,爬取公开的销量、价格、库存信息用于学习分析,相对友好,但同样不能高频干扰服务。珍爱、粉笔这类包含用户个人数据和付费内容的平台,切割掉,不要碰。
说到底,这些场景考察的都是同一套基本功:分析数据来源、处理反爬、设计存储、控制节奏。平台换来换去,核心链路不变。遇到一个新目标,我会先把数据流画清楚,再决定用requests还是selenium、存MySQL还是Redis、要不要上分布式。这个分析能力,才是爬虫项目学习中真正值钱的东西。
8. 学习路线、高频报错与排查技巧
8.1 我的爬虫学习路线建议
经常有读者问我,爬虫怎么学才系统。我基于自己的学习经历和带新人的经验,给出一条实操性强的路线:
- 打底HTTP和HTML:不用全懂,但要明白请求方法、状态码、请求头、Cookie、网页结构这些基本概念。
- 掌握requests + 解析:用requests请求目标网站,用lxml和正则提取字段。这时候做一些单页面的小项目,比如爬取自己的博客列表。
- 接入SQLAlchemy存储:把解析出的数据写入MySQL,学会建表、写入、去重、查询。到这里,一个完整的单机爬虫项目基本成型。
- 处理动态页面:学习浏览器开发者工具的Network抓包,寻找接口;必要时用selenium处理复杂的渲染页面。
- 进阶Scrapy与分布式:用Scrapy重构项目,感受框架带来的调度、并发、去重能力;再引入Redis实现分布式扩展。
- 工程化收尾:加日志、加可视化、加定时任务,让爬虫可以被监控、可维护。
这条路线走下来,大概需要三个月到半年,取决于个人投入程度。不建议一上来就啃Scrapy源码或者分布式架构,先跑通一个完整的小项目比什么都重要。
8.2 高频报错速查表
我把这些年遇到的问题整理成一张速查表,每个问题都是我或身边朋友真实踩过的坑:
| 报错或现象 | 可能原因 | 排查方向 |
|---|---|---|
TimeoutError | 网络不稳定、目标服务器响应慢、没设超时 | 增加timeout参数、设置重试机制 |
| 403 / 451 | 请求头被识别、IP被限制 | 检查UA和Referer、降低频率 |
| 返回乱码 | 编码识别错误 | 用resp.apparent_encoding检测编码 |
| 解析列表一直为空 | 选择器表达式不对、数据是JS渲染的 | 用开发者工具确认节点结构、改抓接口 |
| JSON解析报错 | 返回的不是JSON而是HTML | 先打印resp.text看内容 |
Duplicate entry | 主键或唯一键冲突 | 捕获异常、改用更新策略 |
| SSL证书报错 | 目标站点证书异常 | 考虑verify=False,但需注意安全风险 |
| 内存持续增长 | 数据积压未入库、循环中累积对象 | 边爬边存、及时释放变量 |
举一个排查实例。我之前写短视频平台爬虫,接口一直返回空数据,状态码却是200。先打印resp.text看了一眼,发现返回的是一个JS脚本而非JSON。顺着这条线分析,才确认是缺少签名参数。这类问题用“先看响应内容、再判断拦截层级”的思路,一般都能快速定位。
还有一类问题不是报错,而是数据质量问题。比如解析字段时拿到了带空白和换行的文本、时间格式不统一、金额字符串无法求和。我都会在解析层做一层清洗:统一用strip()去掉空白、规范时间格式、把数字字符串转成数值。不要指望原始数据是干净的,清洗是爬虫流程里的必要环节。
最后分享两条个人经验
踩过几次坑之后,我对爬虫学习最大的体会是:笔记和项目要一起成长。只记知识点不跑项目,记完就忘;只跑项目不总结,下次遇到同样问题还是不会。我每完成一个爬虫项目,都会把链路拆解出来,记录哪些环节用了什么方案、为什么这么用、踩了什么坑。这篇笔记本身就是这么来的。
另外一个建议是,爬虫学习和平台选择有关,但也不完全有关。今天的热门目标可能是短视频平台,明天又可能是另一个新平台。如果你只盯着某一个平台的反爬细节,平台一改版就会很被动;但如果你掌握了HTTP原理、页面解析、数据存储、工程化设计这套通用能力,换什么目标都能举一反三。这也是为什么我反复强调,项目里的requests、selenium、sqlalchemy这些知识点,最终要沉淀成你分析问题和设计系统的思维框架。
最后再分享一个小技巧吧。爬虫项目的代码结构从一开始就分模块来写——请求模块、解析模块、存储模块、调度模块彼此独立,接口定义清楚。哪怕是一个几百行的小项目,也保持这个结构。养成这个习惯之后,你会发现往项目里加新功能、修bug、换目标平台都轻松得多。等到项目变大了,这种清晰的分层简直能救命。