简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取
标题的流程,涵盖GET请求发送、状态码判断、文档解析、标签定位等基础步骤,并延伸介绍了CSS选择器与异常处理思路。资源为单个docx文档,体积仅14KB,内容紧凑无冗余,便于离线阅读随堂实践。文档中提供了可复制的完整代码,包括依赖库安装命令、请求失败时的状态码提示、遍历并打印标题文本的具体写法,并建议读者自行替换URL进行验证;同时通过示例强调实际操作需遵守robots.txt规则、控制访问频率,避免对目标网站造成过大负载,帮助读者建立规范的爬虫开发习惯。目前已有133人浏览学习,适合作为爬虫入门的第一份手边参考,也适合作为教学辅助材料快速上手。
1. 从Requests到BeautifulSoup:一次只抓
的Web Scraping入门
不少人学爬虫,第一反应是找复杂的框架、上分布式采集,结果被环境配置劝退。我的建议正好相反:先用 requests 发一个 HTTP GET 请求,再用 BeautifulSoup 从返回的 HTML 里挑出全部
标签,把这条链路跑通,再谈其他。requests 负责把网页内容原样搬回来,BeautifulSoup 负责从这堆带尖括号的文本里精准切出标题文字,两者分工明确,加起来不到二十行代码。这篇笔记适合有基础 Python 语法、想快速上手网络数据采集的人,代码可以直接复制运行,重点放在参数怎么调、失败时看什么输出。
2. 搭建环境与第一个请求:requests的选型理由与关键参数
2.1 为什么是 requests 而不是 urllib
Python 标准库里其实自带 urllib,也能发 HTTP 请求,但处理 Cookie、重定向、请求头拼接时要写不少样板代码,遇到需要维持会话的站点更是麻烦。requests 把这些底层细节统一封装成 get、post、put 这类方法调用,一个方法对应一个动作,代码可读性高很多。对于入门阶段,requests 几乎是社区公认的首选。
另一个值得知道的点是:requests 底层用的是 urllib3,它把连接池复用、TLS 握手、自动解压这些脏活全接管了。你看到 response = requests.get(url) 这一行,背后其实是 DNS 解析、TCP 连接、发送请求头、接收响应体的完整流程。理解这一点对排查问题很有用——比如状态码返回 200,但 response.text 里是一段「访问异常」提示,问题大概率不在 requests,而在对方服务器识别出这不是浏览器发出的请求。
2.2 安装与最小请求:三步验证网络通不通
安装很简单,在终端里执行下面这行命令即可:
pip install requests beautifulsoup4我习惯先在虚拟环境里操作,避免把全局 Python 环境弄乱。装好后别急着写完整爬虫,先跑一个最小请求,验证网络链路通不通:
import requests url = 'http://example.com' response = requests.get(url, timeout=10) print(response.status_code) print(response.text[:200])这段代码的作用是发送 GET 请求并打印状态码和响应体前 200 个字符,先确认目标网站真的能访问。status_code 等于 200 说明请求成功;如果打印出 403 或 406,说明服务器拒绝了这次请求,后面需要加请求头伪装浏览器。先把这一步跑通,再往下做解析,能省很多排查时间。
2.3 响应对象的关键成员:拿数据前先读懂它
requests.get() 返回的是一个 Response 对象,里面有几个成员在爬虫里几乎天天要用。response.status_code 是 HTTP 状态码;response.text 是响应体按推测编码解码后的字符串,中文网站偶尔会解码错误,需要手动指定 encoding;response.content 是原始字节,下载图片或二进制文件时用它;response.headers 里能看到 Content-Type、Server 字段,判断返回内容到底是什么类型。
params = {'page': 1, 'limit': 20} response = requests.get(url, params=params, timeout=(3, 10)) print(response.url)params 参数会自动把字典拼成查询串追加到 URL 后面,比如 ?page=1&limit=20,这样不用手动拼接字符串。timeout 推荐传元组 (3, 10),前一个数是连接超时,后一个数是读取超时,单位都是秒。只传一个数的话,连接和读取共用同一个值,如果服务器一直不返回数据,脚本可能长时间卡住,这是新手常踩的坑。
不同状态码在爬虫场景里的含义也值得记一下:
| 状态码 | 含义 | 爬虫里常见场景 |
|---|---|---|
| 200 | 请求成功 | 正常返回页面内容 |
| 301/302 | 重定向 | requests 默认自动跟随 |
| 403 | 被拒绝 | 缺少 UA 或触发反爬策略 |
| 404 | 地址不存在 | 链接失效或写错路径 |
| 429 | 请求过于频繁 | 触发限流,需要放慢速度 |
| 500 | 服务器内部错误 | 目标站本身出问题,重试也未必有用 |
实际调试时,优先看 response.text 的开头几百字,而不是只看状态码。很多反爬页面为了稳定会故意返回 200,但内容已经换成了验证提示。
提示:如果目标页面需要登录才能访问,requests.get() 里还可以加 cookies 参数,或者先用 requests.post() 提交表单获取会话,后续章节的 Session 对象会更适合这种场景。
3. 解析HTML提取标题:find_all与CSS选择器的搭配用法
3.1 find_all 与 Tag 对象:从h1标签开始提取
BeautifulSoup 的核心作用是把 HTML 文本解析成一棵标签树。soup.find_all('h1') 会返回一个列表,列表里每个元素是一个 Tag 对象,代表页面上的一个
标签。要从 Tag 里取出纯文字,用 get_text() 方法。
from bs4 import BeautifulSoup html_text = "<html><body><h1>第一个标题</h1><h1>第二个标题</h1></body></html>" soup = BeautifulSoup(html_text, 'html.parser') h1_tags = soup.find_all('h1') for h1 in h1_tags: print(h1.get_text(strip=True))这里我用了一段静态 HTML 字符串做演示,效果和解析真实网页响应是一样的。get_text(strip=True) 会去掉标题首尾的空白字符和换行,避免打印出来带着一大串空格。注意 strip 参数只清理首尾,不会动标题内部的空格,如果正文里有连续空格,需要另做清洗。
find_all 还可以加参数缩小范围,比如 find_all('h1', limit=3) 只取前三个结果,find_all('h1', class_='title') 只查找 class 为 title 的 h1 标签。这里 class_ 后面必须带下划线,因为 class 是 Python 的保留关键字,直接写 class= 会报语法错误。我见过不少人在这一步被卡住,报错信息里明确提示了这一点。
3.2 select() 与 CSS Selectors:层级定位比循环查找更省事
find_all 按标签名和属性筛选时还够用,但遇到「某个区域下的某类元素」这种层级关系,写起来就啰嗦了。比如要抓 id 为 content 的 div 里面所有 h2,用 find_all 要先定位 div 再在结果上继续 find_all,代码会多一层嵌套。soup.select() 方法接受 CSS Selectors 语法,可以直接用一行表达式完成同样的定位。
# 选取 class 为 title 的所有 h1 soup.select('h1.title') # 选取 id 为 header 的容器内部的 h1 soup.select('#header h1') # 选取带 href 属性的所有 a 标签 soup.select('a[href]') # 选取 class 为 content 的 div 下的直接子元素 p soup.select('div.content > p')select() 返回的同样是 Tag 对象列表,取值方式和 find_all 没有区别。实际项目里我通常两个方法混用:先用 select() 把范围锁定到某个区块,再在区块内用 find_all() 提取同类型的多个元素。CSS Selectors 的另一个好用的地方是属性选择器,比如抓所有带 download 属性的链接,一行就能搞定,不用写循环去判断。
3.3 完整脚本与三个边界情况:空列表、嵌套标签和重复标题
把前面的内容串成一个完整脚本,加上异常处理和编码兜底:
import requests from bs4 import BeautifulSoup url = 'http://example.com' try: response = requests.get(url, timeout=10) response.raise_for_status() except requests.exceptions.RequestException as e: print("请求失败:", e) exit() if 'text/html' not in response.headers.get('Content-Type', ''): print("目标不是 HTML 内容,拒绝解析") exit() response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, 'html.parser') h1_tags = soup.select('h1') print(f"共找到 {len(h1_tags)} 个 h1 标题") for h1 in h1_tags: title = h1.get_text(strip=True) if title: print(title)raise_for_status() 会在状态码为 4xx 或 5xx 时主动抛异常,这样就不用每次手动判断状态码。response.encoding 先赋成 apparent_encoding,这是从页面内容里推测出的编码,比 requests 默认按响应头猜测的方式更可靠。脚本最后额外判断了 h1 是否为空文本,避免页面里存在空标签时打印一堆空白行。
边界情况要提前想清楚。第一,页面上可能一个 h1 都没有,返回空列表,代码不报错但也没有输出,这不代表爬虫坏了,而是页面结构问题。第二,h1 内部可能嵌套了 a 链接或 span,get_text() 会连同子标签的文字一起取出,拿到的是拼接后的完整文本。第三,一个页面有多个 h1 时,脚本会全部打印,如果你只想要第一个,改成 soup.select_one('h1') 即可。
注意:解析器我默认用 html.parser,它不需要额外安装第三方库。lxml 解析速度更快,但要单独装编译好的二进制包,入门阶段没必要增加这个变量。
4. 爬虫避坑指南:五个会把新手逼疯的翻车现场
这一章完全来自实战中反复遇到的故障。每个问题都按「现象 → 原因 → 解决」展开,照着排查能省下很多翻论坛的时间。
4.1 状态码200但抓到的是提示页
现象:requests.get() 返回的状态码是 200,打印 response.text 却发现里面不是目标内容,而是「您的访问异常」或一道验证页面。
原因:这是反爬策略的常见手法。服务器判断请求头里的 User-Agent 不像浏览器,就返回一个兜底页面,但状态码仍然保持 200,方便前端正常渲染。只看状态码完全发现不了问题。
解决:先打印 response.text 的前 200 个字符看一眼实际内容。然后给请求加上浏览器请求头,至少带上 User-Agent:
headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get(url, headers=headers, timeout=10)设置 UA 后大部分提示页问题都能解决。如果还不行,可能需要先访问首页拿到 Cookie 再请求目标页,这时就该用下一章的 Session 对象了。
4.2 中文标题乱码:encoding 的黑匣子
现象:抓回来的英文和数字正常,但中文标题全部变成乱码,比如「å»¶」这种无法辨认的字符。
原因:requests 在解码响应体时,会根据响应头里的 charset 推断编码。有些网站响应头里没写 charset,requests 会回退到一个默认值,中文页面在这种默认编码下解码必然出乱码。
解决:手动指定编码,优先用 apparent_encoding,它是从页面内容字节特征推测出来的:
response.encoding = response.apparent_encoding也可以在抓取前先查看响应头里的声明:print(response.headers.get('Content-Type')),如果里面有 charset=utf-8 之类,直接赋给 response.encoding 也行。注意一点,乱码问题出在解码环节,BeautifulSoup 本身没有责任,soup 解析的是已经解码的字符串。
4.3 find_all 结果为空:动态渲染的锅
现象:代码没有任何报错,response.status_code 是 200,但 soup.find_all('h1') 返回空列表,或者只能拿到页面框架里那几个无关标题。
原因:页面内容由 JavaScript 在浏览器端动态渲染,requests 拿到的 HTML 只是空壳骨架,真实标题是脚本执行后生成的,Requests 不执行 JavaScript,自然抓不到。还有一种情况是标题不在 h1 里,而是用 div 套了大量样式来模拟标题的视觉效果。
解决:用浏览器打开目标网页,右键「查看网页源代码」,对比源代码和渲染后页面。如果源代码里没有你要的数据,说明是动态渲染,需要改用 Selenium 或 Playwright 这类浏览器自动化工具。如果数据在源代码里,但标签不是 h1,就调整选择器,比如改成 soup.select('div.article-title')。
4.4 抓几次后被拒绝:403 和 429 的限流
现象:脚本前两次运行正常,第三次开始连续返回 403,或者返回 429 Too Many Requests。
原因:短时间高频率的请求触发了服务器的限流机制。即使你伪装了 UA,单位时间内访问次数太多,服务器同样会识别为脚本行为。
解决:控制抓取节奏,在每次请求之间加延时,常见的做法是 time.sleep(2) 到 time.sleep(5)。同时查看目标网站的 robots.txt,路径一般写在域名根目录下,比如 http://example.com/robots.txt,里面会明确列出哪些路径不允许爬取。做技术练习时,尽量选择允许采集的站点。
4.5 脚本卡死不动:timeout 缺失的后遗症
现象:requests.get() 执行后长时间没有反应,脚本像挂了,最后才抛出一个超时异常。
原因:没有显式设置 timeout,requests 会一直等待服务器响应。有些服务器接受连接后不返回数据,连接就一直挂着,脚本也就卡住了。
解决:所有请求都显式设置 timeout,推荐用元组形式区分连接和读取超时:
response = requests.get(url, timeout=(3, 10))再把每个请求包进 try-except,捕获 requests.exceptions.RequestException,批量任务里配合循环实现失败重试。从那以后我每次写爬虫都强制检查三个地方:有没有 timeout、有没有异常捕获、有没有控制频率,缺一个都不上线。
5. 让爬虫更健壮:Session、Headers与批量抓取三步进阶
前面的脚本每次请求都是独立的,遇到需要登录或维持会话的站点就会失效。requests 的 Session 对象能保持连接和 Cookie,配合自定义 Headers 可以模拟一个稳定的浏览器会话。下面是一个批量抓取多个页面标题的完整示例:
import requests from bs4 import BeautifulSoup import time session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) urls = ['http://example.com', 'http://example.com/about', 'http://example.com/contact'] all_titles = [] for url in urls: try: response = session.get(url, timeout=(3, 10)) response.raise_for_status() response.encoding = response.apparent_encoding soup = BeautifulSoup(response.text, 'html.parser') titles = [h.get_text(strip=True) for h in soup.select('h1') if h.get_text(strip=True)] all_titles.extend(titles) time.sleep(2) except requests.exceptions.RequestException as e: print(f"抓取失败:{url},原因:{e}") print(f"共采集到 {len(all_titles)} 个标题") print(list(dict.fromkeys(all_titles)))Session 会复用底层连接,连续请求同一个站点时比每次新建连接更快,也更不容易被识别为异常流量。headers.update() 把 UA 统一设置到会话级别,后续每个请求自动携带。time.sleep(2) 是给服务器留出的喘息时间,批量采集时这个间隔建议只增不减。
验证环节我一般做两步:第一步对比采集数量和页面实际数量是否一致,不一致说明选择器漏了元素或页面有动态内容;第二步对标题列表去重,检查是否有重复抓取。跑完这套流程,一个入门级的标题采集器就算真正能交付了。希望帮到你。
本文还有配套的精品资源,点击获取