查找重複文件(文件大小一致、md5相同)
思路很簡單:
- 找出指定目錄及子目錄下所有文件
- 找出大小重複的
- 進一步確認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()
視頻查重(部分完成)
思路:對視頻進行抽幀,然後比對是否有關鍵幀的圖片指紋是否一致
這裏寫一下研究過程,實現代碼:
- 視頻抽幀
- 圖像指紋生成
- 找出包含同樣圖像指紋的視頻
這個過程試過一些方案也都記錄一下:
曾經考慮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秒比對200w個感覺是挺快
但是1000個,長度爲1小時的視頻,就需要30分鐘比對完。
這個計算量感覺太大,即使寫出來,爲提高效率可能需要其他算法之類的優化。
關於效率處理這裏,並沒有完全想好,也沒有時間測試,暫時就擱置了。
因爲是個人閒暇研究,扔了可能後續就忘了,撿不起來了。
所以這裏把之前的研究過程記錄一下,希望其他有用到的人能得到一些參考。