带新人入行爬虫开发这些年,我发现大多数人一到"入门阶段"就容易跑偏。网上那些炫酷的案例,不是抓加密接口,就是逆向某个登录流程,看得人热血沸腾,但真正上手时连一个最简单的列表页都抓不干净。想提醒所有准备走这条路的同学:爬虫工程师的入门阶段一,核心不是技巧,而是三块地基——HTTP协议、HTML结构、数据落盘。这三样东西搞明白了,后续的进阶全是锦上添花;搞不明白,学再多高级技巧也是空中楼阁。
这篇文章就是我整理的一份基础知识点完全学习文档,适合刚接触爬虫、或者已经写过几行代码但总觉得"没系统"的同学。我会从发第一个请求讲起,一路讲到数据成功落盘,中间把请求头、状态码、解析方式、存储方案这些绕不开的知识点全部串起来,最后一章专门聊聊我这些年带新人时见到的翻车现场,以及入门阶段必须守住的边界。
1. 先把HTTP协议这层地基打牢,后面才不容易塌
1.1 从按下回车到页面出现,中间到底发生了什么
很多新手以为写爬虫就是"用代码读网页",其实没那么简单。你按下回车,浏览器做的事情远不止"打开一个页面":它先要根据域名找到服务器地址,这一步叫DNS解析;然后跟服务器建立连接;接着发送一个HTTP请求过去;服务器处理完把内容回传;浏览器拿到内容后还要解析HTML、加载图片样式、执行JavaScript,最后才渲染出你看到的完整页面。
而爬虫干的事情比浏览器"偷懒"得多——我们只取自己需要的那部分数据,并不需要把页面渲染给人看。理解爬虫最准确的比喻是:你派了一个机器人去餐厅点菜,机器人只需要把菜单上那道菜的名字和价格抄回来就行,不需要像真人一样坐下来慢慢吃。
这个比喻背后是一个最重要的认知:爬虫拿到的响应是一段文本,而不是"页面"。你能从里面提取什么,完全取决于这段文本里有没有你需要的字段。很多时候抓不到数据不是因为代码写错了,而是因为那段文本里压根没有你想找的东西,后面我会专门讲这个坑。
1.2 请求头里的关键字段:哪些是网站真正在意的
HTTP请求不是只把一个URL扔过去就完事了,它还带着一堆"元信息",也就是请求头。新手阶段不要求你把每个字段都倒背如流,但有四个一定要清楚它们是干什么的:
| 请求头字段 | 含义 | 为什么重要 |
|---|---|---|
| User-Agent | 客户端身份标识,告诉服务器你是什么浏览器或什么程序 | 很多网站会用它对请求做初步识别,缺失或使用默认值(如python-requests)时,请求很可能被直接拒绝 |
| Referer | 表示这个请求是从哪个页面跳过来的 | 部分站点的资源请求会校验它,从页面里直接发出的子请求都会带上它 |
| Cookie | 用于维持会话状态,比如登录信息、浏览记录 | 需要带登录状态才能访问的内容,靠的就是这个字段 |
| Accept / Accept-Language | 告诉服务器你期望什么格式和语言 | 多数情况下不设置也能拿数据,但规范设置能让服务端行为更可预期 |
我见过不少新人一上来就各种伪造UA,其实大可不必。设置一个符合实际客户端的UA是HTTP协议里完全正常的做法。关键是别用那种又长又乱又假的UA,更别每发一个请求就换一个——那样反而暴露你在"刻意制造"请求,属于过度设计。
提示:写爬虫的第一步,永远是先打开浏览器的开发者工具,看一个真实的请求长什么样,再去对照你的代码。新手问"为什么我的请求被拒绝"时,八成答案就藏在请求头的对比里。
1.3 状态码不是在报错,而是在告诉你下一步怎么走
很多同学一看到非200的状态码就慌,其实状态码是服务器给你的"下一步行动建议"。入门阶段把这几个弄熟就够了:
- 200:正常返回,直接处理响应内容。
- 301 / 302:重定向。requests库默认会帮你自动跟随,但如果你发现抓到的内容不对,要检查一下是不是被带到了别的页面。
- 403:请求被拒绝。优先检查请求头、访问频率,确认这不是你自己的程序行为导致的。
- 404:这个路径不存在。检查URL是不是拼错了,页面是不是已经改版。
- 429:请求太频繁被限流。这是服务器在告诉你"你太快了,歇一会儿"。
- 500 / 502 / 503:服务器自身出问题。这时候不是你的错,过几秒重试即可。
把这几个状态码记住,你的调试思路就清晰了:403先检查自己,429先慢下来,5xx先重试。状态码是服务器在跟你"讲话",读懂了,排查问题时能少走一半弯路。
2. 解析不是"找字符串",而是理解页面的结构
2.1 HTML的DOM结构到底怎么读
拿到响应文本之后,下一步是把需要的数据从里面"抠"出来。这一步的前提是看懂HTML的结构。HTML本质上是一棵树,专业说法叫DOM树。每个标签都是这棵树上的一个节点,节点之间有父子关系、兄弟关系。
拿一段最简单的代码举例:
<div class="product-list"> <div class="item"> <span class="name">商品A</span> <span class="price">¥299</span> </div> </div>这段代码里,最外层的div是整个列表容器,它下面依次是商品名称span和价格span。你想要定位"商品A"这个文本,可以顺着div.product-list → div.item → span.name这条路径走。这种从父到子的路径关系,就是解析程序能准确定位数据的根本原因。
我建议新手不要着急写解析代码,先把网页在浏览器里打开,右键"检查"看树状结构,花半小时手动点开几个父节点、子节点的关系,比背十遍文档都管用。这也是我见过所有"解析高手"共同的基本功——他们对DOM结构的直觉,全都来自这一阶段的大量观察。
2.2 正则、XPath、CSS选择器的核心取舍
解析方式五花八门,但入门阶段真正需要掌握的只有三种:正则表达式、XPath、CSS选择器。它们各有各的主场,选错了就会觉得特别别扭。
| 方式 | 优势 | 劣势 | 适合场景 |
|---|---|---|---|
| 正则表达式 | 灵活,能匹配任意文本,甚至可以处理非HTML内容 | 可读性差、难维护,HTML一改就废 | 数据藏在脚本或JSON里、需要跨多行匹配的场景 |
| XPath | 能用路径精确导航DOM,支持根据文本内容、属性条件定位 | 语法稍重,写长了不直观 | 需要根据层级关系、文本内容精确定位的复杂结构 |
| CSS选择器 | 简单、直观、好读,前端同学几乎零成本上手 | 对复杂文本条件支持较弱 | 绝大多数标准HTML页面的首选 |
我的选型经验很简单:能用CSS选择器就首选CSS选择器,搞不定再上XPath,最后才考虑正则。原因很朴素——代码要给别人看,更要给三个月后的自己看。CSS选择器写出来像div.item > span.name,一眼就懂它要找什么;正则写完过两周再看,光转义符就能让你怀疑人生。
2.3 一个真实的解析案例:从杂乱标签里抽数据
光讲概念没意思,直接上一个最常见的场景:商品列表页。假设响应文本里有这么一段结构:
<div class="product-list"> <div class="item"> <span class="name">商品A</span> <span class="price">¥299</span> <div class="tags"><span>热卖</span><span>包邮</span></div> </div> <div class="item featured"> <span class="name">商品B</span> <span class="price">¥199</span> <div class="tags"><span>新品</span></div> </div> </div>用Python的BeautifulSoup解析,配上CSS选择器,代码非常短:
from bs4 import BeautifulSoup html = open('page.html', encoding='utf-8').read() soup = BeautifulSoup(html, 'html.parser') for item in soup.select('div.item'): name = item.select_one('span.name').text.strip() price = item.select_one('span.price').text.strip() tags = [tag.text for tag in item.select('div.tags span')] print(name, price, tags)注意上面这几行里有两个细节:一是用了select_one只取第一个匹配,而select返回全部匹配;二是提取多个标签时用了列表推导式,把所有span的文本都收集起来。这两个细节恰恰是新手最容易写错的地方——用错方法要么只拿到一个值,要么直接报错。
3. requests库入门的完整链路:从发请求到拿到数据
3.1 环境准备与最小可用脚本
很多人不知道基础知识阶段最应该先装什么环境。我推荐的入门组合非常轻量:Python 3.8以上的版本,装两个第三方库requests和beautifulsoup4,解析器可以先用内置的html.parser,后期再按需升级为lxml。不需要装任何爬虫框架,这个阶段也不该碰框架——框架会把底层的请求流程封装得面目全非,不利于打基础。
装完库之后,第一段能跑的最小脚本是这样的:
import requests resp = requests.get('https://example.com', timeout=10) print(resp.status_code) print(resp.url) print(resp.text[:500])这段代码做了三件事:向目标地址发请求、打印状态码和最终URL、打印响应内容的前500个字符。新手跑通这段代码后,最应该做的事情不是继续加功能,而是故意改几次代码制造几个"错误",比如把timeout设成0.001秒,或者把URL改成不存在的地址,看看程序分别会报什么错。提前把错误见一遍,后面真正踩坑时你就不会慌。
3.2 会话、超时与重试:新手最容易忽略的三个参数
requests库的入门教程千篇一律都在讲.get(),但真正决定一个爬虫靠不靠谱的,是另外三个常被忽略的点。
第一,超时必须有。不加timeout的请求,在网络异常时可能一直挂起,让你的程序莫名其妙卡死。一个练手用的爬虫,给请求加上timeout=10是对自己最基本的保护。
第二,要会用Session。直接requests.get()每次都是全新的连接,而requests.Session()会帮你维护连接和Cookie。抓需要翻页、需要保持登录状态的站点时,Session的意义尤其明显。这个阶段哪怕只是抓几个静态页面,我依然建议你从Session开始写,因为这是最接近真实工程习惯的写法。
第三,重试要带退避。网络请求失败很正常,但"失败后立即重试"是坏习惯。比较稳妥的写法是:每次重试前等待的时间按指数增长,比如第一次等2秒、第二次等4秒。这样既给了服务器恢复的时间,也避免把自己变成服务器眼里的"问题来源"。下面这段代码可以直接抄:
from time import sleep import requests s = requests.Session() s.headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}) url = 'https://example.com/data' for attempt in range(3): try: resp = s.get(url, timeout=10) resp.raise_for_status() break except requests.RequestException as e: print(f'第{attempt + 1}次请求失败: {e}') if attempt < 2: sleep(2 ** attempt) else: raise RuntimeError('连续三次请求失败,停止重试')这段代码里最值得学的是for...else的用法:正常break退出时不会走else,三次都失败才会执行最后的报错,这个结构在写重试逻辑时非常好用。
3.3 处理编码和乱码问题:做爬虫的第一堂中文课
抓中文站点的同学八成都会遇到一次乱码问题,这是入门阶段的"必修课"。乱码的本质很简单:服务器返回的是字节流,你解码时用了错误的编码方式。就像对方用粤语说话,你拿普通话的语法去理解,当然听不懂。
requests库对编码的处理逻辑是这样:响应里带了编码声明就用声明的编码,没带就去猜。中国站点常见的编码是UTF-8和GBK,一旦猜错,你看到的就是一堆乱码。处理办法是手动指定编码,一般都会结合响应内容来推断:
resp = requests.get(url, timeout=10) resp.encoding = resp.apparent_encodingapparent_encoding是requests根据响应内容字节特征推算出来的编码,虽然不是每次都准,但在大多数中文站点的场景下很够用。真正要警惕的是HTML里声明的编码和实际内容编码不一致的情况,这时候宁可相信样本统计,也别全信声明。遇到乱码,先看resp.encoding,再手动试UTF-8和GBK,基本两三次就能定位。
4. 数据要能落盘才算结束:存储方案怎么选
4.1 CSV、JSON、SQLite的适用边界
新手往往有个误区:数据打印在控制台上就觉得任务完成了。其实爬虫的价值在于"数据沉淀"——你得把结果安全地保存下来,后续分析和使用才有基础。入门阶段真正需要理解的存储方案就三种。
| 存储方案 | 数据结构 | 优点 | 不适合的场景 |
|---|---|---|---|
| CSV | 表格型,每行一条记录 | 可以用Excel/WPS直接打开,书写简单 | 嵌套结构、字段数量经常变化的场景 |
| JSON | 树形/嵌套结构 | 保持原始层级,和接口返回天然匹配 | 需要频繁查询、更新的场景 |
| SQLite | 关系型表 | 自带查询语法,支持去重和增量更新 | 数据规模极大、需要并发写入的场景 |
选择逻辑其实就三条:数据是规整的平面表格,优先CSV;数据本身就是JSON嵌套结构,直接存JSON;后续需要反复查询和去重,用SQLite。我自己带新人时经常强调一句:入门阶段不要碰MySQL这类重量级数据库。不是学不会,而是SQLite足够轻、足够用,你能用最少的代码把"数据持久化"这个概念真正跑通。
4.2 增量去重这件事,入门阶段就要养成习惯
很多新手跑第二遍爬虫时,会把上一遍已经抓过的数据再抓一遍,导致文件越积越乱。这个问题的专业名字叫"去重",解决思路是:每次抓取前先检查这条数据是不是已经存在。
最朴素也最实用的办法是维护一个"已见集合":
import json seen = set() try: with open('seen.json', encoding='utf-8') as f: seen = set(json.load(f)) except FileNotFoundError: pass data = [{'name': '商品A', 'price': 299}, {'name': '商品B', 'price': 199}] new_records = [] for item in data: key = f"{item['name']}-{item['price']}" if key not in seen: seen.add(key) new_records.append(item) with open('seen.json', 'w', encoding='utf-8') as f: json.dump(list(seen), f, ensure_ascii=False)这个思路在这个阶段已经够用:程序启动时读取已见集合,抓取时判断,结束时落盘。养成这个习惯之后,你会发现后面学任何高级存储方案,核心逻辑都没有跑出这个框架——只是把seen从一个JSON文件换成了数据库表而已。
4.3 写文件时的编码与追加坑
存数据看似简单,实际坑也不少。我挑三个最常见的说。
第一个是CSV写文件时要指定newline='',否则每两行之间会多出一个空行,Windows上尤其明显。第二个是写文件时一定要显式指定encoding='utf-8',不指定的话在某些系统上默认编码会不一致。第三个更冷门一点:如果你生成的CSV文件要发给别人用Excel打开,最好用utf-8-sig而不是utf-8编码保存,否则Excel打开后中文可能是乱码。原因就是Excel需要读取一个BOM头才能正确识别UTF-8。
提示:无论用哪种存储方案,都建议给每一条数据加上"抓取时间"字段。数据一旦带上了时间维度,后面做分析时的价值会大不一样,这个习惯越早养成越好。
5. 新手踩坑实录:我见过最多的五个翻车现场
5.1 明明拿到了HTML却解析不到数据
这是新人问得最多的问题之一:状态码是200,resp.text里也有一堆HTML,但用选择器就是找不到目标数据。遇到这种情况,我的排查链路永远是固定的三步。
第一步,把拿到的HTML原样保存成文件,用浏览器打开,肉眼确认里面到底有没有你要的内容。第二步,如果文件里根本没有,那就说明你要的数据不是这个请求返回的,而是页面加载后通过其他请求拿到的。第三步,如果文件里确实有,但选择器匹配不到,问题就出在你的选择器路径上——可能是少了一层节点,也可能是选错了标签。
最关键的洞察是:浏览器里看到的内容,不等于requests拿到的内容。浏览器里看到的是JavaScript执行完之后的最终状态,而requests拿到的只是最初的HTML源码。我就见过不少同学对着浏览器控制台复制选择器,结果在程序里怎么都匹配不上,那是因为他复制的是渲染后DOM的路径。
5.2 接口返回的内容和你看到的不一样
顺着上面那个坑继续说:当你发现源码HTML里没有目标数据时,多半是因为页面是用JavaScript动态渲染的。这时候很多新人第一反应是上无头浏览器,我的建议恰恰相反——先去浏览器开发者工具的Network面板看看有没有更直接的接口。
很多网站的动态数据并不是自己计算出来的,而是调用了一个后端接口返回一段JSON。这个接口往往就在你打开页面的那一刻被请求过。你在Network面板里找到它,直接请求这个接口,拿JSON解析,比解析渲染后的页面简单十倍。判断方法很简单:在Network里筛选XHR或Fetch类型的请求,挨个看响应内容,哪个里面有你需要的数据,那个就是目标接口。
这个阶段的正确态度是:不要因为页面"动态"就觉得必须学重型工具。先学会跟浏览器开发者工具相处,看请求、看响应、分析接口,这本身就是爬虫工程师最核心的基本功之一。
5.3 被限流了怎么办:频率控制的基本修养
状态码429是服务器在明确告诉你:"你太快了,我扛不住了。"很多人的第一反应是各种想办法继续硬闯,这是完全错误的方向。正确的处理方式只有一条:把频率降下来。
爬虫工程师应该对服务器持有一种朴素的尊重:你访问的是一个真实运行的服务,这个服务背后有很多真实用户在使用。你的请求如果占了太多资源,本质上是在侵占别人的使用体验。所以入门阶段我最强调的一个数字习惯是:小规模练手项目,每次请求之间至少间隔2到5秒,可以用随机数抖动一下,模拟人的节奏。
如果降速之后依然被限制,那大概率说明你的访问规模对目标站点来说已经过大了。这时候该考虑的就不是"怎么绕过去",而是换官方API、联系站点获取授权,或者换一个合法的数据来源。被限流不是技术问题,是边界问题,把心态摆正,很多麻烦根本不会发生。
5.4 动态渲染页面:这个阶段的正确态度
我必须泼一盆冷水:入门阶段完全不需要碰Selenium、Playwright这类浏览器自动化工具。不是它们不好,而是你在基础没打牢时去学它们,会掩盖掉真正的短板——你连普通页面的请求和解析都没完全吃透,就跑去做比较重的动态页面渲染,结果就是代码又长又慢,还会被各种反自动化机制折腾得精疲力尽。
那这个阶段遇到动态页面怎么办?我的答案是:回到5.2的思路,找接口。把动态页面当成一道"找API"的练习题,而不是掏工具解决。当你已经能熟练地从任何页面里快速定位出真正的数据接口,并写出干净简洁的接口解析代码时,你才有资格进入动态渲染的进阶学习。
5.5 日志与断点:调试爬虫的基本功
爬虫写多了必然会出问题,调试能力才是真正拉开差距的地方。新手的通病是:代码报错了,把异常信息贴在搜索引擎里搜一圈,然后照着别人的代码抄一遍,抄完发现还是不对。
我建议每个人都建立一套自己的"三行日志"习惯:请求发出前后各打印一行,解析到数据后打印一行摘要。比如:
print(f'[请求] GET {url}') resp = requests.get(url, timeout=10) print(f'[响应] 状态码={resp.status_code}, 内容长度={len(resp.content)}') # 解析逻辑... print(f'[解析] 共提取 {len(items)} 条数据')这套日志的价值在于,出现问题你立刻能判断是断在请求、响应还是解析环节。另外,解析失败时不要急着改选择器,先把HTML保存成一个文件,反复离线调试选择器,不用一遍遍去请求目标站点,效率能翻好几倍。
6. 入门的边界感:什么能爬、什么不该爬
6.1 robots.txt的正确读法
技术学得越深,越要清楚"能做"和"该做"的区别。robots.txt是站点放在根目录下的一个文本文件,用来告知爬虫哪些路径可以访问、哪些不建议访问。它不是强制性的安全机制,但它表达了站点所有者的意图,是一个负责任的爬虫开发者需要先看一眼的东西。
新手打开一个站的robots.txt时,重点理解两条指令就够:User-agent指定规则适用于谁,Disallow指定不允许访问的路径。比如:
User-agent: * Disallow: /admin/这行的意思就是:对所有爬虫,请不要访问/admin/开头的路径。知道了这条规则,你的爬虫就应该主动跳过这些路径再开发功能。去看一眼robots.txt用不了三十秒,但这是专业和业余的分界线。
6.2 频率控制的底线是什么
上一章说了429的处理,这里把底线说得更具体一点。我给自己定的原则是:任何爬虫的运行都不应该影响到目标站点的正常服务。怎么衡量?一个很简单的标准——用你自己的访问体验去代入,如果你访问这个站点都变慢了,那你的爬虫很可能已经在给别人添麻烦了。
对个人练手项目来说,合理的频率区间大致是:静态列表页每隔3到5秒请求一次;有分页的页面,单轮跑完的请求数控制在几十个以内;绝对不做全天24小时不间断的抓取。这些数字不是金科玉律,但它们背后是同一个道理——让爬虫保持"克制",你才能持续稳定地拿到高质量数据。
6.3 入门阶段的红线清单
最后把红线列清楚,这些内容我在带新人时一定会反复强调:
- 不碰需要登录后才能访问的个人私密数据,哪怕你只是好奇。
- 不对任何站点做超出正常访问节奏的自动化操作,避免影响服务器状态和他人使用。
- 不绕过付费内容和会员权限,这个没有任何正当理由。
- 不要拿抓取的数据去谋利,除非你已经确认了版权和授权问题。
- 不做永不停止的高频抓取,数据库里缺几条数据,永远好过把目标站点拖垮。
这几条看起来像是"道德教育",其实是实实在在的生存法则。我见过太多新人因为一次越界操作,导致整个IP段被站点拉黑,练手项目被迫中断。守住边界,不只是为了别人,更是为了你自己的学习之路能走得长远。
带完一批又一批新人,我最大的体会是:爬虫入门阶段比拼的从来不是技巧密度,而是基础扎实度和工程习惯。HTTP懂了,你会明白每个报错都有它的逻辑;解析懂了,你能在一个小时内把任意静态页面跑通;落盘和去重懂了,你的数据才真正变成资产,而不是临时打印在控制台里的字符。至于那些看起来酷炫的高级操作,等这三块地基打牢之后,你自然会知道该往哪个方向走。
如果你正在这个阶段,我最后的建议很具体:拿一个你自己真正感兴趣的小站点练手,设定一个很小的目标,比如把某个分类下的前50条数据抓下来存成CSV。目标越小越好,因为越小越容易跑完,跑完越有成就感,成就感才是支撑你继续深入最可靠的东西。下一阶段,我们再来聊动态页面、大规模抓取和数据清洗。