☰
Python爬虫实战:批量下载网页图片与音频并按页码自动归档
2026/10/14 18:10:41 网站建设 项目流程

做素材整理的朋友应该都有过这种经历:一个网站上的图片和音频资源质量不错,想批量保存下来,但一页一页手动“右键另存为”,几百个页码、上千个文件,手点到抽筋不说,还特别容易漏。我自己就踩过这种坑,后来干脆用Python爬虫把这件事自动化了——按原网站的页码结构,把图片和音频分别下载,自动归类保存到对应文件夹。这篇文章就把这套方案的完整思路、代码实现和踩坑经验分享出来,适合有Python基础、想做批量资源采集的开发者参考,也能让零基础的人照着步骤跑通整个流程。

1. 需求分析与整体方案设计

1.1 需求拆解

先把这个标题拆开看,其实需求非常明确:批量爬取网站的图片和音频、按页码保存、自动建立对应文件夹。三个关键词分别对应三个环节:

  • “批量”对应的是遍历逻辑,说明目标站点的资源是分页展示的,例如每页有20条记录,每条下面挂1张图和1段音频,我需要循环把每一页都抓下来。
  • “图片和音频”对应的是两种不同类型的资源,图片常见格式有jpg、png、gif、webp,音频常见格式有mp3、wav、m4a,解析策略要兼顾这两类资源,而且命名上要能区分。
  • “按页码保存在对应文件夹”对应的是文件管理策略,比如第3页的文件都放进page_003/目录,这样后续人工复查时能直接追溯到来源页面,发现某个资源有问题也方便重新下载。

很多人在做这类需求时会直接搜“万能爬虫代码”,但爬虫本质上是“一个页面一个规则”,不同站点的HTML结构完全不同。所以动手之前,你先访问目标页面,查看页面源代码,找到图片链接和音频链接的规律。我的经验是:优先找规律明显的标签,比如<img>标签的src属性、<audio>标签的src属性,或者某个包裹资源链接的<a>标签的href属性。找到规律之后,代码逻辑基本就固定下来了。

1.2 目录结构设计

按页码保存听起来很简单,但如果你真的疯狂下载上千个文件,没有清晰的目录结构,后期维护就是一场灾难。我在一开始就规划了这样的目录规范:

downloads/ ├── page_001/ │ ├── image_001.jpg │ ├── image_002.png │ └── audio_001.mp3 ├── page_002/ │ ├── image_001.jpg │ └── audio_001.mp3 └── page_003/ └── ...

这个结构有几个好处:第一,文件夹名直接对应页码,后续排查某页资源缺失时,定位成本极低;第二,图片和音频虽然放在同一目录,但通过文件名前缀区分,例如图片用image_开头,音频用audio_开头,如果后续需要把两类资源拆到不同目录,也只需要改一行文件名处理逻辑;第三,下载失败时我可以根据空文件夹快速定位需要重试的页码。

1.3 技术选型逻辑

做爬虫有很多选择:Scrapy框架、requests + BeautifulSoup、Playwright/Selenium等。为什么我最终选了requests + BeautifulSoup + lxml这个组合?

核心原因是“杀鸡不用牛刀”。大部分诗词、素材、资源分享类站点,资源链接都在服务端渲染的HTML里,直接用requests请求页面就能拿到完整的HTML源码,根本不需要启动一个浏览器内核去渲染。Scrapy虽然是专业爬虫框架,但它有学习成本,对于这种几十行代码就能解决的场景反而显得重。真正需要Selenium或Playwright的情况是:目标站点的资源链接通过JavaScript动态生成,直接请求HTML拿不到有效数据。这种情况我会在后面的“动态加载内容”小节单独说。

选requests而不选urllib,是因为requests对连接池、会话保持、请求头的控制更友好,处理cookie和session也更方便。选BeautifulSoup而不选正则表达式,是因为BeautifulSoup的文档结构解析在面对复杂标签嵌套时,比正则匹配要稳定得多。正则表达式在提取HTML时非常脆弱,稍微换个属性顺序就挂了,而bs4按节点树解析则抗造得多。

