当前位置: 首页 > news >正文

避坑指南: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

常见的错误是:

  1. 误判总数:接口返回的总数(displayNumtotalNum)可能不准确,或者包含了无关的条目。不要完全依赖这个数字来决定循环次数。
  2. 循环条件错误:使用for i in range(total // rn)这样的计算,如果总数不能被rn整除,最后一页的数据就会被遗漏。
  3. 接口终止信号:当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=“下载进度“): # 下载图片

爬虫写到最后,稳定性和可维护性往往比极限速度更重要。处理好异常,做好日志记录,设计好重试机制,你的爬虫才能长时间可靠地工作。我在最初版本因为一个连接超时没处理,导致脚本半夜就挂了,早上起来一看只下了几十张。加上完善的错误处理和日志后,它就能安稳地跑完上万张的任务了。

http://www.jsqmd.com/news/450552/

相关文章:

  • 用Visual Studio打造蚂蚁世界:有限状态机(FSM)游戏AI实战教程
  • Flutter 三方库 fennec 的鸿蒙化适配指南 - 掌控服务端框架资产、精密 Web 治理实战、鸿蒙级全栈专家
  • Cannot Load Flash Programming Algorithm!
  • 3步解决多语言字体兼容难题:Warcraft Font Merger的跨平台解决方案
  • 解锁企业级流程自动化:Flowable工作流引擎3大核心应用场景与实践指南
  • 颠覆传统的3大技术突破:猫抓Cat-Catch网页视频提取全解析
  • Vue3+Element Plus组合拳:手把手教你实现路由离开确认弹窗(含完整代码)
  • 颠覆GUI开发:3步实现Python界面零代码构建
  • 索尼Xperia设备修复与优化工具:Flashtool全方位技术指南
  • CVPR‘26 FastGS 开源!3DGS训练的全能加速器,覆盖静态/动态/表面/大场景/稀疏视角/SLAM六大重建任务!
  • OpCore-Simplify:黑苹果EFI配置自动化流程全解析
  • Rust新手必看:从零开始搭建开发环境到RustRover配置(附常见问题解决)
  • ESP32智能语音助手开发指南:从部署到定制的全流程实践
  • OpCore Simplify:零门槛构建稳定Hackintosh系统的完整指南
  • Ubuntu新手必看:3秒切换图形界面与命令行的隐藏快捷键(附常见登录问题解决)
  • Three.js新手必看:AxesHelper坐标轴辅助器的5个实用技巧
  • 智能EFI构建:OpCore-Simplify自动化黑苹果配置的创新方法
  • 2026油田除砂器优质产品推荐指南 助力精准选型 - 优质品牌商家
  • 拆解OSTrack的Attention魔法:用可视化工具透视Transformer如何锁定运动目标
  • Qwen-Image-2512-Pixel-Art-LoRA部署教程:开源大模型+低秩适应(LoRA)技术落地范本
  • BERT模型配置实战:手把手教你调整参数优化性能(附代码示例)
  • AI系统灾备监控:架构师必用的5款监控工具
  • 如何用Decky Loader实现Steam Deck的5种潜能扩展?
  • ANSYS Autodyn实战:如何用爆炸模拟优化你的汽车安全设计(附案例)
  • 【C/C++】自定义类型:结构体
  • OpCore Simplify:革新Hackintosh体验的智能配置引擎
  • 大模型知识梳理(持续更新)
  • 2026搪瓷拼装罐优质厂家推荐榜适配乳制品场景:海水淡化搪瓷拼装罐/海水淡化环氧拼装罐/消防水搪瓷储罐/选择指南 - 优质品牌商家
  • [C++]std::map用法
  • JFlash实战:如何快速烧录HEX/BIN文件到STM32(附自动运行配置技巧)