sikuliを使って困った点と解決法

最近 sikuli がすごく面白い。
http://www.sikuli.org/

MITのUI学科が作った画面操作マクロツールで、
エディタの未来感もあってすげー面白い

sikuliの特徴

  1. Javaによるクロスプラットフォーム対応(Windows,Mac,Linux対応)
  2. OpenCVを使った画面上の類似画像検索
  3. MIT Licence (商用利用も改造も自由)
  4. Jython(2.5.4rc1)を使った自由度の高いスクリプティング
    1. Jenkinsに連携してGUIテストの自動化も出来る

いくつかの類似のGUIマクロツールと比較しても、
これほど応用が効きそうなツールは見当たらなかった。
こりゃ覚えるっきゃないね!


しかしメリケン製のプログラムらしく、
キッチリ利用するには幾つか乗り越えるべき課題が…

最優先で乗り越えるべき課題

  1. うまく画像を認識しない
  2. 日本語の扱いに難がある

まずはこれらの問題を解決したい。

うまく画像を認識させるには?

sikuliは類似度を元にマッチする場所を決めている。

この類似度はデフォルトで 70 のため、
「パターン設定」で適切な類似度を設定してやる必要がある。


画面上の画像を指定したら、
その画像をクリックして「パターン設定」-> 「マッチングプレビュー」
Similarity を調整する。

この類似度は高めにすると、うまく見つけてくれやすい。

うまく日本語を扱うには?

この日本語の扱いが下手くそなのは
スクリプト基板に使っている Jython( Python ) に起因する問題だ。


つまりPythonでの日本語処理時と同様の対策が必要で、
普通の文字列ではなく、unicode文字列にしてからAPI関数に渡さなければならない。

#↓文字化けするリテラル文字
type("こんにちわ")

#↓文字化けしないリテラル文字
type(u"こんにちわ")

#変数xに入った文字列をutf-8だと仮定して unicode文字列に変換する
type( unicode(x, "utf-8" ))

#cp932(shift-jisのMS拡張)と仮定して変換する場合

type( unicode(x, "cp932" ))

リテラルの頭には u をつけて、
動的に変換する場合はエンコードを指定してunicode関数を通せばいいのだ。

これは普通のPythonでも絶対にハマるポイントなので
十分に把握した上でsikuliを楽しもう

xorのような関数について

概要

今回はxorと似た関数を探してみました。

xorには以下のような性質があります。

f(x,y) = z の場合以下の式すべてが成り立つ
f(y,x) = z
f(x,z) = y
f(y,z) = x

このような特徴があると「暗号文と鍵から平文」がつくれるので大変便利なのです。

eq関数

eq関数は、双方のbitが等しい時に1になるような関数です。
xorとちょうど反対の出力をします。

#xorの真理値表
  0 1
0 0 1
1 1 0
#eqの真理値表
  0 1
0 1 0
1 0 1

これもxorと同様な性質がありました
しかしxorと同様に(2^n)の法の中でしか成り立ちません。

補数の余りを使う関数

任意の法 R を利用できる関数として
補数と割り算の余りを利用する方法がありました。

#Rを法とする
z = (R-x + R-y) mod R
#以下でも同様
z = (-x-y) mod R

これは結構便利そうです。
あとでこれを使った方陣を作ってみます。

方陣の比較

それぞれの関数を使った0から7までの方陣を比較してみました。

方陣の読み方は以下の様な感じ。
xとyの軸を交換しても大丈夫です。

#xorの方陣
  0 1 2 3 4 5 6 7
0 0 1 2 3 4 5 6 7
1 1 0 3 2 5 4 7 6
2 2 3 0 1 6 7 4 5
3 3 2 1 0 7 6 5 4
4 4 5 6 7 0 1 2 3
5 5 4 7 6 1 0 3 2
6 6 7 4 5 2 3 0 1
7 7 6 5 4 3 2 1 0
#eqの方陣
  0 1 2 3 4 5 6 7
0 7 6 5 4 3 2 1 0
1 6 7 4 5 2 3 0 1
2 5 4 7 6 1 0 3 2
3 4 5 6 7 0 1 2 3
4 3 2 1 0 7 6 5 4
5 2 3 0 1 6 7 4 5
6 1 0 3 2 5 4 7 6
7 0 1 2 3 4 5 6 7
#補数と余りの方陣
  0 1 2 3 4 5 6 7
0 0 7 6 5 4 3 2 1
1 7 6 5 4 3 2 1 0
2 6 5 4 3 2 1 0 7
3 5 4 3 2 1 0 7 6
4 4 3 2 1 0 7 6 5
5 3 2 1 0 7 6 5 4
6 2 1 0 7 6 5 4 3
7 1 0 7 6 5 4 3 2

内容は違うのに同じ性質があるのは不思議ですね。
他にも同じ性質の関数がありそうです。

検証用スクリプト

方陣の描画と検証に使ったスクリプトを置いときます。
他の関数を調査するときもつかえるはずです。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

charset = u'01234567'
max = len(charset)

print " ",
for x in range(max):
    print charset[x],
print

def f(x,y):
#    z = (x^y)   #xor
#    z = (~(x ^ y) & (max-1)) #eq
    z = (-x-y)%max  #modr
    return z