2. 环境准备与依赖安装

2.1 Python环境搭建

我要求Python 3.8以上版本,主要是用到了比较新的类型注解语法和海象运算符,当然你用3.6也能跑,只是个别代码需要微调。装好Python之后,建议给这个项目建独立虚拟环境,避免污染全局环境。我自己一直用这种方式管理爬虫项目,因为你可能同时维护好几个爬虫,每个爬虫依赖的库版本不一样,混在一起迟早会出问题。

在项目根目录执行:

python -m venv venv

Windows环境下激活:

venv\Scripts\activate

macOS/Linux环境下激活:

source venv/bin/activate

激活之后,命令行提示符前面会出现(venv)前缀,说明当前已经在虚拟环境内,接下来安装的依赖都只在这个项目里生效。

2.2 核心依赖安装

直接安装所需依赖库:

pip install requests beautifulsoup4 lxml

requests负责发HTTP请求,BeautifulSoup负责解析HTML,lxml是BeautifulSoup的解析引擎,解析速度比Python内置的html.parser快不少。实际项目中我还推荐安装一个fake_useragent,用来随机生成User-Agent,减少被站点识别为爬虫的概率:

pip install fake_useragent

这套依赖组合体积小、安装快,没有重型框架的额外负担。

2.3 确认目标站点结构

依赖装完先别急着写代码,先手动打开目标页面,按F12打开开发者工具,定位到图片元素和音频元素,看看它们所在的标签和属性结构。以某个典型的素材站为例,图片通常是:

<img><audio controls> <source src="https://example.com/audio/sound_001.mp3" type="audio/mpeg"> </audio>

注意这里有个细节:很多懒加载型站点不会直接把真实图片地址放在src属性里,而是放在>import time import requests from fake_useragent import UserAgent def fetch_html(url, retries=3, timeout=10): headers = { "User-Agent": UserAgent().random, "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", } for attempt in range(1, retries + 1): try: resp = requests.get(url, headers=headers, timeout=timeout) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except (requests.RequestException, ConnectionError) as e: print(f"[请求失败] 第 {attempt} 次尝试: {url},原因: {e}") if attempt < retries: time.sleep(2 * attempt) return None

这里的timeout=10很关键。如果不设超时,某次连接卡住时,程序会一直挂在那里,整个批量任务都会卡死。resp.raise_for_status()会在HTTP状态码为4xx或5xx时抛出异常,配合重试逻辑,能天然应对服务器的偶发500错误。而每次重试前递增等待时间(2秒、4秒、6秒),是经典的退避策略,既给服务器喘息时间,也降低自己被封IP的风险。

3.2 解析模块:精准提取资源链接

拿到HTML之后,用BeautifulSoup提取图片和音频链接。我写了extract_media_links函数,返回图片链接列表和音频链接列表两个部分。

from urllib.parse import urljoin from bs4 import BeautifulSoup def extract_media_links(html, base_url): soup = BeautifulSoup(html, "lxml") image_links, audio_links = [], [] # 提取图片:优先取>import os def download_file(url, save_path, headers, retries=3): os.makedirs(os.path.dirname(save_path), exist_ok=True) for attempt in range(1, retries + 1): try: resp = requests.get(url, headers=headers, stream=True, timeout=15) resp.raise_for_status() with open(save_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): if chunk: f.write(chunk) return True except Exception as e: print(f"[下载失败] {url},第 {attempt} 次重试,原因: {e}") if attempt < retries: time.sleep(2) return False

文件命名时要考虑几个因素:保留页码信息、避免非法字符、防止重名覆盖。我习惯用页码加序号组合成文件名,例如page_003_image_002.jpg。网络资源链接末尾的文件名经常是乱码,比如pic.php?id=123,直接拿它当文件名会很难看,所以我用一个统一的命名规则来生成文件名。特别注意,Windows系统下文件名的非法字符包括\/:*?"<>|,如果直接从URL里提取文件名,一定要做一轮清洗。

3.4 主流程:按页码循环调度

