嘿,我是 Agnes。既然你找到了这里,说明你不想再看那些枯燥的教科书定义了。你想知道 Python 怎么真正“跑”在互联网上,怎么跟服务器握手、怎么骗过反爬、怎么优雅地拿数据。别担心,这篇东西我写得很实在,没有任何 AI 的味道,只有实战的血泪和经验。咱们不整那些虚的,直接上手。
第一章:别再只知 requests.get 了
很多初学者(包括当年的我)觉得 HTTP 编程就是 import requests,然后 r = requests.get(url)。没错,这是基础,但如果你只学到这一步,遇到稍微有点脾气的网站,你的爬虫就会报错、被封、或者拿到一堆乱码。
HTTP 的本质是什么?就是一个简单的“请求-响应”模型。你发一句“嘿,给我看看这个页面”,服务器回一句“给你,这是 HTML”。但在实际环境中,这个过程充满了博弈。
我们要讲的不仅仅是代码,而是思维。当你敲下那行代码时,你是在模拟一个浏览器,甚至是一个更聪明的机器人。
第二章:爬虫的进阶之路——从静态到动态
2.1 基础请求的艺术
让我们先看看怎么写一个“像人”的请求。很多新手写出这样的代码:
import requests
url = "https://example.com"
response = requests.get(url)
print(response.text)
这段代码能跑,但在生产环境中,它极其脆弱。为什么?因为绝大多数网站会检查 User-Agent。如果你不设置它,服务器会认为你是个脚本,直接拒绝服务或者返回一个空白页。
拟人化请求的核心技巧:
import requests
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',
'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',
}
url = "https://example.com"
# 添加超时设置,防止服务器卡住导致程序无限等待
response = requests.get(url, headers=headers, timeout=10)
# 检查状态码,不要只相信返回的内容
if response.status_code == 200:
# 自动解码,防止乱码
response.encoding = response.apparent_encoding
print("成功获取页面,长度:", len(response.text))
else:
print(f"请求失败,状态码: {response.status_code}")
为什么这里要这么做?
- User-Agent:这是最重要的“身份证”。没有它,很多网站(比如一些技术博客或新闻站)会直接返回 403 Forbidden。
- Timeout:网络是不稳定的。如果不设超时,你的爬虫可能会卡在某个响应慢的服务器上几小时,浪费资源。
- apparent_encoding:有时候服务器返回的
Content-Type头部信息不准,用apparent_encoding可以让 Python 自动检测真实的编码(比如 GBK 或 UTF-8),避免打印出一堆é这样的乱码。
2.2 反爬与应对:cookies 和 session
有些网站需要登录才能看内容,或者它们会追踪你的会话。这时候,裸奔的 requests.get 就完了。你需要 Session。
import requests
# 创建 session,它会自动管理 cookies,就像一个真实的浏览器窗口
session = requests.Session()
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
# 第一步:访问登录页,获取 CSRF token(假设网站有保护)
login_page = session.get("https://example.com/login", headers=headers, timeout=10)
# 这里通常需要解析 HTML 找到 token,为了简化,我们假设已经获取了
csrf_token = "some_fake_token"
# 第二步:提交登录表单
login_data = {
'username': 'my_user',
'password': 'my_pass',
'csrf_token': csrf_token
}
# 使用 POST 方法登录,session 会自动保存返回的 cookies
session.post("https://example.com/login", data=login_data, headers=headers, timeout=10)
# 第三步:访问需要登录才能看的数据
protected_url = "https://example.com/my-private-data"
resp = session.get(protected_url, headers=headers, timeout=10)
if resp.status_code == 200:
print("登录成功,数据如下:")
print(resp.text[:500]) # 只打印前500个字符
else:
print("登录失败或权限不足")
关键点: Session 对象是爬虫的“记忆体”。它会记住你登录后的状态,后续请求会自动带上 cookies,不需要每次手动发送。
2.3 处理动态加载的内容:如果 requests 不够用
很多现代网站(比如淘宝、豆瓣电影)的内容是通过 JavaScript 动态加载的。你用 requests 拿到的 HTML 只是骨架,真正的数据在 ajax 请求之后才渲染出来。
这时候,你有两个选择:
- 抓包分析:用浏览器的开发者工具(F12),找到背后真正的 API 接口,直接模拟接口请求。这是最高效的方法。
- 使用 Selenium 或 Playwright:启动一个真实的浏览器,让 JavaScript 跑完,然后再爬取。
示例:抓包分析找 API
假设你想爬豆瓣电影 Top250。打开浏览器 F12,刷新页面,你会看到类似这样的请求:
import requests
import json
# 豆瓣电影 Top250 的 API 接口(通过抓包获得)
url = "https://movie.douban.com/j/chart/top_list"
params = {
'type': '5', # 电影类型
'interval_id': '100:90',
'start': '0',
'limit': '25'
}
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://movie.douban.com/top250', # 重要:Referer 让服务器知道你是从哪里来的
}
response = requests.get(url, params=params, headers=headers, timeout=10)
data = response.json() # 直接解析 JSON,比解析 HTML 简单多了
for movie in data:
print(f"评分: {movie['rating']}, 电影: {movie['title']}")
为什么这比爬 HTML 好?
- HTML 结构经常变,今天改个 class 名,你的正则表达式就挂了。
- JSON API 结构相对稳定,而且数据清洗成本低,直接用
.json()就能拿到结构化数据。
第三章:接口调用——与第三方服务对话
爬虫是“读”,接口调用是“交互”。现代软件开发中,调用外部 API 是家常便饭。比如支付、短信、AI 接口等。
3.1 调用外部 API 的标准姿势
假设我们要调用一个虚构的 AI 翻译 API。
import requests
import hashlib
import time
import uuid
def call_translation_api(text, api_key, secret_key):
url = "https://api.example.com/translate"
# 构造签名参数(这是很多安全 API 的要求)
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
# 简单的签名算法示例:MD5(api_key + timestamp + nonce + secret_key)
sign_str = f"{api_key}{timestamp}{nonce}{secret_key}"
sign = hashlib.md5(sign_str.encode()).hexdigest()
headers = {
'Content-Type': 'application/json',
'X-App-Key': api_key,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Sign': sign,
}
payload = {
'text': text,
'from': 'zh',
'to': 'en'
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
response.raise_for_status() # 如果状态码是 4xx 或 5xx,抛出异常
return response.json()
except requests.exceptions.HTTPError as http_err:
print(f"HTTP 错误: {http_err}")
except requests.exceptions.ConnectionError as conn_err:
print(f"连接错误: {conn_err}")
except requests.exceptions.Timeout as timeout_err:
print(f"超时: {timeout_err}")
except requests.exceptions.RequestException as err:
print(f"请求出错: {err}")
except Exception as e:
print(f"未知错误: {e}")
return None
# 使用示例
result = call_translation_api("你好,世界!", "my_app_key", "my_secret")
if result:
print("翻译结果:", result.get('translation'))
这里展示了几个关键点:
- 错误处理:网络请求会失败,必须用
try-except捕获。raise_for_status()可以自动检查 HTTP 错误。 - 签名安全:很多 API 要求签名,防止请求被篡改。虽然这个例子很简单,但逻辑是通用的。
- Timeout:调用外部服务时,超时设置尤为重要,避免你的程序被慢服务卡死。
3.2 批量调用与限流
如果你要调用成千上万次 API,不能像无头苍蝇一样乱冲。很多 API 有限流(Rate Limiting),比如每秒最多 10 次请求。
import time
import requests
def batch_translate(texts, api_key, secret_key, delay=0.5):
results = []
for i, text in enumerate(texts):
result = call_translation_api(text, api_key, secret_key)
results.append(result)
# 限流:每次请求后等待一会儿
time.sleep(delay)
# 每隔 10 次打印进度,方便调试
if (i + 1) % 10 == 0:
print(f"已完成 {i+1}/{len(texts)} 条")
return results
为什么需要限流?
- 避免被 API 提供商封禁 IP。
- 体现你的“职业素养”,不要对一个服务发起 DoS 攻击。
第四章:真实案例——一个完整的新闻爬虫系统
让我们把前面学的东西串联起来,做一个真实的新闻爬虫。目标:爬取某个新闻网站的热点新闻,并存储到本地。
4.1 需求分析
- 目标:获取指定页面的新闻标题、链接、发布时间。
- 挑战:网站有反爬,需要模拟浏览器;数据需要清洗。
- 存储:保存到 CSV 文件,方便后续分析。
4.2 完整代码实现
import requests
from bs4 import BeautifulSoup
import csv
import time
import random
from urllib.parse import urljoin
class NewsCrawler:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
self.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',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
}
self.news_list = []
def fetch_page(self, url):
"""获取页面 HTML"""
try:
response = self.session.get(url, headers=self.headers, timeout=15)
response.raise_for_status()
response.encoding = response.apparent_encoding
return response.text
except Exception as e:
print(f"获取页面失败 {url}: {e}")
return None
def parse_news(self, html):
"""解析 HTML,提取新闻信息"""
if not html:
return []
soup = BeautifulSoup(html, 'html.parser')
# 假设新闻列表在 class 为 'news-list' 的 div 中
news_container = soup.find('div', class_='news-list')
if not news_container:
print("未找到新闻容器")
return []
news_items = news_container.find_all('div', class_='item')
parsed_data = []
for item in news_items:
title_tag = item.find('a', class_='title')
time_tag = item.find('span', class_='time')
if title_tag and time_tag:
title = title_tag.get_text(strip=True)
link = urljoin(self.base_url, title_tag.get('href'))
time_str = time_tag.get_text(strip=True)
if title and link:
parsed_data.append({
'title': title,
'link': link,
'time': time_str
})
return parsed_data
def save_to_csv(self, data, filename='news.csv'):
"""保存数据到 CSV"""
if not data:
print("没有数据可保存")
return
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['title', 'link', 'time']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(data)
print(f"成功保存 {len(data)} 条新闻到 {filename}")
def run(self, target_url, max_pages=3):
"""运行爬虫"""
print(f"开始爬取 {target_url},预计抓取 {max_pages} 页...")
for page in range(1, max_pages + 1):
# 构造分页 URL(假设分页逻辑是 ?page=2)
url = f"{target_url}?page={page}" if page > 1 else target_url
print(f"正在抓取第 {page} 页: {url}")
html = self.fetch_page(url)
news = self.parse_news(html)
if news:
self.news_list.extend(news)
print(f" 第 {page} 页抓到 {len(news)} 条新闻")
else:
print(f" 第 {page} 页无新闻或解析失败")
# 随机延迟,模拟人类行为,降低被封风险
delay = random.uniform(1, 3)
time.sleep(delay)
# 去重
unique_news = {news['link']: news for news in self.news_list}.values()
self.news_list = list(unique_news)
# 保存
self.save_to_csv(self.news_list)
print(f"爬取完成,共获取 {len(self.news_list)} 条唯一新闻。")
# 使用示例
if __name__ == "__main__":
# 注意:这是一个示例 URL,实际使用时请替换为合法的、允许爬取的网站
target_site = "https://example-news-site.com"
crawler = NewsCrawler(target_site)
crawler.run(target_site, max_pages=3)
代码亮点解析:
- 面向对象设计:用类封装爬虫,便于扩展和维护。
- Session 复用:减少 TCP 连接开销,同时保持 cookies。
- 解析容错:使用
BeautifulSoup处理 HTML,比正则表达式更健壮。 - 去重:通过链接去重,避免重复抓取。
- 随机延迟:
random.uniform(1, 3)让请求间隔不规律,更难被反爬系统识别。
第五章:给小朋友的科普时间——HTTP 是怎么“打电话”的
想象一下,你要给朋友打电话问问题。
- 你拨号(发送请求):你拿起电话,输入朋友的号码(URL)。电话线接通了。
- 你说话(请求头):你不是直接说“给我答案”,而是先说“你好,我是小明,我想知道今天天气怎么样”。这些礼貌用语和身份信息,就像 HTTP 的 Headers(User-Agent, Accept 等)。
- 朋友接听并回答(响应):朋友说“今天晴天,25度”。这句话就是响应体(Response Body)。同时,朋友可能还递给你一张名片(Cookies),说“下次记得还我”。
- 你挂电话(关闭连接):拿到答案后,你挂断电话。如果你们约定下次继续聊,这通电话就保持“长连接”(Keep-Alive)。
在电脑的世界里:
- URL 就是电话号码。
- Headers 就是你说话时的语气和身份介绍。
- Body 就是你说出来的具体内容。
- Cookies 就是朋友给你的名片,让他能认出你。
为什么有时候朋友不接电话?
- 404:号码是空的,或者朋友搬家了,找不到人。
- 403:朋友在家,但他不想见你,因为你是陌生人(没有正确的 User-Agent 或权限)。
- 500:朋友接了电话,但他脑子一片空白,不知道怎么回答,自己出错了。
- 超时:你打了很久电话,对方一直没接,你就挂了。
通过这个故事,你应该明白,HTTP 编程不是冷冰冰的代码,而是一场场人与服务器之间的“对话”。你要学会“礼貌”(设置正确的 Headers),学会“耐心”(处理超时和重试
