说实话,我自己最开始学爬虫的时候,就是被一堆术语搞懵了:requests、Selenium、Scrapy、反爬、异步、分布式……每个词单独看都能懂,合在一起就不知道它们到底在项目里扮演什么角色。后来把几个爬虫项目完整做下来,回头再看,才发现所谓“爬虫项目”其实就干四件事:把网页数据拿下来、把有用信息摘出来、把数据存好、再把整个过程管好。这篇学习笔记,就是围绕爬虫项目的功能学习展开的,适合刚入门想搭一个完整爬虫的人,也适合那些已经会写单脚本但总感觉缺了工程化的朋友。我会按照自己实际做项目时的顺序,从最基础的请求讲到数据存储、动态页面处理,再聊到分布式和可视化这些进阶功能,最后把踩过的坑一起整理出来。
1. 爬虫到底在学什么:先把项目功能地图画清楚
1.1 核心功能拆解:爬虫项目不只是“抓网页”
很多人以为爬虫就是“发个请求,拿到HTML,用正则匹配一下”,其实这只是冰山一角。一个完整的爬虫项目,至少要包含四层功能:
- 请求层:负责把目标网站的数据“取回来”。最简单的用requests,复杂一点要处理Cookie、Headers、签名参数,再复杂就要上Selenium这类浏览器自动化工具。
- 解析层:把拿到的HTML、JSON或者二进制流变成结构化数据。常用工具有正则、XPath、BeautifulSoup、pyquery。
- 存储层:把清洗后的数据写到文件、MySQL、MongoDB或者通过SQLAlchemy这样的ORM框架入库。
- 调度与监控层:负责任务分配、断点续爬、日志记录、失败重试,甚至用可视化界面展示爬虫状态。
我自己刚学的时候,总是急于写解析代码,忽略了后面两层。结果脚本跑一次没问题,跑十次就崩溃,数据重复、字段缺失、网站一改版就全废。后来才明白,爬虫项目的难点不在于“能不能爬到”,而在于“能不能稳定地、持续地、规范地爬到并存储”,这才是功能学习的核心。
1.2 一条爬虫请求的生命周期:从URL到数据行
理解爬虫功能,最好的方法是跟着一条请求走完全程。假设我们要爬一个商品列表页:
- 构造URL和请求头,requests发起GET或POST请求。
- 服务器返回响应,可能是HTML、JSON,也可能是图片二进制流。
- 解析响应内容,定位到每个商品节点,提取名称、价格、链接。
- 对提取结果做数据清洗,比如去除多余空格、转换价格格式、处理缺失值。
- 通过SQLAlchemy的ORM模型映射,把每条商品数据插入MySQL或PostgreSQL。
- 记录日志:本次请求状态、解析条数、耗时、是否有异常。
这个过程中,每一个环节都可能出问题:请求被拒绝、解析规则失效、数据库连接超时、字段长度超出限制。所以爬虫项目里的“功能学习”,其实就是把这条链路上每个环节的工具用熟、把异常想全。我后来做项目,会先画一张这样的流程图,哪怕不写代码,思路清晰了,代码自然就顺了。
2. 从requests到异步:网页请求功能的进阶之路
2.1 requests入门:最基础的请求构造
requests是Python爬虫最常用的HTTP库,没有之一。我记着第一次用的时候,真的是两行代码就能拿到数据:
import requests url = "https://example.com/api/list" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", "Accept": "application/json, text/plain, */*", } resp = requests.get(url, headers=headers, timeout=10) print(resp.status_code) print(resp.json())这里有几个容易被忽略的细节:timeout一定要设置。不设置的话,遇到网络波动可能卡住几分钟,影响爬虫整体调度;headers里的User-Agent要带上,很多服务器默认拒绝没有UA的请求;resp.json()只适用于JSON接口,如果是HTML就要用resp.text然后交给解析器。
陆续学着学着你会发现,requests就像一个“快递员”,你把收件地址(URL)、收件人偏好(headers)、包裹内容(data)交给它,它帮你跑腿。但快递员也有脾气:如果目标服务器限流、封IP、要求登录,requests就搞不定了,这时候需要更高级的武器。
2.2 异步爬虫:为什么能快那么多
同步requests爬虫是“一个一个来”:发一个请求,等服务器返回,再发下一个。假如每个请求耗时0.5秒,爬1000个页面就要500秒。但用异步协程,比如aiohttp,可以在等待响应的同时去发起其他请求,相当于同时有几十个请求在空中飞,总时间能缩短到十几秒。
我记得第一次用异步爬虫时,代码比同步难理解不少,核心大概是这样的:
import asyncio import aiohttp async def fetch(session, url): async with session.get(url) as resp: return await resp.text() async def main(): async with aiohttp.ClientSession() as session: tasks = [fetch(session, f"https://example.com/page/{i}") for i in range(20)] results = await asyncio.gather(*tasks) print(len(results)) asyncio.run(main())注意异步不是银弹。如果目标网站对并发有限制,100个并发请求可能会把服务器打挂,也可能让IP被秒封。所以异步爬虫一定要配合限速:用一个信号量控制并发数,比如每100毫秒一个请求。实际做的时候,我会先用10个并发测试,再逐步调大,找到既不封号又足够快的值。
2.3 请求头的伪装与常见反爬应对
我最初天真地以为换了User-Agent就万事大吉,结果还是被弹验证码。后来发现,服务器会看很多维度:Referer、Accept-Language、请求频率、Cookie的一致性、浏览器指纹。所以做功能学习时,一定要建立“请求头完整性”的意识。
一个标准的浏览器请求头,至少要包含:
User-Agent Accept Accept-Language Accept-Encoding Referer Connection遇到最简单的反爬(比如检测UA是不是爬虫),只需要把上面这些补全。遇到频率限制,就加延时或用随机sleep:
import time import random time.sleep(random.uniform(1, 3))遇到需要登录的页面,得先模拟登录拿到Cookie,再带着Cookie去访问。更复杂的反爬会校验TLS指纹、JS生成动态参数,那就得进入自动化工具或者逆向分析的领域。这部分内容在后面的章节细说,这里先记住一个原则:反爬的对抗都是围绕“让你看起来像一个真人”展开的,学习功能时别老想着硬刚,先想想怎么让自己的请求更“像人”。
3. 数据解析与存储:从HTML到数据库的完整链路
3.1 解析库选型:正则、XPath、CSS选择器怎么选
拿到HTML之后,就要从里面“挖”数据。正则表达式是万金油,但写起来容易出错;XPath适合复杂层级结构;CSS选择器语法简洁;BeautifulSoup则是对新手最友好,因为容错率高、API直观。
我个人在项目里的选型规则是:
- JSON接口返回的数据:直接resp.json(),不需要解析库。
- 简单的HTML取一个值:用正则最快捷,比如取标题
<title>(.*?)</title>。 - 复杂列表页、详情页:用XPath或CSS选择器。XPath可以通过浏览器开发者工具右键“Copy XPath”直接得到,但是经常不够严谨,建议学一下语法。
举个例子,用lxml解析一个商品列表:
from lxml import html doc = html.fromstring(page_source) items = doc.xpath("//div[@class='product-item']") for item in items: name = item.xpath(".//a[@class='title']/text()")[0].strip() price = item.xpath(".//span[@class='price']/text()") price = price[0].replace(",", "") if price else "0"这里有个坑:xpath的索引从1开始,写[1]是第一个元素,不是第二个。另外,xpath返回的是一个列表,即使只有一个结果也是列表,取之前一定要判断是否为空,不然IndexError够你调试半小时。
3.2 SQLAlchemy存储:让爬取数据结构化落地
爬虫数据存到哪里?最简单的是CSV,但是功能完善的项目一定会用数据库。SQLAlchemy是Python中最常用的ORM框架,它让你用Python类去描述一张表,然后通过session提交数据。
我习惯先定义一个模型,比如商品表:
from sqlalchemy import Column, Integer, String, Float, DateTime, create_engine from sqlalchemy.orm import declarative_base, sessionmaker from datetime import datetime Base = declarative_base() class Product(Base): __tablename__ = "products" id = Column(Integer, primary_key=True, autoincrement=True) name = Column(String(200), nullable=False) price = Column(Float, default=0) url = Column(String(500), unique=True) created_at = Column(DateTime, default=datetime.now)然后初始化连接和创建表:
engine = create_engine("mysql+pymysql://user:password@localhost/dbname?charset=utf8mb4") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session()插入数据时,我通常用批量操作:
session.add_all([Product(name=item["name"], price=item["price"], url=item["url"]) for item in data]) session.commit()这里必须提醒一个常见坑:unique字段的重复插入会抛异常。所以入库前最好先查重,或者用session.merge()。但merge在大批次插入时效率较低,所以我更倾向于先查询已有URL集合,再过滤掉重复数据,最后批量insert。SQLAlchemy还提供了反爬虫数据存储的完整方案,包括外键关联、事务回滚、连接池管理,这些是进阶功能学习的重点。
3.3 数据清洗与去重:容易被忽略的关键步骤
我见过很多新手同学,解析完就直接插入数据库,结果里面全是空字符串、乱码、重复项。数据清洗虽然不酷,但直接影响后面所有分析。至少要做这几步:
- 去空格和换行:用strip()清洗字符串。
- 统一格式:日期转成datetime,数字去掉千分位逗号,价格统一为浮点数。
- 缺失值处理:如果某个字段缺失,是填空字符串还是跳过整条数据,要提前定规则。
- 编码统一:爬到的中文文本,如果页面是GBK编码而你没有正确解码,就会出现乱码。requests拿到响应后,可以通过
resp.encoding = "gbk"手动指定。 - 去重:最常用的是用URL作为唯一键,在插入前先查一下现有URL集合。
我在一个爬虫项目中踩过坑:某商品页面的价格有时候会返回“面议”两个汉字,我直接用float()转换导致崩溃。后来加了异常处理,把无法转换的值记成None,这才稳定下来。数据清洗看似琐碎,但在功能学习的权重里,至少占三成。
4. 动态页面与自动化:Selenium进阶玩法
4.1 什么时候必须上Selenium
有些网站的数据不是直接藏在HTML里的,而是通过JavaScript动态渲染出来的。你用requests拿到的源码里根本没有数据,只有一堆script标签。这时候,有两条路:一是抓接口,找到浏览器发出的XHR请求,直接用requests模拟;二是上Selenium,让真正的浏览器去渲染页面,再读取渲染后的DOM。
能抓接口就别用Selenium,这是我反复验证过的经验。接口更轻量、更快、更容易解析。但有些接口做了加密参数,或者接口的数据需要复杂的交互才能触发,那就老老实实用Selenium。
Selenium的基本使用流程是:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") options.add_argument("--disable-gpu") options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(options=options) driver.get("https://example.com") # 等待页面加载 driver.implicitly_wait(5) # 拿到渲染后的页面源码 html = driver.page_source driver.quit()headless模式是不显示浏览器窗口的,适合服务器上跑。但调试的时候,我建议不要开headless,能亲眼看到页面状态,定位问题快很多。
4.2 反爬虫检测的破解思路与注意事项
Selenium虽然能渲染页面,但它本身也会暴露自动化特征。网站可以通过navigator.webdriver属性检测你是不是自动化控制。所以就有了各种隐藏特征的技巧,常见思路包括:
- 设置
options.add_experimental_option("excludeSwitches", ["enable-automation"]) - 修改navigator.webdriver的值
- 使用stealth.js或者undetected-chromedriver
不过我要强调,学习反爬虫技术,目的是理解和对抗非法的网络破坏行为,而不是教你攻击某个具体网站。在实际开发中,第一原则永远是遵守网站的robots协议和法律法规。真正有价值的爬虫项目,是爬取自己拥有或者有授权的数据,比如自己的运营后台、开放API、公开的政府数据等。如果只是为了学习,可以用一些沙箱站点,比如httpbin.org、books.toscrape.com,这些是专门给爬虫学习者练习的。
另外,有一类验证码是人机识别。简单的图片验证码可以用OCR,滑块验证码需要模拟拖拽轨迹,更复杂的点选验证码基本要接第三方打码平台。做功能学习时,建议先避开有强验证码的目标,把核心链路走通,再慢慢研究验证码。
4.3 自动化操作中的稳定性优化
Selenium最大的毛病是“不稳定”:网络慢一点、元素没加载出来、弹窗挡住点击,脚本就容易挂。我总结的稳定性优化方案有这几条:
- 不要用time.sleep硬等,改用WebDriverWait和expected_conditions:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, "product-item")) )- 定位元素优先用id或css selector,少用xpath,因为前端微调容易让xpath失效。
- 执行完每一步,把当前页面截图保存下来。排查问题时,截图比日志直观得多。
- 发生异常时,不要立刻退出,而是刷新重试一次。我写了重试装饰器,连续失败3次才告警。
- 定期重启浏览器。长时间运行的Selenium浏览器会越跑越慢,内存占用越来越大,我一般每爬500个页面就重启一次driver。
自动化这部分功能学习,说到底就是“模拟真人操作”,所以慢慢来比快更重要。真人的滑动、输入、点击都是有间隔的,脚本里加上随机延时,反而更不容易触发反爬。
5. 工程化与可视化:从脚本到可用的爬虫工具
5.1 分布式爬虫的设计思路
单机爬虫有瓶颈:带宽有限、CPU有限、IP单一容易被封。当数据量到十万、百万级别时,就要考虑分布式。分布式爬虫不是神秘的黑科技,核心就是“把任务拆成很多小块,分给多台机器同时跑,然后把结果汇总起来”。
常见的架构是:一个中央调度器负责分配URL,多个爬虫节点从队列里取任务、执行抓取、再把结果写回同一个存储。任务队列用Redis非常方便,因为Redis支持列表、Set、延迟队列等结构。举个例子:
- 用
LPUSH往Redis里放URL。 - 每个爬虫节点用
BRPOP阻塞式取URL。 - 抓取完成后,把解析结果写入MySQL,把已抓取的URL加入一个Set,用于去重。
Scrapy本身就支持分布式,配合scrapy-redis就是一套成熟的方案。但说实话,如果你的项目还在起步阶段,不要一上来就搞分布式。单机的性能优化没做好之前,分布式只会让问题更复杂。我用过的经验法则是:单机每天能稳定抓1万条数据,就暂时不需要分布式;等到单机跑不完、或者任务时效性要求太高了,再升级架构。
5.2 可视化界面:让爬虫变成人人可用的工具
以前爬虫都是写脚本,跑完看命令行输出。后来要给不懂代码的同事用,我就把爬虫包成了一个带界面的小工具。所谓“python爬虫可视化界面”,本质上就是给爬虫套一个GUI或者Web前端,让用户填一个关键词、选一个任务,点一下“开始”,就能看到进度条和结果表格。
如果不想写复杂的Web,用Flask起一个本地服务就够了。后端调用爬虫函数,前端用简单的HTML+AJAX展示数据。更轻量的方案是用tkinter做桌面程序,但样式比较丑。我更推荐用Streamlit,写起来最短:
import streamlit as st keyword = st.text_input("输入搜索关键词") if st.button("开始爬取"): progress = st.progress(0) # 模拟爬虫进度 for i in range(100): progress.progress(i + 1) # 实际爬取逻辑 st.success("完成")可视化界面的核心价值在于把参数可配置化。比如把用户输入的关键词传给爬虫函数,把爬虫状态实时回传,把结果表格展示出来。它不改变爬虫本身的逻辑,但让整个功能“可用性”大大提升。做这个功能时,要把后台任务的超时、取消、日志输出都处理好,不然界面总是卡住,体验很差。
5.3 逆向分析与加密参数:进阶路上的硬骨头
爬虫学习进阶到一定程度,一定会遇到“爬虫逆向”。意思是,目标网站的数据接口虽然是JSON,但参数是加密的,比如有个sign或者token,每次请求都要动态生成。你直接用requests模拟,服务器会返回“参数校验失败”。
做逆向分析的典型步骤是:
- 打开浏览器开发者工具,Network面板里找到XHR请求,看它的Query String或Request Body里哪些参数是变化的。
- 在Sources里搜索参数名,定位到JavaScript生成代码。
- 分析加密算法,可能是MD5、SHA、AES,也可能是自定义的混淆算法。
- 用Python重写这个加密过程,或者直接用PyExecJS、js2py执行JS代码。
这里要提醒一下,逆向分析是一门很吃经验的技能,而不是一条路走到黑。很多网站有前端混淆、WebAssembly、加固等技术,破解难度极大。在我做的真实项目中,绝大多数遇到的所谓“逆向”,其实就是定位到某个加盐哈希,用Python的hashlib几行代码就能模拟。真正值得花心思的是分析思路:先找参数入口,再看它的依赖关系,最后化简为Python实现。
另外,绝对不要用爬虫逆向去爬取涉及个人隐私或者受法律保护的数据,比如社交平台私信、银行账户、付费内容。技术本身是中性的,但使用技术的人要有边界感。我建议初学者在合法、公开、允许爬取的平台上练习逆向思路,比如一些公开的天气API、股票行情API。
6. 常见问题与排查实录
6.1 爬虫被封IP怎么办
这是每个爬虫开发者都会遇到的问题。封IP的表现有很多:请求突然全是403、验证码跳出、返回内容为空。处理优先级一般是:
- 降低频率,加随机延时,让请求间隔更像真人。
- 检查请求头完整性,特别是User-Agent、Referer。
- 使用Cookie会话,把登录状态维持住,不要每次新建会话。
- 更换IP资源,比如使用拨号VPS或者云服务商的弹性IP,但要合规使用,不要去碰违法违规的流量代理。
- 搭建更完善的调度系统,把被封的IP放入黑名单,冷却一段时间再放出来。
我在实际项目里,还遇到过“不是IP被封,而是请求头里带了不干净的Cookie”。排查方法是先清空Cookie直接访问,看能不能拿到数据,逐步加回Cookie。这种二分法排查效率很高。
6.2 数据入库乱码/字段缺失排查
入库乱码90%是编码问题。页面声明是UTF-8,但你requests拿到的响应实际上可能是GBK。处理方式是在请求后立刻检查resp.encoding,然后用resp.content.decode('utf-8', errors='ignore')手动解码。另外,MySQL表要统一使用utf8mb4字符集,不然遇到特殊表情符号会直接报错。
字段缺失则往往是解析规则不够健壮。比如页面改版,把class名改了,XPath和CSS选择器就失效。我养成了一个习惯:把解析逻辑独立成一个函数,并且用测试用例来验证提取结果。每次跑完爬虫后,抽样对比一下,看看提取出的字段数量和值是否正常。还有一个坑是页面里明明有数据但解析出来是空的,那可能是数据是通过AJAX异步加载的,requests拿到的源码里确实没有,需要用Selenium或者找接口。
6.3 内存泄漏与调度崩溃处理
长期运行的爬虫,内存会慢慢涨,最后被系统kill掉。常见原因有三个:一是Selenium的浏览器进程没有正常关闭,每次循环都新建driver;二是日志文件无限制变大;三是数据队列没有限制,积压越来越多。
针对这几点,我一般这样做:
- 每次用完Selenium,在finally里执行
driver.quit(),而不是driver.close()。close只关闭当前窗口,quit才释放全部资源。 - 日志采用按天切割,或者只保留最近N个文件。
- 给内网数据库连接加上连接池回收逻辑,避免连接泄漏。
- 设置任务队列的最大长度,超过阈值就暂停新任务。
调度崩溃的排查思路也很直接:先看日志有没有关键字,比如OutOfMemoryError、Too many open files、Connection timed out。在Linux服务器上,可以用dmesg看内核日志,有没有被OOM killer杀掉的进程。
做爬虫项目的时间越长,越会觉得:爬虫真正难的,不是写爬虫,而是让它一直稳定地跑下去。那些网上流传的“爬虫接单一年收入”之类的话题,听听就好。真正靠爬虫做出价值的,都是把它当成一个严谨的软件工程来做,从需求分析、数据建模、调度监控到异常处理,每个环节都做到位。这也是我整理这篇学习笔记的核心目的:把散落的知识点,串成一个能落地的项目功能地图。
最后再分享一个小技巧:学爬虫最好的方法,不是看视频,而是找一个目标网站,从第一步开始,一步步把数据存到数据库里。遇到一个坑,就解决一个坑。等你把一个完整流程走通,再回头去看那些框架和库的文档,你会发现它们全都变亲切了。爬虫这条路,入门不难,但想走得远,靠的是对每一个细节的较真。