python 查找重复文件,以及查找重复视频的一些思路

查找重复文件(文件大小一致、md5相同)

思路很简单:

  1. 找出指定目录及子目录下所有文件
  2. 找出大小重复的
  3. 进一步确认md5也重复的,则认为是重复文件

这里md5,为了加速计算,没有算文件的完整md5。(之前看到过这种算法,忘了在哪里看来的,大概是用于上传文件时,快速判断是否与已有文件对比验证用的)将文件分成256块,每块取前8个字节计算md5,这样能快速计算出一个大概可以用于判断文件唯一性的md5。

完整代码如下:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import re
import time
import hashlib

def main():
    path = 'd:/'
    fp_arr =  file_search(path,repat=r'.*\.mp4')  # 查找文件(文件类型自行填写,不写查所有文件类型)
    du_arr = find_duplicate_file(fp_arr)          # 检查重复
# [fp_arr.remove(l) for j in [i[1:] for i in du_arr] for l in j] # 去重,重复文件只保留第1个即可


def file_search(path='.',repat = r'.*'):
    """
    文件查找:
        文件夹及子文件夹下,所有匹配文件,返回list文件列表,绝对路径形式
    Args:
        path: 文件路径(默认当前路径)
        repat: 文件名正则匹配,不区分大小写(默认匹配所有文件)
        return: 文件列表(绝对路径)
    Returns:
        files_match: 文件列表
    """
    # 获取文件夹,及子文件夹下所有文件,并转为绝对路径
    folders,files = [],[]
    st = time.time()
    repat = '^'+repat+'$'
    # walk结果形式 [(path:文件夹,[dirlist:该文件夹下的文件夹],[filelist:该文件夹下的文件]),(子文件夹1,[子子文件夹],[]),(子文件夹2,[],[])...]
    # 该遍历会走遍所有子文件夹,返回上述形式的结果信息。
    for record in os.walk(path):  
        fop = record[0]
        folders.append(fop)
        for fip in record[2]:
            fip = os.path.abspath(os.path.join(fop,fip)).replace('\\','/')
            files.append(fip)
    # 逐个检查是否符合要求
    files_match = []
    for file in files:
        a = re.findall(repat,file.lower())
        if a:
            files_match+=a
    print('找到{0}个文件'.format(len(files_match)))
    # 返回满足要求的
    return files_match


def fastmd5(file_path,split_piece=256,get_front_bytes=8):
    """
    快速计算一个用于区分文件的md5(非全文件计算,是将文件分成s段后,取每段前d字节,合并后计算md5,以加快计算速度)

    Args:
        file_path: 文件路径
        split_piece: 分割块数
        get_front_bytes: 每块取前多少字节
    """
    size = os.path.getsize(file_path) # 取文件大小
    block = size//split_piece # 每块大小 
    h = hashlib.md5()
    # 计算md5
    if size < split_piece*get_front_bytes: 
        # 小于能分割提取大小的直接计算整个文件md5
        with open(file_path, 'rb') as f:
            h.update(f.read())
    else:
        # 否则分割计算
        with open(file_path, 'rb') as f:
            index = 0
            for i in range(split_piece):
                f.seek(index)
                h.update(f.read(get_front_bytes))
                index+=block
    return h.hexdigest()


def find_duplicate_file(fp_arr):
    """
    查找重复文件

    Args:
        fp_arr:文件列表
    """
    # 将文件大小和路径整理到字典中
    d = {}  # 临时词典 {文件大小1:[文件路径1,文件路径2,……], 文件大小2:[文件路径1,文件路径2,……], ……}
    for fp in fp_arr:
        size = os.path.getsize(fp)
        d[size]=d.get(size,[])+[fp]
    # 列出相同大小的文件列表
    l = [] # 临时列表 [[文件路径1,文件路径2,……], [文件路径1,文件路径2,……], ……]
    for k in d:
        if len(d[k])>1:
            l.append(d[k])
    # 核对大小一致的文件,md5是否相同
    ll = [] # 临时列表 [[文件路径1,文件路径2,……], [文件路径1,文件路径2,……], ……]
    for f_arr in l:
        d = {} # 临时词典 {文件大小1:[文件路径1,文件路径2,……], 文件大小2:[文件路径1,文件路径2,……], ……}
        for f in f_arr:
            fmd5 = fastmd5(f)
            d[fmd5]=d.get(fmd5,[])+[f]
        # 找到相同md5的文件
        for k in d: # 相同大小的文件,核对一下md5是否一致
            if len(d[k])>1:
                ll.append(d[k])
    print('查重完毕,发现{0}处重复'.format(len(ll)))
    for i in ll:
        print(i)
    return ll


