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分鐘比對完。
這個計算量感覺太大,即使寫出來,爲提高效率可能需要其他算法之類的優化。

關於效率處理這裏,並沒有完全想好,也沒有時間測試,暫時就擱置了。
因爲是個人閒暇研究,扔了可能後續就忘了,撿不起來了。
所以這裏把之前的研究過程記錄一下,希望其他有用到的人能得到一些參考。

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