前言
运维和后端日常都绕不开"用程序取数据":调 API、抓页面、做监控探测。这篇把 Python 网络请求从入门到工程化讲一遍。
一、requests:把 HTTP 写成人话
import requests
# GET(永远带 timeout!)
r = requests.get(
"https://httpbin.org/get",
params={"page": 2, "size": 10}, # ?page=2&size=10
headers={"User-Agent": "my-spider/1.0"},
timeout=5,
)
r.status_code # 200
r.json() # 解析响应体
r.text # 文本
r.headers["Content-Type"]
# POST JSON
r = requests.post(
"https://httpbin.org/post",
json={"name": "zy", "level": 5}, # 自动序列化 + Content-Type
timeout=5,
)
# POST 表单 / 文件上传
requests.post(url, data={"k": "v"}, timeout=5)
requests.post(url, files={"f": open("a.png", "rb")}, timeout=5)
# 常见 HTTP 动词
requests.put(url, json={...}); requests.delete(url); requests.head(url)
没有 timeout 的请求 = 随机挂死的脚本。timeout=(3, 10) 分别指连接和读取超时。
二、Session:复用连接
with requests.Session() as s:
s.headers.update({"Authorization": "Bearer xxx"})
for i in range(100):
r = s.get(f"https://api.example.com/items/{i}", timeout=5)
process(r.json())
# TCP 连接复用: 快, 且对目标服务器友好
三、健壮化:重试 + 退避
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=3,
backoff_factor=1, # 退避: 1s, 2s, 4s...
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
)
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retry))
s.mount("http://", HTTPAdapter(max_retries=retry))
try:
r = s.get("https://api.example.com/data", timeout=(3, 10))
r.raise_for_status() # 4xx/5xx 直接抛异常
data = r.json()
except requests.Timeout:
print("超时")
except requests.HTTPError as e:
print(f"HTTP错误: {e.response.status_code}")
except requests.RequestException as e:
print(f"请求失败: {e}")
四、解析 HTML:BeautifulSoup
pip install beautifulsoup4 lxml -i https://mirrors.aliyun.com/pypi/simple/
from bs4 import BeautifulSoup
html = """
<div class="post-list">
<article class="post">
<h2><a href="/posts/1">Linux 权限详解</a></h2>
<span class="date">2023-03-05</span>
<span class="tag">Linux</span>
</article>
<article class="post">
<h2><a href="/posts/2">Python 虚拟环境</a></h2>
<span class="date">2023-03-18</span>
<span class="tag">Python</span>
</article>
</div>
"""
soup = BeautifulSoup(html, "lxml") # lxml 解析器最快
# CSS 选择器(推荐, 和前端知识通用)
for art in soup.select("article.post"):
title = art.select_one("h2 a").get_text(strip=True)
link = art.select_one("h2 a")["href"]
date = art.select_one(".date").get_text()
tag = art.select_one(".tag").get_text()
print(date, tag, title, link)
# 常用 API
soup.find("h2") # 第一个 h2
soup.find_all("span", class_="tag") # 全部匹配
soup.find(id="main")
tag.get_text(), tag["href"], tag.attrs
五、第一个完整爬虫:抓取博客列表
#!/usr/bin/env python3
"""抓取博客文章列表 -> 存 JSON"""
import json, time
import requests
from bs4 import BeautifulSoup
from pathlib import Path
BASE = "https://example-blog.cn"
def fetch_page(session: requests.Session, url: str) -> list[dict]:
r = session.get(url, timeout=10)
r.raise_for_status()
r.encoding = r.apparent_encoding # 自动修正乱码
soup = BeautifulSoup(r.text, "lxml")
items = []
for art in soup.select("article.post"):
items.append({
"title": art.select_one("h2 a").get_text(strip=True),
"url": BASE + art.select_one("h2 a")["href"],
"date": art.select_one(".date").get_text(strip=True),
"tags": [t.get_text() for t in art.select(".tag")],
})
return items
def crawl(max_pages: int = 5) -> list[dict]:
results = []
with requests.Session() as s:
s.headers["User-Agent"] = "Mozilla/5.0 (compatible; zy-crawler/1.0)"
for page in range(1, max_pages + 1):
url = f"{BASE}/page/{page}/"
try:
items = fetch_page(s, url)
except requests.RequestException as e:
print(f"[!] 第{page}页失败: {e}")
continue
if not items:
break # 没有更多了
results.extend(items)
print(f"[+] 第{page}页: {len(items)} 条")
time.sleep(2) # 爬虫礼仪: 别把人家打挂
return results
if __name__ == "__main__":
data = crawl()
Path("posts.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"共抓取 {len(data)} 条, 已存 posts.json")
六、并发提速
# 方案1: 线程池(IO 密集型首选, 简单)
from concurrent.futures import ThreadPoolExecutor, as_completed
urls = [f"https://httpbin.org/delay/1?i={i}" for i in range(20)]
with requests.Session() as s, ThreadPoolExecutor(max_workers=8) as pool:
futures = {pool.submit(s.get, u, timeout=10): u for u in urls}
for f in as_completed(futures):
try:
print(f.result().status_code)
except Exception as e:
print("失败:", futures[f], e)
# 20 个耗时1s的请求: 串行20s -> 并发约3s
# 方案2: httpx 异步(大规模时更省资源)
import httpx, asyncio
async def fetch(client, url):
r = await client.get(url, timeout=10)
return r.status_code
async def main():
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*[
fetch(client, u) for u in urls[:20]
])
print(results)
asyncio.run(main())
七、爬虫礼仪与边界
- 看 robots.txt:
https://site/robots.txt,尊重 Disallow - 限速:
time.sleep或令牌桶,别当 DDoS - UA 标识:别伪装成浏览器干重活
- 优先走官方 API / RSS,别硬爬页面
- 法律边界:不碰个人隐私数据、不绕过登录/付费墙、遵守目标站点条款
八、常见坑
| 现象 | 原因 |
|---|---|
| 乱码 | r.encoding = r.apparent_encoding |
| SSLError | 公司代理证书问题 verify=False(仅测试)或装 certifi |
| 拿到的页面和浏览器不一样 | JS 动态渲染 → 上 playwright/selenium 或找数据接口 |
| 403 被 ban | UA/频率问题;降低频率、换 UA |
| 内存暴涨 | 用流式下载 stream=True + 迭代写盘 |
小结
| 需求 | 工具 |
|---|---|
| 调 API | requests + Session + timeout |
| 要重试 | HTTPAdapter + Retry |
| 并发抓取 | ThreadPoolExecutor / httpx.AsyncClient |
| 解析 HTML | BeautifulSoup + CSS 选择器 |
| 动态页面 | playwright |
本文是「Python」系列第 8 篇。