Requests与BeautifulSoup爬虫入门:从HTML解析到数据提取实战 最近好几个朋友来问我Python爬虫入门怎么学用的是同一个问题模板为什么我用Requests请求网页拿回来的代码里找不到想要的数据或者说拿到HTML之后到底怎么把里面的文字、链接、图片信息抠出来其实这个困惑很典型因为爬虫本质上就两步——先把网页“要”回来再把网页“看懂”。大多数入门教程把这两步混在一起讲导致很多人卡在第二步Requests拿到了HTML但面对一堆尖括号无从下手。这篇博文就把这个组合讲透用Requests完成网络请求用BeautifulSoup解析HTML这也是Python爬虫领域最经典、最不容易劝退新人的入门路线。我会从环境准备开始把请求头、超时、状态码这些细节掰开揉碎再用一个完整案例带着你从零写出一段能真正跑出结果的爬虫代码最后把我自己踩过的一些坑原原本本列出来。内容不需要任何爬虫基础只要你装了Python、能跑pip install就能跟着做下去。1. 为什么入门爬虫首选Requests和BeautifulSoup1.1 爬虫到底在做什么爬虫这个概念听起来很高端说穿了就是把浏览器“自动复制粘贴”的步骤代码化。你在浏览器里打开一个网页看到标题、正文、图片那是浏览器把HTML渲染成漂亮页面的结果。爬虫不一样它只关心两件事服务器返回了什么、我需要的字段在哪里。设想一个日常场景你想把某个网站上几十篇文章的标题和链接整理成一个Excel表。手动操作就是逐页打开、选择、复制、粘贴非常枯燥。爬虫做的事情就是把打开网页这个动作变成requests.get(url)把选择、复制变成BeautifulSoup里的find_all最后统一写入文件。整个过程就像雇了一个不知疲倦的助手你只需要告诉它去哪里取数和取什么数。1.2 Requests和BeautifulSoup各自负责什么这两个库的分工非常清晰Requests负责“对话”。它向服务器发送HTTP请求然后拿到响应内容。它不关心网页内容长什么样只负责把HTML、JSON等原始数据原封不动地带回来。BeautifulSoup负责“解读”。拿到HTML源码之后它把这一大段字符串解析成一棵标签树你可以通过标签名、class、id等属性精准定位到某个节点再提取里面的文字或链接。用生活里的例子类比Requests是外卖员负责把菜送到你手上BeautifulSoup是拆菜的人帮你把包装打开把不爱吃的配菜挑出来只留下你要的那块肉。1.3 为什么不直接用Scrapy很多教程一上来就推荐Scrapy说它是Python爬虫的事实标准。我不否认Scrapy能力强但对于刚接触爬虫的人来说它像是一上来就让你学开重卡——方向盘、档位、离合全部一步到位学完可能连请求一个网页的原理都没弄明白。Scrapy本身是一个完整的爬虫框架包含调度器、下载器、管道、中间件等多个组件背后还有Twisted异步网络模型。这些概念对新手来说负担太重。而Requests BeautifulSoup的逻辑是线性的请求一个URL拿到HTML解析HTML提取数据。每一步都能在代码里看得明明白白也方便打印中间结果来调试。等你能用这两个库完成两三个小项目理解了HTTP状态码、请求头、解析器、数据清洗这些基础概念之后再去学Scrapy会顺利很多。库职责适合场景requests发送HTTP/HTTPS请求获取响应大多数网络请求场景轻量直接urllibPython标准库自带请求能力不想装第三方包时的应急方案scrapy完整爬虫框架含调度、下载、解析、存储大规模、结构化的爬取项目beautifulsoup4解析HTML/XML文档操作标签树中小规模的HTML解析lxml高性能HTML/XML解析支持XPath追求解析速度或需要XPath时parsel在lxml之上封装的解析库支持CSS选择器Scrapy内部使用也可独立使用2. 环境准备安装与解析器选择比想象中重要2.1 安装Python与项目虚拟环境如果你还没有Python环境去官网下载安装包即可。Windows用户安装时记得勾选Add Python to PATH否则后面在命令行里敲python会提示找不到命令。这个细节看上去不起眼但每个入门者几乎都会在这里卡一次。装好Python之后强烈建议为爬虫项目单独建一个虚拟环境而不是把所有依赖直接装到系统环境里。虚拟环境的作用是隔离依赖不同项目可能用到同一个库的不同版本混装容易出现“A项目能跑B项目一运行就报错”的问题。创建和激活方式如下# 创建虚拟环境 python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate激活之后命令行提示符前面会出现(venv)字样后续的pip安装都会进入这个独立环境。2.2 安装Requests和BeautifulSoup激活虚拟环境后执行pip install requests beautifulsoup4如果网络状况不佳可以追加国内镜像源例如pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple我额外建议顺手装一个lxml解析器。BeautifulSoup虽然自带html.parser但lxml的解析速度明显更快后面你会发现它对一些HTML标签闭合不严谨的页面也有更强的容忍度。pip install lxml2.3 解析器之间有什么差别BeautifulSoup支持多种解析器最常用的三种对比如下解析器安装需求优点缺点html.parser内置无需安装零依赖随装随用速度一般容错一般lxmlpip install lxml速度快容错性好需要额外安装html5libpip install html5lib按浏览器方式解析容错最强速度最慢依赖较重入门阶段用html.parser完全没问题。但当你需要解析大量页面时换成lxml会有明显的速度提升。后面所有代码示例我都会用lxml运行前请确认已经安装。2.4 用一行脚本验证环境安装完成后写一个最短的验证脚本确认两个库都能正常导入、请求能发出import requests from bs4 import BeautifulSoup r requests.get(https://httpbin.org/html, timeout5) print(r.status_code) soup BeautifulSoup(r.text, lxml) print(soup.title.text)能打印出200和页面标题说明环境没问题。httpbin.org是一个专门用于HTTP测试的公开网站后面很多调试都可以拿它当靶场。3. 用Requests发请求从“能打开”到“拿得稳”3.1 最基本的GET请求与状态码Requests最常用的就是requests.get()。一个最简单的请求长这样import requests resp requests.get(https://quotes.toscrape.com/, timeout10) print(resp.status_code) # 200 print(resp.text) # 页面HTML内容status_code就是服务器的响应状态码200表示正常403表示拒绝访问404表示页面不存在429表示请求过于频繁。入门阶段最常见的困惑是明明浏览器能打开代码请求却返回403——这是因为服务器通过请求头里的User-Agent判断访问者不是真人。3.2 请求头新手最容易忽略的一步浏览器在请求网页时会携带一组HTTP头信息其中最重要的就是User-Agent它标识了客户端类型。爬虫如果不设置请求头默认的UA会像一把钥匙直接暴露自己不是浏览器不少服务器会直接拒绝。正确的做法是把请求头设置成常规浏览器的样子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, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } resp requests.get(url, headersheaders, timeout10)很多入门教程不提这一层导致读者照着抄代码明明逻辑没问题就是拿不到数据。我一般在写爬虫时第一件事就是定义headers不管目标站点看起来多“朴素”先伪装好再发请求。3.3 超时设置和429状态码的应对思路requests.get()如果不设置timeout会一直等下去网络异常时程序可能挂在那里半天没反应。合理的做法是同时设置连接超时和读取超时resp requests.get(url, headersheaders, timeout(3, 10))前面的3是连接超时秒数后面的10是读取超时秒数。另一件更常见的事是429 Too Many Requests意思是请求频率太高被限流了。我见过不少新手在碰到429时做的一件事是疯狂加重试次数结果越试越被限制最后触发类似exceeded retry limit, last status: 429 too many requests的报错。正确的处理思路不是“硬刚”而是“降温”先停止请求一段时间比如time.sleep(30)让限流窗口过去。把并发压下来不要多线程同时请求同一个站点。在两次请求之间加入随机间隔比如time.sleep(random.uniform(1, 3))。如果服务器返回了Retry-After响应头按它指定的秒数等待。顺带一提resp.text和resp.content是有区别的text是根据响应头推断编码后解码出来的字符串content是原始字节。常规的HTML解析用resp.text就够只有当你需要自行控制编码或者要下载图片、文件等二进制内容时才用resp.content。3.4 Session对象与Cookie保持有的网站需要先登录才能访问目标页面或者你在一次会话中需要携带某些Cookie。每次调用requests.get()都会新建一个独立连接服务端Cookie存不住此时用requests.Session()就能维持同一会话session requests.Session() session.headers.update(headers) # 先访问登录页再提交表单之后发起带会话的请求 resp session.get(url, timeout10)用Session之后整个会话过程中服务端设置的Cookie会被自动保存和携带。对于入门阶段来说只要知道“需要保持登录态就用Session”就够用了。4. BeautifulSoup解析从HTML中精准提取目标字段4.1 把HTML字符串变成可操作的标签树BeautifulSoup的核心功能是把一大段HTML字符串转换为对象树。拿到这个对象之后你不再需要和正则表达式搏斗而是可以按标签、属性、CSS选择器来访问任意节点。from bs4 import BeautifulSoup html htmlbodydiv classposth2Hello Python/h2/div/body/html soup BeautifulSoup(html, lxml) print(soup.h2.text) # Hello Python print(soup.find(h2).text) # Hello Pythonfind()返回第一个匹配的标签对象find_all()返回所有匹配的列表。这两个方法几乎能覆盖日常90%的需求。4.2 常用定位方式find、find_all、select实操中最推荐的定位方式是select()它支持CSS选择器对从浏览器开发者工具里复制出来的路径非常友好。举个例子如果你想提取页面上所有h2标签内的文字titles soup.select(h2) for t in titles: print(t.get_text(stripTrue))如果想提取某个class下面所有链接的地址links soup.select(.post-card a) for a in links: href a.get(href) text a.get_text(stripTrue) print(text, href)和find_all相比select的写法更接近前端CSS的语法基本不用查API文档就能猜出来。我个人的习惯是定位结构简单的元素用find_all定位结构路径比较长的元素用select。4.3 提取文字、属性与链接补全一个标签对象包含三部分信息标签名、属性字典、内部文字。提取时分别用tag.name得到标签名。tag.get(href)或tag[href]得到属性值。tag.get_text(stripTrue)得到内部所有文字并去掉首尾空白。这里有一个新手常踩的坑网页里的链接很多是相对路径比如href/author/Ernest-Hemingway。直接用这个值去发请求会因为缺少域名部分而报错。需要用urljoin补全from urllib.parse import urljoin base_url https://quotes.toscrape.com link /author/Ernest-Hemingway full_url urljoin(base_url, link) print(full_url) # https://quotes.toscrape.com/author/Ernest-Hemingway4.4 解析时的空白文本问题HTML源码里充满换行和缩进空格直接打印tag.text会得到一堆空白行。一般处理办法是stripTruetitle tag.get_text(stripTrue)但如果标签内部包含多个子标签比如div classmeta span张三/span span2024-01-01/span /div直接get_text(stripTrue)会把所有子标签的文本连成一串张三2024-01-01中间没有任何分隔。更好的做法是逐个提取spans div_meta.select(span) name spans[0].get_text(stripTrue) date spans[1].get_text(stripTrue)这也是为什么拿到HTML后不要急着写提取代码而应该先打开浏览器开发者工具看清目标数据在不同标签里的分布再动手。5. 一个完整案例抓取网页文章列表并存成CSV5.1 目标分析与思路拆解为了让你能完整跑通一条爬虫链路我选一个适合新手练习的公开网站quotes.toscrape.com演示。它的结构极简没有验证码没有复杂反爬专门用于爬虫练习。我要抓取首页上的每条名言、作者和标签并保存到CSV文件。整体思路分四步发起GET请求拿到首页HTML。用lxml解析器解析HTML。用select定位每条名言所在的div.quote节点再从中提取名言文本、作者、标签。把提取结果写入CSV文件。5.2 完整代码import csv import random import time import requests from bs4 import BeautifulSoup BASE_URL https://quotes.toscrape.com/ 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_quotes_from_page(url): resp requests.get(url, headersHEADERS, timeout(3, 10)) resp.raise_for_status() resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, lxml) quotes [] for quote in soup.select(div.quote): text quote.select_one(span.text).get_text(stripTrue) author quote.select_one(small.author).get_text(stripTrue) tags [tag.get_text(stripTrue) for tag in quote.select(a.tag)] quotes.append({ text: text, author: author, tags: , .join(tags), }) return quotes def main(): all_quotes [] for page in range(1, 4): url f{BASE_URL}/page/{page}/ print(f正在抓取: {url}) all_quotes.extend(fetch_quotes_from_page(url)) time.sleep(random.uniform(1, 2)) with open(quotes.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([text, author, tags]) for q in all_quotes: writer.writerow([q[text], q[author], q[tags]]) print(f完成共保存 {len(all_quotes)} 条数据) if __name__ __main__: main()5.3 关键代码逐段说明resp.raise_for_status()会在这个状态码不是2xx时抛出异常帮你快速发现页面请求失败的问题。resp.encoding resp.apparent_encoding是通过内容自动推断编码这一步能在目标网站没有明确声明编码时有效避免中文乱码。select(div.quote)定位的是页面上每条名言的容器节点。select_one(span.text)是在容器内部继续向下查找只取第一个匹配项。为什么不直接全局select(span.text)因为名言文本和作者名字各自内部都有span必须在容器范围里层层筛选才不容易抓到无关数据。写入CSV时加encodingutf-8-sig是经验之谈直接用utf-8保存的CSV用Excel打开会乱码加-sig之后Excel能正确识别中文。newline是为了避免Windows下CSV每行之间多出一个空行。5.4 常见运行错误排查新手运行这段代码最容易报错的点是AttributeError: NoneType object has no attribute text。这个错误的意思是select_one()没有匹配到任何元素返回了None。原因通常是两种一是页面结构和你预期的不同比如class名字变了二是目标数据是由JavaScript动态渲染的源代码里根本没有对应标签。排查顺序也很固定先打印resp.status_code确认请求是否成功再打印resp.text[:500]确认页面内容里是否存在目标关键字。这两步能排除掉80%的问题。6. 新手最常踩的坑与绕坑思路6.1 拿到的是“外壳”不是数据很多人在抓一些内容平台时会有这种经历请求状态码是200页面HTML也返回了但查了半天就是找不到想要的文章内容。这时打开浏览器按CtrlU查看网页源代码用关键字搜索一遍如果源代码里也没有基本可以确定数据是页面加载后由JavaScript异步请求接口拿到的。这种情况下Requests拿到的只是空壳模板。判断一个页面是否适合用Requests直接抓最快的方法就是在浏览器里查看源代码搜目标数据的特征文字搜得到就有机会搜不到就要走接口分析或浏览器渲染工具的路子。后者的典型代表是Selenium和Playwright是下一阶段的学习方向这里不展开。6.2 乱码问题的真相是编码声明中文站点常见乱码的根源是编码不一致。有些站点的响应头没有明确给charsetHTML里写的是gbk但resp.text默认按utf-8解码了。解决办法就是上面提到的resp.encoding resp.apparent_encoding或者干脆指定正确的编码resp.encoding gbk遇到乱码时不要慌先试着打印resp.encoding看看当前解码方式再查页面源码的meta charset两者对齐就解决。6.3 请求频率别让自己的程序变成攻击者爬虫入门后最容易犯的错是为了快速完成抓取把请求频率调得很高不设间隔多线程并发狂轰一个网站。轻则触发限流重则导致网站服务受影响。站在写代码的角度看这是一种非常不健康的使用习惯。合理的做法是单线程起步不要一上来就上协程或线程池。每两次请求之间加一个随机延迟比如time.sleep(random.uniform(1, 3))。抓取前看一下目标站点的robots.txt了解网站对爬虫的态度只在允许范围内采集公开信息。控制请求量只抓自己需求所必需的数据不搞全量下载。写爬虫的基本素养是克制。学会控制频率比你学会用多线程并发更要紧。长期高频请求只会让目标网站加固防护对你自己也没有好处。6.4 代码结构混乱导致后期难以维护入门阶段往往谁都是边试边写把请求逻辑、解析逻辑、存储逻辑全部堆在一个脚本里。等需求一改比如要抓三个页面而不是一个页面就会陷入逻辑纠缠。哪怕只是入门练习也应该按职责拆成函数定义fetch_page(url, headers)负责请求。定义parse_page(html)负责解析。定义save_to_csv(data)负责存储。每个函数只干一件事这样即使后续学习Scrapy这类框架也能把思路平滑迁移过去。我在案例里就是这么组织的函数边界清晰调试起来非常省事。6.5 千万不要盲目打印整个HTML面密度很多人拿到响应后第一反应是print(resp.text)然后被几千行HTML刷屏。更好的做法是先保存成文件再看with open(debug.html, w, encodingutf-8) as f: f.write(resp.text)然后用浏览器打开这个文件配合开发者工具去分析标签结构。这样既能保留现场又不会看得眼花缭乱。这个习惯尤其适合页面复杂、嵌套层级深的场景。根据我的个人经验入门阶段最忌讳的就是“急于抓一个看起来很酷的大站”。真正顺畅的学习路径是用一个结构清晰、没有反爬压力的练习站点把整条链路跑通再逐步接触真实站点中会遇到的各种边界情况。Requests和BeautifulSoup这个组合虽然简单但只要你踏实练完一遍HTTP请求、响应、解析、存储这些核心认知都会立起来。等你换到Scrapy或Playwright学到的也是同一套底层逻辑。