1. 爬虫到底是什么,以及为什么建议从零开始学它
1.1 一个被误解的"技术动作"
经常有人问我:爬虫难吗?是不是得先把 Python 学到很高级才能碰?我的回答一直很直接——如果你的目标是入门爬虫,掌握最基本的 Python 语法、能运行一个 .py 文件,其实就已经够开始动手了。爬虫这个事,剥开所有术语之后,本质上就是用脚本代替你手工复制粘贴网页数据:你平时选中一段文字、右键复制、贴到表格里干了半小时的活,程序按你的规则几百次、几千次地自动完成。
爬虫不是黑客工具,它只是一个三步组合技:发请求拿到网页内容,解析 HTML 定位目标,提取你要的数据。这三步没有一步需要高深算法。打个比方,爬网页就像去图书馆借书:你得先知道书在哪个书架(URL),然后翻到具体某一页(解析 HTML),最后把该抄的几句话写进笔记(数据提取)。真正让新手懵住的,往往不是"怎么爬",而是"爬完怎么处理、遇到各种怪情况怎么办",而这些恰恰是练出来的经验,不是背出来的理论。
这篇文章是一整条从零走通的路:装好环境、读懂页面结构、写一个能跑的爬虫、把数据存下来,再告诉你新手最容易翻车的几个坑在哪。无论你是刚学完循环和函数的小白,还是想找个练手项目巩固基础的半熟手,按着文章走一遍,都能跑出自己第一个真正意义上的爬虫。
1.2 学爬虫能解决的实际问题
我见过新手学爬虫,目标其实都很朴素,基本绕不开下面几类:
- 比价和盯价:把几个平台上自己想买的东西的价格抓下来放到一张表里,哪天降了心里有数。
- 信息聚合:把分散在不同页面的活动信息、公告、课程表集中整理成自己的排期表。
- 小规模数据研究:写论文、做作业、搞个人研究时,需要批量收集一批公开数据,手动复制会疯。
- 综合练手:爬虫是一个极佳的 Python 综合练习,它把请求、字符串处理、文件读写、异常处理全部串在了一个真实项目里。
无论你属于哪一种,核心诉求是一样的:用程序替自己把重复劳动去掉。所以这篇文章不会一上来就铺框架、讲架构,而是先把最主线的"请求→解析→存储"走通。路线越短,你越能看到成果,信心也越足。
1.3 这篇文章会带你走到哪一步
我会带你写一个具体的 Demo:在本地搭建一个测试用图书列表页面,用 requests 和 BeautifulSoup 把书名、价格抓下来,清洗后存成 CSV 文件。这一步会让两件事变得扎实:一是你彻底看懂了一页数据是怎么从 HTML 变成自己表格里的记录的;二是你拥有了一个可以反复扩展的模板,以后换成任何公开页面,改改选择器就能复用。
这一篇不教的东西也同样重要:分布式爬虫、大规模并发、登录态模拟、浏览器自动化,这些先放到后面。原因很简单,它们解决的是规模和复杂性问题,而你现阶段真正的问题是"对网页和数据还没有手感"。先把最笨但最可靠的一条路走熟,后面学什么都是水到渠成。起点低一点没关系,方向对就行。
2. 环境准备:装好这三样,后面就顺了
2.1 Python 版本和虚拟环境
写爬虫之前,先确认你本机的 Python 环境。打开终端(Windows 上是命令提示符或 PowerShell,macOS/Linux 上是终端),运行:
python --version正常情况下会看到类似Python 3.10.x的输出。如果提示找不到命令,试试python3 --version。只要是大版本 3.8 以上,学习本文内容都够用。
接下来强烈建议给项目建一个独立的虚拟环境。虚拟环境听起来很高级,其实一句话就能讲明白:它相当于给你这个爬虫项目单独隔出一间小房间,里面装的库不会跟你别的项目互相打架。我早期嫌麻烦不建环境,后来被"为什么这个项目跑得好好的,换个项目就报错"折磨过好几回,从此再也不敢省这一步。
python -m venv spidey_envWindows 环境下激活:
spidey_env\Scripts\activatemacOS/Linux 环境下激活:
source spidey_env/bin/activate激活后,命令行提示符前面会出现(spidey_env),说明你已经进房间了。关掉终端再回来时,记得重新激活一次。
2.2 安装 requests 和 BeautifulSoup
爬虫界有一句话叫"requests 负责把网页拿回来,BeautifulSoup 负责把数据挑出来",这俩是入门阶段最顺手的组合。激活虚拟环境后执行:
pip install requests beautifulsoup4 lxml我额外装了 lxml,它是一个解析 HTML 速度更快的解析器,让 BeautifulSoup 用它来干活。如果装不上,也不影响课程主线,代码里把解析器换成"html.parser"一样能跑,只是慢一些。
装完顺便验证一下环境,新建一个check_env.py文件,写上:
import requests from bs4 import BeautifulSoup print("requests 版本:", requests.__version__) print("BeautifulSoup 可用")能打印出版本号,说明环境已经通了。如果 pip 下载很慢,可以配置一个离你近的镜像源,但要注意只认官方渠道公布的地址,不要随手抄来路不明的配置,这也是一个基本的安全习惯。
2.3 为什么入门不直接上 Scrapy
很多教程喜欢一上来就推 Scrapy,但我带新手时从来不推荐这么干。Scrapy 本身其实挺优秀,可它是个完整的框架,有引擎、爬虫类、管道、中间件、配置文件,对一个连选择器还没写熟的人来说,框架概念反而成了最大的拦路虎。我把两者放在一起对比一下:
| 对比项 | requests + BeautifulSoup | Scrapy |
|---|---|---|
| 上手难度 | 低,一次 get 就是一次请求 | 高,要先理解框架概念 |
| 请求方式 | 自己写循环,直观 | 内置异步并发 |
| 排错难度 | 简单,print 出来看 | 要翻日志、查管道 |
| 适合阶段 | 理解爬虫本质 | 大规模采集 |
| 扩展成本 | 手动加逻辑 | 框架内扩展 |
结论很简单:入门阶段先把"请求→解析→存储"这条主线走通,等你真正开始觉得"循环里手动发请求太慢了、我想并发"的时候,再花一周时间学 Scrapy,那时你能一眼看穿框架在帮你干什么,学起来效率完全不同。
3. 读懂 HTML:在动手写代码前先学会"看页面"
3.1 HTML 就是一棵"节点树"
爬虫核心是把 HTML 当数据库来查,所以你必须先懂一点 HTML 结构。好消息是,只需要懂最基础的标签就够了。看一个最典型的片段:
<div class="book-item"> <span class="title">Python 生活指南</span> <span class="price">59.8</span> </div>HTML 是一棵倒着长的树:最上面是<html>,往下分枝出<body>,再往下一层一层长出<div>、<span>这样的节点。class="book-item"是这个节点的"标签牌",方便我们按名字找到它。在上面的片段里,class="book-item"的 div 是一个可重复的"图书条目",抓数据就是把这些条目一个个挑出来,再从中取.title和.price里的文字。
为什么爬虫老爱盯着 class 看?因为网站前端为了让样式复用,同类内容通常用同一批 class 命名。你抓住这个规律,就等于抓住了整页数据的骨架。
3.2 用浏览器开发者工具定位目标数据
在任意网页上按 F12,或者右键点击你想抓的内容选择"检查",会打开开发者工具,Elements 面板里就是浏览器帮你解析好的 HTML 树。你可以点左上角那个箭头图标,然后去页面上点数据,光标会帮你定位到对应节点。
这里有一个很容易踩的坑:有人图省事,右键直接"复制 selector",把浏览器生成的那串超长路径粘进代码。这个路径通常长成#root > div:nth-child(3) > div:nth-child(2) > div > span.title这样,看着很专业,实际上脆得像饼干——网站只要改一点点结构,它就失效。我的做法是把它当参考线索,看完之后自己归纳一个短而稳的选择器,比如.book-item .title。记住一个原则:选择器越短、越贴近业务含义,以后维护的成本越低。
3.3 自己搭一个测试页面,避免误伤真实站点
练手时最不应该做的事,就是直接对着别人家的线上页面疯狂发请求——既容易把对方服务器搞烦,也容易把自己账号搞封。所以我建议:第一个爬虫,目标就用自己的本地页面。
新建一个books.html文件,内容如下:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>零基础图书角</title> </head> <body> <div class="book-item"> <span class="title">Python 生活指南</span> <span class="price">59.8</span> </div> <div class="book-item"> <span class="title">爬虫开发十日谈</span> <span class="price">79.0</span> </div> <div class="book-item"> <span class="title">算法与数据结构入门</span> <span class="price">99.5</span> </div> <div class="book-item"> <span class="title">Linux 命令行笔记</span> <span class="price">68.0</span> </div> </body> </html>然后在同一个目录下执行:
python -m http.server 8000浏览器打开http://127.0.0.1:8000/books.html,你就有自己的测试网站了。这个页面完全由你掌控,随便爬、随便改,跑出问题一眼就能看出是页面问题还是自己代码问题。这是我想强调的一个习惯:练习永远优先用自己能掌控的目标,既练熟了技术,也不给人添麻烦。
4. 完整 Demo:抓一个图书列表页面,把数据存成 CSV
4.1 第一步:发请求,先看状态码
新建spider.py,先写第一段代码:
import requests url = "http://127.0.0.1:8000/books.html" 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)如果前面的本地服务器还开着,这里应该打印出200。requests.get干的事情很简单:模拟一个浏览器去访问你给的网址,然后把服务器的回应装进resp对象里。
很多新手看到别人代码里写 headers,不知道为什么要写。简单说,服务器的日志里会记录每个访客的"自我介绍",也就是 User-Agent。没有它,服务器可能会认为你这个程序连浏览器都不像,直接拒绝。所以我给它戴上了一个常见浏览器的"铭牌"。这是爬虫新手需要学的第一个细节,但它不意味着可以乱来——这只是让服务器至少知道,访问它的是一个还算正常的程序访客。
关于状态码,先记住这几个就够了:
| 状态码 | 含义 | 常见原因 |
|---|---|---|
| 200 | 成功 | 一切正常 |
| 301/302 | 重定向 | 页面搬家了 |
| 404 | 不存在 | 地址写错 |
| 403 | 拒绝访问 | 服务器不想给你看 |
| 429 | 请求太频繁 | 你太快了,歇会儿 |
看到200还只是万里长征第一步,因为服务器给回来的是一整页 HTML,真正的内容还得靠解析。
4.2 第二步:解析 HTML,按节点路径取数据
接着写:
from bs4 import BeautifulSoup soup = BeautifulSoup(resp.text, "html.parser") items = soup.select(".book-item") for item in items: title = item.select_one(".title").text price = item.select_one(".price").text print(title, price)这里做了两件事:soup.select(".book-item")把所有带book-itemclass 的节点全部挑出来,返回一个列表;然后循环里用select_one在每个条目里取第一个.title和.price,再用.text把标签里的文字拎出来。跑一下,你会在终端看到四个书名和四个价格,这说明你已经从 HTML 里成功抽出了结构化数据。
如果输出一堆 None,十有八九是选择器没匹配上;如果输出空列表,则是 soup 解析出来的内容和你想的不一样。遇到这种情况先打印一段soup看看它到底拿着什么,别急着怀疑后面的逻辑。
4.3 第三步:清洗数据并写入 CSV
光在终端打印不算完事,爬虫的终点一般是把数据落到文件里。CSV 是最通用的格式,任何表格软件都能打开。继续写:
import csv with open("books.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["书名", "价格"]) for item in items: title = item.select_one(".title").text.strip() price = item.select_one(".price").text.strip() writer.writerow([title, price]) print("写入完成")这段有几个细节值得多说两句。第一,.text拿到的文字经常带着页面里的换行和空格,所以我对它调用了.strip()去掉首尾空白,这就是最基础的"数据清洗"。第二,encoding 用的是utf-8-sig而不是常见的utf-8,这个后缀会在文件开头写入一个标记,让电子表格软件打开 CSV 时不会把中文显示成乱码。这个坑我当年踩了整整一个下午,今天直接帮你填平。第三,newline=""是为了防止写入时出现多余的空行,属于 Python 写 CSV 的固定套路。
打开生成的books.csv,如果看到两列整齐的书名和价格,恭喜,你的第一个爬虫已经完整跑通了。
4.4 让程序"礼貌"一点:请求间隔
Demo 跑通之后,你会忍不住想去爬真正的网站。可以,但请先加上一个习惯:在请求之间睡一会儿。给代码加上:
import time # 每次请求之间暂停 1 秒 time.sleep(1)为什么非要加这个?因为对方服务器不是你朋友开的专用测试机,它同时要服务大量真实用户。你用自己的脚本以每秒十个请求的速度连续打过去,跟拿着喇叭在图书馆里喊没什么两样。1 秒只是一个起步值,正式做项目时我会根据对方情况调成 2 到 3 秒。记住一句话:爬虫写得多快不重要,爬得让站长不反感才重要。
5. 翻页、编码和异常:新手最容易在这三处翻车
5.1 翻页抓取的正确姿势
大多数列表页面都有翻页,URL 往往有规律可循。常见形式是问号后面的参数,比如第一页是http://127.0.0.1:8000/books?page=1,第二页是...page=2。这种翻页最简单,直接用循环拼 URL:
import time import requests from bs4 import BeautifulSoup headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } for page in range(1, 6): url = f"http://127.0.0.1:8000/books?page={page}" resp = requests.get(url, headers=headers, timeout=10) print(f"正在抓第 {page} 页") # 这里接上解析和写入 CSV 的逻辑 time.sleep(1)另一种翻页形式是页面底部有一个"下一页"按钮,对应 HTML 里一个<a class="next" href="...">链接。这种就别硬猜 URL 了,每抓完一页就去找a.next的链接,找到就继续,找不到说明到底了。这两种模式覆盖了市面上九成的翻页场景。第一种翻页时要注意确认网页是不是真的按 page 参数返回不同内容,不要抓了半天发现每页都一样,那是动态页面的问题,后面会讲。
5.2 中文乱码不是玄学,是编码没对齐
爬中文网站,乱码可能是你遇到的第一个"玄学"问题:明明代码没写错,打印出来却是一堆看不懂的符号。原因十有八九是编码不对齐。服务器返回网页时,会通过 HTTP 头告诉程序员文档用什么字符编码,但一些老服务器没说清楚,requests 就会猜,猜错时中文自然崩。
最稳妥的办法是你直接去网页源码的<meta charset="...">里看真实编码。如果对方网页用 UTF-8,可以强制指定:
resp.encoding = "utf-8"如果你懒得手动查,也可以让 requests 自己猜:
resp.encoding = resp.apparent_encodingapparent_encoding是 requests 根据页面内容自动推断出来的编码,大多数时候靠得住,但偶尔会猜偏。所以我的建议是:能手动指定就手动指定,实在不知道再让它猜。看到乱码先别慌,先想想编码,八成是这里的问题,不是你的数据抓错了。
5.3 异常处理写得好,半夜不用爬起来
新手写爬虫经常陷入一种"裸奔"状态:代码在没有异常处理的情况下直接循环抓取,跑到第 3 页突然网络抖动超时了,整个脚本崩掉,前面抓的数据也打了水漂。真实世界的网络从来不稳定,所以异常处理不是选修课,是必修课。
入门阶段先写一个朴素的容错版本:
import time import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } for page in range(1, 6): url = f"http://127.0.0.1:8000/books?page={page}" try: resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() # 状态码不是 2xx 时抛出异常 except requests.Timeout: print(f"第 {page} 页超时,跳过") continue except requests.HTTPError as e: print(f"第 {page} 页 HTTP 错误:{e}") continue except requests.RequestException as e: print(f"第 {page} 页请求失败:{e}") continue # 到这里才说明请求成功,继续解析和存储 print(f"第 {page} 页抓取成功") time.sleep(1)raise_for_status()是个好东西,它会帮你检查状态码,只要不是 2xx 就抛异常。配合try/except,脚本最多跳过失败页,不会整个崩掉。还有一个我强烈推荐的习惯:每抓到一页就把数据立刻写入文件,而不是全部抓完再一次写。这样即使中途脚本真崩了,你手里也已经有大部分数据了。增量落盘,是爬虫工程里被验证过无数次的稳妥策略。
6. 遇到动态加载的页面怎么办(先别慌)
6.1 怎么判断页面是不是动态加载
爬虫新手最容易困惑的一幕是:requests 拿到手的 HTML 里找半天,明明浏览器页面上有数据,解析结果却是空的。原因通常是页面用了动态加载——数据不是服务器直接塞在 HTML 里的,而是浏览器执行了一段 JavaScript 后,才从后台接口取回来填充到页面上的。requests 不执行 JavaScript,所以它拿到的原始 HTML 里自然没有数据。
判断方法很简单:在页面上右键选择"查看网页源代码",对比一下源代码里有没有你要的数据。如果浏览器里看得见,源代码里却找不到,基本可以确定是动态加载。另外,如果你看到页面里一大片空空的<div id="app"></div>之类框架容器,通常也是动态页面的信号,数据是 JS 后来填进去的。
6.2 入门阶段的优先思路:去后台找接口
遇到动态加载,很多人第一反应是上浏览器自动化工具,但我建议你先试试另一条更省力的路:找接口。打开开发者工具的 Network 面板,刷新页面,筛选出 XHR 或 Fetch 类型的请求,你能看到浏览器向后台发的各种请求。很多时候你会找到一个返回 JSON 的接口,页面上显示的数据,其实都来自这个接口。
用 requests 直接请求这个接口,往往比解析渲染后的 HTML 还简单:
import requests data_url = "http://127.0.0.1:8000/api/books" resp = requests.get(data_url, timeout=10) books = resp.json() for book in books: print(book.get("title"), book.get("price"))找接口需要一丁点网络调试经验,刚开始不一定能看明白,这是正常的,多试几次就有感觉了。而且这给了你一个很好的视角:你爬网页,本质就是在跟网站前后端打交道的两端之间摸索。找到接口之后,注意观察它返回的字段名,那才是你数据处理的真正依据。
6.3 浏览器自动化工具留到后面学
等你把 requests 这整套玩熟了,再学浏览器自动化也不迟。市面上的主流自动化框架能真的把浏览器打开,等 JS 执行完再拿渲染后的页面,几乎什么动态页面都能搞定。但为什么我坚持建议入门阶段先不碰?因为它是资源消耗和心智负担的双重开销:每次请求都要启动一个完整浏览器,速度慢、内存占用高,而且能解决问题的思路明明更简单时,何必杀鸡用牛刀。
还有一个更实际的原因:很多动态页面背后的 JSON 接口是完全可以直接请求的,等你养成"先看接口、再考虑自动化"的习惯,很多看起来复杂的项目其实会变得很轻。记住一个优先级:能直接请求的就直接请求,请求不了再看有没有接口,实在没有接口再考虑浏览器。这个顺序能帮你省下大量时间。
7. 边界与底线:爬虫能写,但不能乱写
7.1 robots.txt 是站方的"使用须知"
聊技术聊到这里,必须认真聊聊边界。爬虫不是技术写出来就能随便用的,它有一套行业默认的规则。第一件事是看robots.txt,这个文件放在网站根目录,比如http://某站点.com/robots.txt,它像一张"使用须知"贴在门口,告诉爬虫哪些路径被允许访问、哪些不被允许。一个典型的文件长这样:
User-agent: * Disallow: /internal/意思是:对所有爬虫程序,别碰/internal/这个路径。robots.txt 不是法律,但它表达的是站方的明确意愿,尊重它是从业者之间的默认共识。写爬虫前花一分钟看它,是最基本的职业礼貌。除此之外,网站的服务条款里关于自动采集的条款也值得看一眼,尤其是你打算长期、规模化地使用某个网站数据的时候。
7.2 请求频率:克制才是真正的专业
前面讲 sleep 时说的是礼貌,这里再往深说一层。你访问一个网站的速度,是服务器管理员最敏感的指标。如果你用并发几十甚至上百的请求去打一个普通站点,对方服务器可能直接就扛不住了,轻则你的 IP 被限制,重则影响网站正常用户的体验。这跟去图书馆复印一本书是一个道理:你可以复印,但没理由整台复印机搬回家连轴转。
实操层面有几条铁律:单线程起步,先别上并发;请求间隔至少 1 到 3 秒;看到 429 或 503 状态码时立刻停手,退避一段时间再继续;尽量避开对方网站的流量高峰时段。很多人把"能爬到数据"等同于"技术好",我却觉得,能控制住自己不去给服务器添麻烦,才是更高一级的专业能力。
7.3 哪些数据不应该碰
有些数据,即使技术上能爬到,也不该碰。我把红线列在这里,帮助你一眼避开:未公开的个人身份信息(手机号、身份证号、家庭住址)、需要登录才能查看的会员内容、未成年人的任何个人信息、有明确版权标识的图文作品、医疗和金融等敏感数据。大规模收集这些内容,轻则违约,重则触犯法律,不是一句"我只是学习"就能带过去的。
我的建议是把四条基本线刻在脑子里:只采集公开的、合法的、非敏感的、不给服务器造成压力的数据。练手优先用本地页面;想长期使用某个真实网站的数据,主动联系站方说明用途、争取许可,才是最稳妥的路径。我一直觉得,爬虫这个技能本身是中性的,它的价值取决于你拿着它往哪个方向走。用好了它是效率工具,用歪了它是麻烦制造机,选择权始终在你手里。
最后聊一点我自己的体会。刚开始写爬虫那阵子,我的习惯是把所有逻辑塞进一个脚本,一口气把所有页面全抓完,结果经常跑到一半被服务器拒绝,或者电脑被自己的请求卡得动弹不得。踩过几次坑之后,我的固定姿势变得很朴素:先抓一页、打印出来确认结构;再抓三页、确认翻页和编码没问题;最后才上完整循环,并且每一页的结果都立即落盘。你花在"确认结构"上的五分钟,能帮你省掉后面调小半天 bug 的时间。第二个小技巧是:失败的请求不要直接丢掉,把它记录到一个单独的文件里,跑完一遍对着日志补齐数据,比自己盯着屏幕猜强太多。爬虫入门一点都不难,难的是保持克制,把它规范地用在值得用的地方。祝你的第一个爬虫顺利跑通。