你是不是也有过这种时刻:看到某个网站上的一份榜单、一批资料,特别想把它整理成表格,但一条条复制又太累。于是你听说了 Python 可以写爬虫,于是你打开了一个教程,装好了环境,然后……卡在了不知道从哪里入手。这篇博文就是写给这样的你:用最直接的方式,从零写出你的第一个 Python 爬虫。我会把关键概念、最小可用代码和常见问题拆开讲清楚,你照着敲一遍就能跑通,跑通之后再回头理解原理,会轻松很多。
无论你是零基础还是只会一点 Python 语法,只要会用命令行、能打开文本编辑器,这篇文章都适用。我会避开复杂框架,只用requests和BeautifulSoup这两个库完成一整套流程。目标只有一个:让你亲手把一个网页上的数据抓下来,并保存成 Excel 能打开的表格。跑通了,你对爬虫的恐惧就消失了一半。
1. 写爬虫前,先想清楚三件事:目标、页面结构和访问礼仪
很多初学者学爬虫失败,不是因为代码写不对,而是因为目标太模糊。"我想爬某个网站"不是一个目标,"把某图书社区一周热门书单前 100 本的书名、作者、评分和价格取下来,按从高到低排序,存成一个 CSV 文件"才是。目标越具体,你后面每一步都会越有方向。
1.1 把目标写成一句话:数据源 + 字段 + 输出
在你打开编辑器之前,先拿出一张纸,用一句话回答三个问题:
- 数据从哪个页面来?最好是单一的、公开的、不需要登录的网页。
- 想保留哪几个字段?比如书名、作者、评分、价格。
- 最终输出成什么?对初学者来说,表格文件 CSV 是最佳选择。
为什么要这么做?因为爬虫本质上就是三件事:发请求、解析页面、存数据。如果一开始把三件事混在一起写,稍微报错你都不知道问题出在哪一环。我现在带新手写爬虫,都会先逼着他们把目标从"我要爬某某网站"改成"我要从某某列表页拿某几个字段再存成 CSV",改完这句话,代码就等于有了一张施工图。
1.2 用开发者工具判断页面是"死的"还是"活的"
打开目标网页,按键盘上的 F12 切换到开发者工具,先看"元素(Elements)"面板。你自己看着页面上的某条数据,在元素面板里搜一下,看能不能搜到同样的文字:
- 如果能搜到,说明这条数据直接写在 HTML 源码里,这类页面叫静态页面,最适合作为你的第一个爬虫目标。
- 如果搜不到,只看到一个空架子,说明数据是通过脚本从后台接口加载的。真正的数据藏在一个 JSON 文件或接口里,这种页面虽然也能爬,但你需要先分析接口地址和参数,对新手来说难度会陡增。
所以我强烈建议:第一次练手,挑一个内容直接在 HTML 里的网站。比如历史公告页、公开的书单页、文档站列表页。别一上来就挑战那些页面滚动到哪才加载到哪的"现代化站点",那不是初中生拿小学生试卷练手,是直接去考大学高数。
1.3 访问礼仪:把自己当成一个礼貌的访客
爬虫本身是中立的技术,但"怎么爬"体现了你的工程素养。我的个人准则是这样三条,也建议你从第一天就养成:
- 事先看看目标网站的
robots.txt文件。你可以在浏览器地址栏输入目标域名加/robots.txt,比如https://example.com/robots.txt。这个文件会告诉你哪些路径允许抓取、哪些不允许。它不是一个强制的法律文件,更像网站管理员留的一张告示牌,尊重它是基本礼仪。 - 控制请求频率。即使只抓几百页,每两次请求之间也至少间隔 1 到 2 秒。你访问网站是为了拿数据,不是为了让别人的服务器过载。
- 只采集公开信息。涉及个人隐私、需要登录才能看的内容、明显属于某个平台私有数据的内容,最好不要碰。新手练手选公开列表页就够了,没必要去试探边界。
遵守这些原则,能帮你避开绝大多数由"爬虫"引发的纠纷。记住,爬虫技术的第一课不是"如何绕过",而是"如何好好访问"。
2. 环境准备:从零搭一个干净的工具台
写爬虫只需要三个工具:一个能运行 Python 的环境、一个叫requests的库负责发网络请求、一个叫BeautifulSoup的库负责解析 HTML。安装过程不复杂,但很多人栽在乱装包上,所以我单独用一章讲环境,是有原因的。
2.1 用虚拟环境管理依赖,而不是直接装进系统
我见过不少初学者一上来就pip install requests,装完就开写。前几个项目没问题,等过两三个月,你会被各种第三方库的版本冲突折磨得想重装系统。更稳妥的做法是从一开始就用虚拟环境。
打开终端,先确认 Python 已经安装:
python --version # 或者 Python 3.11 等类似输出如果你的电脑没装 Python,先去 Python 官网下载安装包,安装时记得勾选"Add Python to PATH"。装好之后,创建一个虚拟环境:
python -m venv spider_env然后激活它:
# macOS / Linux source spider_env/bin/activate # Windows spider_env\Scripts\activate激活后,你的命令行前面会多出(spider_env)这样的标识。接着安装两个核心库:
pip install requests beautifulsoup4 lxml我的习惯是一并安装lxml。BeautifulSoup 支持多种解析器,而lxml是速度和容错性平衡得最好的一款。当然也可以只用内置的html.parser,完全零依赖,但对页面中一些不规范 HTML 的处理不够理想。既然你有 Python,装一个lxml不亏。
2.2 安装完先做三项验证,别急着写代码
很多人装完库就急着敲爬虫代码,结果第一行就报ModuleNotFoundError,然后开始怀疑人生。安装完成之后,建议先跑三条命令验证:
python -c "import requests; print(requests.__version__)" python -c "import bs4; print(bs4.__version__)" python -c "import lxml; print(lxml.__version__)"三条命令都没有报错,说明环境你就搭到位了。如果提示找不到模块,多半是当前终端没有激活虚拟环境,或者你同时装了多个 Python 版本,pip装到了另一个解释器上。这时候,先检查虚拟环境是否激活,再检查pip --version对应的路径。
2.3 第一个最小脚本:把网页保存到本地
先做一个很小但很有用的动作:用requests把页面抓下来,原样保存成本地 HTML 文件。这一步我称之为"落地",它有两个好处:
- 后续解析基于本地文件,调试时不反复请求服务器,减少被封风险。
- 遇到解析问题,可以直接用浏览器打开本地文件,对照实际结构做分析。
import requests url = "https://example.com" resp = requests.get(url) print("状态码:", resp.status_code) print("网页长度:", len(resp.text)) with open("page.html", "w", encoding="utf-8") as f: f.write(resp.text) print("页面已保存到 page.html")example.com是专门留给示例用的域名,不会给任何人造成困扰,适合你测试环境。跑通之后,再把url换成你真正想爬的页面地址。这里有一个值得养成的习惯:抓下来的 HTML 先保存,再动手解析,而不是每次都重新请求一遍。
3. 用 requests 把网页拿下来,先别急着解析
很多人学习爬虫时,脑子里只有"我只要那个数据"这一个念头,忽略了最基本的 HTTP 请求知识。结果一遇到状态码异常就懵了。这一章我会把requests的常用要点讲透,你以后写任何爬虫都用得上。
3.1 GET 请求到底发生了什么
你可以把浏览器想象成一位顾客,网站服务器是一家餐厅。你输入网址回车,相当于告诉服务员"我要看菜单上那道菜的信息",服务员把菜单卡片拿给你看。这个"拿"的动作,在 HTTP 里就是 GET 请求。
requests.get()的任务,就是替你跟服务器完成一次这样的对话。它返回一个 Response 对象,里面包含你真正要的东西:
resp = requests.get("https://example.com") print(type(resp)) # <class 'requests.models.Response'> print(resp.status_code) # 200 表示成功 print(resp.headers) # 服务器返回的响应头 print(resp.text) # 网页正文(通常是 HTML 源码)resp.text是字符串形式的网页源码,也就是你下一步要拿来解析的对象。你不需要背住所有响应头,但至少要会看Content-Type和Content-Length,前者告诉你内容是什么类型,后者告诉你消息有多大。
3.2 学会通过状态码判断下一步行动
状态码是服务器给你的第一句反馈。我给初学者整理了一张速查表,你把它记下来:
| 状态码 | 含义 | 你的行动 |
|---|---|---|
| 200 | 请求成功,页面正常返回 | 继续解析 |
| 301 / 302 | 页面跳转 | requests默认会自动跟随,通常不用处理 |
| 403 | 服务器拒绝访问 | 优先检查请求头,尤其是 User-Agent |
| 404 | 页面不存在 | 检查网址是否拼错了,或页面已下线 |
| 500 | 服务器内部错误 | 不是你的问题,稍等再试 |
你不可能让所有请求都返回 200,所以代码里最好加一个判断,避免在错误页面里解析出一堆"垃圾数据":
resp = requests.get(url) if resp.status_code == 200: print("页面获取成功,开始解析") else: print(f"请求失败,状态码:{resp.status_code}")3.3 User-Agent 是多数爬虫遇上的第一个坎
你第一次用requests.get()抓某个稍微有点防护的网站时,很可能得到 403。一个常见原因是:requests默认会在请求头里带上类似python-requests/2.31.0的标识,目标服务器一看就知道你是脚本,不是浏览器,于是拒绝访问。
解决方法是模拟常见浏览器的请求头。你不需要自己记,直接在代码里放一段常用的 User-Agent:
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Safari/537.36" } resp = requests.get(url, headers=headers, timeout=10)这段字符串看起来很长,但它就是浏览器在跟服务器打招呼时自报家门的一串标识。加一个timeout=10也很重要,它让请求最多等 10 秒,避免某个网页迟迟没有响应时,你的程序卡在原地无法结束。
3.4 拿到 403 先别慌,按顺序排查
新手遇到 403,第一反应往往是"是不是要换更高级的技术",其实 90% 的情况都不需要。我建议你按这个顺序排查:
- 确认网址没拼错,直接用浏览器打开能访问。
- 检查是否加了
User-Agent,没有就先加上。 - 给请求之间加入 1 秒以上的延迟,别让程序像连珠炮一样打过去。
- 换一个访问时段,有些网站对集中的流量敏感。
- 如果页面明确要求登录,Leave it。第一个爬虫不要碰要登录的网站。
这个顺序里没有一步是"绕过"什么,全部是正常访问的修正手段。摆正心态很重要:爬虫是写给自己用的工具,它的价值在于稳定地拿公开数据,而不是跟别人的服务器玩躲猫猫。
4. 数据无处可逃:用 BeautifulSoup 提取标题和链接
现在你已经把 HTML 完整地拿到了,接下来的任务是从这一大堆标签里精准地"抠"出你需要的内容。BeautifulSoup 就是那把镊子。
4.1 把 HTML 变成一棵可以查询的树
HTML 本质上是嵌套的标签,BeautifulSoup 把它解析成一棵"树"结构。你可以从树根往下找各个节点:
from bs4 import BeautifulSoup with open("page.html", encoding="utf-8") as f: html = f.read() soup = BeautifulSoup(html, "lxml")lxml解析器负责把字符串变成树。之后你有三种最常用的查询方式:
soup.find("h1"):找到第一个指定的标签,适合页面里唯一的元素。soup.find_all("a"):找到所有指定的标签。soup.select("div.item .title"):用 CSS 选择器定位,支持标签、类名、id、层级组合,这是我最推荐的。
CSS 选择器是另一个非常适合顺手掌握的小技能。它和网页开发里的 CSS 选择器语法一致,你只需要记住几种写法:直接写标签名选标签,点号后面跟类名,井号后面跟 id,空格表示后代关系。比如soup.select("div.article h2")就是在所有class="article"的 div 里找 h2 标题。
4.2 提取标题、链接和列表数据的完整示例
先看最直接的读取操作:
print("页面标题:", soup.title.string) for link in soup.find_all("a", href=True): text = link.get_text(strip=True) href = link["href"] print(text, href)get_text(strip=True)会取标签里的文字,并去掉首尾空白。link["href"]直接取出链接地址。抓取多个结构相同的列表项时,思路是"先缩小范围,再提取字段":
for item in soup.select("li.book-item"): name = item.select_one(".name").get_text(strip=True) author = item.select_one(".author").get_text(strip=True) price = item.select_one(".price").get_text(strip=True) print(name, author, price)这段代码的意思是:先定位所有li.book-item,然后在每个li内部再分别找.name、.author、.price。这样写的好处是逻辑清晰,即使页面里出现多处同名类名,也不会互相污染。
4.3 选择器无效时,先回看 HTML,别急着改代码
爬虫初学者的沮丧时刻大概是这样的:运行了,没报错,但select()返回空列表。这时候第一件事不是猜,是回到 HTML 里确认。
你把页面保存到本地后,直接用浏览器打开page.html,按 F12 定位到你想要的那条数据,看它真正所在的标签和 class 是什么。然后回到代码里,先用一段临时文本确认:
# 临时确认:看看原始 HTML 里能不能找到某个关键字符串 print("该书名字符串是否在 HTML 中:", "某实际书名" in html)如果html里连这个字符串都没有,说明你抓下来的页面根本没有这条数据,可能是页面是动态加载的,或者你请求的地址没对。这时候改选择器也没用,问题出在更早的请求环节。如果字符串在 HTML 里,但select()找不到,再检查你的选择器是不是写错了类名或层级。这套排查顺序帮我省下了大量无效调试时间。
5. 存成 CSV:让爬到的数据真正可用
数据只有存成文件,爬虫才算真正完成。这一章我用 Python 标准库里的csv模块,做一件你每天都会用到的事:把抓到的内容保存成 Excel 能直接打开的表格。
5.1 为什么选 CSV,而不是先上数据库
对于第一个爬虫项目,数据量通常在几十到几千条之间,CSV 是最合适的格式。它有三个天然优势:
- 不用安装任何额外依赖,Python 内置的
csv模块就能写。 - Excel、WPS、记事本都能直接打开,无需转换。
- 数据量不大时,用它做后续整理操作已经绰绰有余。
等将来数据量上了几万条、需要复杂查询时再上数据库不迟。初学者一上来就折腾数据库,很容易把"写爬虫"变成"学数据库",偏离主线。
5.2 用 utf-8-sig 编码解决 Excel 中文乱码
我收到的初学者提问里,"CSV 打开后中文乱码"绝对能排进前三。原因很简单:Excel 在打开 CSV 时对编码的识别和 Python 默认的utf-8不一致。解决办法也简单,写入时用utf-8-sig编码,Python 会在文件开头写入一个 BOM 标记,Excel 就能正确识别了。
import csv with open("books.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["书名", "作者", "价格"]) writer.writerow(["某入门书", "某些作者", "59.00"])注意newline=""这个参数。在 Windows 平台上如果不指定,写出的 CSV 在 Excel 打开时每两行之间会多一个空行。加上newline=""就是一个干净的表格。
5.3 从请求到保存的一条龙完整脚本
现在把前面的知识点串起来,写一个完整的、可以直接套用的脚本。这里我用一个虚构的列表页结构做演示,假设页面里有这样一段 HTML:
<li class="book-item"> <span class="name">Python 入门到放弃</span> <span class="author">张三</span> <span class="price">59.00</span> </li>你抓到的真实页面结构可能不一样,只需要把.book-item、.name、.author、.price换成实际的选择器即可。
import time import csv import requests from bs4 import BeautifulSoup url = "https://example.com/books" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0.0.0 Safari/537.36" } resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" if resp.status_code != 200: print(f"页面请求失败,状态码:{resp.status_code}") exit() soup = BeautifulSoup(resp.text, "lxml") books = [] for item in soup.select("li.book-item"): name = item.select_one(".name").get_text(strip=True) author = item.select_one(".author").get_text(strip=True) price = item.select_one(".price").get_text(strip=True) books.append([name, author, price]) with open("books.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["书名", "作者", "价格"]) writer.writerows(books) print("共保存", len(books), "条数据")整段代码不长,但已经覆盖了"设置请求头 -> 发起请求 -> 解析页面 -> 提取字段 -> 写入 CSV"这一整条链路。你需要掌握的只是把这个骨架里所有写死的选择器和字段替换成自己的目标页面结构。
6. 初学爬虫最常见的坑,和我的排查习惯
就算你照着前面的代码跑通了,我也几乎可以确定,你在自己换网站练手时会遇到新的问题。这一章我把我自己踩过的、带新手时反复见到的问题集中写出来。
6.1 乱码问题:看到"锟斤拷"说明编码搞错了
如果你抓到的文本是满屏乱码,尤其是那种很有年代感的"锟斤拷"三件套,问题基本出在编码上。requests会根据响应头里的charset推断网页编码,但有些网站没有声明字符集,或者声明得不对。
处理方式是在拿到resp后,先确认并手动指定编码:
resp = requests.get(url, headers=headers) resp.encoding = "gbk" # 根据页面实际的字符集来定 text = resp.text怎么确定该用哪个编码?在浏览器里打开页面,右键查看源文件,找到类似<meta charset="...">的标签,里面写的gbk就用gbk,写的是utf-8就用utf-8。如果拿不准,可以先用一条命令让程序自动猜测:
resp.encoding = resp.apparent_encoding这个猜测方法并不总是准,但用来排查方向很管用。
6.2 同一个选择器今天能用、明天不能用
网站页面结构改版是常态,你的爬虫代码跟着挂掉很正常,别怀疑自己。真正的问题是很多新手把选择器散落在代码各处,改版时需要满文件找。
我的习惯是把解析选择器集中定义,就像配置项一样:
SELECTORS = { "item": "li.book-item", "name": ".name", "author": ".author", "price": ".price", }等到网页改版时,只需要修改这个字典的值,而不需要一处处地找代码。这个习惯在大项目中尤其重要。
6.3 请求过快被拒绝,先想想自己是多快
如果你打算用循环一口气抓几百上千个页面,那几乎必然会撞上网站的反爬策略。最常见的情况是抓了十几页之后,突然连续返回 403 或者让你填验证码。
应对方式没什么玄乎的,就是慢下来:
import time for page_url in url_list: resp = requests.get(page_url, headers=headers, timeout=10) # 处理数据... time.sleep(1.5) # 每抓一页休息 1.5 秒这背后的道理很简单:你不是在和网站对抗,你只是把一个一个页面慢慢读完。1.5 秒的间隔足够友好,也足够让你的小爬虫稳定跑完。如果对方允许你正常浏览,为什么不能允许你慢速抓取?把心态放平,速度放慢,大多数问题都能自动消失。
6.4 哪些项目不适合当你的第一个爬虫
最后我要泼一点冷水:并不是所有页面都适合做第一个爬虫项目。根据我见过的失败案例,下面这几类启动前最好三思:
- 需要登录才能查看内容的网站。登录涉及会话维持、验证码,对新手来说难度跨越太大。
- 明显有较强反爬策略的平台。这类平台往往需要浏览器渲染、行为分析,最基础的 requests 方案很难跑通。
- 数据量动辄几十万条的站点。第一个项目就上巨型任务,既消耗别人服务器资源,也容易让你挫败感拉满。
选第一个爬虫目标,应该挑公开、结构清晰、数据量小、不涉及登录的页面。记住,你现在的核心任务是跑通"请求->解析->存储"这套流程,而不是攻克某个域名。
最后再分享一个我自己的习惯:不管脚本多简单,每写一个爬虫,我都会先把单个页面完整跑通,确认能拿到数据、能存好文件,再套循环去抓多页。这个习惯救了我太多次,因为单页跑通意味着所有基础环节都正常,剩下的问题只有"循环边界对不对"和"网页结构是否一致"而已。你的第一个爬虫,如果最后能做到"输入一个列表页地址,自动抓取所有条目并生成 CSV",那你就已经跨过了爬虫入门最关键的一道坎。