说实话,刚开始学爬虫的时候,我最头疼的不是写代码,而是面对那些莫名其妙的报错。记得第一次用 urllib 抓一个页面,结果返回了个 403,我当时就懵了,以为程序写错了,折腾了半天才发现是对方服务器在拦我。今天我就把这两个库的 GET 和 POST 请求掰开了揉碎了讲给你听,顺便把那些让人头秃的错误排查技巧也一并送上。咱们不整那些虚头巴脑的理论,直接上干货,让你看完就能动手写代码。
为什么我们要用 Python 抓网页数据?
在讲代码之前,先聊聊背景。网页本质上就是一堆 HTML、JSON 或者 XML 文本,它们通过 HTTP 协议从服务器传到你电脑上。Python 之所以成为爬虫的首选,是因为它的生态太丰满了。你不需要像 Java 那样写几十行代码来建立一个连接,也不像 C++ 那样要手动管理内存。Python 里有两个“元老级”的 HTTP 库:urllib 和 requests。
urllib 是 Python 标准库的一部分,这意味着你安装完 Python 就能直接用,不需要 pip install。它适合那些不喜欢引入第三方依赖、或者在极简环境下运行代码的场景。但是,urllib 的 API 设计有点老旧,使用起来代码冗长,处理错误也比较麻烦。
而 requests 呢?它是第三方库,但几乎是 Python 爬虫圈的“事实标准”。它的语法简洁优美,一个 requests.get() 就能搞定大部分请求,错误处理也更直观。对于新手来说,我建议先精通 requests,因为它能让你快速看到结果,建立信心;然后再回头学 urllib,理解底层的 HTTP 连接机制。这样你既会用“神器”,又懂“原理”。
用 requests 实现 GET 请求:最简单也最常用的场景
GET 请求是网页抓取中最基础的操作,就像你去图书馆借书,只是伸手拿一下,不需要提交任何复杂的信息。咱们从一个最简单的例子开始。
假设我们要抓取一个公开的 JSON 数据接口,比如那个著名的 JSONPlaceholder API,它专门用来测试 API,返回模拟的帖子数据。
import requests
# 定义请求的 URL
url = "https://jsonplaceholder.typicode.com/posts/1"
try:
# 发起 GET 请求
response = requests.get(url)
# 检查响应状态码
if response.status_code == 200:
# 解析 JSON 数据
data = response.json()
print(f"标题: {data['title']}")
print(f"内容: {data['body']}")
print(f"用户ID: {data['userId']}")
else:
print(f"请求失败,状态码: {response.status_code}")
except requests.exceptions.RequestException as e:
# 捕获所有 requests 相关的异常
print(f"发生网络错误: {e}")
这段代码看起来很简单,但里面有几个关键点值得细讲。首先,requests.get() 是核心,它返回一个 Response 对象。这个对象里装满了我们需要的所有信息:状态码、响应头、Cookie、以及响应体(也就是网页内容)。
其次,我用了 try-except 块来捕获异常。这是爬虫稳健性的基础。网络环境千变万化,服务器可能宕机,DNS 可能解析失败,甚至你自家的网都可能断一下。如果不加异常处理,程序可能会突然崩溃,让你不知所措。requests.exceptions.RequestException 是一个基类,它能捕获几乎所有与请求相关的错误,比如超时、连接错误、SSL 验证失败等。
再来看看状态码。200 代表成功,这是最理想的情况。但如果状态码不是 200 呢?比如 404 表示资源不存在,403 表示被拒绝访问,500 表示服务器内部错误。在实际爬虫中,我们经常需要针对不同状态码做不同的处理逻辑,而不是简单地打印一句话。
比如,你可以这样优化一下:
if response.status_code == 200:
data = response.json()
process_data(data)
elif response.status_code == 404:
print("抱歉,你要找的数据不存在")
elif response.status_code == 403:
print("访问被禁止,可能需要检查 Headers 或 Cookie")
elif response.status_code == 500:
print("服务器忙,稍后再试")
else:
print(f"未知的错误状态码: {response.status_code}")
处理响应头:让服务器以为我们是“正规访客”
很多时候,直接发起请求会被拒绝,原因很简单:服务器的反爬机制会检查请求头。一个标准的浏览器请求头里包含很多信息,比如 User-Agent(浏览器标识)、Accept(接受的数据类型)、Referer(来源页面)等。如果我们直接用 Python 的默认请求头,服务器可能会认出这是机器人,然后把我们要的数据藏起来。
所以,学会自定义请求头是爬虫的必备技能。
import requests
url = "https://httpbin.org/get"
# 自定义请求头,模拟 Chrome 浏览器
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/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Connection": "keep-alive",
}
response = requests.get(url, headers=headers)
print(f"状态码: {response.status_code}")
print(f"响应头: {response.headers}")
print(f"响应体: {response.text[:500]}...") # 只打印前500个字符,避免刷屏
这里我用了 httpbin.org 这个网站,它专门用来测试 HTTP 请求,会把收到的请求原封不动地返回给你,非常适合用来调试。你看,通过添加 headers 参数,我们成功地“伪装”成了一个正常的浏览器用户。
在实际项目中,你还需要注意 User-Agent 的轮换。如果一直用一个固定的 UA,很容易被识别。你可以准备一个 UA 池,每次请求随机选一个,这样更保险。
import random
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0",
]
headers = {
"User-Agent": random.choice(user_agents),
# 其他头信息...
}
POST 请求:向服务器提交数据
GET 请求是“拿”,POST 请求是“给”。当你需要登录网站、提交表单、或者调用写数据的 API 时,就要用到 POST。POST 请求和 GET 请求在写法上非常相似,只是参数位置不同。
GET 的参数通常放在 URL 里,而 POST 的参数放在请求体(body)里。
import requests
url = "https://httpbin.org/post"
# 提交的数据,通常是一个字典
payload = {
"username": "test_user",
"password": "secret123",
"action": "login"
}
response = requests.post(url, data=payload)
print(f"状态码: {response.status_code}")
print(f"响应内容: {response.json()}")
这里用了 data 参数来传递数据。requests 会自动把字典编码成 application/x-www-form-urlencoded 格式,这是最常见的表单提交格式。
有时候,你需要提交的是 JSON 格式的数据,比如很多现代 API 都要求 JSON。这时候,你应该用 json 参数,而不是 data。
import requests
url = "https://httpbin.org/post"
# 使用 json 参数,requests 会自动序列化为 JSON 并设置正确的 Content-Type
payload = {
"title": "foo",
"body": "bar",
"userId": 1
}
response = requests.post(url, json=payload)
print(f"响应头 Content-Type: {response.headers['Content-Type']}")
print(f"响应内容: {response.json()}")
用 json 参数的一个好处是,requests 会自动帮你设置 Content-Type: application/json 头,并序列化数据。如果你用 data 参数传字典,它是表单格式;传字符串,它就是原始字符串。所以,明确知道你要提交什么格式很重要。
用 urllib 实现 GET 和 POST:理解底层机制
现在咱们来说说 urllib。虽然 requests 很好用,但有些场景下你没法用第三方库,或者你想更深入地理解 HTTP 请求是怎么构建的,这时候 urllib 就派上用场了。
urllib 是 Python 标准库,它分为几个模块:urllib.request、urllib.parse、urllib.error 等。用起来比 requests 啰嗦一些,但逻辑很清晰。
urllib GET 请求
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
import json
url = "https://jsonplaceholder.typicode.com/posts/1"
# 创建请求对象,可以设置 headers
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
req = Request(url, headers=headers)
try:
# 发起请求
with urlopen(req) as response:
status_code = response.status
if status_code == 200:
# 读取响应内容
data = response.read().decode('utf-8')
# 解析 JSON
json_data = json.loads(data)
print(f"标题: {json_data['title']}")
else:
print(f"状态码: {status_code}")
except HTTPError as e:
print(f"HTTP 错误: {e.code} - {e.reason}")
except URLError as e:
print(f"URL 错误: {e.reason}")
except Exception as e:
print(f"其他错误: {e}")
你看,urllib 的流程是:创建 Request 对象 -> 用 urlopen 打开 -> 读取内容。它没有 requests 那么智能,比如你需要手动解码字节流(.decode('utf-8')),手动解析 JSON。而且,错误处理分成了 HTTPError 和 URLError,需要分别捕获。
urllib POST 请求
from urllib.request import urlopen, Request
from urllib.parse import urlencode
import json
url = "https://httpbin.org/post"
data = {
"username": "test_user",
"password": "secret123"
}
# 将字典编码为 URL 编码的字符串
encoded_data = urlencode(data).encode('utf-8')
# 创建 POST 请求
req = Request(url, data=encoded_data, headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}, method='POST')
try:
with urlopen(req) as response:
status_code = response.status
if status_code == 200:
result = json.loads(response.read().decode('utf-8'))
print(f"提交成功,响应: {result['form']}")
else:
print(f"状态码: {status_code}")
except Exception as e:
print(f"错误: {e}")
在 urllib 中,POST 数据必须先编码成字节流,然后通过 data 参数传给 Request。如果你想提交 JSON,需要手动设置 Content-Type 头,并把字典序列化成 JSON 字符串。
# urllib 提交 JSON
import json
from urllib.request import urlopen, Request
url = "https://httpbin.org/post"
payload = {"title": "foo", "body": "bar"}
# 手动序列化为 JSON 字节
json_data = json.dumps(payload).encode('utf-8')
req = Request(url, data=json_data, headers={
"User-Agent": "Mozilla/5.0",
"Content-Type": "application/json"
}, method='POST')
response = urlopen(req)
print(response.read().decode('utf-8'))
响应状态码错误排查:从新手到专家的进阶
抓取网页时,遇到错误状态码是家常便饭。很多人看到 403 就慌了,不知道该怎么办。其实,只要掌握了排查思路,这些错误都能解决。下面我把最常见的状态码和处理方法整理给你。
403 Forbidden:被拦下了
403 是最常见的反爬拦截信号。服务器认出你不是正常用户,拒绝给你数据。
排查步骤:
- 检查 User-Agent:这是最基本的。确保你设置了看起来像浏览器的 UA。
- 添加 Referer:有些网站会检查请求来源,模拟一个合法的 Referer 可能有用。
- 使用 Cookie:如果目标网站需要登录或追踪会话,你可能需要带上 Cookie。可以用
requests.Session()来自动管理 Cookie。 - 降低请求频率:如果你短时间内发了太多请求,服务器可能把你临时封禁。加个
time.sleep()休息一下。 - 使用代理 IP:如果 IP 被封了,换个代理试试。
import requests
import time
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://example.com/"
})
for i in range(5):
try:
response = session.get("https://example.com/page")
if response.status_code == 200:
print(f"第 {i+1} 次请求成功")
# 处理数据...
elif response.status_code == 403:
print("被 403 拦截,等待 5 秒后重试...")
time.sleep(5)
else:
print(f"其他状态码: {response.status_code}")
except Exception as e:
print(f"请求异常: {e}")
time.sleep(3)
404 Not Found:资源不存在
404 通常意味着 URL 错了。可能是链接拼写错误、页面被删除、或者动态加载的内容 URL 发生了变化。
排查步骤:
- 仔细检查 URL:确认链接是否正确,有没有多余的字符。
- 查看网页源码:有时候数据是通过 JavaScript 动态加载的,直接抓 HTML 是拿不到的,需要用浏览器开发者工具找到真正的数据接口。
- 尝试不同的参数组合:有些 API 需要特定的查询参数,缺了就可能 404。
429 Too Many Requests:请求太频繁
429 是服务器在说“你太急躁了,慢点来”。这是限流机制在起作用。
排查步骤:
- 增加请求间隔:在每次请求之间加
time.sleep(),比如 2-5 秒。 - 使用指数退避:如果连续被限流,可以逐步增加等待时间。
- 优化代码逻辑:确保你没有在循环里做不必要的重复请求。
import requests
import time
url = "https://api.example.com/data"
for attempt in range(5):
response = requests.get(url)
if response.status_code == 429:
# 指数退避:2^attempt 秒
wait_time = 2 ** attempt
print(f"请求太频繁,等待 {wait_time} 秒...")
time.sleep(wait_time)
else:
# 成功或其他错误,跳出循环
break
500 Internal Server Error:服务器崩了
500 是服务器内部错误,通常不是你的问题。可能是网站后台出了 bug,或者数据库挂了。
排查步骤:
- 稍后再试:服务器错误通常是暂时的,等几分钟再请求可能就好了。
- 检查网站状态:有些网站会有状态页,或者在社交媒体上公告维护信息。
- 联系网站管理员:如果是你经常访问的网站,可以反馈问题。
超时错误:请求太久没响应
除了状态码,requests 还会抛出超时异常。这通常发生在服务器响应慢或者网络不通畅的时候。
import requests
try:
response = requests.get("https://slow-example.com", timeout=5)
except requests.exceptions.Timeout:
print("请求超时了,服务器响应太慢")
except requests.exceptions.ConnectionError:
print("连接错误,可能是网络问题或服务器不可达")
建议始终设置 timeout 参数,否则请求可能会无限期挂起,占用资源。
实战案例:抓取一个新闻网站并处理各种异常
光
