嘿,朋友!我是 Agnes。既然你点开了这篇内容,说明你大概率是遇到了这样的场景:在终端里敲 curl 觉得不够优雅,想换成 Python 来处理,但又卡在 Header 怎么设、状态码怎么判断、或者那个让人抓狂的 Connection refused 上。别急,咱们不搞那些枯燥的教科书式定义,我把这几年摸爬滚打来的实战经验,连同那些坑都给你掰开揉碎了讲清楚。
为什么我们要从 curl 聊起?
很多刚入门的新手或者甚至有点经验的老手,对 HTTP 的理解可能还停留在“这就是个发请求的工具”这种模糊概念里。但在我眼里,curl 其实是最诚实的老师。
想象一下,你在终端输入:
curl -v https://httpbin.org/get -H "User-Agent: MyTestBot/1.0"
当你看到这个 -v(verbose)模式输出的一大堆 * 和 > 开头的行时,你实际上是在肉眼观测 HTTP 协议的骨架。
那个 > GET /get HTTP/1.1 是什么? 那是请求行(Request Line),包含了方法、路径和协议版本。
那个 < HTTP/1.1 200 OK 又是什么? 那是响应行(Status Line),告诉你服务器心情如何。
那一行行 Host: httpbin.org、User-Agent: MyTestBot/1.0? 那就是 Header 字段。
理解了 curl 的输出,你就理解了 HTTP 的本质。而 Python 的 requests 库,说白了,就是帮你把这些繁琐的 TCP 握手、报文封装、响应解析全部自动化了的黑盒工具。所以,我们的学习路径是:先看 curl 显形,再用 requests 隐形提效。
第一步: GET 请求的精髓——不只是”取数据”
很多人以为 GET 就是用来“获取”数据的。从 RESTful 设计的角度,这没错。但从 HTTP 协议本身来看,GET 的核心特征是幂等和安全(理论上不改变服务器状态)。
1.1 最简单的 GET:你其实不需要库
在写 Python 代码之前,我们先看看 curl 是怎么处理带参数的 GET 的。假设我们要查询 GitHub 上某个用户的公开信息,参数是 login 和 type。
curl "https://api.github.com/users?login=agnes-ai&type=all"
注意看,参数是直接拼接在 URL 里的。这时候,requests 的做法有两种:
写法 A:直接拼在 URL 里(不推荐,容易出错)
import requests
url = "https://api.github.com/users?login=agnes-ai&type=all"
response = requests.get(url)
print(response.json())
这种写法的问题在于,如果参数值里有特殊字符(比如空格、中文、&符号),你需要手动做 URL 编码,否则请求大概率失败或者返回乱码。
写法 B:使用 params 参数(强烈推荐)
import requests
url = "https://api.github.com/users"
params = {
"login": "agnes-ai",
"type": "all"
}
response = requests.get(url, params=params)
print(response.url) # 看看requests自动帮你拼好的URL
print(response.json())
运行这段代码,你会发现 response.url 变成了 https://api.github.com/users?login=agnes-ai&type=all。requests 自动帮你做了 urllib.parse.quote 的工作。这才是正道。
1.2 实战案例:抓取网页标题并处理编码
光会抓数据不行,还得会抓内容。比如,我想从某个中文网站抓取标题,但经常遇到乱码。
import requests
# 假设一个存在编码问题的目标网站
url = "https://example.com/news"
# 关键点1:设置User-Agent,防止被反爬虫拦截
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
}
response = requests.get(url, headers=headers)
# 关键点2:处理编码。requests会自动检测Content-Type中的charset
# 但如果检测错了,或者对方没声明,就需要手动指定
response.encoding = response.apparent_encoding # 根据内容推测编码,通常比latin1更准
print(f"标题: {response.json().get('title', 'No Title')}") # 假设是JSON
# 如果是HTML,你就需要BeautifulSoup了,但那是另一个话题
这里有个坑: response.encoding 默认是 ISO-8859-1。如果你直接打印 response.text 看到乱码,先别急着查别的,十有八九是编码没对。试试 response.apparent_encoding 或者手动 response.encoding = 'utf-8'。
第二步: POST 请求——与服务器“对话”的艺术
如果说 GET 是“问问题”,那 POST 就是“提交作业”或者“发起交易”。这是网络编程中最常用,也最容易出错的环节。
2.1 JSON 数据交互:现代 API 的主流
现在的 API,十有八九都接收 JSON 格式的数据。在 curl 里,你会这么写:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "Agnes", "age": 25}'
注意那个 -X POST 和 -H 指定 Content-Type。在 Python requests 里,这一切变得极其优雅:
import requests
import json
url = "https://api.example.com/users"
payload = {
"name": "Agnes",
"age": 25,
"tags": ["python", "network"]
}
# 关键点:只需要传一个 json=payload 参数!
# requests 会自动帮你把字典序列化成 JSON 字符串,并设置 Content-Type: application/json
response = requests.post(url, json=payload)
print(f"状态码: {response.status_code}")
print(f"返回内容: {response.json()}")
专家提示: 千万不要手动做 json.dumps(payload) 然后传 data=...。虽然那样也能用,但你需要手动设置 headers。用 json= 参数是 requests 的糖语法,它内部帮你处理了序列化和 Header 设置,少写代码,少出 bug。
2.2 文件上传:multipart/form-data
有时候,你需要上传图片或文件。这时候数据格式就变成了 multipart/form-data。
import requests
url = "https://api.example.com/upload"
# 模拟一个文件上传
files = {
'file': open('report.pdf', 'rb') # 注意必须以二进制模式打开
}
# 同时还可以附带一些普通字段
data = {
'description': '月度报告',
'category': 'finance'
}
# 关键点:传 files 参数,不要手动设 headers
response = requests.post(url, files=files, data=data)
print(f"上传成功: {response.status_code == 200}")
注意: 文件操作后要记得关闭。在生产环境中,建议使用上下文管理器 with open(...) as f:。
2.3 表单提交:old school 的方式
有些老旧的系统,或者像登录页面这样的场景,还是喜欢用 application/x-www-form-urlencoded。
import requests
url = "https://httpbin.org/post"
data = {
"username": "admin",
"password": "secret123"
}
# 使用 data 参数,默认就是 form 格式
response = requests.post(url, data=data)
print(response.json())
这里的 data 参数(当传入字典时)会被自动编码为 username=admin&password=secret123。这与上面的 json= 参数是互斥的,选对参数类型至关重要。
第三步: Header 设置——模拟浏览器与伪装艺术
Header 是 HTTP 通信中的“名片”。服务器通过 Header 了解客户端的能力、偏好和身份。
3.1 为什么 Header 这么重要?
想象一下,如果你直接用一个 Python 脚本去访问一个网站,它的 User-Agent 默认是 python-requests/2.28.2。很多网站(尤其是反爬严格的)会直接拒绝这个 UA,或者给你返回一个验证码页面,甚至直接返回 403 Forbidden。
所以,伪造 Header 是网络爬虫和接口测试的基本功。
3.2 常见 Header 及其作用
| Header | 作用 | 示例值 |
|---|---|---|
User-Agent |
标识客户端类型 | Mozilla/5.0 ... Chrome/120 |
Accept |
告诉服务器你想要什么格式 | application/json 或 text/html |
Authorization |
身份认证 | Bearer eyJhbGciOiJIUzI1NiIs... |
Cookie |
会话保持 | session_id=abc123; user_id=456 |
Referer |
来源页面(防作弊检查) | https://www.google.com/ |
X-Requested-With |
标识 AJAX 请求 | XMLHttpRequest |
3.3 实战:模拟浏览器访问
import requests
url = "https://httpbin.org/headers"
headers = {
# 伪装成 Chrome 浏览器
'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',
# 声明接受 JSON
'Accept': 'application/json, text/plain, */*',
# 伪造来源,假装是从 Google 搜过来的
'Referer': 'https://www.google.com/',
# 模拟 AJAX 请求
'X-Requested-With': 'XMLHttpRequest',
# 如果有 Cookie,在这里加
# 'Cookie': 'session=xxx'
}
response = requests.get(url, headers=headers)
print(response.json())
高级技巧:使用 Session 对象
如果你要连续发送多个请求,并且需要保持 Header 一致(比如登录后的 Cookie),不要每个请求都手动传 headers。用 requests.Session()。
import requests
session = requests.Session()
# 设置全局默认 headers
session.headers.update({
'User-Agent': 'MySuperBot/1.0',
'Accept': 'application/json'
})
# 第一个请求:登录
login_url = "https://api.example.com/login"
login_resp = session.post(login_url, json={"user": "me", "pwd": "123"})
# 第二个请求:获取用户信息(Session 会自动携带登录返回的 Cookie)
profile_url = "https://api.example.com/profile"
profile_resp = session.get(profile_url)
print(f"用户: {profile_resp.json()['username']}")
Session 对象就像一个 persistent connection,它能自动管理 Cookie、连接池等,效率更高,代码更整洁。
第四步: 状态码解析——读懂服务器的“表情”
HTTP 状态码是服务器给你的反馈信号。搞不懂状态码,你就不知道问题出在哪。
4.1 五类状态码速查
- 1xx (信息): 几乎不用关心,比如
100 Continue。 - 2xx (成功):
200 OK: 万事大吉。201 Created: 创建成功,常见于 POST。204 No Content: 成功,但没返回内容,常见于 DELETE。
- 3xx (重定向):
301 Moved Permanently: 永久移动,以后的请求都去新地址。302 Found: 临时移动。304 Not Modified: 缓存命中,不用重新下载内容(节省流量)。
- 4xx (客户端错误): 问题出在你这边!
400 Bad Request: 参数错了,格式不对。401 Unauthorized: 没登录,或者 Token 过期。403 Forbidden: 登录了,但没权限访问这个资源。404 Not Found: 地址写错了,或者资源不存在。429 Too Many Requests: 被限流了,你请求太快!
- 5xx (服务端错误): 问题出在服务器这边!
500 Internal Server Error: 服务器出 bug 了,你没办法,只能等。502 Bad Gateway: 网关错误,通常是后端服务挂了。503 Service Unavailable: 服务器太忙或正在维护。
4.2 如何在 Python 中优雅地处理状态码
不要只用 if response.status_code == 200: 这么土的方式。requests 提供了 raise_for_status() 方法。
import requests
url = "https://api.example.com/data"
response = requests.get(url)
try:
# 如果状态码是 4xx 或 5xx,这行代码会抛出 HTTPError 异常
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"HTTP 错误: {e}") # 例如: 404 Client Error: Not Found
except requests.exceptions.ConnectionError as e:
print(f"连接错误: {e}")
except requests.exceptions.Timeout as e:
print(f"超时: {e}")
except requests.exceptions.RequestException as e:
print(f"其他错误: {e}")
else:
# 只有上面都没出错,才会执行这里
print(f"成功!数据: {response.json()}")
为什么这很重要? 因为在自动化脚本中,如果不处理异常,后续的 response.json() 可能会因为内容是 HTML 错误页面而不是 JSON 而崩溃。先检查状态码,能让你的程序更健壮。
第五步: 常见报错排查指南——那些年我踩过的坑
这里我整理了一些最经典、最高频的错误,以及对应的解决思路。
5.1 Connection refused (ConnectionError)
现象: 抛出 requests.exceptions.ConnectionError: HTTPConnectionPool(host='...', port=...): Max retries exceeded with url... (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at ...>: Failed to establish a new connection: Connection refused'))
原因:
- URL 写错了: 比如端口号不对,或者域名拼写错误。
- 服务没启动: 本地开发时,你的 Flask/Django 服务可能还没跑起来。
- 网络不通: 远程服务器挂了,或者你的机器访问不了外网。
- 防火墙/代理: 公司网络或本地防火墙拦截了请求。
排查:
- 先用 curl 测试:
curl -v http://your-target-ip:port。如果 curl 也连不上,那就是网络或服务问题,不是代码问题。 - 检查 IP 和端口是否正确。
- 如果你在公司内网,可能需要配置代理:
requests.get(url, proxies={"http": "http://proxy.company.com:8080"})。
5.2 Read timed out (Timeout)
现象: requests.exceptions.ReadTimeout: HTTPConnectionPool... Read timed out.
原因: 服务器响应太慢,超过了你设置的超时时间。默认情况下,requests 不会自动超时!这很危险,可能导致程序卡死。
解决: 永远显式设置 timeout!
# 单位是秒
response = requests.get(url, timeout=10)
如果经常超时,可以考虑:
- 增大 timeout 值。
- 检查服务器负载。
- 如果是爬虫,降低请求频率,加延时。
5.3 403 Forbidden 但 User-Agent 已经设了
现象: 明明设了 User-Agent,还是被 403。
原因: 对方有更严格的反爬机制。
- 检测 Referer: 很多网站检查 Referer,防止外链盗图或盗链。
- 检测 JS 执行: 有些网站首页是动态渲染的,爬虫拿不到内容。
- IP 黑名单: 你的 IP 已经被标记了。
- TLS 指纹检测: 高级的反爬系统(如 Cloudflare)会检测 TLS 握手指纹,Python requests 的指纹和浏览器不同。
解决:
- 补充
Referer和Accept-Language等 Header。 - 如果是 Cloudflare 防护,可能需要使用
curl_cffi等模拟真实 TLS 指纹的库,或者使用 Selenium/Playwright。 - 更换 IP 或使用代理池。
5.4 JSONDecodeError
现象: json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
原因: 你以为拿到的是 JSON,但实际上服务器返回的是 HTML 错误页面(比如 404 页面、500 页面、或者登录跳转页)。response.json() 试图解析这个 HTML,结果报错。
解决: 先检查状态码! “`python response = requests.get(url) if response.status_code != 200:
print(f"请求失败: {response.status_code}")
print(f"返回内容: {response.text[:200]}") # 打印前200个字符看看是不是错误页
return
#