主流程把请求、解析、下载三个模块串起来。核心逻辑是:构造第N页的URL → 请求HTML → 解析资源链接 → 创建对应文件夹 → 逐个下载保存。

def crawl_page(page_num, base_url, total_downloaded): page_url = f"{base_url}/list_{page_num}.html" html = fetch_html(page_url) if not html: return total_downloaded image_links, audio_links = extract_media_links(html, page_url) page_dir = f"downloads/page_{page_num:03d}" os.makedirs(page_dir, exist_ok=True) for idx, url in enumerate(image_links, start=1): ext = os.path.splitext(url.split("?")[0])[1] or ".jpg" filename = f"page_{page_num:03d}_image_{idx:03d}{ext}" save_path = os.path.join(page_dir, filename) if download_file(url, save_path, HEADERS): total_downloaded += 1 time.sleep(0.5) for idx, url in enumerate(audio_links, start=1): ext = os.path.splitext(url.split("?")[0])[1] or ".mp3" filename = f"page_{page_num:03d}_audio_{idx:03d}{ext}" save_path = os.path.join(page_dir, filename) if download_file(url, save_path, HEADERS): total_downloaded += 1 time.sleep(0.5) return total_downloaded if __name__ == "__main__": START_PAGE, END_PAGE = 1, 50 total = 0 for page in range(START_PAGE, END_PAGE + 1): total = crawl_page(page, "https://example.com", total) print(f"第 {page} 页处理完成,累计下载 {total} 个文件")

主流程里有几个设计值得说明:一是page_{num:03d}使用了格式化补零,这样文件夹排序时page_002会排在page_010前面,而不是page_10排在page_2前面,排序不混乱;二是每个文件下载后强制sleep(0.5),故意降低请求频率,这个稍后详细解释;三是total_downloaded作为计数器传入传出,方便在主线程掌握整体进度。

4. 实操过程中的抗坑经验

4.1 相对路径与绝对路径转换

前面我提到了urljoin,这里再强调一次,因为这个坑实在太常见了。站点页面URL一般是https://example.com/list/3.html,而资源src可能是/images/pic_001.jpg,也可能是images/pic_001.jpg,还可能是../images/pic_001.jpg。如果直接把这三种字符串丢给requests去请求,第一种拼接规则是“网站根路径”,第二种是“当前目录下”,第三种是“上级目录下”,三者结果完全不同。用urljoin一步到位解决,不需要程序员自己判断使用哪种拼接规则。

4.2 动态加载内容的处理方案

有些站点的音频链接不在HTML里,而是页面加载后用JavaScript异步请求后端接口拿到的。如果fetch_html返回的HTML里压根找不到audio标签,你需要打开开发者工具的Network面板,看页面加载时浏览器实际发出了哪些网络请求,往往能找到返回JSON数据的接口。这种接口通常也是一个HTTP GET请求,只是返回的是JSON而不是HTML。解析JSON比解析HTML还简单,用resp.json()直接转成字典就行。

如果接口藏在JavaScript逻辑里,手工逆向困难,才考虑用Selenium或Playwright模拟浏览器操作。这是最后的备选方案,因为它会显著增加资源消耗和稳定性风险。我在实际项目中只有不到10%的站点需要走到这一步。

4.3 请求频率控制与限速策略

新手最容易犯的错误是“狂轰滥炸式请求”——循环里不加停顿,几百个页面一秒内全部请求完,结果就是IP被站点封禁。有些站点封IP是永久性的,你的网络出口直接访问不了整个站点,只能等运营商重新分配IP,或者换个网络环境,代价非常大。

我的经验是两类请求都要限速:页面请求间隔建议1秒以上,文件下载请求间隔建议0.5秒以上。如果你要爬的是中小型站点,间隔再翻倍也不过分。每次重试之间的等待也属于限速的一部分,用退避策略而不是固定间隔,能进一步提高成功率。不要觉得等几秒浪费时间——被封IP之后浪费的时间是以天计的。

4.4 断点续传与增量更新

