Python爬虫入门:Requests与BeautifulSoup实战百度图片批量下载 1. 项目概述从零到一理解静态网页抓取的核心如果你刚接触编程尤其是Python听到“爬虫”这个词可能会觉得既神秘又有点“黑客”范儿。其实它远没有那么玄乎。简单来说网络爬虫就是一个自动化的程序它模仿我们人类在浏览器里输入网址、浏览网页、复制粘贴内容的动作只不过速度更快、更不知疲倦。我刚开始学爬虫时也以为要掌握多么高深的技术后来发现只要理解了网页的构成和基本的HTTP请求入门真的不难。今天我就以一个最经典的静态网页抓取案例——批量下载百度图片带你走一遍完整的流程。这个案例麻雀虽小五脏俱全涵盖了爬虫最核心的几个步骤发送请求、解析数据、保存结果。通过它你不仅能学会写代码更能理解爬虫背后的工作原理以及在实际操作中会遇到哪些“坑”。为什么选择静态网页作为起点因为它的结构最简单。静态网页就是服务器上已经生成好的HTML文件你每次访问看到的都是一样的内容比如很多新闻网站的文章页、企业官网的介绍页。抓取这类网页就像拿到一份固定格式的文档我们只需要学会如何“阅读”它。与之相对的是动态网页内容由JavaScript在浏览器端实时渲染生成抓取起来会更复杂一些。我们先从静态的练手把基础打牢。本次我们的目标很明确用Python写一个脚本从百度图片搜索特定关键词比如“风景”然后自动下载至少500张图片并以图片的原名保存到本地的imgs文件夹里。听起来是不是很实用接下来我们就一步步拆解如何实现它。2. 核心思路与工具选型为什么是Requests BeautifulSoup在动手写代码之前我们先得把“作战计划”定好。一个爬虫脚本的核心流程通常可以概括为“请求-解析-存储”三步走。对于我们的百度图片下载器思路如下首先模拟浏览器向百度图片搜索页面发送请求并传入我们想搜索的关键词其次从服务器返回的HTML页面代码中找到所有图片的真实地址最后根据这些地址逐个发起请求将图片数据下载下来并保存到本地文件夹。要实现这个思路我们需要选择合适的工具。Python生态里爬虫库非常多对于新手和静态网页抓取这个场景我的选择是Requests库和BeautifulSoup库。这里我解释一下为什么这么选这也是你以后自己做技术选型时需要思考的。Requests库被誉为“HTTP for Humans”意思是给人类用的HTTP库。它的API设计极其简洁优雅用来发送HTTP请求、获取响应内容几乎只需要一两行代码。相比Python内置的urllib模块Requests在易用性和功能上都是碾压级的。我们用它来完成爬虫的第一步“请求”。拿到服务器返回的HTML文档一堆字符串标签后我们需要从中提取出图片链接。直接使用字符串查找比如find不仅麻烦而且极易出错。这时就需要一个HTML解析器。BeautifulSoup简称bs4就是一个强大的HTML/XML解析库它能够将复杂的HTML文档转换成一个复杂的树形结构DOM树然后让你可以像在页面里点选元素一样通过标签名、属性、CSS选择器等轻松地找到你想要的数据。我们用它来完成爬虫的第二步“解析”。至于存储Python内置的os和shutil库就足以应对创建文件夹、保存文件的需求。整个工具链清晰、轻量、学习曲线平缓非常适合入门。注意有些教程可能会提到Scrapy框架。Scrapy确实强大是一个完整的、异步的爬虫框架适合大型、复杂的爬取任务。但对于新手和“下载500张图片”这种一次性任务引入Scrapy就像用高射炮打蚊子学习成本高配置复杂。我们坚持“简单问题简单解”的原则。3. 环境准备与基础库安装工欲善其事必先利其器。在开始写爬虫之前确保你的Python环境已经就绪。我强烈建议使用Python 3.6及以上版本因为新版本对字符串编码、异步等特性的支持更好社区库的兼容性也最强。首先打开你的命令行终端Windows上是CMD或PowerShellmacOS/Linux上是Terminal检查Python和包管理工具pip是否安装正确python --version pip --version如果都能正确显示版本号说明基础环境没问题。接下来安装我们需要的两个核心库。在终端中执行以下两条命令pip install requests pip install beautifulsoup4beautifulsoup4是库在PyPIPython包索引上的正式名称安装后导入时使用from bs4 import BeautifulSoup。这里有个小细节pip install命令默认从国外的源下载速度可能较慢。如果你在国内可以使用清华、阿里云等镜像源来加速例如pip install requests beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple安装过程通常很快看到“Successfully installed”字样就表示成功了。为了编写和运行代码你还需要一个代码编辑器。新手我推荐使用VS Code或PyCharm Community Edition。VS Code轻量、插件丰富配置Python环境也很方便PyCharm则是专业的Python IDE开箱即用对项目管理、调试的支持更完善。选择哪一个看个人喜好它们都能很好地完成工作。最后在你电脑上找一个合适的位置新建一个文件夹比如叫做baidu_image_spider我们的所有代码和下载的图片都会放在这里。用你选择的编辑器打开这个文件夹新建一个Python文件例如download_images.py准备工作就全部完成了。4. 第一步分析网页结构与请求参数写爬虫的第一步永远不是直接写代码而是先“侦察”——用浏览器手动访问目标网站分析它的结构。我们打开百度图片https://image.baidu.com在搜索框输入“风景”并回车。此时浏览器的地址栏URL会变成一长串可能类似这样https://image.baidu.com/search/index?tnbaiduimageipnrct201326592cl2lm-1st-1fmresultfrsf1fmq1234567890_Rpvicnc1zhdlatest©rightse1showtab0fb0widthheightface0istype2ieutf-8sidword风景这一长串就是我们的“突破口”。它告诉我们百度图片搜索是通过GET请求将搜索关键词word风景以及其他一大堆参数传递给服务器的。对于爬虫来说我们最关心的是word参数它决定了搜索内容。其他参数如tn来源、ie编码等为了模拟得更像真实浏览器我们通常也需要一并带上。接下来是关键图片数据在哪里按F12打开浏览器的开发者工具切换到“Network”网络选项卡然后刷新页面。你会看到一大堆请求。找那些类型Type为“img”或者请求地址包含图片格式如.jpg, .png的请求点开一个在“Headers”里可以看到它的真实图片地址Remote Address。但是在列表页搜索结果页的HTML里我们通常看到的并不是这些真实地址而是缩略图的地址或者藏在JavaScript数据中的地址。经过分析这也是爬虫工程师的日常工作百度图片的列表页HTML中图片的真实地址是放在一个叫>img classmain_img img-hover>import requests from bs4 import BeautifulSoup import os import time import rerequests用于网络请求BeautifulSoup用于解析HTMLos用于操作文件和目录time用于在请求间加入延时避免请求过快被封re正则表达式模块可能用于清洗文件名。接下来我们定义一个函数来获取单个搜索页的HTML内容def fetch_page(keyword, page_num0): 获取指定关键词和页码的百度图片搜索结果页HTML :param keyword: 搜索关键词如风景 :param page_num: 页码每页通常30张图page_num0是第一页 :return: 网页HTML文本 # 百度图片搜索的基础URL base_url https://image.baidu.com/search/flip # 构造请求头模拟浏览器访问 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 } # 构造查询参数 params { tn: baiduimage, ie: utf-8, word: keyword, pn: page_num * 30, # 百度图片的pn参数是图片序号每页约30张 gsm: hex(30 * page_num)[2:], # 一个经验值与pn配合 } try: # 发送GET请求 response requests.get(base_url, headersheaders, paramsparams, timeout10) # 检查请求是否成功状态码200 response.raise_for_status() # 设置正确的编码通常为utf-8 response.encoding utf-8 return response.text except requests.RequestException as e: print(f请求第{page_num1}页失败: {e}) return None这段代码有几个关键点User-Agent这是HTTP请求头的一部分用来告诉服务器我们是什么类型的客户端浏览器。不带这个或者使用Python默认的很容易被网站识别为爬虫而拒绝服务。所以我们伪装成一个常见的Chrome浏览器。参数params我们将分析得到的参数以字典形式传给requests.get()。pn参数控制翻页经过测试它代表的是从第几张图片开始显示所以第二页就是pn30第三页pn60以此类推。异常处理网络请求可能失败超时、连接错误等用try...except包裹起来并打印错误信息能让程序更健壮不会因为一页失败就整个崩溃。response.raise_for_status()如果HTTP响应状态码不是200成功这个方法会抛出一个异常让我们能及时处理错误。拿到HTML后下一步就是解析它提取图片链接def parse_image_urls(html): 从HTML中解析出图片的原始URLdata-objurl :param html: 网页HTML文本 :return: 图片URL列表 if not html: return [] soup BeautifulSoup(html, html.parser) image_urls [] # 查找所有包含图片数据的元素 # 百度图片的图片链接藏在多个地方我们主要找有data-objurl属性的元素 for img_tag in soup.find_all(img, class_main_img): objurl img_tag.get(data-objurl) if objurl: image_urls.append(objurl) # 如果上述方法没找到尝试更宽泛的查找根据实际情况调整 if not image_urls: # 有时图片链接在其他的img标签或div的data-*属性里 # 这里需要根据实际页面结构调整可能需要用正则表达式辅助 pattern re.compile(robjURL:(.*?)) image_urls pattern.findall(html) return image_urls这个函数使用了BeautifulSoup的find_all方法寻找所有class属性为main_img的img标签然后获取它们的>def download_image(img_url, save_dir, filenameNone): 下载单张图片并保存到指定目录 :param img_url: 图片的URL地址 :param save_dir: 保存图片的目录路径 :param filename: 指定文件名如果为None则从URL提取 :return: 成功返回True失败返回False if not img_url: return False try: # 再次使用requests获取图片二进制数据 headers {User-Agent: Mozilla/5.0 ...} # 可以复用之前的headers # 图片请求可以不加Referer但有些网站会校验百度图片一般不需要 img_response requests.get(img_url, headersheaders, timeout15, streamTrue) img_response.raise_for_status() # 处理文件名 if not filename: # 从URL中提取文件名 filename os.path.basename(img_url).split(?)[0] # 去掉URL参数 # 如果提取不到有效文件名比如URL是动态生成的就用时间戳 if not filename or . not in filename: filename f{int(time.time()*1000)}.jpg # 确保文件名是安全的移除非法字符 filename re.sub(r[\\/*?:|], _, filename) # 确保文件有扩展名如果没有默认设为.jpg if . not in filename: filename .jpg # 完整的保存路径 save_path os.path.join(save_dir, filename) # 以二进制写入模式保存图片 with open(save_path, wb) as f: # 使用iter_content以块的方式写入适合大文件 for chunk in img_response.iter_content(chunk_size8192): if chunk: f.write(chunk) print(f图片已保存: {save_path}) return True except Exception as e: print(f下载图片失败 [{img_url}]: {e}) return False这个函数有几个值得注意的细节streamTrue在requests.get()中设置这个参数意味着以流模式下载。对于像图片这样可能较大的文件这样做不会一次性将整个响应内容读入内存而是分块读取更节省内存。文件名处理os.path.basename(url)可以获取URL路径的最后一部分作为文件名。但很多图片URL会附带查询参数?后面的部分我们用.split(?)[0]将其去掉。我们还用正则表达式移除了Windows文件名中不允许的字符确保文件能成功创建。文件写入使用with open(... , wb)上下文管理器wb代表以二进制写入模式打开文件。这样即使程序中途出错文件也能被正确关闭。我们通过iter_content方法循环读取响应数据块并写入文件。异常处理下载过程中可能遇到网络错误、图片链接失效、服务器返回403/404等问题用try...except捕获所有异常并打印错误信息避免单个图片下载失败导致程序中断。接下来我们需要创建主函数将“获取页面 - 解析链接 - 下载图片”这个流程串联起来并实现翻页逻辑以达到下载500张的目标def main(): keyword input(请输入要搜索的图片关键词例如风景、猫).strip() if not keyword: keyword 风景 # 默认关键词 # 创建保存图片的目录 save_dir imgs if not os.path.exists(save_dir): os.makedirs(save_dir) print(f创建目录: {save_dir}) total_downloaded 0 target_count 500 page 0 failed_urls [] print(f开始搜索并下载关键词 {keyword} 的图片目标 {target_count} 张...) while total_downloaded target_count: print(f\n正在获取第 {page1} 页...) # 1. 获取页面HTML html fetch_page(keyword, page) if not html: print(f第 {page1} 页获取失败跳过。) page 1 time.sleep(2) # 失败后也等待一下 continue # 2. 解析图片URL img_urls parse_image_urls(html) print(f第 {page1} 页解析到 {len(img_urls)} 个图片链接。) if not img_urls: print(f第 {page1} 页未解析到图片链接可能已无更多结果或页面结构已变。) break # 3. 遍历下载本页图片 for idx, img_url in enumerate(img_urls): if total_downloaded target_count: break print(f 正在下载第 {total_downloaded 1} 张图片...) success download_image(img_url, save_dir) if success: total_downloaded 1 else: failed_urls.append(img_url) # 重要的延时避免请求过快被服务器封禁IP。 time.sleep(0.5) # 每下载一张图休息0.5秒 # 4. 翻页 page 1 # 翻页间隔稍长一些 time.sleep(1) # 安全限制最多爬取20页防止无限循环 if page 20: print(已达到最大爬取页数限制20页。) break # 下载完成输出总结 print(f\n{*50}) print(f下载完成) print(f成功下载: {total_downloaded} 张) print(f失败链接: {len(failed_urls)} 个) if failed_urls: print(失败的URL列表已保存到 failed_urls.txt) with open(failed_urls.txt, w, encodingutf-8) as f: for url in failed_urls: f.write(url \n) print(f图片保存在 {save_dir} 目录中。) print(*50) if __name__ __main__: main()主函数main控制着整个流程的节奏。它通过一个while循环不断翻页直到下载的图片数量达到target_count500张或达到最大页数限制。循环内部清晰地分为四步获取页面、解析链接、逐一下载、翻页等待。这里我引入了两个非常重要的实战经验点延时time.sleep这是爬虫的“道德”和“生存”准则。不加延时地疯狂请求服务器会对目标网站造成压力轻则你的IP被暂时限制访问重则可能被视为攻击行为。我们在下载每张图片后休息0.5秒翻页后休息1秒这是一种基本的礼貌和自我保护。失败处理与记录不是每个图片链接都能成功下载。我们将失败的URL记录在failed_urls列表中并在程序最后保存到一个文本文件中。这样如果因为网络波动导致部分失败我们事后可以查看这个文件有针对性地进行重试而不是全部重新爬取。7. 代码优化与健壮性提升上面的代码已经可以工作了但作为一个有追求的开发者我们还可以让它更健壮、更高效、更友好。这里分享几个优化方向你可以根据需求选择实现。7.1 处理多种图片格式与URL清洗百度图片的链接可能指向.jpg,.png,.gif,.webp等多种格式。我们的download_image函数虽然通过检查扩展名做了一定处理但还可以更强。有些URL可能没有扩展名或者扩展名不正确。一个更稳妥的方法是尝试从HTTP响应头中获取内容类型Content-Type。def download_image_enhanced(img_url, save_dir): try: headers {User-Agent: Mozilla/5.0 ...} # 先发送一个HEAD请求只获取头部信息判断文件是否存在及类型 head_response requests.head(img_url, headersheaders, timeout5, allow_redirectsTrue) head_response.raise_for_status() content_type head_response.headers.get(content-type, ) # 根据Content-Type决定扩展名 if jpeg in content_type or jpg in content_type: ext .jpg elif png in content_type: ext .png elif gif in content_type: ext .gif elif webp in content_type: ext .webp else: ext .bin # 未知二进制文件 # 生成文件名使用URL的MD5值避免重复和长文件名问题 import hashlib url_hash hashlib.md5(img_url.encode(utf-8)).hexdigest() filename f{url_hash}{ext} save_path os.path.join(save_dir, filename) # 如果文件已存在跳过下载断点续传的基础 if os.path.exists(save_path): print(f文件已存在跳过: {filename}) return True # 正式下载 img_response requests.get(img_url, headersheaders, timeout15, streamTrue) img_response.raise_for_status() with open(save_path, wb) as f: for chunk in img_response.iter_content(8192): f.write(chunk) print(f已下载: {filename}) return True except Exception as e: print(f下载失败 [{img_url}]: {e}) return False这个增强版函数做了几件事1) 先用HEAD请求探路检查链接有效性和文件类型2) 根据Content-Type分配合适的扩展名3) 使用URL的MD5哈希值作为文件名唯一且长度固定避免了奇怪字符和长URL的问题4) 下载前检查文件是否已存在实现简单的去重和断点续传逻辑。7.2 使用Session保持连接requests.Session()可以复用底层的TCP连接在需要发送大量请求到同一主机时能显著提升速度。session requests.Session() session.headers.update({User-Agent: Mozilla/5.0 ...}) # 然后在fetch_page和download_image中使用session.get()代替requests.get() response session.get(base_url, paramsparams, timeout10)7.3 添加代理支持如果你的IP因为爬取速度过快被暂时封禁或者需要访问某些有地域限制的网站可能需要使用代理。proxies { http: http://your_proxy_ip:port, https: https://your_proxy_ip:port, } response requests.get(url, headersheaders, proxiesproxies, timeout10)重要提示请务必使用合法合规的代理服务并遵守目标网站的服务条款。本案例仅为技术演示。7.4 更优雅的翻页与结束判断我们的翻页逻辑是简单的页码递增。但更健壮的做法是解析HTML中的“下一页”按钮的链接或者判断当前页是否还有图片。当解析到的图片数量为0时就可以认为没有更多结果了。8. 常见问题、反爬策略与应对技巧实录在实际运行爬虫时你几乎一定会遇到各种各样的问题。下面是我根据多年经验总结的一些典型问题及其解决方案这可能是比代码本身更有价值的部分。8.1 请求被拒绝返回状态码403/404现象requests.get()抛出异常或response.status_code是403禁止访问或404未找到。原因缺少或错误的请求头服务器通过User-Agent等头部信息识别爬虫。我们的代码已经添加了常见的User-Agent但有些网站还会检查Referer来源页、Accept-Language接受语言等。你可以从浏览器开发者工具的Network选项卡中复制真实请求的所有Headers尽量模拟。请求频率过高即使加了time.sleep可能对于某些网站来说还是太快。需要进一步增加延时或者使用随机延时time.sleep(random.uniform(1, 3))来模拟人类操作的不规律性。IP被封锁短时间内来自同一IP的请求过多服务器直接封锁了你的IP地址。这是最麻烦的情况。解决方案完善请求头除了User-Agent尝试添加Referer通常设置为搜索页的URL、Accept、Accept-Language等。headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Referer: https://image.baidu.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }降低请求频率增加time.sleep的间隔并在翻页、下载等不同操作间使用不同的、随机的间隔时间。使用代理IP池这是应对IP封锁的终极方案。你需要有一组可用的代理IP然后在请求时随机选用。但这涉及代理IP的获取、验证和维护复杂度较高属于中级以上爬虫技术。8.2 解析不到图片链接image_urls为空现象能成功获取HTML但parse_image_urls函数返回的列表是空的。原因网页结构改变了。这是爬虫最常遇到的问题。网站前端工程师更新了页面导致我们之前赖以定位的HTML标签、类名或属性发生了变化。解决方案重新分析立即回到浏览器按F12重新检查页面元素。看看图片数据现在藏在哪里。可能换了一个新的CSS类名或者数据被放在了script标签的JSON对象里。使用更通用的选择器如果classmain_img失效了可以尝试查找所有img标签然后通过其他属性过滤比如src包含特定域名或者父级标签有特定特征。正则表达式备用正如我们在代码中做的准备一个正则表达式作为后备方案直接在全文本中搜索图片URL的模式。虽然脆弱但有时能救命。打印HTML片段调试在解析函数里如果找不到可以打印出一小段HTML比如soup.prettify()[:2000]到文件里人工查看结构到底变成了什么样。8.3 下载的图片损坏或无法打开现象文件保存成功了但用图片查看器打开时提示损坏或者文件大小异常小比如只有几KB。原因链接指向的不是真实图片有些图片URL可能是一个重定向链接返回302状态码或者是一个需要JavaScript加载的占位符。requests默认会自动处理重定向但如果重定向链的终点不是一个图片就会出错。网站反爬服务器可能识别出你是爬虫返回了一个错误的页面比如验证码页面或一段警告文本而不是图片数据。这些内容被当成二进制数据保存下来自然无法作为图片打开。解决方案检查响应头在download_image函数中打印或检查img_response.headers[content-type]。如果是text/html说明下回来的根本不是图片而是HTML页面。检查文件大小下载完成后检查文件大小。一张正常的网络图片至少是几十KB。如果只有1-2KB几乎可以肯定是出错了。手动测试链接将出错的图片URL复制到浏览器地址栏中直接访问看是否能正常显示图片。如果不能说明这个链接本身就有问题可能是动态的、过期的或需要特定Cookie。8.4 程序运行一段时间后突然崩溃现象爬虫跑了几十张或一百多张图后报连接超时、连接重置等错误然后停止。原因网络不稳定、服务器主动断开连接、或本地资源如Socket耗尽。解决方案加强异常处理确保每一个网络请求fetch_page,download_image都被try...except包裹并且发生异常时不是直接崩溃而是记录错误、跳过当前项继续执行下一个。设置超时和重试requests.get(timeout10)设置了超时。可以为其添加重试机制。可以使用requests的适配器HTTPAdapter或第三方库如retrying来实现遇到特定异常时自动重试几次。释放资源确保文件操作使用了with语句连接能正确关闭。对于非常长时间的爬取可以考虑定期重启脚本或者分批次运行。9. 扩展思考从静态抓取到动态渲染通过这个案例你已经掌握了静态网页抓取的基本功。但现代网站越来越多地使用JavaScript动态加载内容。比如你打开百度图片初始页面只有30张图当你滚动到底部时会自动加载出更多这个过程就是通过JavaScript发起的AJAX请求获取新数据并更新到页面的。抓取这类动态网页有几种主流思路分析AJAX请求像我们分析搜索URL一样打开开发者工具的Network选项卡筛选XHR/Fetch请求当你滚动页面时会看到新的请求出现。分析这些请求的URL、参数和响应通常是JSON格式直接模拟这些请求来获取数据。这种方法效率最高但需要一定的分析能力。使用无头浏览器工具如Selenium、Playwright或Puppeteer可以自动化控制一个真实的浏览器如Chrome。你可以用代码命令浏览器打开网页、滚动、点击等页面完全渲染好后再获取最终的HTML源代码。这种方法最接近真人操作能应对极其复杂的页面但速度慢资源消耗大。寻找数据接口有些网站虽然前端是动态的但数据可能来自一个公开或半公开的API接口。如果能找到这个接口抓取就变得和静态页面一样简单。对于百度图片其“滚动加载”的机制完全可以通过分析其AJAX请求来模拟。你会发现滚动时浏览器会向一个类似https://image.baidu.com/search/acjson?....的地址发送请求返回的是JSON数据里面包含了后续图片的所有信息包括高清大图地址。直接请求这个接口解析JSON会比解析HTML更稳定、更高效。这可以作为你下一个进阶练习。爬虫技术是一个实践性极强的领域核心原则就是“具体问题具体分析”。没有一成不变的代码只有不断调整的策略。从分析页面开始到写出能稳定运行的程序这个过程本身就是最大的收获。希望这个详细的案例能为你打开爬虫世界的大门。记住保持礼貌设置延时尊重robots.txt只爬取公开且允许爬取的数据用在正当的用途上。