重複ファイルチェック

写真や動画のフォルダを整理したい。なるべくオリジナルを残そうとするあまり、

cp ./orig/photo.jpg ./flower.jpg

のように、リネームしてコピーすることが多い。そんなこんなで結構めちゃくちゃに重複が多い。

指定したディレクトリ以下にある同一バイナリを検出するRubyスクリプトを書いてみた。ちょっとコマンドラインで試したところ、GB単位になるとディスクI/Oの時間がバカにならないようなので、まずファイルサイズで同一バイナリと疑われるものをリストアップして、それらについてだけSHA1でハッシュを取って一致しているものだけを一覧表示するようにしてみた。

実行結果は以下。

% ls -lR
.:
合計 8019
-rw-r--r-- 1 yarb yarb    2698 2009-08-02 14:48 code.txt
-rw-r--r-- 1 yarb yarb     138 2009-08-02 15:37 find.rb
-rwxr-xr-x 1 yarb yarb    1623 2009-08-02 17:39 findsame.rb
-rw-r--r-- 1 yarb yarb 4095108 2009-08-02 15:02 intel.pdf
-rw-r--r-- 1 yarb yarb 4095108 2009-08-02 15:02 intel2.pdf
drwxr-xr-x 2 yarb yarb     128 2009-08-02 17:24 test

./test:
合計 4012
-rw-r--r-- 1 yarb yarb    2698 2009-08-02 17:24 code2.txt
-rw-r--r-- 1 yarb yarb    2698 2009-08-02 16:09 fake.txt
-rw-r--r-- 1 yarb yarb 4095108 2009-08-02 15:51 some.pdf

% ./findsame.rb ./
/home/yarb/src/findsame/code.txt 2698
/home/yarb/src/findsame/test/fake.txt    2698

/home/yarb/src/findsame/intel.pdf        4095108
/home/yarb/src/findsame/intel2.pdf       4095108
/home/yarb/src/findsame/test/some.pdf    4095108

checked 8 files.

% md5sum code.txt test/fake.txt test/code2.txt
d8aa7a8ca3c39561f659afec5f10087a  code.txt
d8aa7a8ca3c39561f659afec5f10087a  test/fake.txt
0174581b5f645602404de5a1aedd19cc  test/code2.txt

上の例ではtest/code2.txtは、数十行のテキストファイルでcode.txtと1文字だけ変えてみたものでファイルサイズは同じ。期待通りの動作をしている。

スクリプトは以下。

#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
require 'find'
require 'digest'

class MyFile
  include Comparable

  attr_reader :sha1, :size

  def initialize(path, size)
    @path = path
    @size = size
    @sha1 = ""
  end

  def <=>(other)
    return @size - other.size
  end

  def calc_sha1
    @sha1 = Digest::SHA1.hexdigest(File.read(@path))
  end

  def to_s
#    @path+"\t"+@size.to_s+"\t"+@sha1+"\n"
    @path+"\t"+@size.to_s+"\n"
  end
end

def listup_files(dirs)
  list = []

  dirs.each do |dir|
    unless (FileTest.directory?(dir))
      raise("you can only specify directory names!")
    end
    Find.find(dir) do |f|
      if (File.ftype(f) == "file") 
        list << MyFile.new(File.expand_path(f), File.size(f))
      end
    end
  end
  list
end

def list_same_size(list)
  redun = []
  tmp = 0.1

  list.sort.each do |f|
    if f.size == tmp
      redun << f.size   # we push all the files with the same size to redun
    end
    tmp = f.size
  end
  redun
end

def show_dup(redun, list)

  redun.uniq.each do |dup_size|

    sha1_list = []
    num = 0
    
    list.find_all {|f| f.size == dup_size}.each do |f|
      sha1_list << f.calc_sha1
      num += 1
    end

    if(num > sha1_list.uniq.size) then
      sha1_list.uniq.each do |sha1|
        next if list.find_all  {|f| f.sha1 == sha1}.size == 1
        list.find_all {|f| f.sha1 == sha1}.each do |f|
          print f
        end
        print "\n"
      end
    end

  end
end

## main

list = listup_files(ARGV) # can be multiple directories
redun = list_same_size(list)
show_dup(redun, list)
print "checked "+list.size.to_s+" files.\n"

これだけ動かすのに2時間ぐらいかかった。Ruby力が足りていなくて、Comparableモジュールをincludeすべきところを、何を間違えたかEnumerableをincludeしたり、FileTestを発見するのに一手間かかったりと、予想したよりずっと時間がかかった。

実際に重複感が強いディレクトリの動画ファイルや写真ファイルはザクザクとリストアップできて、結構いい感じ。容量的には30GB中1GBとかで大したことないけど、無駄に分身がいっぱいあって、それらを引き継いでコピーし続けているようなイヤな感覚が除去できる。「オレのディスクはぐちゃぐちゃだ」という精神衛生上の問題が解決するかもしれない。