if __name__ == '__main__':
    main()

视频查重(部分完成)

思路:对视频进行抽帧,然后比对是否有关键帧的图片指纹是否一致

这里写一下研究过程,实现代码:

  1. 视频抽帧
  2. 图像指纹生成
  3. 找出包含同样图像指纹的视频

这个过程试过一些方案也都记录一下:
曾经考虑subprocess.Popen()执行ffmpeg抽帧,但是太慢了


def external_cmd(cmd, msg_in=''):
    # 将subprocess.call(cmd)包装了一下,这样就能获取到执行cmd命令时,产生的输出内容了。
    try:
        proc = subprocess.Popen(cmd,
                                shell=True,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                )
        stdout_value, stderr_value = proc.communicate(msg_in)
        return stdout_value, stderr_value
    except ValueError as err:
        # log("ValueError: %s" % err)
        return None, None
    except IOError as err:
        # log("IOError: %s" % err)
        return None, None

'''方法一'''
# 1秒抽0.05帧,也就是20s抽1帧,1420s长度视频抽73镇,耗时94s
external_cmd('ffmpeg -i "{0}" -r 0.05 -q:v 2 -f image2 ./%08d.000000.jpg'.format(video_path))


'''方法二'''
# 20s抽1帧,1420s长度视频抽70帧,耗时18s
timeF = 20
for i in range(1,video_duration//timeF):
    h,m,s = (i*timeF)//3600, ((i*timeF)%3600)//60, (i*timeF)%60
    external_cmd('ffmpeg -i "{0}" -ss {1:0=2}:{2:0=2}:{3:0=2} -vframes 1 {4}.jpg'.format(video_path,h,m,s,i)) # 抽取指定时间点起的第一帧


'''方法三'''
# 20s抽1帧,1420s长度视频抽70帧,并压缩到100*100耗时17s(对图像的压缩处理基本不影响速度,时间开销的大头也不是出在文件存储上,而是ffmpeg定位时间为位置然后抽帧本身就慢)
timeF = 20
for i in range(1,video_duration//timeF):
    h,m,s = (i*timeF)//3600, ((i*timeF)%3600)//60, (i*timeF)%60
    hw = '{0}x{0}'.format(100)
    external_cmd('ffmpeg -i "{0}" -ss {1:0=2}:{2:0=2}:{3:0=2} -vframes 1 -s {5} -f image2 {4}.jpeg'.format(video_path,h,m,s,i,hw))

最后选定的还是cv2抽帧

这个是一开始想的,将抽到的帧保存为单张图像,发现还是慢。
'''
# 视频抽帧测试,这种抽帧方式太慢了,1000帧大概45秒长度视频,花费5秒左右
videopath = '01.mp4'
vc = cv2.cv2.VideoCapture(videopath)
if vc.isOpened(): # 是否正常打开
    rval,frame = vc.read()
else:
    rval = False
timeF =1000 # 抽帧频率
c = 1
while rval:
    rval,frame = vc.read()
    if(c%timeF==0):
        cv2.imwrite('{0:0=3}.jpg'.format(c),frame)
    cv2.waitKey(1)
    c+=1
vc.release()
'''

然后换成了这种,不存图像了,直接将抽到图像计算成dhash保存,总算速度上来了。
# 视频,取指定时间点图片,转指定宽高后,计算图像指纹
v = 'c:/users/kindle/desktop/test/01.mp4'
cap = cv2.VideoCapture(v)  #打开视频文件
n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))  #视频的帧数
fps = cap.get(cv2.CAP_PROP_FPS)  #视频的帧率
dur = n_frames / fps  #视频的时间
cap.set(cv2.CAP_PROP_POS_MSEC, (5*1000)) # 跳到指定时间点,单位毫秒
success, image_np = cap.read()  # 返回该时间点的,图像(numpy数组),及读取是否成功
img = Image.fromarray(cv2.cvtColor(image_np,cv2.COLOR_BGR2RGB)) # 转成图像格式
imgrsz = img.resize((100,100)) # 缩放到指定宽高(后来发现是否缩放基本不影响)
# imgrsz.save('5.jpg') # 保存图像
# imgrsz.show()  # 显示图像

