资讯动态

Python爬虫批量下载图片音频并按页码自动归档的完整方案

发布时间:2026/10/14 22:54:12 来源:尧图企业网站定制
一说到Python爬虫很多人的第一反应是抓网页文字、抓表格数据。可真到了需要批量爬取网站图片和音频的时候画风就完全不一样了图片下载回来打不开、音频文件只有几KB、几百个文件全部堆在同一个目录里翻起来跟天书一样。这篇文章想分享的就是一个能直接跑的批量下载方案——用Python爬虫实现图片和音频的批量采集并按页码自动保存到对应文件夹。只要目标网站是公开可访问的分页列表页这套思路基本就能复用翻页、解析页面、提取资源地址、下载落盘、按页码归档。不管是拿来做素材备份还是练手requests与BeautifulSoup的配合都有参考价值。这个项目看着不大但它把爬虫的完整链路都串起来了网页结构分析、HTTP请求、HTML解析、文件流式下载、异常处理、目录管理。真正动手做一遍你对爬虫的理解会比只看教程深刻得多。下面我从需求拆解开始把关键步骤和我在实测中踩过的坑都讲一遍。如果你之前只写过爬文字的小脚本这篇也能帮你少走不少弯路。我会尽量把每一步“为什么这样做”讲清楚而不只是贴代码。代码基于常见的requests和BeautifulSoup环境是Python 3Windows和macOS都能跑。1. 需求拆解与方案选型为什么按页码归档是刚需1.1 先想清楚要爬什么再决定怎么写代码很多人拿到“批量爬取网站图片和音频”这个需求第一件事就是打开编辑器开始写requests。这个顺序其实是反的。先想清楚三件事目标页面长什么样、资源藏在哪、数量级有多大目标页面长什么样是传统的多页列表还是一条长页面滚动加载如果是分页列表URL规律是什么资源藏在哪图片是在列表页直接展示缩略图还是需要进入详情页才能拿原始大图音频地址是直接写在标签里还是通过接口异步加载数量级有多大100张和10000张方案的复杂度完全不同。如果只是几百张资源requests加单线程完全够用到了上万张就得考虑断点续传、并发下载和失败重试否则半夜爬起来重新爬的滋味不好受——我经历过一次所以后来每次都先算量级再定方案。按页码归档在这里就不是“可选项”了。假设你爬了一个20页的图库如果不归档所有文件都混在同一个文件夹里一旦某张图片下载失败你根本不知道它属于第几页只能全部重爬。归档到page_1、page_2这样的目录之后问题就变成了“某页缺哪张补哪张”排查和重爬的成本都会大幅下降。这也是标题里“按页码保存在对应文件夹”这个需求的真正意义不是强迫症而是可维护性。1.2 技术栈选择requests BeautifulSoup 标准库我最终选的是这组组合环节方案说明HTTP请求requests简单直观session管理、stream流式下载都内置HTML解析BeautifulSoup lxml比正则易读适合定位标签结构正则兜底re处理藏在script里的资源地址文件处理os pathlib创建目录、拼接路径、清洗文件名线程并发可选concurrent.futures文件特别多时用ThreadPoolExecutor加速为什么不直接上Scrapy因为这个项目的核心是“批量下载并归档”不是“分布式抓取”。Scrapy的学习成本和项目结构成本对一次性采集任务来说偏重。requests方案写成一个脚本就能跑顶多一百来行出了问题也容易调试。同理为什么不推荐直接用Selenium/Playwright去渲染页面只有当你确认资源地址必须执行JavaScript才能拿到才需要引入浏览器自动化工具。能用requests解决的问题就不要把浏览器引擎拖进来性能和稳定性都会更好。2. 动手前先看清网页定位图片和音频的真实地址2.1 用浏览器开发者工具找资源的“老巢”要写爬虫第一件事不是写代码而是打开目标网页按F12进入开发者工具切到Network面板然后刷新页面。这一步能让你快速看清页面到底加载了什么资源。最常见的几种情况图片HTML里的img标签src属性就是图片地址。但很多网站列表页用的是缩略图URL里带了thumb、_small、w_200之类的参数真正的大图需要点进详情页才能拿到。遇到这种情况不仅要解析列表页还要额外做一层详情页解析难度会上升一个台阶。图片懒加载src里放的是灰色占位图真实地址在>def has_next_page(soup): # 假设分页控件里有一个class为next的链接 return soup.select_one(a.next) is not None3. 图片批量下载的实现从链接提取到文件落盘3.1 提取图片链接先过滤干扰项再处理懒加载请求到HTML之后最基础的是用BeautifulSoup找出所有img标签。但直接找会带出一堆页头页尾的Logo、广告图和分享图标。我的习惯是先把图片链接过一遍关键词黑名单logo、avatar、ad_、icon、banner这些词命中就直接跳过。另一个注意点是页面里可能同时存在缩略图和原图链接。如果要的是原图在解析列表页时就要仔细看。有些网站的缩略图URL通过尺寸参数控制去掉?w200之类的参数可能就是原图有些站点则直接在>from urllib.parse import urljoin from bs4 import BeautifulSoup def parse_img_urls(html, base_url): soup BeautifulSoup(html, html.parser) img_urls [] blacklist [logo, avatar, icon, banner, ad_] for img in soup.find_all(img): src img.get(src) or img.get(data-src) or img.get(data-original) if not src: continue if not src.startswith(http): src urljoin(base_url, src) if any(key in src.lower() for key in blacklist): continue img_urls.append(src) return img_urlsurljoin在处理相对路径时特别好用。Web页面里大量资源地址是/uploads/xxx.jpg这种写法不补全域名的话下载时直接404。这个函数比手动拼接字符串靠谱得多既能处理../路径也能保留查询参数。3.2 流式下载为什么一定要用iter_content图片文件大小从几十KB到几MB不等。如果用response.content一次性读入内存小图问题不大但遇到几百个几MB的原图内存会吃紧。更稳妥的方案是streamTrue配合iter_content()逐块写入磁盘这也是经验里最稳的下载姿势。同时一定要设置timeout。批量任务卡死的原因往往不是反爬而是某个连接一直挂着不返回。设置超时后单文件下载失败会及时抛出异常程序才能继续走重试逻辑。我习惯把10秒作为默认值慢速网络下可以调到15但不要超过30否则异常链会拖慢整体节奏。import time import requests def download_file(url, save_path, headersNone, max_retry2): for i in range(max_retry 1): try: resp requests.get(url, headersheaders, streamTrue, timeout10) if resp.status_code ! 200: return False with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size1024): if chunk: f.write(chunk) return True except requests.RequestException as e: if i max_retry: print(f下载失败: {url}, {e}) return False time.sleep(2) return False这里每次失败后重试两次中间间隔2秒。重试次数太多会拉长任务时间太少则扛不住偶发的网络抖动两次是实战中比较平衡的经验值。3.3 文件命名与完整性检查保存文件时不要直接用URL里截出来的文件名。不同页面的图片可能重名直接覆盖就丢了而且URL里可能带随机参数截出来的名字乱七八糟。我建议统一用“页码_序号.后缀”的方式命名比如001_003.jpg表示第1页第3张图排序稳定还能一眼看出资源所属位置。后缀名不要全信URL。有的图片URL不带扩展名或者扩展名是.php?xxx这时候可以用Content-Type来映射扩展名。image/jpeg对应.jpgimage/png对应.png这样比猜后缀可靠。甚至有更简单的兜底直接写成.jpg打不开再排查也能解决问题但不够严谨。完整性检查最简单的一招是看文件大小。正常图片通常大于几KB如果某个文件只有几百字节多半是下载到了404错误页或防盗链跳转页。进阶一点可以读文件前几个字节判断“魔数”JPEG的文件头是FF D8 FFPNG是89 50 4E 47。这一招在排查“假文件”时非常有用尤其是在批量跑完几千个文件之后能快速定位损坏批次。4. 音频爬取与图片的差异请求头、格式与文件名处理4.1 音频文件在网页里的几种存在形式音频资源和图片最大的区别是图片一般都在img标签里音频则可能出现在好几个位置。常见的位置有audio srcxxx.mp3最简单直接取src。source srcxxx.mp3 typeaudio/mpegaudio标签内部的source子标签需要遍历。a hrefxxx.mp3下载/a以超链接形式存在常见于资源下载页。JavaScript变量里var musicUrl xxx.mp3需要正则从script里抠。前三种用BeautifulSoup就能定位第四种我习惯先抓整段HTML然后用re.findall去匹配音频扩展名import re pattern re.compile(rhttps?://[^\s\]?\.(?:mp3|m4a|wav|aac|flac)(?[\s\]), re.I) audio_urls pattern.findall(html)这段正则的核心是扩展名匹配加上边界判断能把音频链接从script和普通HTML里一并捞出来。实测下来它对绝大多数直接暴露链接的站点都有效而且比逐个解析节点更省事。当然如果网站用特别加密的方式隐藏音频地址这招会失效那就得去逆向播放器逻辑了不在本文范围内。4.2 Referer和User-Agent音频下载最容易翻车的两件事音频文件比图片更容易遇到防盗链。原因很简单音频带宽成本高站长不希望自己的文件被其他站点直接引用。很多站点会在服务器端校验Referer只有从本站页面发起的请求才返回真正文件否则返回一个明显的错误提示页或者直接404。所以在下载音频时我几乎总是带上两样东西headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Referer: page_url, }User-Agent告诉服务器“我是一个真实浏览器”Referer告诉服务器“我是从本站页面过来的”。注意 Referer 要填你当时解析出播放地址的那个页面URL不是随便写一个网站首页。被拒绝时服务器主要校验的就是来源是否可靠。这里要特别说明一下带UA和Referer是为了让自己看起来像正常访客前提是目标站点本身允许公开访问。不要用这些手段去绕过登录、付费墙或者验证码那越界了。4.3 文件名清洗、格式兼容与中文乱码音频文件名里经常出现空格、、?这些字符。Linux下还好Windows下就麻烦?是通配符:是盘符分隔符/是路径分隔符这些字符都有可能导致保存失败。所以在保存之前要做一层清洗def sanitize_filename(name): for ch in [\\, /, :, *, ?, , , , |]: name name.replace(ch, _) return name.strip()另一个容易被忽略的点是有些站点的音频地址或文件名用了UTF-8百分号编码比如%E6%AD%8C%E6%9B%B2.mp3直接保存会是一串乱字符。用unquote还原成中文就可以了。顺便提一嘴编码处理在图片文件名上也会遇到中文资源站特别常见。from urllib.parse import unquote filename unquote(filename)5. 按页码自动归档目录设计、主流程与断点续爬5.1 目录结构设计图片和音频分开页码作为二级目录目录结构我见过很多种踩过坑之后觉得最稳的是下面这种output/ ├── picture/ │ ├── page_1/ │ ├── page_2/ │ └── ... └── audio/ ├── page_1/ ├── page_2/ └── ...为什么把图片和音频分开两层因为图片通常以千计、音频相对少混在一起时你想单独查看某类资源会很痛苦。而且后续如果要压缩图片、转码音频分开处理也方便。页码作为二级目录创建的核心逻辑就一行os.makedirs(save_dir, exist_okTrue)exist_okTrue是个很省心的参数目录已存在也不会报错不用自己写“如果不存在才创建”的判断能给主流程省掉不少冗余代码。5.2 主流程整合翻页、解析、下载三步走把前文拆开的片段拼起来就是一个可运行的完整脚本。核心流程是遍历页码 → 请求列表页 → 解析图片和音频URL → 逐一下载到对应目录 → 休眠控制频率。代码大致如下import os import time import requests from bs4 import BeautifulSoup from urllib.parse import urljoin, unquote BASE_URL https://example-site.com/list HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, } def get_page_html(page_url): resp requests.get(page_url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text def parse_media_urls(html): soup BeautifulSoup(html, html.parser) img_urls, audio_urls [], [] for img in soup.find_all(img): src img.get(src) or img.get(data-src) or img.get(data-original) if src: img_urls.append(urljoin(BASE_URL, src)) for audio in soup.find_all(audio): src audio.get(src) if src: audio_urls.append(urljoin(BASE_URL, src)) for src_tag in audio.find_all(source): src src_tag.get(src) if src: audio_urls.append(urljoin(BASE_URL, src)) return img_urls, audio_urls def save_media(url, save_dir, index): os.makedirs(save_dir, exist_okTrue) ext os.path.splitext(url.split(?)[0])[1] ext ext if ext else .jpg filename unquote(f{index:03d}{ext}) save_path os.path.join(save_dir, filename) if os.path.exists(save_path): return True try: resp requests.get(url, headersHEADERS, streamTrue, timeout10) if resp.status_code 200: with open(save_path, wb) as f: for chunk in resp.iter_content(chunk_size1024): if chunk: f.write(chunk) return True except requests.RequestException as e: print(f下载失败 {url}: {e}) return False def main(start_page, end_page): for page in range(start_page, end_page 1): page_url f{BASE_URL}?page{page} html get_page_html(page_url) img_urls, audio_urls parse_media_urls(html) print(f页码 {page}: 找到 {len(img_urls)} 张图片, {len(audio_urls)} 个音频) for idx, url in enumerate(img_urls, start1): save_media(url, foutput/picture/page_{page}, idx) for idx, url in enumerate(audio_urls, start1): save_media(url, foutput/audio/page_{page}, idx) time.sleep(1) if __name__ __main__: main(1, 20)代码里time.sleep(1)很关键。哪怕目标站点没有任何反爬机制连续高速请求也会对对方服务器造成压力这是基本的礼貌问题。每分钟请求数控制住任务只是跑得慢一点但不会把IP搭进去也不会给对方添麻烦。5.3 断点续爬中断了也能接着跑批量任务跑一半网络断了或者电脑休眠太常见了。如果脚本没有断点续爬能力你要么忽略已下载的文件从头再跑要么手动确认“第几页没下完”。我习惯用两个简单机制解决文件存在就跳过。上面save_media里已经做了这个判断os.path.exists(save_path)直接返回True不重复下载。记录已完成页码。每完成一页往done.log里追加一行page_1 done。下次启动时先读这个文件得到还未处理的页码集合直接续跑。这个机制看起来简单但是在几千页的任务里价值极大。第二个机制可以配合文件数判断如果某个页码目录里的文件数量已经等于该页解析出的资源数就认为这一页已完成。这样即使没有日志文件也能通过文件数量粗略判断进度算是双保险。6. 实测中的高频坑403、假文件与动态加载页面6.1 请求被拒403 Forbidden最经典的坑。明明浏览器能打开代码却返回403。原因几乎都是服务器识别出这不是真实浏览器。对策也很直接把浏览器Network面板里看到的默认请求头复制过来至少包含完整的UA和必要的Referer再把请求间隔拉大到2秒到3秒。如果仍然403可以试试把Accept-Language和Accept也带上。请求头尽量与正常访问一致命中率会显著提高。这里要提个醒遇到403不要想歪门邪道也不要试图无限换IP。正确做法是降低访问频率、遵守站点的robots规则只爬公开允许访问的内容。爬虫项目跑得慢没关系跑得合规才安心。6.2 下载下来的“假文件”后缀正确但打不开图片后缀是.jpg但双击打开提示格式损坏音频后缀是.mp3文件却只有1KB。这些都叫“假文件”。排查思路分三步。先看Content-Type。请求文件时打印resp.headers.get(Content-Type)如果图片请求返回的是text/html说明服务器返回了一个网页多半是防盗链页或者404提示页自然打不开。再看文件头。JPEG文件头是FF D8 FFPNG是89 50 4E 47MP3通常包含ID3标记。写个简单的十六进制读取函数一眼就能判断文件到底是什么格式比后缀可靠得多。最后看文件大小。低于1KB甚至只有几百字节的“图片/音频”几乎都是假文件直接删掉进入重试流程就行。这三步可以写成一个校验函数放在下载完成后调用批量跑完会自动筛出异常文件。6.3 图片和音频地址是动态加载的怎么办如果整页HTML里完全没有图片和音频地址需要执行JavaScript才能拿到那requests方案就失效了。这时候有两个思路。思路一是把隐藏的网络请求扒出来。打开开发者工具切到Network过滤XHR在页面上翻页或点击播放找到返回图片/音频列表的接口然后用requests直接请求这个接口。接口一般返回JSON解析起来比HTML还方便。这个思路其实是首选稳定性最好。思路二是引入浏览器自动化。用Playwright或Selenium渲染页面等JavaScript执行完再取页面源码。好处是逻辑简单坏处是内存占用大、执行慢。我的经验是优先找接口让requests直接对接只有接口明显加密、难逆向时才用浏览器方式。毕竟这个项目的目标是快速批量下载不是研究前端加密技术。7. 合规边界与后续优化方向7.1 爬虫使用的几条边界爬虫能力本身是中性的但如果用来爬取需要登录、付费墙后的内容或者绕过robots协议性质就会发生变化。我给自己定了几条硬规矩只爬公开可访问的页面不碰需要登录或付费的内容。尊重robots规则网站明确表示不希望被爬的路径绕开。控制请求频率不给目标服务器制造压力。采集的内容只用于个人学习、素材整理和备份不二次分发、不商用。这些边界不需要多高深的觉悟只是将心比心你也不希望别人一分钟访问你的网站几百次。慢一点没问题安全合规才是长久之计。7.2 并发下载与去重让脚本更进一步如果资源量到了几千、几万级单线程下载确实太慢。最简单的提速方式是引入ThreadPoolExecutor把下载部分改成线程池并发from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers8) as executor: executor.map(lambda url: save_media(url, save_dir, index), url_list)并发之后别忘了控制线程数量和请求间隔否则本机带宽吃满不说目标服务器压力也大。8个线程是一个相对保守的经验值再高很容易触发对方限流。另一个实用扩展是图片去重。不同页面下同一张图可能重复出现下载前记录每个文件的MD5发现相同就直接跳过能省大量磁盘空间。音频同理只是计算量会稍微大一点但也值得做。7.3 什么时候值得迁移到Scrapy如果项目变成每周定时增量抓取、需要管理大量任务、要对接数据库或分布式存储那时候再考虑迁移到Scrapy也不迟。Scrapy的Item Pipeline、下载中间件和调度器天然适合做这类工程化任务。迁移过程也不难前面整理好的解析函数可以直接拿来做Spider的解析方法目录归档逻辑改写成Pipeline即可。到这一步你其实已经越过“会写爬虫”的阶段了。从requests脚本到Scrapy工程的迁移更多是工程思维的升级而不是技术本身的颠覆。最后说点实际感受。这类批量下载脚本的最大价值不是省下“右键另存为”的时间而是它逼着你把一个模糊的需求拆成“翻页、解析、下载、归档、容错”五个环节每个环节都有对应的技术选型和排错手段。这种拆解问题的思路比脚本本身更值钱。如果你照着这套逻辑把代码跑通以后再遇到任何采集需求第一反应都不会是到处搜代码而是先看结构、再定方案、然后动手实现。我在实际使用中还有个习惯脚本上线前先小范围跑一两页确认目录结构、文件名、下载文件都能正常打开然后再批量执行。这个习惯帮我省下过无数个返工晚上的时间。如果你也遇到过爬虫下载的怪问题欢迎留言交流。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