重複ファイルがいやだぁ、という状態に陥るのは多分こんな理由だ:
- 「こわいので編集前にバックアップ取っとこうっと」式。
- 「ダウンロード魔、インストール魔の管理不行き届き」式。
というだけではなくて、まぁブラウザのキャッシュであるとか、Windows の場合は「システムファイル」にも重複が多かったりもするのではあるが。
なお、「こわいので編集前にバックアップ取っとこうっと」を「良いこと」として触れ回るのはもうやめにして欲しい。リビジョン管理ツールを日常に取り入れるべきだ。
で、一昔前は、ディスク容量なんてありあまってはいなかった、デショ。全部で 4GB だった頃には「なんて幸せな時代になったものだ、これでいくらでも画像が入る」と思ったものだが、それすらも今は懐かしい。ので、あんまり重複ファイルを気にしなくなってしまった。
そうはいっても、重複ファイルは、ディスクを圧迫することはあまりなくなったとしても、厄介者であることは確かで、「同じ」である間は害はなくても、双方が独自に進化し始めると混乱が始まる。「どれが最新なんだ、いったい?」。
こういうの、探せばいくらでも出てくるはずです。けど例によって「Pythonしか持っていなくてインターネットにも繋げることが出来ない環境で生きる」ためのネタ。
前置きが長くなった。プログラムは以下:
1 # -*- coding: utf-8 -*-
2 #
3 # 重複ファイルを探す、というお題。
4 #
5 # ワタシ、あんまりコメント書かないヒトなんだけれど、さすがに
6 # ダラダラと長くなってしまったのでコメントいっぱい入れた。
7 #
8 # 行儀悪いと思うんで、「良いもの」としての参考にはしないで
9 # くださいな。
10 #
11 import os
12 import sys
13 import multiprocessing
14 import pickle
15 from multiprocessing import Process, Queue
16 from Queue import Empty
17
18 # --------------------------------------------------------
19 #
20 #
21 def _build_key(root, basename):
22 # Windows 固有と思われるが長過ぎるファイル名が「置けて、列挙
23 # 出来るのに読めない」ことが起こる。_build_key なんて関数は
24 # ダルいから作りたくはないが、try/except するのでやむなく。
25 # (UNC 形式なら 16bit 長までパス名を持てるが非UNC形式では
26 # MAXPATH(=240だっけ?)文字を超えられない、という迷惑な仕様の
27 # せい。Windows XP ではエクスプローラすらこのバグを持っていた。)
28 fn = os.path.join(root, basename)
29 try:
30 return os.path.getsize(fn), fn
31 except Exception as e:
32 return None, None
33
34
35 # --------------------------------------------------------
36 #
37 #
38 def _calc_hash(fn):
39 import hashlib
40 import mmap
41
42 # mmapが速い、のかどうかまでは試してはいない。ので、
43 # mmapでなくてもいいのかも。ただ、チャンク読みしないと膨大
44 # なメモリを抱え込んでしまい、速度も出ないことだけは確か。
45 with open(fn, "r+b") as fi:
46 d = hashlib.md5()
47 mm = mmap.mmap(fi.fileno(), 0)
48 chunksz = 16 * 1024
49 for idx in range(0, mm.size(), chunksz):
50 d.update(mm[idx:idx + chunksz])
51 mm.close()
52 hd = d.hexdigest()
53 return hd
54
55
56 # --------------------------------------------------------
57 #
58 #
59 def _child(q_from_par, q_to_par):
60 # 並列の子。親が Queue で渡してくるものをチマチマとハッシュ。
61 # ちなみに、親が put する速度が何万倍も速いのであって、一個処理
62 # している間に Queue には必要なタスクが全部詰まってることになる
63 # と思ふ。
64 result = []
65 while True:
66 try:
67 r = q_from_par.get(block=False, timeout=60*1000)
68 if not r:
69 q_from_par.put(None) # notify for other child
70 break
71 sz, fn = r
72 result.append(
73 (_calc_hash(fn),
74 sz,
75 fn.decode(sys.stdout.encoding)))
76 except IOError:
77 pass # ignore `permission denied'
78 except Empty as e:
79 pass
80
81 # tempfile.mkstemp()で衝突しない名前のファイルに pickle。
82 # 親には Queue を介して書き出したファイル名をお知らせ。
83 import tempfile
84 wrfn = tempfile.mkstemp()[1]
85 pickle.dump(result, open(wrfn, "wb"))
86 q_to_par.put(wrfn)
87
88
89 # --------------------------------------------------------
90 #
91 #
92 if __name__ == '__main__':
93 # ファイルサイズをキーにファイル名収集(直列実行)
94 list_by_size = {}
95 for root, dirs, files in os.walk(sys.argv[1]):
96 for bn in files:
97 # 特定の拡張子だけ、などしたければ、fnmatch でどうぞ。
98 # ここではやってないけど。
99
100 sz, fn = _build_key(root, bn)
101 if not sz:
102 continue
103 if sz not in list_by_size:
104 list_by_size[sz] = []
105 list_by_size[sz].append(fn)
106
107 # 同一ファイルサイズにファイルが一つしかなければ重複ファイルでは
108 # ないので用済み
109 for sz in list_by_size.keys():
110 if len(list_by_size[sz]) == 1:
111 del list_by_size[sz]
112
113 # ここから並列。子は本気でハッシュする。ハッシュ結果は各々 pickle
114 # で書き出し、書き出したファイル名を(q_to_par経由で)返却する。
115 q_from_par, q_to_par = Queue(), Queue()
116 procs = []
117 for i in range(multiprocessing.cpu_count()):
118 # このループは子を作って「タスク待ち開始」させてるだけ。
119 p = Process(target=_child,
120 args=(q_from_par, q_to_par))
121 procs.append(p)
122 p.start()
123 for sz in list_by_size.keys():
124 # このループで、子にハッシュ指示を。
125 for fn in list_by_size[sz]:
126 q_from_par.put((sz, fn))
127 q_from_par.put(None) # エンドマーカ。行儀良く死ね。
128
129 # あとは子がお仕事終えるまで待つ
130 for p in procs:
131 p.join()
132
133 # 子が個々に書き出した結果をマージする。
134 merged = {}
135 while not q_to_par.empty():
136 wrfn = q_to_par.get()
137 result = pickle.load(open(wrfn, "rb"))
138 for line in result:
139 h, sz, fn = line
140 if (sz, h) not in merged:
141 merged[(sz, h)] = []
142 merged[(sz, h)].append(fn)
143 os.unlink(wrfn)
144 # ここでも同一キーに一つしかないものは重複でないので用済み
145 for k in merged.keys():
146 if len(merged[k]) == 1:
147 del merged[k]
148
149 # ここから先はお好きにどうぞ。
150 for sz, h in reversed(sorted(merged.keys())):
151 print(u"%ld, %s, %s" % (sz, h, merged[(sz, h)]))
鬱陶しいほど長くなってしまった。すまん。
まぁまぁ速いです。並列化はしてるけれどそっちの効果ではなくて。あぁ、かなりデカいファイル持ってる場合じゃないと速い実感はないと思います。小さいファイルばかりの場合は速くはないと思う。