爬虫(网络爬虫)是这几年被反复提及的技术名词。简单来说,它就是用一段程序模拟浏览器去访问网页,然后把页面里的结构化数据提取出来,存成我们需要的格式。从搜索引擎抓取网页建立索引,到电商比价、舆情监控、行业报告的数据采集,背后都离不开爬虫的身影。很多朋友觉得爬虫门槛高,既有HTTP协议又有HTML解析,还要管存储和调度,一上来就被一堆工具绕晕。其实爬虫的核心链路非常固定:发起请求、拿到响应、解析数据、落地存储,再按需加上数据清洗和任务调度。这篇文章不会堆砌概念,我会从零开始把这套链路的每个环节拆开,结合我这些年实际调试过程中踩过的坑、验证过的方案,把第一章的基础打扎实。
这个系列是给两类人看的:一类是完全没接触过爬虫,但手里有数据需求,想自己动手解决问题的新手;另一类是看了不少教程但始终没能在真实项目里跑通完整流程,卡在环境配置和反爬策略上的初级开发者。第一章先把基础框架立住,后续章节再逐层深入具体协议细节、解析库原理和分布式采集架构。
1. 爬虫到底是什么
1.1 从浏览器到爬虫程序的本质差异
很多人会把爬虫想象成“高深莫测的黑科技”,但本质上,它只是在复刻人用浏览器看网页的过程。
你打开浏览器,输入网址,按下回车,浏览器向服务器发起一次HTTP请求,服务器把HTML文档、图片、样式文件和脚本一股脑返回给你,浏览器再把它们渲染成你看得懂的图文页面。爬虫做的事更粗暴,它跳过视觉渲染这一步,直接拿返回的原始HTML文本,然后用代码去定向提取你关心的那部分信息。换句话说,浏览器把你把内容“画”出来,爬虫替你把内容“拆”下来。
这个认知非常关键。因为一旦你理解了爬虫是面向原始文本的,就能明白为什么有些页面你肉眼能看见内容但爬虫抓不到——那些内容很可能是JavaScript动态渲染出来的,初始HTML文档里根本没有。这是第一章就要建立的最重要的思维模型。
1.2 爬虫工作流程的最小闭环
任何爬虫程序,不管架构多么复杂,最终都逃不开下面这个最小闭环:
- 构造一个HTTP请求,指定目标URL、请求方法、请求头和请求体。
- 接收服务器响应,拿到状态码、响应头和页面内容。
- 解析页面内容,常见手段是正则表达式、CSS选择器或XPath。
- 按业务需求清洗和转换数据。
- 把结果存储到文本文件、数据库或消息队列。
这个闭环一天跑一次叫定时采集,一天跑一万次叫批量采集,分布在多台机器上跑叫分布式爬虫。名字听起来很多,底层模型完全一致。我一直建议新人在本地用单线程脚本把以上五步在代码里跑通一次,再考虑Scrapy框架或并发优化。地基没打牢就上框架,出了问题都不知道去哪层排查。
2. 为什么需要爬虫:从数据需求到技术选型
2.1 有数据的地方就有爬虫需求
我大概统计过自己接手过的项目需求,能驱动爬虫开发的场景比大多数人想得更宽。
- 比价与商品监控:盯着竞品价格变动,辅助定价决策。
- 舆情与社媒分析:采集公开评论、话题热度、传播路径。
- 行业研究与市场报告:把招聘信息、房产挂牌、新闻资讯聚合起来做趋势分析。
- 学术研究数据集构建:论文、专利、气象水文数据等多来源采集。
- 搜索与推荐引擎的底层数据:整站抓取建立索引库。
- 企业内部数据同步:多个信息系统之间没有API,只能靠页面抓取打通数据孤岛。
这些需求的共同点是:目标数据在公开网页上存在,但对方没有提供现成的API,或者API的访问限制比页面抓取更严格。爬虫就是在这个缝隙里补位。
2.2 先评估合规边界,再谈技术方案
我见过不少新人拿到需求就急着写代码,我反而会先花时间确认一件事:数据获取渠道是否合规。
首先是robots协议,它不是法律文件,但代表网站所有者对爬虫访问范围的声明。正规的爬虫项目应当遵守。其次是目标网站的服务条款,有些平台明确规定了页面内容不允许未经授权的程序化采集。第三是数据本身的属性,涉及个人隐私、版权保护内容的数据,即使技术上能抓到,也不应该用于商业用途或对外传播。
合规评估不阻碍技术学习,我在系列文章里讲的都是通用的数据采集技术,但拿到具体项目时,你必须在动手前把这一关过了。这既是保护自己,也是对目标网站基本的尊重。
2.3 选择自研脚本还是成熟框架
进入技术选型时,新手通常会纠结:是直接用requests库写脚本,还是学Scrapy框架?
我的建议是分阶段。第一阶段,也就是本章对应的学习阶段,只用心写一个单文件脚本就够了。requests负责HTTP,BeautifulSoup或parsel负责解析,再加一个time.sleep做访问间隔控制。这个组合可以在半小时内跑通一个demo,帮助你把前面说的五步闭环完整体验一遍。
当你的采集任务开始具备以下特征时,再迁移到Scrapy这类框架:
- 目标网站数量超过十个,需要统一管理爬虫调度。
- 抓取频率高、数据量大,需要并发控制、去重和断点续爬。
- 需要对接中间件,处理代理切换、请求重试和反爬策略变动。
框架解决的是规模化的问题,但框架不帮你理解HTTP。所以我强烈建议,在写满两百行裸脚本之前,别急着开Scrapy项目。顺序反了,你学到的是API调用,不是爬虫原理。
3. 核心技术与工具选型:每个环节都在解决什么问题
3.1 HTTP请求层:requests库为何是首选
Python生态里做HTTP请求,requests库是事实上的标准。它以urllib为基础,把连接池管理、Cookie持久化、重定向跟随、超时控制全部封装成了直观的API,对初学者极其友好。
写一个基础的GET请求只需要几行代码:
import requests url = "https://example.com/page" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } resp = requests.get(url, headers=headers, timeout=10) print(resp.status_code) print(resp.text)有几点需要强调。
第一,headers中的User-Agent不是可有可无的参数。很多服务器的安全策略会把缺少或异常的UA识别为爬虫请求,直接拒绝服务。虽然设置一个常见浏览器的UA算不上一劳永逸的反反爬手段,但这是最基础的要求。
第二,timeout必须设置。我见过很多初学者的脚本因为没有设置超时,遇到某个慢响应接口时直接卡死整个任务队列。请求挂起一分钟和一小时的差别,往往就体现在这一个参数上。
第三,Session对象用来保持会话状态。如果你需要先登录再采集登录后的数据,通过requests.Session()创建的会话能自动管理Cookie,避免手动从响应头里提取并拼接的麻烦。
3.2 响应解析层:从正则到选择器
拿到HTML文本后,解析是下一步的核心。这里有三个常用方案,对应不同复杂度。
正则表达式适合结构极其简单、且模式规律明确的内容,比如从一段JSONP回调里提取特定字段。但正则的局限也很明显,HTML标签的嵌套和属性顺序变化会让表达式写得又长又脆。我一般只在解析JSON格式的接口返回时用正则,处理HTML主体内容则很少使用。
BeautifulSoup的老牌地位不用多说,find和find_all配合CSS选择器基本能应付绝大多数静态页面。它的容错能力很强,即使页面存在一定程度的标签残缺,也能构建出可遍历的解析树。
更推荐的做法是使用parsel,它其实是Scrapy框架的解析内核独立出来的库。parsel基于lxml,解析速度更快,XPath和CSS选择器语法都支持,如果你后续要过渡到Scrapy,掌握的知识完全能复用。我自己的经验是,从第一天解析HTML就用XPath的思路去写,两年后回头看,这个起点帮我少踩了很多选择器失效的坑。
一个用parsel提取标题和所有链接的例子:
import parsel selector = parsel.Selector(text=resp.text) title = selector.css("h1::text").get() links = selector.xpath("//a/@href").getall()3.3 数据存储层:按数据量级选存储方案
很多教学文章只讲到把数据打印出来就结束,但真实项目必须要落库。
采集量在几千条以内时,标准库的csv模块直接写文件是最省事的方案。注意编码要统一使用utf-8,打开文件时设置newline=""避免空行。小批量再开个SQLite,连数据库服务都不用装,非常适合个人兴趣项目。
数据量达到十万级以上,就应该交给MySQL或PostgreSQL这类关系型数据库。设计表结构时,每个字段的长度和索引要提前规划好。特别是URL字段,建议加上唯一索引,做去重时能用INSERT IGNORE这种语法省掉一次查询。
如果还涉及大量非结构化内容,比如正文快照、评论原文本,可以考虑MongoDB这类文档数据库。它没有严格的表结构限制,字段增删方便,但与关系型数据库相比,事务和复杂查询的能力要弱一些,需要根据业务权衡。
3.4 任务调度层:单机定时与断点续爬
不要一上来就考虑分布式,单机环境做好调度已经能覆盖大部分长周期采集需求。
最经典的方案是操作系统的cron或Windows任务计划程序,在每天的固定时间点执行一次爬虫脚本。把脚本写成可从上次停止位置继续运行的模式,比盲目扫全量页面要高效得多。
断点续爬的实现思路不复杂:用一个本地文件或数据库表记录已成功完成的请求指纹,比如URL的哈希值。每次请求前先查指纹表,存在就跳过。这套逻辑看起来简单,但它是后续学习增量采集、URL去重的基础。
4. 实操中跑通真实项目要注意的细节
4.1 页面结构变化的无形风险
爬虫最脆弱的部分不是网络故障,而是页面改版。目标网站的HTML结构一调整,你精心写的选择器就会指向空值。
我经历过一次印象很深的项目:某数据源网站把列表页里的价格标签从p.price改成了div.price-wrap span.amount,按原有选择器抓到的字段全部为空。当时线上脚本已经跑了半个月,如果不是我每周做了数据完整性校验,这批坏数据会静默污染整个分析结果。
从那以后我养成了两个习惯。
第一,每次上线前,对关键字段做非空率和类型检查。抓取结果里的数值型字段如果出现大量空值或字符串,直接触发报警。
第二,对HTML结构变化做快速嗅探。解析前看一眼页面长度,如果与历史均值偏差超过一定比例,说明页面可能发生了变化,这时不急着入库,先把原始HTML保存下来人工核对。
4.2 访问频率控制不只是礼貌问题
很多网站没有明说禁止爬虫,但访问频率过快时,服务器负载压力上升,触发限流是必然的。这里的核心策略是“慢就是快”。
我通常会给每个目标网站设定一个基础请求间隔,默认是三到五秒。如果网站响应时间本身较长,间隔还会相应调大。遇到分页较多的采集任务,每抓完一页随机sleep 1到5秒,让请求时间轴不那么规律。
如果你担心随机延迟影响效率,我算过一笔账:一个10万页的站点,请求间隔从0.5秒提升到3秒,整体用时会从14小时涨到83小时,但换来的是账号存活率和数据完整率的大幅提升。实际项目里,数据完整性的价值远高于速度。
4.3 处理响应内容时最容易犯的编码错误
编码问题是新手踩得最多、又最容易被忽略的坑。如果你抓回来的是乱码,九成是编码处理不对。
HTTP响应头里的charset字段、HTML文档里的meta charset标签和实际字节流使用的编码,三者经常不一致。requests库会尝试从响应头推断编码,但很多网站的响应头不声明charset,导致推断失败。
我建议在拿到文本后手动指定编码:
resp.encoding = resp.apparent_encoding text = resp.textapparent_encoding是根据页面字节内容做统计推断的结果,比响应头里的声明更可靠。但要注意,它对中文网页的效果很好,对某些小语种可能不准。更稳妥的做法是先查看原始HTML里meta标签声明的字符集,再针对性设置。
5. 常见问题与排查技巧实录
5.1 请求返回403 Forbidden
403是最常见的反爬信号,出现时不要慌,按顺序排查。
先看User-Agent是否正确。哪怕只是默认的python-requests,某些服务器也会直接拒绝。替换成常见浏览器的UA,通常能解决一半问题。
再看是否缺少必须的请求头。有些网站会校验Referer、Origin甚至Accept-Language,虽然听起来严格,但每个浏览器都会默认带上这些。你可以拿浏览器开发者工具里的Network面板,拷贝一份真实请求的headers完整填进代码里。
最后看是否触发了IP级别的封禁。如果换浏览器UA依然403,换个网络环境试试。如果换了网络就能正常访问,说明你原来的IP已经被限流,这个时候需要降低访问频率,等待封禁解除。
5.2 内容能打开但抓到的字段是空的
页面能正常返回但解析不到数据,原因通常有两个方向。
第一个方向是内容确实是动态渲染的。你看到的网页是JavaScript执行后才生成的内容,初始HTML里根本没有这些结构。判断方法很简单:用浏览器的“查看源代码”功能,而不是开发者工具里的Elements面板。如果源代码里没有你想抓的数据,那就要考虑使用无头浏览器或直接找后台的JSON接口。
第二个方向是选择器写错了。把页面HTML保存成文件,在终端里用parsel或BeautifulSoup单独调试选择器,比一遍遍重跑爬虫效率高得多。调试通过后再把代码合回去。
5.3 爬取过程中断了,重跑时数据重复
没有做去重是根本原因。断点续爬依赖一个持久化的URL指纹表,判断逻辑要放在实际网络请求之前。
用数据库的唯一索引做兜底是常见组合方式:指纹表负责跳过高频重复的请求,数据表的唯一索引负责在极端情况下防止重复插入。两者结合,即使脚本中途崩溃,重启后也不会产生脏数据。
5.4 编码、超时与重试机制的黄金组合
长时间运行的项目里,单次请求的失败率远比你想象的高。超时、重试、编码这三件事打包处理,是稳定性的基石。
我惯用的重试逻辑是:单次请求设置timeout=10,连续失败三次就放弃当前URL,记录到失败日志。重试之间间隔10秒,避免对服务器形成连续的密集访问。每次重试都重新创建请求会话,防止半打开的连接被服务器拒绝。
这套组合在绝大多数静态页面上都能达到99%以上的成功率,剩下的1%失败请求通过日志回放机制人工补采。
6. 一个最小可运行的入门Demo
我用一套最简单的逻辑串起整个章节的内容,你不妨直接复制到本地跑一下,感受完整链路。
目标:采集一个示例新闻列表页的标题、发布时间和详情链接,保存到CSV文件。
import csv import time import parsel import requests BASE_URL = "https://example-news.com/list" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } def fetch_page(url): resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = resp.apparent_encoding resp.raise_for_status() return resp.text def parse_page(html): selector = parsel.Selector(text=html) results = [] for item in selector.css("div.news-item"): title = item.css("h2 a::text").get() link = item.css("h2 a::attr(href)").get() pub_time = item.css("span.date::text").get() results.append({ "title": title.strip() if title else "", "url": link, "publish_time": pub_time.strip() if pub_time else "", }) return results def save_to_csv(data, filename="news.csv"): with open(filename, "w", encoding="utf-8", newline="") as f: writer = csv.DictWriter(f, fieldnames=["title", "url", "publish_time"]) writer.writeheader() writer.writerows(data) def main(): html = fetch_page(BASE_URL) news = parse_page(html) save_to_csv(news) print(f"成功保存 {len(news)} 条数据") time.sleep(3) if __name__ == "__main__": main()代码里值得注意的细节:
resp.encoding = resp.apparent_encoding解决中文乱码。resp.raise_for_status()保证非200响应直接抛出异常,方便排查。- CSV写入时设置
utf-8和newline="",避免出现编码问题和多余空行。 - 每次请求结束后的
time.sleep(3)是爬虫的基本礼仪。
你在本地跑这个demo时,把BASE_URL替换成任何一个内容结构简单的真实网页,然后把CSS选择器改成对应的结构即可。
7. 后续扩展方向与学习路径
第一章的核心目标是建立爬虫最小闭环的完整心智模型。掌握了请求、解析、存储这三板斧之后,后续章节可以在这个框架上逐层加码。
- 第二章可以深入HTTP协议细节,讲解Cookie、Session、请求头中的关键参数,以及登录态维持的原理。
- 第三章可以覆盖动态页面采集方案,包括接口分析、无头浏览器使用场景和性能取舍。
- 第四章可以进入Scrapy框架,讲解爬虫项目的工程化组织、中间件编写和并发调度。
- 第五章可以探讨采集数据的清洗、去重、增量更新,让数据管道具备投产能力。
我个人在实际操作中的体会是,爬虫能力的成长曲线不是线性的,瓶颈从来不在代码本身,而在对HTTP协议、页面结构和数据规律的理解深度。很多人卡在“只会照着教程改”的阶段,就是因为他从没试过从一个空文件开始,按自己的思路把整条链路写出来。所以这一章的demo虽然简单,我依然建议你亲手敲一遍,然后尝试改造:把标题换成正文、把单页换成翻页、把CSV换成SQLite。
第一次跑通这套闭环之后,你会忽然觉得所有爬虫框架都变得亲切起来,因为它们本质上都是在帮你管理这个闭环里的某一环。这也就是我为什么愿意花一整章篇幅只讲“概述”的原因。方向比速度重要,理解比工具重要。