计算图像指纹,直接用了现成的模块,imagehash里的dhash

h5 = str(imagehash.dhash(imgrsz)) # 生成图像指纹

在上述基础上,视频转换为图像指纹组的函数基本如下


def video2imageprint(filepath):
    """
    返回整个视频的图片指纹列表
    从3秒开始,每60秒抽帧,计算一张图像指纹
    """
    cap = cv2.VideoCapture(filepath)  ##打开视频文件
    n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))  #视频的帧数
    fps = cap.get(cv2.CAP_PROP_FPS)  #视频的帧率
    dur = n_frames / fps *1000  #视频大致总长度
    cap_set = 3000
    hash_int_arr = []
    while cap_set<dur-3000: # 从3秒开始,每60秒抽帧,计算图像指纹。总长度-3s,是因为有的时候计算出来的长度不准。
        cap.set(cv2.CAP_PROP_POS_MSEC, cap_set)
        # 返回该时间点的,图像(numpy数组),及读取是否成功
        success, image_np = cap.read()
        if success:
            img = Image.fromarray(cv2.cvtColor(image_np,cv2.COLOR_BGR2RGB))  # 转成cv图像格式
            h = str(imagehash.dhash(img))
            hash_arr.append(h) # 图像指纹
        else:
            print('fail',cap_set/1000,filepath)
        cap_set+=1000*60
    cap.release() # 释放视频
    return hash_arr

然后将建立字典,key为图像指纹,value为地址列表。

# shelve用来做python的字典型数据库,并将其存储在磁盘上。
# shelve的key要求必须是字符串,value则可以是任意合法的python数据类型
db = shelve.open('videocheck.db')
# 写入数据库
for h in hash_arr:
    fp_arr = db.get(h, []) # 具有相同指纹的对应的视频路径列表
    if fp_arr==[]:
        db[h]=[filepath]
    elif filepath not in fp_arr:
    	db[h]=db[h]+[filepath] 
db.close()

后面就是检查哪个指纹,对应的地址列表中,大于1个文件。
则说明有多个视频包含该指纹。
为了验证指纹相同的图像是否一致,还写了一个合并图像输出的函数。这个函数写成了这样,是考虑以后可以用作给视频生成多图合并的缩略图玩。

