学爬虫的人,第一个念头大多是“把网页上的数据弄下来”。这个念头本身没有错,但真正动手之后你会发现,爬虫的门槛从来不在“怎么把页面抓下来”,而在“抓下来之后怎么处理”以及“哪些能抓、哪些不能抓”。这一节我打算用最基础的方式,带你把一条合规抓取、解析、存储的链路完整走一遍。
先说明白:这里说的“爬虫入门”,不是教你写一个能横扫全站的采集工具,也不是把某个平台的数据搬空。那是非常危险的做法,技术上也许不难,但法律和道德上会把你拖进深坑。我们这一节的目标很朴素——学会用 Python 里的 requests 和 BeautifulSoup 这两个基础库,从公开网页中提取信息,把它整理成结构化的数据存下来,同时搞清楚整个过程中哪些红线不能碰。
这套内容适合谁?一句话:刚接触 Python、想把“网页上的文字表格图片”变成“自己能处理的干净数据”的人。如果你已经有 Python 基础,但对网络请求、HTML 结构、XPath 这些东西还是似懂非懂,那这一节的内容同样能帮你把地基打牢。我们用到的工具非常轻量,不需要重型框架,打开一个编辑器就能跑。
1. 理解爬虫的本质:它就是在模拟一个人浏览网页
1.1 爬虫的三个基础组件:抓取、解析、存储
如果把爬虫拆开来看,其实它只干了三件事。第一是“请求”——把目标网页的地址发给服务器,服务器把 HTML 内容返回给你;第二是“解析”——从这一大段 HTML 标签里,把你想要的那部分文字、链接、属性等等信息摘出来;第三是“存储”——把摘出来的数据按照统一的格式写入本地文件或数据库。
这三步听起来简单,但每一环都有大量细节。比如“请求”环节,你不仅要发请求,还要考虑带什么样的请求头、服务器返回了什么样的状态码、内容用了什么编码;再比如“解析”环节,HTML 本身就是一堆嵌套的标签,你得决定是用正则硬抠、用 BeautifulSoup 按标签找、还是用 XPath 按路径取;到了“存储”环节,数据清洗会让很多新手崩溃,明明页面上显示“¥ 199.00”,抓下来却变成了“¥\u3000199.00”,这中间全是编码和清洗的坑。
我们这一节用一句话总结爬虫的运行逻辑:模拟浏览器的行为,获取服务器返回的内容,从非结构化数据中提取结构化信息。一旦你把这个逻辑吃透了,后面学 Scrapy、学异步爬虫、学分布式爬虫,都只是在“怎么更快更稳地完成这三步”上做优化,本质不会变。
注意:这里说的“模拟浏览器行为”,指的是技术层面的模拟,而不是让你伪装成真人去突破对方设置的技术屏障。两者有本质区别,后面我会单独讲。
1.2 合规不是口号,而是要落地的三条线
很多教程会把“合规”当成一句话带过,但实际上合规是爬虫这个行当里的生命线。我把它拆成三条具体可执行的原则,你写代码的时候随时对照。
第一条线叫“看规则”。绝大多数正规网站都会在域名根路径下放一个 robots.txt 文件,用标准格式写明哪些路径允许爬虫访问、哪些不允许。比如你访问/robots.txt,会看到类似Disallow: /admin/这样的规则。动工之前先读这个文件,这是最基本的尊重。有些网站还会在服务条款中明确写明禁止爬虫采集,那这种站点就坚决不碰。
第二条线叫“看身份”。爬虫请求时应该带上明确的 User-Agent 标识自己是爬虫,而不是伪装成一个完全不存在的浏览器版本去绕过检测。这与反爬对抗完全是两码事——合规爬虫主动亮明身份,频率控制在合理范围,服务器允许就继续,不允许就停下来。
第三条线叫“看频率”。哪怕一个网站允许爬虫访问,也不代表你可以每秒发几十个请求。服务器的带宽和数据库资源都是有限的,你一个人的脚本可能把整个站点拖垮。新手最容易犯的错就是写了个死循环忘了加延时,结果几分钟内把目标站点打挂。合规爬虫的标准是:用尽可能低的频率获取足够的数据,同时主动设置请求间隔。
这三条线下,你再去看网上那些“反爬虫绕过技巧”,就会发现大部分内容根本不该出现在合规爬虫的语境里。遇到封锁就退让、降速、换公开接口,或者直接放弃这个数据源,这才是正路。
1.3 工具选型:Requests + BeautifulSoup 还是直接上 Scrapy?
入门阶段,我的建议非常明确:先用requests + BeautifulSoup手动写,不要一上来就上 Scrapy。
原因有两点。第一,Scrapy 虽然是业界主流的爬虫框架,性能好、功能全,但它的学习曲线会分散你的注意力。你要同时理解引擎、下载器、中间件、Item Pipeline 一大堆概念,很容易陷入“会用框架但不知道底层在干嘛”的状态。而 requests 和 BeautifulSoup 组合简单直接,每一个环节都能看得见摸得着。
第二,手工写一遍请求、解析、存储的流程之后,你才能真正理解框架帮你做了什么。比如 Scrapy 里一个不起眼的AutoThrottle设置,背后其实是对请求频率的自动控制;你要是不知道手动控制请求间隔有多麻烦,就不会理解这个功能的价值。
这是一个非常经典的对比:
| 方案 | requests + BeautifulSoup | Scrapy |
|---|---|---|
| 上手难度 | 低,适合入门 | 中高,需要理解框架机制 |
| 请求控制 | 完全手动,直观 | 内置并发与限速 |
| 解析方式 | BeautifulSoup 灵活但稍慢 | Selector 基于 lxml,性能高 |
| 适合场景 | 小规模、学习、快速原型 | 大规模采集、分布式部署 |
| 调试便捷度 | 高,打印即所见 | 相对复杂 |
2. 核心细节拆解:从 GET 请求到数据定位
2.1 前置动作:先判断目标值不值得抓
选什么样的网站练手,是新手面临的第一道选择题。我的建议是选一个静态页面、结构稳定、没有登录墙的公开信息站。比如图书榜单、菜谱、名词解释、开源文档这类站点就非常合适。
一定不要选什么?第一是电商平台,它们的数据是商业核心资产,反爬措施极其严密,你一开始碰大概率会被封 IP;第二是需要登录才能看到内容的站点,因为那部分内容属于用户私有数据,采集有法律风险;第三是含有个人信息的数据,比如手机号、身份证号、住址,哪怕页面公开也不该抓。
实操中你还要注意站点是否经常改版。有些个人博客非常典型,今天用这个主题,明天换那个模板,一周后 HTML 结构全变了,你的解析代码就得跟着重写。所以说,第一版的爬虫代码最好只针对单个页面写,等流程跑通了再考虑扩展多页面。别一上来就想搞一个大而全的采集系统,那不现实也不必要。
2.2 请求头里的门道:为什么 User-Agent 是关键参数
用 requests 发请求时,最常被忽略的就是请求头。很多新手直接写requests.get(url),把请求头发出去,然后被服务器返回 403 拒绝,却不知道问题出在哪。
服务器是怎么识别爬虫的?第一眼看的就是 User-Agent。浏览器发请求时,User-Agent 会带上一长串版本信息,比如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36。而 requests 库默认的 User-Agent 是python-requests/2.31.0,服务器一眼就能看出来这不是浏览器,直接拒绝。
所以爬虫的第一步,就是给 requests 加上一个常见的浏览器 User-Agent。这段代码非常重要:
import requests headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" } response = requests.get("https://example.com", headers=headers, timeout=10)注意这里有个timeout参数,我建议你每次请求都带上它。不设置超时意味着如果目标服务器一直不响应,你的脚本会一直挂在那里,像一块石头一样卡死,不仅浪费资源还影响调试效率。设置 10 秒超时,请求超过 10 秒就抛异常,程序能继续往下走。
2.3 响应里的编码问题:一个让无数人抓狂的细节
请求发出去之后,服务器返回的response.text就是网页正文。但这里有个隐藏的天坑:编码。
HTTP 响应头里的charset参数并不总是可靠的。有些旧站点根本不在响应头里声明编码,只会在 HTML 的<meta>标签里写<meta charset="utf-8">,而 requests 库默认会用ISO-8859-1去解码,结果中文全变成乱码。
解决办法是主动检查并设置正确的编码:
import requests response = requests.get("https://example.com", timeout=10) # 优先从响应头获取编码,没有就用 apparent_encoding 猜测 if response.encoding is None or response.encoding.lower() == "iso-8859-1": response.encoding = response.apparent_encoding text = response.textapparent_encoding是 requests 基于内容字节统计推断出来的编码,对中文页面通常很准确。但要注意,这个推断是基于整个文本内容的,如果页面内容很短,推断结果可能不准。另一个思路是直接从response.content(字节数据)中按指定编码解码,比如response.content.decode("utf-8"),你自己控制编码过程。
2.4 解析方式对比:正则、BeautifulSoup、XPath 怎么选
拿到 HTML 之后,问题就变成了“怎么从一堆标签里把数据抠出来”。我见过三种主流方案,每种都有各自的应用场景。
正则表达式是最原生的方式,直接对字符串做模式匹配。它的优点是灵活、不需要额外库,缺点是写起来容易出错,尤其是 HTML 这种带嵌套结构的文本,正则很难处理复杂嵌套关系。我建议新手不要用正则解析 HTML,容易把自己绕晕。
BeautifulSoup 是最适合入门的方式。它会把 HTML 解析成一棵树,然后你可以通过find()、find_all()这样的方法按标签名、属性、class 名称来查找元素。它最大的优点是容错性强,即使 HTML 写得不规范也能尽量解析正确。缺点是纯 Python 实现,性能上比 lxml 慢不少。
XPath 是相对进阶一点的方式,它把 HTML 看成 XML 树,通过路径表达式来定位节点。比如//div[@class="book"]//h2/a/text()这样的写法,可以直接提取出 class 为 book 的 div 下所有 h2 里的 a 文本。XPath 表达式简洁、功能强大,配合 lxml 解析器速度很快,很多实际项目都用这种方式。新手理解 XPath 需要一点时间,但一旦掌握,效率提升非常明显。
我个人的建议是:入门用 BeautifulSoup 建立直觉,熟悉之后切换到 lxml + XPath。两者在 requests 生态里都能很好地工作,你大可不必纠结使用哪个。
2.5 数据清洗与存储:抓到之后别急着高兴
解析出来的数据往往是脏的。比如文本前后有换行符和空格、价格数字里混入了货币符号、时间格式五花八门。这些都需要在存储之前清洗掉。
清洗的核心方法就三板斧:strip()去掉首尾空白、replace()替换不需要的字符、正则提取想要的模式。等你处理的数据类型多了,你会发现清洗浪费的时间往往比抓取和解析加起来还多。
存储方面,入门阶段只需掌握两种。CSV 是通用性最强的格式,Excel 和 Pandas 都能直接打开,注意写入时要加上encoding="utf-8-sig"否则 Excel 里中文会乱码;SQLite 是单文件数据库,适合数据量稍大、需要查询的场景,Python 内置 sqlite3 模块,不需要额外安装。
3. 实操环节:写一个真正能跑的合规爬虫
3.1 目标选择:用公开测试站练手
这里我挑一个非常适合入门的公开目标:quotes.toscrape.com,一个专门为爬虫学习准备的公共测试站点。它没有登录墙,没有复杂的验证码,内容就是一些名人名言和作者信息。最关键是它欢迎爬虫访问,我们在这个站点上练习既安全又能完整覆盖所有核心知识点。
动工之前,按惯例先看一下它的 robots.txt,内容很简单,基本没什么限制。然后随便打开一个页面,观察它的 HTML 结构。你会发现每条名言都被包含在一个div标签里,class 叫quote,里面又有一个span class="text"存放名言正文,一个span class="author"存放作者名字,还有一个a标签指向作者详情页。
3.2 三步走:请求、解析、打印结果
第一步写请求逻辑。获取首页 HTML,设置好 User-Agent 和超时,检查状态码是否为 200。这个步骤上面已经铺垫过,直接组合起来即可。
第二步写解析逻辑。用 BeautifulSoup 把 HTML 转成对象,然后找到所有div下的quote块:
from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, "html.parser") quotes = soup.find_all("div", class_="quote") for quote in quotes: text = quote.find("span", class_="text").text author = quote.find("span", class_="author").text # tags 是链接列表,先取文本即可 tags = [tag.text for tag in quote.find_all("a", class_="tag")] print(text, author, tags)看到结果打印出来的那一刻,你会觉得爬虫原来并不神秘。但要注意一个小细节:find_all("div", class_="quote")中,class_后面有个下划线,因为class是 Python 的关键字,这个写法是 BeautifulSoup 固定的 API,不能省略。
第三步暂时用打印代替存储,先确认解析逻辑没问题。这一步非常关键,做全量爬取之前先小范围验证,能避免你把错误数据写进文件。
3.3 分页逻辑与频率控制:优雅地抓取多页数据
首页只有 10 条名言,要抓更多就得处理翻页。观察测试站的 URL 规律:首页是/page/1/,第二页是/page/2/,依次类推。有些站点翻页是通过 URL 参数实现的,比如?page=2,原理都一样——确认规律后,用一个循环生成不同的 URL 即可。
分页爬取的完整流程大概是这样的:
import time base_url = "https://quotes.toscrape.com/page/{}/" all_quotes = [] for page_num in range(1, 5): url = base_url.format(page_num) resp = requests.get(url, headers=headers, timeout=10) if resp.status_code != 200: print(f"第 {page_num} 页请求失败,跳过") continue soup = BeautifulSoup(resp.text, "html.parser") for quote in soup.find_all("div", class_="quote"): text = quote.find("span", class_="text").text author = quote.find("span", class_="author").text all_quotes.append({"text": text, "author": author}) print(f"第 {page_num} 页抓取完成,累计 {len(all_quotes)} 条") time.sleep(2) # 每页之间休息 2 秒这里有两个值得留意的地方。第一是状态码判断,请求失败时不中断程序,打印一句提示然后继续下一轮。第二是time.sleep(2),我在每页请求之间强制休息两秒——不要小看这个设置,它能让你的爬虫在目标服务器眼里显得“礼貌”得多,也能避免因为请求过快被封。在真实项目中,这个间隔时间要根据目标网站的响应速度和规模动态调整,一般建议 1 到 5 秒之间。
3.4 存储到文件:把中间结果变成最终成果
看到解析结果正常之后,再把它写入文件。Python 自带的 csv 模块足够用:
import csv with open("quotes.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["text", "author", "tags"]) writer.writeheader() writer.writerows(all_quotes)这里用encoding="utf-8-sig"是为了兼容 Excel,如果你用记事本打开文件出现中文乱码,多半就是这里漏掉了。newline=""则是为了避免 csv 模块在 Windows 平台下写出多余空行的问题。
到这一步,你已经完成了一个完整的合规爬虫:发送请求、解析 HTML、翻页、频率控制、存储文件。麻雀虽小,五脏俱全。
4. 常见问题与排查技巧实录
4.1 请求被拒,状态码 403
这是新手遇到最多的报错。看到 403,第一反应别想着“怎么绕过去”,而是先自查三件事:User-Agent 设置了吗?请求频率是不是太高了?目标站点是不是明确禁止爬虫?
如果 User-Agent 没问题,试着把请求间隔拉长到 5 秒以上再跑一次。有时候服务器只是临时限制,休息几分钟就恢复了。如果是目标站点明确禁止,那就是你自己选错了目标,换数据源才是正解。
4.2 解析结果为空,明明页面上有数据
这种情况 80% 是因为页面里的数据是用 JavaScript 动态加载的。你用 requests 拿到的只是服务器返回的初始 HTML,数据还没渲染上去。判断方法很简单:用浏览器右键查看网页源代码,搜索你要的数据,如果源码里没有,那就是动态渲染的。
动态页面的合规解决方案不是硬写一个无头浏览器去模拟点击,而是先找找页面有没有公开的 API 接口。很多网站虽然页面是动态的,但数据都是通过一个 JSON 接口返回的,直接用 requests 请求那个接口,解析 JSON 反而比解析 HTML 更简单。这也是我在实际项目中很常用的思路。
4.3 中文乱码问题
乱码的原因基本都集中在编码环节。按这个顺序排查:先看response.encoding是什么,如果是ISO-8859-1这类非中文编码,就手动改成utf-8或使用apparent_encoding;如果response.text正常但写入文件后乱码,检查 CSV 写入时的编码是不是用了utf-8-sig。
4.4 数据里有大量空白和杂项
处理这种问题不要用正则一刀切,要分析来源。如果是标签内部文本自带的前后空白,strip()就能解决;如果是 HTML 里的换行符和缩进,先把文本按行切割,去掉空行再拼接;如果是像 “\u3000” 这样的全角空格,直接用replace("\u3000", "")清除。
4.5 爬虫被限流,页面突然打不开
这几乎是每个爬虫学习者的必经之路。第一次遇到时很容易慌,但请记住:你的需求永远没有别人的服务器稳定重要。正确的做法是立即停止脚本,等待几分钟,检查自己的请求频率,向目标网站表达友好。如果你在限制解除后继续高频访问,那就不再是“技术问题”而是“态度问题”了。
我把这些典型问题整理成一个速查表,方便你以后对照:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 403 拒绝 | 请求头缺失或频率过高 | 设置 UA,降低频率 |
| 中文乱码 | 解码编码不匹配 | 手动指定 utf-8 |
| 解析为空 | 动态脚本渲染 | 寻找公开 JSON 接口 |
| 文本带空白 | HTML 源码含格式字符 | strip() + replace() |
| 请求超时 | 网络波动或服务器慢 | 设置 timeout,异常重试 |
| 数据量过大 | 没有分页控制 | 限制页数,调节 sleep |
我在实际使用中发现,很多人在爬虫入门阶段踩的坑,不是因为代码逻辑有多复杂,而是因为太急于求成,跳过了“先看规则、再定方案、小范围验证、最后全量执行”这个过程。其实你只要慢下来,把一个页面完整地走通,再考虑扩展,整个流程会顺畅得多。
最后再分享一个小技巧:写完爬虫后,随手把目标页面的 HTML 保存一份到本地,当作调试用的“快照”。后续解析代码改来改去,不需要每次重新请求线上服务器,直接用本地文件测试就行,既快又安全。这个习惯我一直保持到现在,非常管用。