for x in range(max):
    print charset[x],
    for y in range(max):
        print charset[f(x,y)],
    print

#check
for x in range(max):
    for y in range(max):
        z = f(x,y)
        if (f(y,x) <> z or
            f(x,z) <> y or
            f(y,z) <> x ):
            print "error %s %s" % (charset[x],charset[y])

ファイル変更監視

ファイル/フォルダの変更のロギングをしてみた。

System.IO.FileSystemWatcherクラス のサンプルを参考に、
実行ファイルと同じディレクトリにログを吐く機能を追加した。

こんなログがとれる。

20130510-12:05:53.207 変更 : D:\share\hai
20130510-12:58:07.435 作成 : D:\share\新しいテキスト ドキュメント.txt
20130510-12:58:11.990 変更 : D:\share\新しいテキスト ドキュメント.txt
20130510-12:58:11.990 変更 : D:\share\新しいテキスト ドキュメント.txt


何かに使えるはず?

http://msdn.microsoft.com/ja-jp/library/system.io.filesystemwatcher%28v=vs.80%29.aspx

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Security.Permissions;

namespace FileSystemWatcher
{
    class Program
    {

        static void Main(string[] args)
        {
            Run(args);
        }

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public static void Run(string[] args)
        {
            if (args.Length < 1 || !Directory.Exists(args[0]))
            {
                Console.Error.WriteLine("usege: FileSystemWatcher <target dir>");
                return;
            }

            //watcher初期化
            System.IO.FileSystemWatcher watcher = new System.IO.FileSystemWatcher();

            watcher.Path =args[0];
            watcher.Filter = "";

            watcher.NotifyFilter =
                ( System.IO.NotifyFilters.LastWrite
                | System.IO.NotifyFilters.FileName
                | System.IO.NotifyFilters.DirectoryName);

            //再帰的にチェックする
            watcher.IncludeSubdirectories = true;

            watcher.Changed += new FileSystemEventHandler(OnChanged);
            watcher.Created += new FileSystemEventHandler(OnChanged);
            watcher.Deleted += new FileSystemEventHandler(OnChanged);
            watcher.Renamed += new RenamedEventHandler(OnRenamed);

            // Begin watching.
            watcher.EnableRaisingEvents = true;

            // Wait for the user to quit the program.
            Console.WriteLine("Press \'q\' to quit the sample.");
            while (Console.Read() != 'q') ;
        }

        /// <summary>
        /// ファイル変更イベント
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private static void OnChanged(object source, FileSystemEventArgs e)
        {
            string logText = "";
            switch (e.ChangeType)
            {
                case System.IO.WatcherChangeTypes.Changed:
                    logText = "変更 : " + e.FullPath;
                    break;
                case System.IO.WatcherChangeTypes.Created:
                    logText = "作成 : " + e.FullPath;
                    break;
                case System.IO.WatcherChangeTypes.Deleted:
                    logText = "削除 : " + e.FullPath;
                    break;
            }
            logging(logText);
        }

        /// <summary>
        /// ファイル移動イベント
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private static void OnRenamed(object source, RenamedEventArgs e)
        {
            string logText = "移動 : " + e.OldFullPath + " => " + e.FullPath;
            logging(logText);
        }

        /// <summary>
        /// ログファイルに1行書き込む
        /// 文字コードにはshift-jis(cp932)を利用する
        /// </summary>
        /// <param name="line"></param>
        public static void logging(string line)
        {
            DateTime dtmNow = DateTime.Now;
            line = dtmNow.ToString("yyyyMMdd-HH:mm:ss.") + String.Format("{0:D3}", dtmNow.Millisecond) + " " + line;
            Console.WriteLine(line);
            System.IO.File.AppendAllText(GetLogFilePath(), line + Environment.NewLine, Encoding.GetEncoding(932));
        }

        /// <summary>
        /// ログファイルのパスを返す
        /// 必要なディレクトリも全て作成する
        /// 
        /// "[実行ファイルのパス] / log / yyyyMMdd / yyyyMMdd-HH.log"
        /// </summary>
        /// <returns></returns>
        public static string GetLogFilePath()
        {
            string root = System.IO.Path.Combine(GetAppPath(), "log");
            string dayDir = System.IO.Path.Combine(root, DateTime.Now.ToString("yyyyMMdd"));
            string filePath = System.IO.Path.Combine(dayDir, DateTime.Now.ToString("yyyyMMdd-HH") + ".log");
            System.IO.Directory.CreateDirectory(dayDir);
            return filePath;
        }

        public static string GetAppPath()
        {
            return System.IO.Path.GetDirectoryName(
                System.Reflection.Assembly.GetExecutingAssembly().Location);
        }
    }
}

伝書鳩用暗号化プロトコル(草案)

概要

コンピュータなしで使えそうな、
2点間の通信で鍵交換を含めて伝書鳩で行うための
そこそこ強度のありそうな暗号化手段ってどんなものか考えてみた

手順

  1. 文字のXOR方陣を事前に用意し共有
  2. 鍵伸長方陣を事前に用意し共有
  3. DH 鍵交換で共有数を作成
  4. 鍵伸長方陣で共有数から共通鍵を生成
  5. XOR方陣を利用し共通鍵と平文からCBC方式で暗号文を生成

