1. 从零开始摸爬虫先搞清楚 Requests 和 BeautifulSoup 到底在干嘛先抛个结论爬虫没你想的那么玄乎也没你想的那么难。很多人一听到“爬虫”两个字脑子里就浮现出黑客代码、数据库里噼里啪啦跑数据的画面觉得一定要有很深的编程功底才能玩。其实不是。Python 爬虫的核心就两个步骤把网页拿下来再从网页里把想要的数据抠出来。拿下来这件事用的是 Requests抠出来这件事用的是 BeautifulSoup。这俩库加起来学习成本大概就是一个周末的事。你不需要懂什么高深的网络协议不需要背什么算法只要会 Python 基础语法哪怕刚学完 for 循环、if 判断就能写一个像模像样的小爬虫。这篇文章我不打算给你堆概念而是按照我实际出发写爬虫的顺序把 Requests BeautifulSoup 这条路完整走一遍。我尽量用大白话讲清楚每一步在做什么、为什么要这么做、坑在哪让你学完之后不只是会复制粘贴而是真的能自己动手改、自己写。先花一分钟想清楚你学爬虫想干嘛。比如你是想抓某个小说网站更新想批量下载壁纸想把某个榜单的数据拉到 Excel 里做分析还是想监控某个商品的价格波动。不管哪种需求只要你理解了 Request请求和 Response响应——也就是“你问网站要东西”和“网站把东西给你”——你就能把它落地成代码。2. 环境准备别在这一步栽跟头2.1 Python 本体怎么装不管你用什么系统先确认你的电脑上有 Python 环境。我见过太多人卡在这一步代码还没写两行先被环境搞得心态崩了。如果你用 Windows去 Python 官网下载安装包装的时候一定要勾选“Add Python to PATH”这个选项。这一步不勾后面你在命令行里敲python就会提示不是内部或外部命令十个人里有八个栽在这。装完以后打开命令行WinR 输入 cmd敲python --version能输出版本号就说明没问题。Mac 用户相对省心macOS 自带 Python 2但那个版本太老千万别用。建议直接去官网下 Python 3或者如果你装了 Homebrew一条brew install python3搞定。Linux 用户一般自带 Python 3没有的话sudo apt install python3也行。注意2026 年了Python 2 早就是古董了任何教程只要让你用 Python 2直接关掉别看。2.2 安装 Requests 和 BeautifulSoup 的正确姿势Python 装好之后用自带的 pip 工具装第三方库。命令行里依次执行pip install requests pip install beautifulsoup4如果你是用 PyCharm 或 VS Code 这类开发工具也可以直接在终端里输同样的命令。装完之后别急着写代码先在编辑器里跑一下验证import requests import bs4 print(环境OK)能正常打印说明两个库都装好了。这里有个常见小坑有些教程会让你装BeautifulSoup注意没有 4那个是老版本语法不兼容。记住现在要装的是beautifulsoup4导入的时候import bs4。2.3 选对开发工具能省一半时间工具这块我给个建议新手别用记事本写代码也别一上来就装大型 IDE比如 PyCharm把自己吓着。VS Code 就足够或者 Jupyter Notebook 也行。VS Code 的优点是你写代码的时候会有颜色区分括号没闭合、变量名拼错这些问题能一眼看出来。Jupyter Notebook 的优点是你可以一段一段跑代码每跑一步都能马上看到输出结果特别适合调试爬虫——比如你先拿一下页面看看能不能通再解析一下看看结构对不对每一步都看得见。我自己的习惯是写爬虫脚本用 VS Code做数据分析用 Jupyter Notebook。两个工具的安装和配置网上教程一大把这里不展开。总之人在环境上多花半小时后面写代码能省五个小时。3. Requests 实战把网页完整地拿下来3.1 第一个请求requests.get 就是这么简单Requests 库的核心就一个函数族get、post、put、delete对应 HTTP 协议里不同的请求方式。日常写爬虫90% 的情况用get就够。import requests url https://example.com response requests.get(url) print(response.status_code) print(response.text)这段代码做了什么它向 example.com 这个网址发了一个 GET 请求服务器返回了一个响应对象里面包含了 HTTP 状态码200 代表成功404 代表页面不存在403 代表禁止访问和页面的 HTML 源码文本。response.text拿到的是经过解码的网页源码。有时候你会看到中文乱码那是因为网页的编码方式和你本地解码方式不一致。解决的办法是手动指定编码response.encoding response.apparent_encodingapparent_encoding是 Requests 通过分析网页内容推断出的编码方式。大多数情况下这一句就能搞定乱码问题。如果还乱你就再手动指定response.encoding utf-8多数网站都吃这套。3.2 headers 和 User-Agent爬虫的第一道门槛新手最容易忽略的一件事就是不加 headers 直接访问可能会被服务器拒绝。原理很简单你浏览网页用的是浏览器浏览器在请求头里会带上自己的身份信息比如“我是 Chrome 浏览器”“我来自哪个系统”之类。服务器看到这些信息知道这是一个正常的用户访问就放心地把数据发给你。而你的 Requests 默认发出去的请求没有这些身份信息服务器一看“这个请求没有 User-Agent”就像门口保安看到一个没带工牌的人第一反应就是拦住你盘问。所以写爬虫的第一条潜规则就是模拟浏览器的请求头。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, } response requests.get(url, headersheaders)User-Agent 这一段字符串你不需要背直接在你浏览器的开发者工具里就能复制打开任何一个网站按 F12切到 Network网络面板随便点一个请求里面 Request Headers 里就能找到你当前浏览器真实的 UA。复制过来用就行。这个细节非常关键。很多网站的反爬机制第一步就是校验 User-Agent你要是裸奔去访问基本一抓一个 403。3.3 超时和重试你的代码要能扛得住网络波动写爬虫有个我很想强调的原则永远不要让你的请求无限期等下去。网络环境不可能永远稳定有时候是对方服务器卡了有时候是你自己的网络抖了一下。如果你没设超时时间Requests 可能会一直挂在那里等你回过神来代码早就卡死不知道多久了。正确的写法是给get加上 timeout 参数response requests.get(url, headersheaders, timeout10)这个 10 表示“最多等 10 秒”。超时之后会抛出一个requests.exceptions.Timeout异常你用 try-except 接住就行。配上异常处理你的爬虫才算有了基本抗打击能力import requests from requests.exceptions import RequestException try: response requests.get(url, headersheaders, timeout10) response.raise_for_status() except RequestException as e: print(请求出错了, e)raise_for_status()是 Requests 的一个好习惯只要 HTTP 状态码是 4xx 或 5xx它就主动帮你抛异常免得你拿到错误响应之后还以为成功了继续去解析一个不存在的页面。3.4 Session 和 Cookie做到“登录状态”的请求有些网站你不登录就没法看数据或者你每次请求都要维护一个会话。这时候你可以用 requests.Session。Session 的意思是“会话对象”它会把你的 Cookie、请求头等信息保留下来带到下一次请求里。就像你逛商城第一次进门验了票之后在商场里逛保安不会再让你出示入场凭证了当然现实中不同门店还会查别抬杠懂意思就行。session requests.Session() session.headers.update(headers) # 第一次请求后Cookie 会被自动记录在 session 对象中 resp1 session.get(https://example.com/login) # 第二次请求会带上第一次请求设置的 Cookie resp2 session.get(https://example.com/profile)Session 的另外一个用途是维持登录状态你先 POST 提交表单登录服务器返回的 Cookie 会被 Session 自动保存后续请求自动携带这样你就能以登录用户的身份去抓取一些需要登录才能看到的数据。3.5 高频请求与 429 状态码新手最容易触碰的“红线”我在社交平台上经常会看到有人求助说自己写了个循环抓数据结果爬到一半代码报错提示什么429 Too Many Requests。这个问题太典型了值得单独拿出来说。429 的意思是“请求太多了”。服务器的意思是我不拒绝你但你短时间来得太频繁我处理不过来了你先冷却一下。这背后其实是网站的反爬策略之一——频率控制。你 1 秒钟发 100 个请求和普通人 1 秒钟点 1 次页面服务器的压力差太多了。为了不影响普通用户服务器会设置一个阈值超过这个阈值就直接返回 429。解决思路有几个第一个思路放慢速度。在循环里加一个time.sleep()让每次请求之间间隔几秒。这是最笨但最有效的方法。import time for page in range(1, 11): url fhttps://example.com/list/{page} resp requests.get(url, headersheaders, timeout10) print(f第{page}页状态{resp.status_code}) time.sleep(2)第二个思路设置随机延时。固定间隔也容易被识别因为真实用户不可能卡着秒表每隔 2 秒访问一次。用 random 模块生成一个随机数让节奏更接近真人。import random time.sleep(random.uniform(1, 3))第三个思路使用重试机制。遇到 429 或者 5xx 错误时退避重试——先等几秒再试再失败就等更久直到成功或达到最大重试次数。Requests 本身有个HTTPAdapter可以直接配置重试策略这是广泛使用的方案from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session requests.Session() retry_strategy Retry( total3, # 总重试次数 backoff_factor2, # 重试间隔2^n * backoff_factor 秒 status_forcelist[429, 500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry_strategy) session.mount(http://, adapter) session.mount(https://, adapter)backoff_factor2的意思是第一次重试前等 2 秒真实约 0.5 秒这里是按 urllib3 的公式 2^0 乘系数具体公式有细节但不用深究第二次等 4 秒第三次等 8 秒依次倍增。这种退避策略的好处是既保证了重试又不会在服务器还没缓过来的时候疯狂打请求。关于 429写爬虫的底线是不要为了赶速度而失去礼貌。网站数据不像物理定律你用蛮力得不到更多反而可能因封 IP 而前功尽弃。有的网站不仅返回 429还会临时封你的 IP 一段事件那你的爬虫就彻底瘫痪了。4. BeautifulSoup 解析从 HTML 源码里精准挖出数据4.1 HTML 和标签的基本认知Requests 帮你拿到了网页源码但这只是一堆字符串。要从里面把目标数据比如商品价格、新闻标题、用户昵称提出来你还需要一个能理解 HTML 结构的工具——这就是 BeautifulSoup。先建立最基本的 HTML 认知。HTML 是由成对出现的标签组成的比如div classarticle h1 classtitle这是一个标题/h1 p这是一段正文内容/p span price99价格/span /divdiv是区块容器h1是一级标题p是段落标签可以带class或id这样的属性。BeautifulSoup 就是让你能按标签名、属性、文本内容去做查找。4.2 把 Response 转成 BeautifulSoup 对象承接上面的 Requests 代码解析操作一般长这样from bs4 import BeautifulSoup soup BeautifulSoup(response.text, html.parser)这里第二个参数html.parser是解析器。BeautifulSoup 内部需要把字符串解析成一个树状结构html.parser是 Python 内置的解析器零依赖还有个lxml解析器速度更快但需要先安装lxml库。新手用内置的就好完全够用。4.3 find 和 find_all最常用的两个方法BeautifulSoup 最核心的就是这两个方法find()返回第一个匹配的元素find_all()返回所有匹配的元素列表按标签查找是最基础的用法first_h1 soup.find(h1) # 找到第一个 h1 all_paragraphs soup.find_all(p) # 找到所有 p 标签按属性查找稍微进阶一点但也是高频用法title soup.find(h1, class_title) # 注意是 class_不是 class divs soup.find_all(div, class_article)这里的坑是class是 Python 的关键字所以 BeautifulSoup 用了class_这个写法。还有一种是直接用字典title soup.find(h1, attrs{class: title})这两种等价用哪个看你习惯。我偏好class_写起来更短。文本内容用.text获取属性值用方括号获取print(title.text) # 拿到标签里的文本内容 link soup.find(a) print(link.get(href)) # 拿到 a 标签的链接地址4.4 选择器像 CSS 一样定位元素如果只是 find 和 find_all你写复杂页面的时候会有点费劲。BeautifulSoup 还支持 CSS 选择器通过select()方法使用# 等价于 soup.find(div, class_article) soup.select(div.article) # 多个层级查找 soup.select(div.article h1) # 通过 id 查找 soup.select(#main-title) # 找某些 class 里的所有链接 soup.select(div.list a)select()的写法和前端写 CSS 样式选择器一模一样.代表 class#代表 id空格代表后代关系。这个对写过前端的人十分友好没写过也没关系现学现用很快就能上手。我的建议是简单的页面用 find_all复杂的嵌套结构用 select。实际写代码时两者经常混用只要你自己读得懂就行。4.5 实战从信息列表页提取文章列表我们把前面所有内容串起来写一个完整的示例。假设要抓一个博客网站的文章标题和链接import requests from bs4 import BeautifulSoup url https://example.com/blog 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, } resp requests.get(url, headersheaders, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) articles soup.find_all(article) for article in articles: title_tag article.find(h2) link_tag article.find(a) if title_tag and link_tag: title title_tag.text.strip() link link_tag.get(href) print(title, link)这段代码干了什么先请求页面再解析然后遍历所有 article 标签在每个 article 里找 h2 标题和 a 链接最后打印出来。看起来很简单但这就是爬虫从“拿页面”到“拿数据”的完整链路。.strip()是顺手去掉字符串两端多余的空格和换行符提取文本内容时的常见操作不然打印出来的标题会带着一堆看不见的空白字符。5. 完整实战爬取一个有真实结构的网站理论说了这么多不如完整跑一个案例。我挑一个适合练手的场景抓取一个静态博客网站的分页文章标题和发布时间。这类网站结构清晰、没有复杂的反爬机制适合拿来验证整个学习过程。等你能完整跑通这个案例再往真实场景升级也不会慌。5.1 先观察目标页面结构写爬虫第一步不是写代码而是先打开网页按 F12 看结构。我通常在浏览器开发者工具里点选元素看看我要抓的内容在哪个标签里class 叫什么。比如目标页面的文章列表长这样简化示意div classpost-list div classpost-item h2 classpost-titlea href/post/1第一篇文章/a/h2 span classpost-date2026-01-01/span /div div classpost-item h2 classpost-titlea href/post/2第二篇文章/a/h2 span classpost-date2026-01-05/span /div /div观察完结构你的解析思路就有了先用soup.find_all(div, class_post-item)拿到每一篇文章的容器再在每个容器里分别取标题、链接、日期。5.2 分页抓取的思路大部分网站都不只一页。如果列表页有翻页按钮你在浏览器里点“下一页”观察一下 URL 的变化规律。常见的翻页 URL 有两种一种是 path 形式的/page/1、/page/2、/page/3。另一种是 query 参数形式的?page1、?page2。只要发现了规律就可以在一个循环里动态拼接 URL实现多页抓取base_url https://example.com/blog/page/{} for page in range(1, 6): url base_url.format(page) # 请求 解析 提取5.3 保存数据到 CSV 文件抓到的数据如果只打印在屏幕上程序一关就没了。保存下来才有价值。对新手来说CSV 是最简单的存储格式用 Python 的 csv 模块直接搞定Excel 也能直接打开。import csv import time import requests from bs4 import BeautifulSoup base_url https://example.com/blog/page/{} 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, } with open(articles.csv, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([标题, 链接, 日期]) # 表头 for page in range(1, 6): resp requests.get(base_url.format(page), headersheaders, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) items soup.find_all(div, class_post-item) for item in items: title item.find(h2).text.strip() link item.find(a).get(href) date item.find(span, class_post-date).text.strip() writer.writerow([title, link, date]) print(f第{page}页抓取完成共{len(items)}条) time.sleep(1)几个细节值得注意newline是防止 CSV 写入时出现多余空行Windows 上必须加。encodingutf-8是防止中文写入乱码但考虑到 Excel 打开 CS​​V 默认按 GBK 解码如果乱码就改成encodingutf-8-sig。这是 Excel 的一个兼容用法utf-8-sig会在文件开头加一个 BOM 标识Excel 就能正确识别为中文字符。每页抓完主动 sleep 1 秒。这个习惯非常重要既降低被封风险也照顾对方服务器的压力。5.4 抓取结果的简单去重和清洗爬到的数据往往不是直接就能用的。常见的问题有标题前后有空白字符、链接是相对路径缺了域名前缀、部分字段为空。清洗数据这个环节新手往往觉得无关紧要实际上决定了你的数据最终能不能用。链接拼接的话可以用urllib.parse.urljoin补全相对路径from urllib.parse import urljoin full_url urljoin(https://example.com/blog, link)去重可以用 set 数据结构在写入前检查是否处理过seen set() # 在循环里 if link in seen: continue seen.add(link)这些细节看着小实际写爬虫的时候会反复遇到。自己动手实践时多留意数据清洗这一环后期分析才能省心。6. 常见报错排查照着问题来找原因就行写爬虫报错太正常了每天不报几个错都感觉没写过代码。我把新手最常踩的坑整理成一张表你遇到了直接对号入座。报错信息原因解决思路ModuleNotFoundError: No module named requests没有安装 Requests 库命令行执行pip install requestsConnectionError网络不通或对方服务器不稳定检查网络、域名能不能打开加 timeout 和重试Timeout请求超过了设定的等待时间调大 timeout 参数或检查网络质量HTTP 403被服务器拒绝通常是 headers 缺失加入完整的 User-Agent 和相关请求头HTTP 404页面不存在或 URL 拼接错误手动在浏览器打开这个 URL看是否真实存在HTTP 429请求频率太高触发了限流降低请求频率加 sleep配置退避重试AttributeError: NoneType object has no attribute textfind 没找到元素返回了 None加判断if element:或者检查选择器是不是写错了IndexError: list index out of rangefind_all 返回了空列表取值下标越界先打印列表长度确认页面结构没变中文乱码解码方式和服务器实际编码不一致用resp.encoding resp.apparent_encoding页面内容和浏览器看到的不一样网页可能是 JS 动态渲染的Requests 拿不到 DOM 加载后的数据改方案找数据背后的接口或换 Selenium 等浏览器自动化工具这里特别提一下NoneType object has no attribute text这个错它是爬虫新手最容易撞上的错误。原因往往是页面结构和你预期的不一样可能 class 名改了可能页面被反爬拦截返回了错误提示页可能目标内容是通过 JS 动态加载的。排查的关键就是打印出response.text前几百个字符亲眼看看到底返回了什么。7. 写爬虫的三个核心习惯7.1 先打印再解析不要憋大招我见过不少新手一上来就写一个 50 行的完整爬虫跑起来报错又不知道哪一步出了问题只能一行一行瞎猜。我的习惯是用 Jupyter Notebook 或者直接写临时脚本一步一看结果。先请求页面打印resp.status_code确认状态码是 200打印resp.text[:500]确认拿到了正常页面然后才开始写解析代码。每写一步输出一步的结果确认无误再往下一步走。这样做的好处是出问题的时候你能精准定位到是哪一步出了问题而不是抓瞎。7.2 尊重目标网站不硬来爬虫能不能长期稳定运行很大程度上取决于你的请求“礼貌不礼貌”。设置合理的 User-Agent、控制请求频率、设置超时和重试、优先考虑对方网站的 robots.txt包括识别429 Too Many Requests并做出退避响应这些不是多余的谨慎而是写爬虫的基本素质。反爬手段是可以无限升级的但你的时间精力是有限的。很多情况下你不需要跟反爬硬刚只要你的请求像一个正常用户在浏览大部分网站都不会为难你。如果对方明确拒绝了调低频率、加延时比换各种技巧去绕过要聪明得多。7.3 用调试工具理解网站的请求过程有一点我必须反复强调按 F12 打开浏览器开发者工具去 Network 面板看请求和响应这是学爬虫最值得花时间的动作——没有之一。通过 Network 面板你能看到页面到底请求了哪些接口哪些是 HTML 页面哪些是 JSON 数据接口每个请求的状态码、耗时请求头、请求体长什么样返回的数据结构是什么样子。很多在现代流行的动态网站页面內容已经是通过 JS 调接口渲染出来的。这种情况你再直接请求页面 URL 只能拿到一个空壳真正有价值的数据藏在那些 XHRXMLHttpRequest请求里。通过开发者工具找到那个返回 JSON 的接口直接请求它解析 JSON 数据效率高得多。当你熟练掌握这个思路之后你会发现很多网站的“爬取难度”都会大大降低。8. 看完这篇文章之后下一步学什么Requests BeautifulSoup 是爬虫的入门组合但绝对不是终点。随着你抓的网站越来越多你会遇到以下这些场景对应的学习路线也帮你列好了场景对应技术页面是 JS 动态渲染的Selenium、Playwright浏览器自动化需要高频抓取大量数据Scrapy 框架更高效的爬虫框架数据不是 HTML 而是 JSON直接用 requests 解析.json()响应接口有签名校验逆向 JS 算法、抓包分析需要分布式抓取任务队列 去重调度验证码识别OCR、第三方打码平台或尽量规避但不管你去学哪个方向底子还是这几样HTTP 请求、HTML 结构、JSON 解析、异常处理。基础打扎实了遇到新工具也只是换汤不换药。我在实际带新人的时候发现能够快速上手的人都有一个共同特点愿意把一个简单例子反复拆开看搞清楚每一行代码为什么这么写而不是急着把代码跑通就完事。你现在学 Requests 和 BeautifulSoup就像学开车先练挂挡和踩离合枯燥但重要。这个过程跑一遍后面学什么都快。