避坑指南:Python爬取百度图片时常见的5个错误及解决方法
避坑指南:Python爬取百度图片时常见的5个错误及解决方法
最近在帮一个做设计的朋友批量找素材,他需要根据不同的主题词收集大量图片。我第一个想到的自然是百度图片,毕竟资源库够大。于是,我顺手写了个Python脚本,本以为分分钟搞定,结果却踩了一连串的坑。从请求被拒到图片乱码,再到莫名其妙地只下载了几十张就停了,整个过程简直像在玩扫雷。如果你也正打算用Python从百度图片抓取点东西,那我这一路踩过来的坑,或许能帮你省下不少折腾的时间。这篇文章,我就把这些常见的“雷区”和我的“排雷”经验,系统地梳理给你。
1. 请求被拒:破解反爬机制的头道关卡
刚开始写爬虫,最容易碰到的就是请求直接被拒绝,返回一堆错误码,或者干脆没有数据。百度图片虽然不像一些电商网站那样有极其复杂的验证,但其基础的反爬措施也足够让新手头疼一阵子。
首先,一个最容易被忽视的细节是请求头(Headers)的模拟。 很多教程会告诉你加个 User-Agent 就行,但实际远不止如此。百度的服务器会检查请求的完整性。如果你只用了一个简单的、来自 requests 库的默认 User-Agent,它很可能被识别为爬虫。
一个更真实的请求头应该包含多个字段。下面是我调整后稳定可用的一个示例:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 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',
'Referer': 'https://image.baidu.com/',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
提示:
Referer字段非常重要,它告诉服务器请求是从哪个页面发起的。对于图片搜索这类场景,将其设置为百度图片的首页或搜索页域名,能显著提高请求的通过率。
其次,请求频率控制是另一个关键。即使你的请求头伪装得很好,如果以机器般的速度连续发送请求,IP地址也很快会被暂时限制。一个简单有效的策略是在请求之间加入随机延时。
import time
import random
def request_with_delay(url, headers):
response = requests.get(url, headers=headers)
# 在每次请求后,随机等待1到3秒
time.sleep(random.uniform(1, 3))
return response
对于需要大量抓取的情况,仅仅延时可能不够。你可以考虑以下进阶策略:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| IP代理池 | 使用多个代理IP轮换发送请求,避免单一IP被封。 | 大规模、长时间抓取任务。 |
| 会话保持 | 使用 requests.Session() 对象,它可以自动管理cookies,使一系列请求看起来更像一个完整的浏览器会话。 |
需要维持登录状态或应对基于会话的反爬。 |
| 降低并发 | 即使使用多线程/异步,也要严格控制并发数量,例如同时只进行3-5个请求。 | 任何并发爬取场景。 |
最后,留意一下百度图片接口URL的构造。它的参数(如 pn, rn, gsm 等)可能会变化,且某些参数值(如 gsm)看起来像随机数或加密值。如果直接复制别人代码里的固定URL,可能很快失效。最稳妥的方式是实时打开浏览器开发者工具(F12),在“网络”(Network)选项卡中筛选XHR或Fetch请求,找到最新的图片数据接口,观察其URL规律。 通常,pn 参数代表偏移量(第几张开始),rn 代表返回数量。
2. 数据解析陷阱:JSON格式与正则的抉择
成功拿到接口返回的数据后,下一步就是解析出图片的真实URL。这里常见的坑有两个:一是误判了数据格式,二是使用了脆弱的解析方法。
百度图片的搜索接口(/search/acjson)返回的数据通常是 JSONP 或纯 JSON 格式。早期可能是JSONP(即数据包裹在一个函数调用里),现在更常见的是纯JSON。你需要先确认你拿到的是什么。
- 如果是JSONP:响应内容可能像
callbackFunction({...data...})。你需要先剥离外层的函数调用,再解析内部的JSON对象。 - 如果是纯JSON:直接使用
response.json()方法即可。
一个健壮的解析流程应该是这样的:
import requests
import json
url = ‘你找到的接口URL‘
response = requests.get(url, headers=headers)
content = response.text
# 尝试判断是否为JSONP
if content.startswith(‘callback‘) or content.startswith(‘jQuery‘):
# 找到第一个左括号和最后一个右括号的位置
json_start = content.find(‘{‘)
json_end = content.rfind(‘}‘) + 1
json_str = content[json_start:json_end]
data = json.loads(json_str)
else:
# 尝试直接解析为JSON
try:
data = response.json()
except json.JSONDecodeError:
print(“响应不是有效的JSON格式,请检查URL和请求头。“)
data = None
if data and ‘data‘ in data:
for item in data[‘data‘]:
if item and ‘thumbURL‘ in item: # 也可能是 ‘middleURL‘, ‘hoverURL‘等
image_url = item[‘thumbURL‘]
# 处理图片URL
强烈不建议在解析JSON结构稳定的数据时,首选正则表达式(regex)。 虽然正则很强大,但对于嵌套的、结构可能变化的JSON来说,正则表达式写的规则非常脆弱,页面结构或字段名稍有变动,解析就会失败。使用 json 库是更标准、更可靠的做法。
注意:即使使用JSON解析,也要注意数据可能为空或字段缺失的情况。务必在代码中添加判断(如上面的
if item and ‘thumbURL‘ in item:),避免因个别数据项异常导致整个程序崩溃。
3. 编码与存储乱象:从URL到本地文件的完整链路
当你千辛万苦提取出图片的URL,并准备下载时,新的问题可能出现在编码和文件存储环节。
首先是URL编码问题。 你的搜索关键词可能是中文,如“风景”。这个关键词需要被正确地编码后拼接到请求URL中。urllib.parse 库里的 quote 函数就是干这个的。
from urllib import parse
keyword = “自然风景“
encoded_keyword = parse.quote(keyword) # 输出:%E8%87%AA%E7%84%B6%E9%A3%8E%E6%99%AF
如果忘记编码,服务器可能无法识别你的请求,或者返回错误结果。
其次是图片存储时的文件名冲突和非法字符问题。 图片URL可能很长,或者包含一些不能作为文件名的字符(如 /, ?, : 等)。直接截取URL的一部分作为文件名,很容易导致重复或写入失败。
我的建议是,使用内容哈希或时间戳来生成唯一文件名,这样既避免了冲突,也屏蔽了非法字符。
import hashlib
import os
def save_image(image_content, save_dir, keyword):
# 为图片内容生成MD5哈希值作为文件名
file_hash = hashlib.md5(image_content).hexdigest()
# 可以结合关键词组织目录
filename = f“{file_hash}.jpg“
filepath = os.path.join(save_dir, keyword, filename)
# 确保目录存在
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, ‘wb‘) as f:
f.write(image_content)
print(f“图片已保存:{filepath}“)
此外,在保存大量图片时,要考虑到文件系统对单个文件夹内文件数量的限制(虽然现在很少见),以及目录结构清晰的需求。可以按关键词、日期等建立子文件夹进行分类存储。
4. 数量不对与循环终止:深入理解分页逻辑
你有没有遇到过这种情况:脚本运行得很顺利,但最终下载的图片数量远少于搜索结果显示的总数,或者循环莫名其妙提前结束了?这多半是分页逻辑处理有误。
百度图片的接口通常是“懒加载”或分页加载的。关键参数是 pn(可以理解为 offset)和 rn(page size,通常是30)。假设你要爬取150张图片,rn=30,那么你需要循环请求 pn=0, 30, 60, 90, 120。
常见的错误是:
- 误判总数:接口返回的总数(
displayNum或totalNum)可能不准确,或者包含了无关的条目。不要完全依赖这个数字来决定循环次数。 - 循环条件错误:使用
for i in range(total // rn)这样的计算,如果总数不能被rn整除,最后一页的数据就会被遗漏。 - 接口终止信号:当
pn超出实际范围时,接口可能返回空数据或状态码错误,但你的循环可能还在继续。
一个更健壮的分页循环逻辑应该这样写:
import requests
base_url = “https://image.baidu.com/search/acjson?tn=resultjson_com&word={}&pn={}&rn=30“
keyword = “猫“
encoded_keyword = parse.quote(keyword)
headers = { ... } # 你的请求头
pn = 0
rn = 30
image_count = 0
max_images = 200 # 设置一个最大抓取限制,防止无限循环
while image_count < max_images:
url = base_url.format(encoded_keyword, pn)
try:
resp = requests.get(url, headers=headers, timeout=10)
data = resp.json()
except Exception as e:
print(f“请求失败: {e}“)
break # 或加入重试机制
items = data.get(‘data‘, [])
if not items:
print(“没有更多数据,终止爬取。“)
break
# 过滤掉无效条目(有时最后一个条目是空的)
valid_items = [item for item in items if item and item.get(‘thumbURL‘)]
if not valid_items:
print(“当前页无有效图片数据,终止爬取。“)
break
for item in valid_items:
img_url = item[‘thumbURL‘]
# 下载并保存图片
# download_image(img_url, save_path)
image_count += 1
print(f“已下载第 {image_count} 张图片“)
pn += rn # 翻到下一页
time.sleep(random.uniform(1, 2)) # 礼貌性延时
这种“基于响应内容驱动”的循环,比单纯依赖一个预设的总数要可靠得多。
5. 效率瓶颈与资源管理:从能跑到跑得好
当你的爬虫能稳定运行后,下一个要面对的问题就是效率。同步请求、单线程下载几百上千张图片会非常慢。同时,不加以管理的网络请求和文件操作也可能耗尽系统资源。
提升效率最直接的方法是引入并发。 Python的 concurrent.futures 库中的 ThreadPoolExecutor 是一个简单易用的选择,它适合I/O密集型任务(如下载图片)。
from concurrent.futures import ThreadPoolExecutor, as_completed
def download_single_image(img_info):
url, save_path = img_info
try:
content = requests.get(url, timeout=15).content
with open(save_path, ‘wb‘) as f:
f.write(content)
return True, save_path
except Exception as e:
return False, f“{save_path}: {e}“
# 假设 image_url_list 是包含(url, filepath)元组的列表
image_url_list = [(url1, path1), (url2, path2), ...]
# 使用线程池,max_workers控制并发数,不宜过大(如5-10)
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_url = {executor.submit(download_single_image, img_info): img_info for img_info in image_url_list}
for future in as_completed(future_to_url):
success, result = future.result()
if success:
print(f“下载成功: {result}“)
else:
print(f“下载失败: {result}“)
提示:并发数(
max_workers)不是越大越好。设置过高会加重本地网络和CPU的调度负担,也可能触发目标服务器的反爬机制。建议从较小的数字(如3)开始测试,逐步增加。
资源管理同样重要:
- 设置超时:每个网络请求都应设置连接超时和读取超时,避免因某个请求卡住而阻塞整个程序。
response = requests.get(url, headers=headers, timeout=(5, 15)) # (连接超时, 读取超时) - 异常处理:将下载、保存等可能失败的操作用
try...except包裹,记录错误日志,而不是让整个程序崩溃。 - 管理会话:在并发场景下,考虑为每个线程创建独立的
requests.Session,或者使用全局会话但注意线程安全。
最后,别忘了给你的爬虫加上一些运行状态反馈,比如进度条。这不仅能让你知道进展,也能在出现问题时帮你快速定位。可以使用 tqdm 库轻松实现。
pip install tqdm
from tqdm import tqdm
# 在循环或并发任务外包裹tqdm
for img_url in tqdm(image_urls, desc=“下载进度“):
# 下载图片
爬虫写到最后,稳定性和可维护性往往比极限速度更重要。处理好异常,做好日志记录,设计好重试机制,你的爬虫才能长时间可靠地工作。我在最初版本因为一个连接超时没处理,导致脚本半夜就挂了,早上起来一看只下了几十张。加上完善的错误处理和日志后,它就能安稳地跑完上万张的任务了。
更多推荐



所有评论(0)