要約すると、DH鍵交換した共有数と暗号表を組み合わせた方法となる。

XOR方陣について

暗号化の時に利用するxor演算を簡単にするため
事前に方陣を用意しておく、
縦軸の文字と横軸の文字からxorの結果を求められる便利な方陣だ。
他にも何かにつかえるはず?

xor計算は以下のような特性がある

a^b=c の時以下の式が全て成り立つ
b^a=c
a^c=b
c^a=b
b^c=a
c^b=a

注意点として文字数が 2^n 個にする事が必要

#XOR方陣スクリプト

chars="abcdefghijklmnopqrstuvwxyz012345"
chars_len = len(chars)

for x in range(chars_len):
  for y in range(chars_len):
    idx = (x ^ y)
    print "%s\t" % chars[idx],
  print

このスクリプトでは以下のような出力が得られる。

abcdefghijklmnopqrstuvwxyz012345
badcfehgjilknmporqtsvuxwzy103254
cdabghefklijopmnstqrwxuv01yz4523
dcbahgfelkjiponmtsrqxwvu10zy5432
efghabcdmnopijkluvwxqrst2345yz01
fehgbadcnmpojilkvuxwrats3254zy10
ghefcdabopmnklijwxuvstqr452301yz
hgfedcbaponmlkjixwvutsrq543210zy
ijklmnopabcdefghyz012345qrstuvwx
jilknmpobadcfehgzy103254rqtsvuxw
klijopmncdabghef01yz4523stqrwxuv
lkjiponmdcbahgfe10zy5432tsrqxwvu
mnopijklefghabcd2345yz01uvwxqrst
nmpojilkfehgbadc3254zy10vuxwrqts
opmnklijghefcdab452301yzwxuvstqr
ponmlkjihgfedcba543210zyxwvutsrq
qrstuvwxyz012345abcdefghijklmnop
rqtsvuxwzy103254badcfehgjilknmpo
stqrwxuv01yz4523cdabghefklijopmn
tsrqxwvu10zy5432dcbahgfelkjiponm
uvwxqrst2345yz01efghabcdmnopijkl
vuxwrqts3254zy10fehgbadcnmpojilk
wxuvstqr452301yzghefcdabopmnklij
xwvutsrq543210zyhgfedcbaponmlkji
yz012345qrstuvwxijklmnopabcdefgh
zy103254rqtsvuxwjilknmpobadcfehg
01yz4523stqrwxuvklijopmncdabghef
10zy5432tsrqxwvulkjiponmdcbahgfe
2345yz01uvwxqrstmnopijklefghabcd
3254zy10vuxwrqtsnmpojilkfehgbadc
452301yzwxuvstqropmnklijghefcdab
543210zyxwvutsrqponmlkjihgfedcba

それと、これはただの方陣なので、
秘匿する必要はまったくない。


鍵伸長方陣について

共有数から共有鍵を作成するためのテーブルを用意する。
文字をランダムに入れ替えただけの方陣で、
本番ではトランプをシャッフルするような形で作成する。

import random

chars="abcdefghijklmnopqrstuvwxyz012345"
chars_len = len(chars)

rowcount = 17

for x in range(rowcount):
  myrow = list(chars)
  random.shuffle(myrow)
  for y in range(chars_len):
    print "%s\t" % myrow[y],
  print

出力は以下のような形になる。
#乱数を利用するため出力は毎回異なる

fxygu5mdv2hjne0bslp43owtiz1qackr
lzph4wxsn2kbiye3vdagj05qrftu1ocm
mghbcl1en0x2ys4qjpkiftrwouv3zd5a
s0odu3ye2rhc1gpxtj5bwivnzmafqkl4
jyvm4shlfak0guqx5tcr23wzb1iendpo
3tpymfdwo42xuehvgiqnj5zkr0slab1c
quhzty0cwl4km5p3ajeio1dfbnvs2gxr
a3psylxjonw4v0izmebug25rht1kfcqd
ludxo4rpifsgvkmtwy1a5hc32j0beznq
yzspgntmhqrfx1a3ouil0edb5k42jcvw
vu0sjxrbwqinpgktmc153hdaf4olyze2
ez43mdj1yqtwl0igcksrxhv2ufaonp5b
earubq3l2gdjmx0tsnychv4wpof1kz5i
hui0zeb53rsqvtg1ajmpxdkln2cyfow4
ut15n3gwmyhrosqcxeak0fvjz24dipbl
oa5xde0qfjsrlgp4bmwh1c32ykutnvzi
pomg0lr4xkaiw1hn2e5sbycdvjfqzu3t

これも、ただの乱数テーブルなので秘匿の必要はあまりない?
漏れてもそこまで深刻ではないはず

DH鍵交換について

今回の暗号化方式の要。
通信内容がすべて傍受されていても、
2者が秘密の共有数を持てる
現代でも利用されているDH鍵交換について。

http://d.hatena.ne.jp/rikunora/20120514/p1
http://www.techscore.com/tech/Java/JavaSE/JCE/11/

鍵交換の手順

1. aとbはそれぞれ公開の数 G P と 秘密の数 A B を決める
2.Aを直接送る代わりに「G^A mod P」を送る
 Bを直接送る代わりに「G^B mod P」を送る