def imgjoin(imgs,tags=[],width_height=(0,0),column_row=(0,0),blank=(0,0,0,0,0,0)):
    '''
    多张图片,合并成一张视频抽帧缩略图合并大图那种。
    可以每张图片上方加注释,也可以文件顶部只加一行注释。
    每张图片宽高,行列间距,四外边距都可以自定义
    args:
        imgs: pil图片数组
        tags: 如果标签数和图片数相同,每张图片上方加文字。如果只有一个标签,则只在图片最顶部加1条文字。
        width_height: 合并后图片中,每张缩略图宽高,如未指定以第一张图标为基准
        column_row:横排和竖排数量
        blank_cr:空白分布(列间,行间,左右,上下,标题,标签)
    return: 返回合并好的图片
    '''
    from PIL import Image,ImageDraw,ImageFont
    # 检查是否符合规则
    if len(imgs)>100:
        print('imgs当前上限100张图合并')
        return ''
    elif imgs==[]:
        print('imgs中没有包含图片,请检查')
        return ''
    elif 1<len(tags)<len(imgs):
        print('tags文字数组和图片对不上,请只输入1条或和图片一样多')
        return ''
    else:
        pass
    # 每行每列个数
    if column_row==(0,0):
        cr = 1
        while len(imgs)>cr**2:
            cr+=1
        column_row=(cr,cr)
    c,r = column_row
    # 调整每张图片到指定宽高,如未指定,以第一张图片宽高为基准:
    if width_height==(0,0):
        width_height = imgs[0].size
    for i,m in enumerate(imgs):
        if m.size!=width_height:
            imgs[i] = m.resize(width_height) # 缩放到指定宽高
    w,h = width_height
    # 空白分布
    bw,bh,blr,btb,btitle,btag = blank # (列间,行间,左右,上下,标题,标签)
    if blank==(0,0,0,0,0,0):
        if len(tags)==1:
            btitle = h
    # 生成输出图像尺寸
    J_width = w*c + bw*(c-1) + blr*2 # 总计图像宽度+列间距+左右边距
    J_height= h*r + bh*(r-1) + btb*2 + btitle + btag*r # 总计图像高度+行间距+顶底边距+标题高度+标签高度
    J_img = Image.new('RGB', (J_width,J_height),(255,255,255))
    draw=ImageDraw.Draw(J_img)
    newfont=ImageFont.truetype('simkai.ttf',12)
    # 合并图像
    for i,m in enumerate(imgs):
        if i==0: # 第一张图
            x,y=blr,btb+btitle+btag # 第一张图左上角位置
        elif i%c==0: # 新的一行
            x,y=blr,y+bh+btag+h
        else:
            x,y=x+bw+w,y
        J_img.paste(m, (x, y, x+w, y+h))
        # 添加文字
        if len(tags)>1:
            draw.text((x,y-btag),tags[i],(0,0,0),font=newfont)
    return J_img

到这里最开始的研究就完成了,
最开始的实现思路,就是上面这样。

================

后来发现图像指纹是有可能不是完全一致的,
而是相似的,还要考虑到相似的图像指纹。

imagehash.dhash算出来的图像指纹,本身的type类型不是字符串。
为了保存,转为字符串后,后续计算两个字符串的相似度,哪怕是很简单的字符串每一位是否与另一字符串每一位相等,数以10w个图像指纹,互相计算都要花费很长时间。
计算两个指纹的相似度,试了几种方法效率,最后发现bin最快,这个方法还是从dhash的官网看来的。
2020-5-13 看到还有一种写法是
num = 1 - (aHash - bHash)/len(aHash.hash)**2
直接imagehash计算,速度和bin的差不多,推荐使用这个。

'''关于dhash相似度比较方法研究,得到bin的方法计算最快,我的家用电脑10w次大概0.057秒。'''
import time
a = 'a1a8739f324eb01c'
b = 'a1a8749f323eb01c'
ai = int(str(a),16)
bi = int(str(b),16)
st = time.time()
# 10w次执行速度,bin方式最快
for i in range(100000):
    # num = [a[j] is b[j] for j in range(16)].count(True)/16 # 0.2097s
    # num = [a[j] == b[j] for j in range(16)].count(True)/16 # 0.2082s
    # num = difflib.SequenceMatcher(None, a,b).ratio()       # 4.2250s
    num = 1-bin(ai^bi).count("1")/64                         # 0.0568s
et = time.time()
print(num,et-st)

接下来的考虑思路就是

  1. 计算得到相似图像指纹
  2. 找到相似指纹对应的视频
  3. 检查视频是否有连续相同地方
  4. 列出相似视频对比缩略图

1秒比对200w个感觉是挺快
但是1000个,长度为1小时的视频,就需要30分钟比对完。
这个计算量感觉太大,即使写出来,为提高效率可能需要其他算法之类的优化。

关于效率处理这里,并没有完全想好,也没有时间测试,暂时就搁置了。
因为是个人闲暇研究,扔了可能后续就忘了,捡不起来了。
所以这里把之前的研究过程记录一下,希望其他有用到的人能得到一些参考。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章