记得大三那年,我第一次想抓取某论坛的帖子做数据分析,代码写得那叫一个行云流水,结果跑出来一片红。控制台里那一排排 404 Not Found 和 Connection aborted 像极了我当时破碎的自尊心。那时候我才意识到,HTTP协议这几个字,课本上两页纸轻飘飘带过,现实里却是千军万马拦路虎。
今天咱们不聊虚的,我就把自己踩过的坑、熬过的夜,连同最后那几行能跑通的代码,掰开了揉碎了讲给你听。不管你是刚入门的小白,还是卡在某个Bug里怀疑人生的同学,这篇文应该能帮你把“从404到200”这条路铺平。
为什么你的请求总是404?身份伪装的艺术
刚开始写爬虫,我最 naive 的想法是:直接 requests.get(url) 不就完了吗?
确实,对百度首页来说,这就完了。但对于任何一个稍微有点反爬意识的网站,你这么做,简直就是穿着睡衣去参加黑帮晚宴——太显眼了。
默认情况下,Python 的 requests 库发出去的 Header 长这样:
import requests
response = requests.get('https://example.com')
print(response.headers['Server'])
# 很多网站会识别出你是 python-requests,直接拒绝或者返回404
404 不一定代表页面不存在,很多时候代表“你不配看”。
要解决这个问题,你得学会“伪装”。真正的浏览器发出去的请求,Header 里带着 User-Agent、Accept、Referer 甚至 Cookie。你得把这些字段补全,让服务器误以为你是 Chrome 或 Firefox。
下面这段代码,是我个人非常喜欢的“裸考”伪装模板,你可以直接复用:
import requests
url = 'https://httpbin.org/get' # 用这个测试网站可以直接看到它接收到的请求信息
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/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Cache-Control': 'max-age=0',
}
response = requests.get(url, headers=headers)
print(response.status_code)
# 输出 200,说明伪装成功,服务器把你当正常人对待了
这里有个小技巧:你可以先在浏览器里打开目标网站,按 F12 打开开发者工具,选中 Network 标签,刷新页面,然后点击任意一个请求,查看 Request Headers。把里面最关键的几项抄下来,就是你的完美伪装。
超时问题:别让程序卡在半空死等
第二个大坑,就是超时。
有时候网络慢,有时候目标服务器抽风,你的爬虫发出去请求,石沉大海。默认情况下,requests.get() 会一直挂着,直到天荒地老。如果你的脚本要抓几千个页面,几个卡死,整个任务就废了。
我在生产环境里,从来不信任默认超时。我会明确设置 timeout 参数,它是一个元组:(连接超时, 读取超时)。
import requests
import time
url = 'https://httpbin.org/delay/5' # 故意延迟5秒返回
try:
# 连接超时3秒,读取超时5秒
# 如果3秒内连不上,或者5秒内没收到数据,就抛出异常
response = requests.get(url, timeout=(3, 5))
print(response.text)
except requests.exceptions.Timeout:
print("请求超时了,别急,换个姿势再来")
except requests.exceptions.ConnectionError:
print("连接错误,可能是网络问题或者目标挂了")
except requests.exceptions.HTTPError as e:
print(f"HTTP错误: {e}")
except requests.exceptions.RequestException as e:
print(f"其他错误: {e}")
为什么要用元组? 想象一下:你先打电话(建立连接),然后对方开始说话(传输数据)。如果对方一直没接电话,你等30秒太久了,所以连接超时设短点(比如3秒)。如果电话接通了,但对方半天不说话,你可以多等会儿(比如10秒)。这样设置,既灵活又高效。
还有一个进阶技巧:重试机制。
网络抖动是常态,一次失败不代表永久失败。我会写一个简单的重试装饰器,让它自动重试几次:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_with_retry(url, retries=3, backoff_factor=0.3):
session = requests.Session()
# 配置重试策略
# status_forcelist 指定哪些状态码触发重试(比如500, 502, 503)
# allowed_methods 指定哪些方法可重试(GET, POST等)
retry_strategy = Retry(
total=retries,
backoff_factor=backoff_factor, # 重试间隔:0.3s, 0.6s, 1.2s...
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
try:
response = session.get(url, headers=headers, timeout=(3, 5))
response.raise_for_status() # 如果是4xx或5xx,抛出HTTPError
return response
except requests.exceptions.RequestException as e:
print(f"重试{retries}次后仍失败: {e}")
return None
finally:
session.close()
# 使用示例
resp = get_with_retry('https://example.com/api/data')
if resp:
print("终于拿到数据了!")
这段代码看起来有点长,但它可是爬虫稳定性的保障。当你面对一个不稳定的服务器时,这个“不死小强”式的重试机制,能帮你捞回大部分数据。
重定向迷宫:如何追踪200的最终目标
第三个常见困扰:重定向。
你输入 http://baidu.com,浏览器会自动跳转到 https://www.baidu.com/。这个过程叫重定向,HTTP 状态码是 301 或 302。
requests 默认是自动跟随重定向的(allow_redirects=True)。这通常没问题,但有时候它会出问题:
- 跳转圈:A跳到B,B跳回A,死循环。
- 安全策略:从 HTTP 跳到 HTTPS,或者跨域跳转,可能被中间件拦截。
- 登录态丢失:重定向过程中,Cookie 没带过去,导致跳转到登录页。
为了彻底搞懂重定向,我们手动关闭自动跳转,自己来追踪:
import requests
url = 'http://httpbin.org/redirect/3' # 会重定向3次
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
# 禁止自动跟随重定向
response = requests.get(url, headers=headers, allow_redirects=False)
print(f"初始状态码: {response.status_code}")
print(f"初始位置: {response.url}")
history = []
while response.status_code in [301, 302, 303, 307, 308]:
history.append({
'from': response.url,
'to': response.headers['Location'],
'status': response.status_code
})
print(f"302跳转: {response.headers['Location']}")
# 手动请求下一个位置
# 注意:有些重定向会改变方法,比如POST变GET,这里简化处理只处理GET
response = requests.get(response.headers['Location'], headers=headers, allow_redirects=False)
print(f"\n最终状态码: {response.status_code}")
print(f"最终URL: {response.url}")
print(f"跳转历史: {history}")
为什么我们要手动处理? 因为在实际爬虫中,你经常需要知道“我到底跳了几次”、“我最终去了哪个域名”。这些信息对于日志记录、反反爬分析、以及避免被重定向到恶意页面都非常重要。
另外,还有一种情况:相对路径重定向。
有些服务器返回的 Location 头是 /new-path,而不是完整的 URL。这时候你需要用 urljoin 来拼接:
from urllib.parse import urljoin
base_url = 'https://example.com'
relative_location = '/api/v2/data'
full_url = urljoin(base_url, relative_location)
# 结果: https://example.com/api/v2/data
这一步如果不做,你的爬虫会在拼接 URL 时跑偏,最后抓回来的全是 404。
整合:一个健壮的爬虫请求模板
把上面讲的所有坑都填上,我为你准备了一个“万能请求模板”。这个模板集成了伪装、超时控制、重试机制、以及重定向追踪。你可以把它存成一个模块,以后每次爬虫直接调用:
import requests
import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from urllib.parse import urljoin
class RobustSpider:
def __init__(self, default_timeout=(5, 10), max_retries=3):
self.session = requests.Session()
self.default_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,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
}
self.timeout = default_timeout
self.max_retries = max_retries
# 配置重试策略
retry_strategy = Retry(
total=self.max_retries,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def get(self, url, headers=None, params=None, follow_redirects=True):
"""
发送GET请求,包含完整的错误处理和重试逻辑
"""
# 合并Headers
request_headers = self.default_headers.copy()
if headers:
request_headers.update(headers)
try:
# 如果是相对路径,自动补全
if url.startswith('/'):
# 假设基准域名从第一个请求的URL获取,这里简化处理
# 实际应用中可能需要维护base_url
pass
response = self.session.get(
url,
headers=request_headers,
params=params,
timeout=self.timeout,
allow_redirects=follow_redirects
)
# 检查HTTP错误
response.raise_for_status()
# 如果是手动追踪重定向,这里可以返回history
if not follow_redirects and response.history:
print(f"页面重定向历史: {[r.url for r in response.history]}")
return response
except requests.exceptions.Timeout:
print(f"请求超时: {url}")
except requests.exceptions.ConnectionError:
print(f"连接错误: {url}")
except requests.exceptions.HTTPError as e:
print(f"HTTP错误 {e.response.status_code}: {url}")
except requests.exceptions.TooManyRedirects:
print(f"重定向次数过多: {url}")
except Exception as e:
print(f"未知错误 {url}: {e}")
return None
def close(self):
self.session.close()
# === 使用示例 ===
if __name__ == "__main__":
spider = RobustSpider()
# 1. 正常请求
url = 'https://httpbin.org/get'
resp = spider.get(url)
if resp:
print(f"成功抓取 {url}, 状态码: {resp.status_code}")
# 2. 带参数的请求
search_url = 'https://httpbin.org/get'
params = {'q': 'python爬虫', 'page': 1}
resp = spider.get(search_url, params=params)
if resp:
print(f"带参数请求成功, URL: {resp.url}")
# 3. 自定义Headers(比如登录后的Cookie)
login_url = 'https://httpbin.org/headers'
custom_headers = {'Cookie': 'session_id=123456'}
resp = spider.get(login_url, headers=custom_headers)
if resp:
print(f"自定义Header请求成功")
spider.close()
写给小朋友的话:这像什么?
如果你还不太理解这些概念,没关系,我给你打个比方。
想象你要去邻居家借书(抓取网页数据)。
- HTTP协议 就是你们之间的对话规则。你得说“你好”,邻居才能回你“你好”。如果你直接闯进去不敲门,邻居就不理你(404)。
- User-Agent(伪装) 就是你穿的衣服。如果你穿得奇形怪状,邻居可能以为你是怪人,就把门关上了。你穿上普通的衣服(浏览器Header),邻居才愿意开门。
- 超时(Timeout) 就是打电话。你拨过去,如果3秒没人接,你就挂掉,换个时间再打。你不能一直傻等,等到天荒地老。
- 重定向(Redirect) 就像你问邻居书在哪,邻居说“不在我这,你去隔壁老王那问”。你就得跑去隔壁。如果隔壁说“去楼下”,你就得再跑。你得知道最终书在哪个房间(最终URL),而不是在半路上晕头转向。
最后的一点心得
爬虫这件事,技术上是门槛,但经验上全是坑。
我见过太多人,代码写得完美无缺,但跑半天没数据。最后发现,是被网站的 IP 频率限制封了,或者是 JavaScript 渲染的页面,requests 根本拿不到数据。
所以,当你解决了 404、超时和重定向这三个基础问题后,记得:
- 检查响应内容:确认拿到的是真正的 HTML,而不是登录页或验证码页。
- 尊重 robots.txt:不要爬禁止爬的地方。
- 控制请求频率:给服务器一点喘息的时间,也给自己留点后路。
希望这篇从 404 到 200 的指南,能帮你少掉几根头发。代码已备好,剩下的,就是你的实战演练了。去吧,抓取属于你的数据!