3.aは (G^B mod P)^A mod P を計算
 bは (G^A mod P)^B mod P を計算
4. お互いの手元に同じ数が共有される

#Pはなるべく大きな素数、GはPより小さな自然数
#AとBはそれぞれpより小さくする

暗号鍵の生成について

共有数 と 文字数 の剰余算と除算を行い
共有数から数列を取得する。

例: 共有数が 1234 の場合

1回目 1234 / 32 = 38 あまり 18
2回目 38   / 32 = 1 あまり 6
3回目 1    / 32 = 0 あまり 1

このように{18,6,1}の数列を作成

この数列と鍵伸長方陣を利用して暗号鍵を作る
0番目から数える事がポイントになる。

1行目の18番目は"p"
2行目の6番目は"x"
3行目の1番目は"g"

以上で p x g の暗号鍵が得られた。

暗号文の生成

XOR方陣を利用し共通鍵と平文からCBC方式で暗号文を生成する。
以下に擬似コードを書く

C[]     //暗号文
M[]     //平文
K[]     //鍵
L       //鍵長

暗号化
C[i] = M[i] ^ C[i-1] ^ K[i % L]

復号化
M[i] = C[i] ^ K[i % L] ^ C[i-1]

例として"hellworld"を暗号化してみる。
鍵は先程の"pxg"を利用する

#暗号化
h ^ p             = i
e ^ x ^ i = t ^ i = 1
l ^ g ^ 1 = n ^ 1 = w
l ^ p ^ x = e ^ w = s
w ^ x ^ t = b ^ s = t
o ^ g ^ s = i ^ t = 1
r ^ p ^ 0 = 4 ^ 1 = f
l ^ x ^ e = 2 ^ f = z
d ^ g ^ y = f ^ z = 2

暗号文は "i1wst1fz2" になった。
復号化も行なってみる

i ^ p             = h
1 ^ x ^ i = m ^ i = e
w ^ g ^ 1 = q ^ 1 = l
s ^ p ^ w = 3 ^ w = l
t ^ x ^ s = e ^ s = w
1 ^ g ^ t = 3 ^ t = o
f ^ p ^ 1 = k ^ 1 = r
z ^ x ^ f = o ^ f = l
2 ^ g ^ z = 0 ^ z = d

無事に復号化もできた

win用 重複排除スクリプト 2

概要

いろいろ修正

  1. 複数ファイルのD&Dに対応
  2. オープン出来ないファイルをスキップ
  3. 処理対象の拡張子を限
  4. kickstartスクリプトで複数フォルダのD&Dに対応
  5. 処理結果を日本語に
  6. 膨大なファイル数に対応するため 1号をベースに作り直し

hashmargedb.py

#!/usr/bin/python
# -*- coding: cp932 -*-

import sqlite3,sys,os,pickle,hashlib,base64
import win32file,locale,re

def target_patterns():
  return [
    ".*\.7z$",".*\.aif$",".*\.aifc$",".*\.aiff$",".*\.arj$"
    ,".*\.asf$",".*\.asx$",".*\.au$",".*\.avi$",".*\.bmp$"
    ,".*\.bz2$",".*\.bzip2$",".*\.cab$",".*\.cda$",".*\.chm$"
    ,".*\.chw$",".*\.cpio$",".*\.cramfs$",".*\.deb$",".*\.dmg$"
    ,".*\.dvr-ms$",".*\.exe$",".*\.flac$",".*\.gif$"
    ,".*\.gz$",".*\.gzip$",".*\.hxs$",".*\.ico$",".*\.iso$"
    ,".*\.ivf$",".*\.jpeg$",".*\.jpg$",".*\.lha$",".*\.lzh$"
    ,".*\.lzma$",".*\.m1v$",".*\.m3u$",".*\.mbr$",".*\.mid$"
    ,".*\.midi$",".*\.mov$",".*\.mp2$",".*\.mp3$",".*\.mp4$"
    ,".*\.mpa$",".*\.mpe$",".*\.mpeg$",".*\.mpg$",".*\.mpv2$"
    ,".*\.msi$",".*\.png$",".*\.qt$",".*\.ra$"
    ,".*\.ram$",".*\.rar$",".*\.rm$",".*\.rmi$",".*\.rpm$"
    ,".*\.snd$",".*\.squashfs$",".*\.swm$",".*\.tar$",".*\.taz$"
    ,".*\.tbz$",".*\.tbz2$",".*\.tgz$",".*\.tiff$",".*\.wav$"
    ,".*\.wax$",".*\.wim$",".*\.wm$",".*\.wma$",".*\.wmd$"
    ,".*\.wms$",".*\.wmv$",".*\.wmz$",".*\.wpl$",".*\.wvx$"
    ,".*\.xar$",".*\.xz$",".*\.z$",".*\.zip$"
    ,".*.pdf$"
  ]

pattern_re = []
for pat in target_patterns():
  pattern_re.append(re.compile(pat))

def matches_file_pattern(fname):
  for p_re in pattern_re:
    if p_re.match(fname):
      return True
  return False

def get_read_handle (filename):
  if os.path.isdir(filename):
    dwFlagsAndAttributes = win32file.FILE_FLAG_BACKUP_SEMANTICS
  else:
    dwFlagsAndAttributes = 0
  return win32file.CreateFile (
    filename,
    win32file.GENERIC_READ,
    win32file.FILE_SHARE_READ,
    None,
    win32file.OPEN_EXISTING,
    dwFlagsAndAttributes,
    None
  )

def get_unique_id (hFile):
  (
    attributes,
    created_at, accessed_at, written_at,
    volume,
    file_hi, file_lo,
    n_links,
    index_hi, index_lo
  ) = win32file.GetFileInformationByHandle (hFile)
  return volume, index_hi, index_lo

def files_are_equal (filename1, filename2):
  hFile1 = get_read_handle (filename1)
  hFile2 = get_read_handle (filename2)
  are_equal = (get_unique_id (hFile1) == get_unique_id (hFile2))
  hFile2.Close ()
  hFile1.Close ()
  return are_equal

class HmDBO:
  con = None
  def __init__(self,dbpath):
    self.con = sqlite3.connect(dbpath)
    if not self.existFileTable():
      self.createFileTable()
    if not self.existCacheTable():
      self.createCacheTable()

  def commit(self):
    self.con.commit()

  def close(self):
    self.con.close()
    self.con = None

  #
  #filehash
  #
  def existFileTable(self):
    sql = u"""SELECT * FROM sqlite_master WHERE type='table' AND name='filehash';"""
    c = self.con.execute(sql)
    for o in c:
      return True
    return False

  def createFileTable(self):
    sql = u"""create table filehash(path varchar(64) primary key, hash varchar(64));"""
    self.con.execute(sql)

  def insertFile(self,path,hash):
    sql = u"""insert into filehash values (?,?); """
    hxpath = hashlib.sha256(path).hexdigest()
    self.con.execute(sql,(hxpath,hash))
  
  def selectFile(self,path):
    sql = u"""select hash from filehash where path=?;"""
    hxpath = hashlib.sha256(path).hexdigest()
    c = self.con.execute(sql,(hxpath,))
    for o in c:
      return o[0]
    return None

  #
  #linkcache
  #
  def existCacheTable(self):
    sql = u"""SELECT * FROM sqlite_master WHERE type='table' AND name='linkcache';"""
    c = self.con.execute(sql)
    for o in c:
      return True
    return False

  def createCacheTable(self):
    sql = u"""create table linkcache(hash varchar(64) primary key, path text);"""
    self.con.execute(sql)

  def insertCache(self,hash,path):
    sql = u"""insert into linkcache values (?,?)"""
    b64path = base64.b64encode(path)
    self.con.execute(sql,(hash,b64path))

  def selectCache(self,hash):
    sql = u"""select path from linkcache where hash=?;"""
    c = self.con.execute(sql,(hash,))
    for o in c:
      return base64.b64decode(o[0])
    return None

  def deleteCache(self,hash):
    sql = u"""delete from linkcache where hash=?;"""
    self.con.execute(sql,(hash,))

def calcSHA256(path):
  sha = hashlib.sha256()
  fp = open(path,'r')
  while True:
    cache = fp.read(65536)
    if not cache: break
    sha.update(cache)
  fp.close()
  return sha.hexdigest()

class TeeErr:
  fp = None
  def __init__(self,path):
    self.fp = open(path,'w')
  
  def write(self,line):
    self.fp.write(line)
    sys.stderr.write(line)
  
  def close(self):
    self.fp.close()

def hashmarge(dbpath,log):
  filecount=0
  margecount=0
  registercount=0
  skip=0
  reduction=0
  
  dbo = HmDBO(dbpath)
  tee = TeeErr(log)

  for rwln in iter(sys.stdin.readline,""):
    path = rwln.rstrip('\n')
    if os.path.isfile(path) and matches_file_pattern(os.path.basename(path)):
      tee.write(path + '...')
      filecount += 1
      try:
        #filehash
        hash = dbo.selectFile(path)
        if hash == None:
          registercount += 1
          tee.write('register ')
          hash = calcSHA256(path)
          dbo.insertFile(path,hash)
        #linkcache
        cache = dbo.selectCache(hash)
        if cache == None:
          dbo.insertCache(hash,path)
        elif os.path.isfile(cache) and not files_are_equal(cache, path):
          tee.write('marge ')
          os.remove(path)
          win32file.CreateHardLink (path, cache, None)
          reduction += os.path.getsize(path)
          margecount += 1
      except:
        skip+=1
        tee.write('skip ')

      tee.write('done\n')

  dbo.commit()
  dbo.close()

  tee.write('処理ファイル数                 = %s\n' % str(filecount))
  tee.write('データ内容を登録したファイル数 = %s\n' % str(registercount) )
  tee.write('内容を共有させたファイル数     = %s\n' % str(margecount))
  tee.write('処理をスキップしたファイル数   = %s\n' % str(skip))
  tee.write('開放されたディスクサイズ       = %s byte\n' % locale.currency(reduction, symbol=False, grouping=True))
  
  tee.close()

def main():
  if len(sys.argv) < 3:
    exit()
  else:
    hashmarge(sys.argv[1],sys.argv[2])

if __name__ == '__main__':
  #locale.setlocale(locale.LC_NUMERIC, 'ja_JP')
  locale.setlocale(locale.LC_ALL, '')
  main()

kickstart.bat

IF "%1" EQU "" (
  echo "ファイルをドロップしてください"
) ELSE (
  ( for %%a in (%*) do dir /s/b %%a ) | C:\python27\python "%~dp0hashmargedb.py" "%~dp0hash.db" "%~dp0output.log"
)
pause

あとがき

これで一応人に使わせられる状態になったかな?

追記

doc,xls,ppt も処理対象になっていたので取り除いた

win用 重複排除スクリプト

概要

前々回のエントリで紹介した 重複排除スクリプト2号を windows対応させた。

動作に必要なランタイム

python 2.7 (32bit版)

Python Release Python 2.7.3 | Python.org
ここからWindows x86 MSI Installer (2.7.3) よりダウンロードする

win32extensions for python

http://starship.python.net/~skippy/win32/Downloads.html
ここから、python 2.7(32bit版)対応のバイナリをダウンロードする。

dedupwin.py

#!/usr/bin/python

# -*- coding: utf-8 -*-

import sqlite3,sys,os,pickle,hashlib,base64

import win32file

def get_read_handle (filename):
  if os.path.isdir(filename):
    dwFlagsAndAttributes = win32file.FILE_FLAG_BACKUP_SEMANTICS
  else:
    dwFlagsAndAttributes = 0
  return win32file.CreateFile (
    filename,
    win32file.GENERIC_READ,
    win32file.FILE_SHARE_READ,
    None,
    win32file.OPEN_EXISTING,
    dwFlagsAndAttributes,
    None
  )

class dmyStat:
  st_size = 0
  st_dev = 0
  st_ino = 0
  
  def __init__(self, path):
    hFile = get_read_handle (path)
    (
      attributes,
      created_at, accessed_at, written_at,
      volume,
      file_hi, file_lo,
      n_links,
      index_hi, index_lo
    ) = win32file.GetFileInformationByHandle (hFile)
    hFile.Close ()
    self.st_size = file_hi * (2**32) + file_lo
    self.st_dev = volume
    self.st_ino = index_hi * (2**30) + index_lo

class HmDBO:
  con = None
  def __init__(self,dbpath):
    self.con = sqlite3.connect(dbpath)
    if not self.existFileInfoTable():
      self.createFileInfoTable()

  def commit(self):
    self.con.commit()

  def close(self):
    self.con.close()
    self.con = None

#
#fileinfo
#
  def existFileInfoTable(self):
    sql = u"""SELECT * FROM sqlite_master WHERE type='table' AND name='fileinfo';"""
    c = self.con.execute(sql)
    for o in c:
      return True
    return False
  
  def createFileInfoTable(self):
    sql = u"""
      create table fileinfo(
        hxpath varchar(64) primary key,
        b64path text,
        size integer,
        hash varchar(64),
        st_dev integer,
        st_ino integer
        );"""
    self.con.execute(sql)
    sql = u"""create index sizeindex on fileinfo(size);"""
    self.con.execute(sql)
    sql = u"""create index devindex on fileinfo(st_dev,st_ino);"""
    self.con.execute(sql)
    sql = u"""create index size2index on fileinfo(size,hash,st_dev);"""
    self.con.execute(sql)
    sql = u"""create index inoindex on fileinfo(st_ino);"""
    self.con.execute(sql)
    sql = u"""create index hashindex on fileinfo(hash,st_dev);"""
    self.con.execute(sql)

  def insertFileInfo(self,path,hash,st):
    sql = u"""insert into fileinfo values (?,?,?,?,?,?); """
    hxpath = hashlib.sha256(path).hexdigest()
    b64path = base64.b64encode(path)
    self.con.execute(sql,(hxpath,b64path,st.st_size,hash,st.st_dev,st.st_ino))
  
  def updateFileInfo(self,path,hash,st):
    sql = u"""update fileinfo set size = ?, hash = ?,st_dev = ?,st_ino = ? where hxpath = ?; """
    hxpath = hashlib.sha256(path).hexdigest()
    self.con.execute(sql,(st.st_size,hash,st.st_dev,st.st_ino,hxpath))
  
  def deleteFileInfo(self,path):
    sql = u"""delete from fileinfo where hxpath = ?;"""
    hxpath = hashlib.sha256(path).hexdigest()
    self.con.execute(sql,(hxpath,))
    
  def selectFileInfo(self,path):
    sql = u"""select size,hash,st_dev,st_ino from fileinfo where hxpath=?;"""
    hxpath = hashlib.sha256(path).hexdigest()
    c = self.con.execute(sql,(hxpath,))
    for o in c:
      return (o[0],o[1],o[2],o[3])
    return (None,None,None,None)
  
  def searchHashCode(self,st):
    sql = u"""
      select b64path,hash from fileinfo
      where st_dev = ?
        and st_ino = ?
        and hash is not null ;""";
    c = self.con.execute(sql,(st.st_dev,st.st_ino))
    ret = list()
    for o in c:
      path =  base64.b64decode(o[0])
      hash = o[1]
      if os.path.isfile(path):
        return hash
      else:
        self.deleteFileInfo(path)
    return None

  def listupFileSize(self):
    sql = u"""
      select size from fileinfo 
      group by size 
      having count(size) > 1 
         and count(st_ino) > 1 ;"""
    c = self.con.execute(sql)
    for size in c:
      yield size[0]

  def searchFileFromSize(self,size):
    sql = u""" select b64path from fileinfo where size = ? ; """
    c = self.con.execute(sql,(size,))
    for o in c:
      yield base64.b64decode(o[0])
  
  def searchFileDupHash(self):
    sql = u"""
      select hash,st_dev from fileinfo
      group by size,hash,st_dev
      having count(st_ino) > 1
    ;"""
    c = self.con.execute(sql)
    for o in c:
      yield (o[0],o[1])
  
  def searchFileFromHashOne(self,hash,st_dev):
    sql = u"""
      select b64path,st_ino from fileinfo
       where hash   = ?
         and st_dev = ?  ; """
    c = self.con.execute(sql,(hash,st_dev))
    for o in c:
      return (base64.b64decode(o[0]), o[1])
    return None
  
  def searchDedupFiles(self,hash,st_dev,st_ino):
    sql = u"""
      select b64path from fileinfo
        where hash  = ?
          and st_dev = ?
          and st_ino != ? ;"""
    c = self.con.execute(sql,(hash,st_dev,st_ino))
    for o in c:
      yield base64.b64decode(o[0])

_registercount = 0

def calcSHA256(dbo,path,st):
  global _registercount
  uhash = dbo.searchHashCode(st)
  if uhash != None :
    return uhash
  sys.stderr.write('.')
  _registercount += 1
  sha = hashlib.sha256()
  fp = open(path,'r')
  while True:
    cache = fp.read(65536)
    if not cache: break
    sha.update(cache)
  fp.close()
  return sha.hexdigest()

def updateCache(dbo,path,calcHash):
  size,hash,st_dev,st_ino = dbo.selectFileInfo(path)
# st = os.stat(path)
  st = dmyStat(path)
  
  if size == None:
    if calcHash and hash == None:
      hash = calcSHA256(dbo,path,st)
      dbo.insertFileInfo(path,hash,st)
    else:
      dbo.insertFileInfo(path,None,st)
  else:
    if (calcHash and hash == None) or hash != None and (size != st.st_size or st_dev != st.st_dev or st_ino != st.st_ino):
      hash = calcSHA256(dbo,path,st)
      dbo.updateFileInfo(path,hash,st)
  return (st.st_size, hash, st.st_dev, st.st_ino)

def releaseVer():
  return True

def hashmarge(dbpath):
  margecount=0
  filecount=0
  global _registercount
  _registercount=0
  
  dbo = HmDBO(dbpath)
  #register size
  for rwln in iter(sys.stdin.readline,""):
    path = rwln.rstrip('\n')
    if os.path.isfile(path):
      sys.stderr.write('stat ' + path + '...')
      filecount += 1
      #fileinfo
      updateCache(dbo,path,False)
      sys.stderr.write(' done\n')
  dbo.commit()
  #update hash
  updates = list()
  for size in dbo.listupFileSize():
    for path in dbo.searchFileFromSize(size):
      updates.append(path)
  updates.sort()
  for path in updates:
    sys.stderr.write('read ' + path + '...')
    if os.path.isfile(path):
      updateCache(dbo,path,True)
      sys.stderr.write(' done\n')
    else:
      dbo.deleteFileInfo(path)
      sys.stderr.write(' not found\n')
  dbo.commit()
  
  #linking
  for hash,st_dev in dbo.searchFileDupHash():
    (centerPath,c_ino) = dbo.searchFileFromHashOne(hash,st_dev)
    print "hash=%s,st_dev=%s,st_ino=%s,centerPath=%s" % (hash,st_dev,c_ino,centerPath)
    for tpath in dbo.searchDedupFiles(hash,st_dev,c_ino):
      sys.stderr.write('link ' + tpath + '...')
      if releaseVer():
        os.remove(tpath)
        #os.link(centerPath,tpath)
        win32file.CreateHardLink (tpath, centerPath, None)
        updateCache(dbo,tpath,True)
      margecount += 1
      sys.stderr.write(' done\n')
 
  dbo.commit()
  dbo.close()

  sys.stderr.write('files  = ' + str(filecount) + '\n')
  sys.stderr.write('register = ' + str(_registercount) + '\n')
  sys.stderr.write('marges = ' + str(margecount) + '\n')


def main():
  if len(sys.argv) < 2:
    exit()
  elif len(sys.argv) == 2:
    hashmarge(sys.argv[1])

if __name__ == '__main__':
  main()

このファイルを適当な位置に設置する

起動用バッチファイル

windowsでも使いやすいように以下のようなバッチファイルを用意した

dir /s/b %1 | C:\python27\python "%~dp0dedupwin.py" "%~dp0dedup.db"

pause

あとはこのbatファイルに、重複排除したいフォルダをドラック&ドロップするだけで実行できる

(2013-03-07) 修正

・file_hiのmsbが1の場合に例外終了していたので修正
・バッチファイルにスペースが含まれるパスがD&Dされた場合の修正

重複排除スクリプト 1号

概要

ファイル数がめちゃくちゃ多い場合にも対応する
よりシンプルな重複排除スクリプトについて。

動作概要
  1. 渡されたファイルをキャッシュから探し、無ければ内容を読む。
  2. 内容が同じファイルがあったら「ハードリンク」する
  3. 1 にもどる
hashmarge.py
#!/usr/bin/python

# -*- coding: utf-8 -*-

import sqlite3,sys,os,pickle,hashlib,base64

class HmDBO:
  con = None
  def __init__(self,dbpath):
    self.con = sqlite3.connect(dbpath)
    if not self.existFileTable():
      self.createFileTable()
    if not self.existCacheTable():
      self.createCacheTable()

  def commit(self):
    self.con.commit()

  def close(self):
    self.con.close()
    self.con = None

#
#filehash
#
  def existFileTable(self):
    sql = u"""SELECT * FROM sqlite_master WHERE type='table' AND name='filehash';"""
    c = self.con.execute(sql)
    for o in c:
      return True
    return False
  
  def createFileTable(self):
    sql = u"""create table filehash(path varchar(64) primary key, hash varchar(64));"""
    self.con.execute(sql)

  def insertFile(self,path,hash):
    sql = u"""insert into filehash values (?,?); """
    hxpath = hashlib.sha256(path).hexdigest()
    self.con.execute(sql,(hxpath,hash))
  
  def selectFile(self,path):
    sql = u"""select hash from filehash where path=?;"""
    hxpath = hashlib.sha256(path).hexdigest()
    c = self.con.execute(sql,(hxpath,))
    for o in c:
      return o[0]
    return None

#
#linkcache
#
  def existCacheTable(self):
    sql = u"""SELECT * FROM sqlite_master WHERE type='table' AND name='linkcache';"""
    c = self.con.execute(sql)
    for o in c:
      return True
    return False

  def createCacheTable(self):
    sql = u"""create table linkcache(hash varchar(64) primary key, path text);"""
    self.con.execute(sql)

  def insertCache(self,hash,path):
    sql = u"""insert into linkcache values (?,?)"""
    b64path = base64.b64encode(path)
    self.con.execute(sql,(hash,b64path))

  def selectCache(self,hash):
    sql = u"""select path from linkcache where hash=?;"""
    c = self.con.execute(sql,(hash,))
    for o in c:
      return base64.b64decode(o[0])
    return None

  def deleteCache(self,hash):
    sql = u"""delete from linkcache where hash=?;"""
    self.con.execute(sql,(hash,))

def calcSHA256(path):
  sha = hashlib.sha256()
  fp = open(path,'r')
  while True:
    cache = fp.read(65536)
    if not cache: break
    sha.update(cache)
  fp.close()
  return sha.hexdigest()

def hashmarge(dbpath):
  filecount=0
  margecount=0
  registercount=0
  
  dbo = HmDBO(dbpath)

  for rwln in iter(sys.stdin.readline,""):
    path = rwln.rstrip('\n')
    if os.path.isfile(path):
      sys.stderr.write(path + '...')
      filecount += 1
      #filehash
      hash = dbo.selectFile(path)
      if hash == None:
        registercount += 1
        sys.stderr.write('register ')
        hash = calcSHA256(path)
        dbo.insertFile(path,hash)
      #linkcache
      cache = dbo.selectCache(hash)
      if cache == None:
        dbo.insertCache(hash,path)
      elif os.path.isfile(cache) and not os.path.samefile(cache, path):
        margecount += 1
        sys.stderr.write('marge ')
        os.remove(path)
        os.link(cache, path)
          

      sys.stderr.write('done\n')

  dbo.commit()
  dbo.close()

  sys.stderr.write('files  = ' + str(filecount) + '\n')
  sys.stderr.write('register = ' + str(registercount) + '\n')
  sys.stderr.write('marges = ' + str(margecount) + '\n')


def rebuild(olddb,newdb):
  old = HmDBO(olddb)
  new = HmDBO(newdb)
  for rwln in iter(sys.stdin.readline,""):
    sys.stderr.write(rwln)
    path = rwln.rstrip('\n')
    if os.path.isfile(path):
      hash = old.selectFile(path)
      if hash <> None:
        new.insertFile(path,hash)
        cache = new.selectCache(hash)
        if cache == None:
          new.insertCache(hash, path)

  new.commit()
  old.close()
  new.close()

def main():
  if len(sys.argv) < 2:
    exit()
  elif len(sys.argv) == 2:
    hashmarge(sys.argv[1])
  elif len(sys.argv) == 3:
    rebuild(sys.argv[1], sys.argv[2])

if __name__ == '__main__':
  main()
使い方

前回と一緒

$ find <対象ディレクトリ> | hashmarge.py <dbファイル>
あとがき

前回のエントリで公開したスクリプトと比較して
こちらはファイル数が異常に多くても問題なく動作することが強み

シンプルなのでおそらくwindows移植も可能かも?
samefile()が未対応のため windowsでは動作しませんでした

なんとかしてみる

windowsでsamefile()する方法をググったら出てきた
Tim Golden's Python Stuff: See if two files are the same file

なるほど。

これでwindows対応版を作ればよさそうだ