批量下载到第47页突然断网,重新跑一遍要重新下前46页,这很浪费。两种策略可以缓解:第一种是下载前先判断目标文件是否已存在且大小大于0,存在就跳过;第二种是为每个页面维护一个下载完成标记文件。我个人更倾向于第一种,因为判断文件是否存在几乎没有额外成本,而“存在且非空”基本可以代表下载成功。

增量更新的场景更实用:今天是第1到50页,一周后站点新增了第51到60页,这时只要把起始页码改成END_PAGE=60,程序会自动跳过已存在的文件,只下载新内容。所以我在主流程里用range遍历页码而不是写死URL列表,也让重跑局部页码变得非常方便。

5. 常见报错与排查速查表

5.1 403与418反爬错误

403 Forbidden代表服务器拒绝你的请求,最常见原因是User-Agent被识别。某些站点对常见爬虫UA有黑名单机制,比如直接识别到Python-requests的默认UA。前面引入fake_useragent就是解决这个问题的。418则是服务器明确识别为“你是茶壶”,也就是确定为爬虫后的调侃性响应码,处理思路和403一致——换UA、降频率、加Referer头。

如果换了UA还不行,把请求头补全,尤其是Referer字段,很多站点会校验请求来源是不是本站页面。下载文件时把页面URL塞进Referer,往往能解决防盗链问题。

5.2 超时与连接异常

requests.exceptions.ConnectTimeout以我自己的实测经验,出现频率不低。原因通常是目标服务器带宽有限,或者当前网络环境到目标站点的路由质量差。解决办法就是把超时时间调大、增加重试次数、每次重试等待时间拉长。我的默认参数是超时15秒、重试3次。如果连续重试3次还失败,大概率是资源本身失效了,直接跳过并用日志记录,不要死磕。

5.3 页面编码乱码

中文站点有些是UTF-8编码,有些是GBK编码。如果直接用resp.text,requests会根据响应头里的charset参数解码,但很多站点的响应头不规范或根本没写charset,这时解码出来就是乱码。resp.apparent_encoding是requests基于内容自动推断的编码,准确率很高,我在fetch_html里做的就是强制覆盖编码。遇到乱码优先检查这一环。

5.4 文件占用与磁盘空间

批量下载大量文件时,我遇到过磁盘写满导致保存失败的情况。下载前先用shutil.disk_usage检查剩余空间,如果剩余空间小于某个阈值,提前终止任务。另外一个容易被忽略的坑是:有些文件名结尾没有扩展名,保存时系统可能无法直接打开。我在命名时兜底补了.jpg或.mp3,就是防止这种情况。

6. 爬虫应用的规范建议

6.1 遵守站点访问协议

爬虫虽然是技术活,但技术从来不是孤立的。国内很多站点都有robots.txt文件,打开https://目标站点/robots.txt可以看到哪些路径允许抓取、哪些路径禁止抓取。我在正式抓取前一定会看一眼。虽然robots.txt没有法律强制力,但它代表了站点的访问意愿,遵守它是行业内的基本礼仪。

6.2 控制全局请求频率

除了解析和下载间隔,你应该对整个程序设置全局速率限制。比如用装饰器或者简单的请求计数器,确保每分钟请求数不超过某个阈值。我给自己定的经验值是:中小站点每分钟不超过20次请求,大型站点也不超过60次。爬虫的目标是获取数据,不是给站长的运维制造压力,更不是给自己制造风险。

6.3 版权与使用边界

批量下载的图片和音频大概率有版权。用来做个人学习、素材备份、本地归档,问题不大;但如果要分发到公开平台、做商业项目,务必确认目标资源的授权范围。爬虫本身是中性工具,但使用工具的人需要为自己的行为负责。我在项目交付时都会提醒使用者注意这一点,这篇博文同样适用。

最后再分享一个我实操中的体会:别迷信万能代码,拿到目标站点先花30分钟手工研究页面结构,比写出200行代码再调试2小时高效得多。爬虫工作的本质不是写代码,是读懂目标站点的数据组织逻辑。把这套按页码下载的方案吃透之后,图像素材批量归档、音频语料收集、播客资源备份这些场景都可以直接套用,核心只需要改资源链接的解析规则而已。希望这篇文章能帮你少走一些我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询