如何快速批量检索损坏的图片文件—python开发学习笔记(一)
为了解决几十万量级图片库内异常图片的检测,折腾出了一个基于 python 的图片检测程序。
文章目录
一、混乱的图片仓库
用前一段发现的本地 AI 图片视频搜索引擎 MaterialSearch 整理十几年间积累的几十万张图片时,遇到了一个令人崩溃的场景:有上百张图片报损坏,经过部分核查,很多文件打开后呈现诡异色块,亦或者只有半截图,还有些文件大小为 0 KB。这些损坏的图片零散的散布在数千个子文件夹中,手动一个一个检查无异于大海捞针,累死也搞不定。于是 VS code 启动!!
二、初版方案
用 Pillow 暴力验证,直接用 Pillow verify()
看看是否报错来解决。
代码V1.0方案(基础检测逻辑)
from PIL import Image
def check_img_v1(path):
try:
Image.open(path).verify()
return True
except:
return False
V1.0方案的情况:
1. 误报文件:很多图片会报损坏,但是用图片浏览器打开却十分正常,经过研究之后才知道,原来大量网站在使用一种叫做渐进式JPEG的技术,通过将图像数据分为多个扫描逐层渲染,可以在网速不好时图片先绘制出低分辨率的模糊轮廓,随着数据被下载逐步变为清晰图像(现代图片编码如WEBP、AVIF也都有类似的渐进式加载机制)。这导致需要完整解码才能验证所有扫描数据。因此被verify()
误认为损坏。
2. 漏检文件:未完整下载的图片有时也能通过验证。
3. 性能问题:慢,按照测试计算,10万张图片的检测起码需要耗时4、5小时了。
三、改进方案
经过对 MaterialSearch 日志报错图片的抽查,发现损坏的文件主要是文件不完整导致的半截图,于是我打算改为:先检查文件结尾是否存在结束符来判定图片是否损坏,然后再做进一步检查。
代码V2.0(尾部校验部分代码)
def check_img_v2(path):
with open(path, 'rb') as f:
f.seek(-32, 2) #只用获取文件最后32字节就行
trailer = f.read()
if path.lower().endswith('.jpg'):
return trailer.endswith(b'\xff\xd9') # JPEG的结束符
elif path.lower().endswith('.png'):
return trailer.endswith(b'\xaeB`\x82') # PNG的IEND块
V2.0方案的情况:
1. 捕获到了异常文件 :下载一半的文件确实被检测出来了。
2. 检测了个寂寞 :如果图片附加了元数据,图片文件很可能就不是以\xff\xd9
结尾了,结果就是1000张的测试图片,在尾部检测部分逻辑,有800多张都报了损坏……想快速检查了个寂寞。
四、再次优化
使用img.load()
强制加载所有数据。对渐进式jpeg图片做特殊处理逻辑。
代码V3.0(验证部分的修改)
def check_img_v3(path):
try:
with open(path, 'rb') as f:
img = Image.open(f)
img.load() # 强制加载完整图片
# 特殊处理渐进式JPEG
if img.format == 'JPEG' and 'progressive' in img.info:
img.tile = []
return True
except Exception as e:
print(f"损坏文件: {path} | 错误类型: {type(e).__name__}")
return False
V3.0方案的情况:
1. 漏报率下降了很多
2. 渐进式JPEG兼容处理
3. 打印异常类型方便处理
4. 实际代码中自己傻逼了在verify()
之后调用load()
,导致文件指针不可用,说人话就是:代码逻辑中verify()
做完检查后,就把图片文件关闭了,load()
啥也获取不到。
5. 性能就很一般了,基本和初版差不多的速度。
五、终局之战
又经过一番研究和查证其实 Pillow verify()
对渐进式图片检测是没问题的,误报率并没有我在V1测试时那么高,只是我本地环境的 Pillow 版本不够新而已,但也确实会有漏报。只用load()
也会有漏报,有一点误报可以接受,但是漏报就无法接受了,所以还是需要联合检查。
最终决定采用如下逻辑
1. 先检测文件路径是否存在,收集所有路径。
2. img.verify()
先上
3. 同一个循环内使用img.load()
再来一次检测
4. 并行处理加快处理速度
5. 不在控制台显示扫描 log,毕竟绝大部分图片都是好的,没问题的显示出来无意义,只显示有问题的又很容易看起来像是卡住了,所以用 tqdm 做个进度条。还能大概估计下完成时间。
6. 用 jinja2 做个 html 格式的检测报告,毕竟在终端里复制粘贴起来也不方便。
代码V4.0(完善使用)
实在太长了就放github上了:img_validator.py
命令:python img_validator.py <"目录路径"> [并发数]
ex:python img_validator.py "D:\Download\图片" 8
最终会在脚本的同级目录下生成 html 格式的检测报告image_validation_report.html
- 路径最好用””框住
- 并发数可以不填,默认使用CPU全部线程。除非你用非常大的图,不然全线程跑也费不了多少CPU占用。
- 理论上 10W 张图片在 12 线程下,用时应该半小时都不到,不过实际上受制于仓库盘使用的是 HDD 只能到 20~40files/s,机械硬盘的读出速度跟不上检测速度,特别是到最后20%的时候,速度更是掉到只有个位数了。18W 张图片的文件夹,检测用时接近5个小时,平均速度 ≈ 11 files/s
- windows 和 unix 系统都可用,代码会自动处理两者的路径差异。
为什么会有这么多图片损坏
自己十年前写的P站抓取代码不完善,如果因为网络超时导致图片下载失败,爬虫会重试,但是之前损坏的图片有可能并不会被正确清除(删除部分代码没有正确处理超长文件名和带特殊符号的文件名),虽然带问题代码只使用了从14年到16年这大约一坤年,但是也积累了接近 600 张问题文件。不过倒是挺奇怪的,抓全年龄的部分出现大量这种问题,抓R18的几乎就没出错过,这是又为什么呢,沉思中…………
石樱灯笼
2025-03-21 01:49
我上次遇到图片大范围损坏还是iphone
去年夏天
2025-03-24 10:15
嘛,我遇到最大规模是硬盘炸了……坏扇区上所有东西都坏了。
ACEVS
2025-03-20 10:35
图片大师这么多图.
损坏原因是硬盘坏道还是?
去年夏天
2025-03-20 10:49
下载的时候就有问题,就如最后那一段说的:纯粹是代码问题,图片下载出错超时会重试,但是代码里没有正确删除错误的图片(很多P站的图,作者名+作品名会超级长)所以导致实际上是一张好图 + 一张半截图。
之前因为磁盘坏道损坏过几次(我是炸硬盘体质,每年都要炸一个U盘/硬盘),不过家里NAS和云盘里都还有一份备份,这次属于是最开始的原始文件就有问题……所以备份的也是有问题的版本。
obaby
2025-03-19 20:50
类似的东西我也写过,不过现在不爬图片了
去年夏天
2025-03-19 21:07
主要是:反正是自动定时爬取的,看不看的先存了再说:joy: