os.walkを便利に使える関数strollを書いた

  • リスト指定と正規表現マッチに対応し、ディレクトリパスとファイル名を返すようにして、関数名をnicewalkからstroll*1に変更(短い単語の方が良かった)

  • ディレクトリの扱いを明確化。rootはカレントディレクトリのディレクトリ名で、pathはカレントディレクトリのパス。ignorecaseを追加。

  • Python 3.5以上ではglobにrecursiveオプションが付いたため、"**"とrecursive=Trueを与えることで再帰的に辿ることができるようになった。そのため、以下はPython3.4以下が対象。

os.walk()はディレクトリを再帰的に辿りながら、ファイルを読み込んだり書き込む処理をする際に非常に便利だ。globだとディレクトリを再帰的に辿ることができず、ディレクトリ階層を意識しないといけない*2し、ディレクトリとファイルの違いをワイルドカードで示す必要があるので柔軟さに欠ける。またos.walk()では、取得したディレクトリやファイルのリストに対して、インプレースでの変更が可能になっているため、任意の条件で自由にソートやフィルタを行うことができる。そこで、各ディレクトリ・ファイルに対する条件を簡単に盛り込めるstrollを書いた。

reduceのこの使い方は好きで、リストをループでチェックするよりワンラインで書ける方が見やすくて良い。一度慣れてしまえば、それほど考えこまなくても流用できるので、いろんなところで多用している。リストが空の場合には、reduceに与える初期値がそのまま判定結果になってしまうが、それが嫌ならリストを三項演算で判定して初期値を与えれば良い。

gist.github.com

$ python --version
Python 3.4.3

$ cat main.py 
from stroll import stroll
import os

print()
print("=== ex1 (mtime) ===")
for filepath in stroll("basedir",sortby=os.path.getmtime):
    print( filepath )

print()
print("=== ex2 (mtime/reverse) ===")
for filepath in stroll("basedir",sortby=os.path.getmtime,reverse=True):
    print( filepath )

print()
print("=== ex3 (no_file) ===")
for filepath in stroll("basedir",no_file=["1$","2$"],):
    print( filepath )

print()
print("=== ex4 (yes_path) ===")
for filepath in stroll("basedir",yes_path=["1$"],):
    print( filepath )

print()
print("=== ex5 (no_root/len/reverse) ===")
for filepath in stroll("basedir",no_root=["2$"],sortby=len,reverse=True):
    print( filepath )

print()
print("=== ex6 (yes_file/yes_root) ===")
for filepath in stroll("basedir",yes_file=["2$"],yes_root=["2$"]):
    print( file path )
$ find basedir
basedir
basedir/dir1
basedir/dir1/file11
basedir/dir1/file111
basedir/dir1/file1111
basedir/dir1/file12
basedir/dir1/file13
basedir/dir2
basedir/dir2/file21
basedir/dir2/file22
basedir/dir2/file222
basedir/dir2/file2222
basedir/dir2/file23
basedir/dir3
basedir/dir3/file31
basedir/dir3/file32
basedir/dir3/file33
basedir/dir3/file333
basedir/dir3/file3333
basedir/file0

$ ls -Rlrt --time-style=+"%m/%d_%H:%M:%S" basedir/ | awk '{print $5 " " $6 " " $7}'
  
0 02/09_21:24:50 file0
238 02/09_21:25:29 dir1
238 02/09_21:25:33 dir2
238 02/09_21:25:38 dir3
  
0 02/09_21:24:57 file11
0 02/09_21:24:58 file12
0 02/09_21:24:59 file13
0 02/09_21:25:28 file111
0 02/09_21:25:29 file1111
  
0 02/09_21:25:06 file22
0 02/09_21:25:06 file21
0 02/09_21:25:07 file23
0 02/09_21:25:32 file222
0 02/09_21:25:33 file2222
  
0 02/09_21:25:10 file31
0 02/09_21:25:11 file32
0 02/09_21:25:12 file33
0 02/09_21:25:37 file333
0 02/09_21:25:38 file3333

$ python main.py 

=== ex1 (mtime) ===
basedir
('basedir', 'file0')
basedir/dir1
('basedir/dir1', 'file11')
('basedir/dir1', 'file12')
('basedir/dir1', 'file13')
('basedir/dir1', 'file111')
('basedir/dir1', 'file1111')
basedir/dir2
('basedir/dir2', 'file21')
('basedir/dir2', 'file22')
('basedir/dir2', 'file23')
('basedir/dir2', 'file222')
('basedir/dir2', 'file2222')
basedir/dir3
('basedir/dir3', 'file31')
('basedir/dir3', 'file32')
('basedir/dir3', 'file33')
('basedir/dir3', 'file333')
('basedir/dir3', 'file3333')

=== ex2 (mtime/reverse) ===
basedir
('basedir', 'file0')
basedir/dir3
('basedir/dir3', 'file3333')
('basedir/dir3', 'file333')
('basedir/dir3', 'file33')
('basedir/dir3', 'file32')
('basedir/dir3', 'file31')
basedir/dir2
('basedir/dir2', 'file2222')
('basedir/dir2', 'file222')
('basedir/dir2', 'file23')
('basedir/dir2', 'file21')
('basedir/dir2', 'file22')
basedir/dir1
('basedir/dir1', 'file1111')
('basedir/dir1', 'file111')
('basedir/dir1', 'file13')
('basedir/dir1', 'file12')
('basedir/dir1', 'file11')

=== ex3 (no_file) ===
basedir
('basedir', 'file0')
basedir/dir1
('basedir/dir1', 'file13')
basedir/dir2
('basedir/dir2', 'file23')
basedir/dir3
('basedir/dir3', 'file33')
('basedir/dir3', 'file333')
('basedir/dir3', 'file3333')

=== ex4 (yes_path) ===
basedir/dir1
('basedir/dir1', 'file11')
('basedir/dir1', 'file111')
('basedir/dir1', 'file1111')
('basedir/dir1', 'file12')
('basedir/dir1', 'file13')

=== ex5 (no_root/len/reverse) ===
basedir
('basedir', 'file0')
basedir/dir1
('basedir/dir1', 'file1111')
('basedir/dir1', 'file111')
('basedir/dir1', 'file11')
('basedir/dir1', 'file12')
('basedir/dir1', 'file13')
basedir/dir3
('basedir/dir3', 'file3333')
('basedir/dir3', 'file333')
('basedir/dir3', 'file31')
('basedir/dir3', 'file32')
('basedir/dir3', 'file33')

=== ex6 (yes_file/yes_root) ===
basedir/dir2
('basedir/dir2', 'file22')
('basedir/dir2', 'file222')
('basedir/dir2', 'file2222')

*1:to walk somewhere in a slow relaxed way

*2:rubyのDir.glob()は**/を指定することで再帰的にディレクトリ階層を辿ることができる

OpenCV入門

Max OS X(10.10.5)でOpenCVに入門してみた。写真はtoeさんです。 導入はbrewで簡単にインストールできるし、インクルード検索パスやライブラリ検索パスもpkg-configで簡単に指定できる。

ネガポジ反転は、~演算子(ビット反転・1の補数)を実行するだけで取り出せる、というところも面白い。Mat型において、~演算子オーバーロードされているのだろう。 最後に、演算子オーバーロードのコード例を追記してみた。

CascadeClassifierによる分類では、loadするデータを変えることによって、どんな特徴を矩形抽出するかを変えている。顔の特徴と下半身の特徴。顔の矩形座標が取れれば、あとはFacebookのようにフレンドリストの教師データから名前を割り当てることはできそう。これはやってみたいところ。

$ brew update
$ brew tap homebrew/science
$ brew install opencv
$ g++ opencv.cpp `pkg-config --cflags opencv` `pkg-config --libs opencv`
$ pkg-config --cflags opencv
-I/usr/local/Cellar/opencv/2.4.12_2/include/opencv -I/usr/local/Cellar/opencv/2.4.12_2/include 

$ pkg-config --libs opencv
-L/usr/local/Cellar/opencv/2.4.12_2/lib -lopencv_calib3d -lopencv_contrib -lopencv_core -lopencv_features2d -lopencv_flann -lopencv_gpu -lopencv_highgui -lopencv_imgproc -lopencv_legacy -lopencv_ml -lopencv_nonfree -lopencv_objdetect -lopencv_ocl -lopencv_photo -lopencv_stitching -lopencv_superres -lopencv_ts -lopencv_video -lopencv_videostab

gist.github.com

f:id:nishidy:20160202001020j:plainf:id:nishidy:20160202001027j:plainf:id:nishidy:20160202001033j:plainf:id:nishidy:20160202001617j:plainf:id:nishidy:20160202001048j:plainf:id:nishidy:20160202001053j:plainf:id:nishidy:20160202003702j:plain

#include <iostream>
using namespace std;

class Klass{
    public:
    int i;
    int j;
    Klass():i(0),j(1){
    }
    Klass* operator ~ () {
        Klass* obj = new Klass();
        obj->i = ~(this->i);
        obj->j = ~(this->j);
        return obj;
    }

};

int main(int argc, char* argv[]){
    Klass* obj = new Klass();
    Klass* rev = ~(*obj);
    printf("%d,%d\n",obj->i,obj->j);
    printf("%d,%d\n",rev->i,rev->j);
    return 0;
}
$ ./a.out 
0,1
-1,-2

Pythonのpickleでserializeできない問題の回避策(dill)

読み込んだデータをシリアライズしておきたい場合、pickleを使えば任意のクラスのオブジェクトを扱える。ファイルを開いて、dumpで書き出し、loadで読み出しを行えば良い。しかし、ある構造を持つクラスのオブジェクトをpickleでシリアライズしようとすると、以下のようにエラーとなる。

$ python --version
Python 3.4.3

$ python pickle_test.py 
Traceback (most recent call last):
  File "pickle_test.py", line 15, in <module>
    pickle.dump(klass,f)
_pickle.PicklingError: Can't pickle <function Klass.__init__.<locals>.<lambda> at 0x107728ae8>: attribute lookup <lambda> on __main__ failed

クラスの内容は以下の通りで、defaultdictをネストしているので、lambdaを与えている。

$ cat pickle_test.py
# coding: utf-8

import pickle
from collections import defaultdict

class Klass:
    def __init__(self):
        self.prop = defaultdict(lambda:defaultdict(int))

klass = Klass()

klass.prop["x"]["y"] += 10

with open("test.pickle","wb") as f:
    pickle.dump(klass,f)

with open("test.pickle","rb") as f:
    newklass = pickle.load(f)

print(newklass.prop["x"]["y"])

以下をみると、同じようなエラーがlambdaを使った際に発生するようだ。

PicklingError: Can't pickle <type 'function'>: attribute lookup __builtin__.function failed - あと5分だけ

そこで、defaultdictの引数にlambdaではなく関数を渡すようにするとうまくいく。一応、defaultdictのネストにも対応できる。もう少し賢いやり方があるかもしれない。

$ cat pickle_test2.py 
# coding: utf-8

import pickle
from collections import defaultdict

class Val:
    def __init__(self):
        self.vprop= defaultdict(int)

def ddv():
    return defaultdict(Val)

def dd2():
    return defaultdict(dd)

def dd():
    return defaultdict(int)

class Klass:
    def __init__(self):
        self.prop = defaultdict(dd)
        self.prop2= defaultdict(dd2)
        self.prop3= defaultdict(ddv)

klass = Klass()

klass.prop["x"]["y"] += 10
klass.prop2["x"]["y"]["z"] += 100
klass.prop3["x"]["y"].vprop["z"] += 1000

with open("test.pickle","wb") as f:
    pickle.dump(klass,f)

with open("test.pickle","rb") as f:
    newklass = pickle.load(f)

print(newklass.prop["x"]["y"])
print(newklass.prop2["x"]["y"]["z"])
print(newklass.prop3["x"]["y"].vprop["z"])

$ python pickle_test2.py 
10
100
1000

追記 dill

コメントで教えていただきましたdillを使えば良いことが分かりました。もうpickle必要無さそうですね。dillはpipでインストールします。

$ cat dill_test.py
# coding: utf-8

import dill
from collections import defaultdict

class Val:
    def __init__(self):
        self.vprop= defaultdict(int)

class Klass:
    def __init__(self):
        self.prop = defaultdict(lambda:defaultdict(int))
        self.prop2= defaultdict(lambda:defaultdict(lambda:defaultdict(int)))
        self.prop3= defaultdict(lambda:defaultdict(Val))

klass = Klass()

klass.prop["x"]["y"] += 10
klass.prop2["x"]["y"]["z"] += 100
klass.prop3["x"]["y"].vprop["z"] += 1000

with open("test.pickle","wb") as f:
    dill.dump(klass,f)

with open("test.pickle","rb") as f:
    newklass = dill.load(f)

print(newklass.prop["x"]["y"])
print(newklass.prop2["x"]["y"]["z"])
print(newklass.prop3["x"]["y"].vprop["z"])
$ pip install dill
...

$ python dill_test.py 
10
100
1000

Pythonで統計処理した結果を表示する場合の便利関数を書いた

  • 全てのキーの組み合わせが無い場合でも、defaultdictの初期値を用いて表を完成させるように修正。
  • pandasのMultiIndexを使って表示する処理を追記。

統計処理ではネストされた辞書(dict)が多用することで、比較的簡単に所望の複合キーに対応する値を取得しておくことができる。例えば、population[prefecture][gender][age]のようなものだ。これを例えば、横軸prefecture、縦軸gender x ageという具合に、三次元のグラフが出来る表として出力するとき、第一次辞書のキー(この場合はprefecture)を横軸に取れば良いのだが、横軸方向に値を表示していくことを考えると、少し面倒な処理が必要になる。このような表は良く使うので、そのための関数を持つライブラリを書いて使ってみた。もし、横軸にageを取りたければ、population[age][prefecture][gender]というネストされた辞書を作って集計しておけば良い。

実質的にネストの次元はおそらく際限なく増やすことが可能だが、今回は5次元までとしている。もう少し機能を追加したら、PYPIしてみるのもいいかも。月や曜日でもうまくソートできるようにしたい。また、第一次辞書のキーだけではなく任意の辞書のキーを横軸に取れるようにする、など。表示形式としてはセパレータを指定できるので、TSVやCSVにも対応できる。

なお、defaultdictは、キーに対するデフォルトの値を指定することができる(実際には値ではなく、新しいオブジェクトを返すファクトリ関数を指定する)。キーの存在を確認する必要なく、そのキーに対するデフォルトの値を仮定して処理が書けるのでキーの存在確認を行う必要がなく、複雑な辞書を扱う際にシンプルに書ける。

gist.github.com

$ cat a.py 
from collections import defaultdict
from chart import chart

adata = defaultdict(lambda:defaultdict(int))

n = 0
for a in ["1","2","3"]:
    for b in ["xxx","yyy","zzz"]:
        n += 1
        adata[a][b] = n

chart(adata)
print()

bdata = defaultdict(lambda:defaultdict(lambda:defaultdict(int)))

n = 0
for a in ["A","B","C","D","E","F","G"]:
    for b in ["xxx","yyy","zzz"]:
        for c in ["hhh","iii","jjj"]:
            n += 1
            bdata[a][b][c] = n
chart(bdata,"\t")
print()


cdata = defaultdict(lambda:defaultdict(lambda:defaultdict(int)))

cdata["A"]["xxx"]["hhh"] = 1
cdata["B"]["yyy"]["iii"] = 2
cdata["C"]["zzz"]["jjj"] = 3
cdata["D"]["zzz"]["iii"] = 4
cdata["E"]["yyy"]["hhh"] = 5
cdata["F"]["xxx"]["iii"] = 6
cdata["G"]["xxx"]["jjj"] = 7

chart(cdata,"\t")
print()
chart(cdata)
$ python a.py 
,1,2,3
xxx,1,4,7,
yyy,2,5,8,
zzz,3,6,9,

        A   B   C   D   E   F   G
xxx hhh 1   10  19  28  37  46  55  
xxx iii 2   11  20  29  38  47  56  
xxx jjj 3   12  21  30  39  48  57  
yyy hhh 4   13  22  31  40  49  58  
yyy iii 5   14  23  32  41  50  59  
yyy jjj 6   15  24  33  42  51  60  
zzz hhh 7   16  25  34  43  52  61  
zzz iii 8   17  26  35  44  53  62  
zzz jjj 9   18  27  36  45  54  63  

        A   B   C   D   E   F   G
xxx hhh 1   0   0   0   0   0   0   
xxx iii 0   0   0   0   0   6   0   
xxx jjj 0   0   0   0   0   0   7   
yyy hhh 0   0   0   0   5   0   0   
yyy iii 0   2   0   0   0   0   0   
yyy jjj 0   0   0   0   0   0   0   
zzz hhh 0   0   0   0   0   0   0   
zzz iii 0   0   0   4   0   0   0   
zzz jjj 0   0   3   0   0   0   0   

,,A,B,C,D,E,F,G
xxx,hhh,1,0,0,0,0,0,0,
xxx,iii,0,0,0,0,0,6,0,
xxx,jjj,0,0,0,0,0,0,7,
yyy,hhh,0,0,0,0,5,0,0,
yyy,iii,0,2,0,0,0,0,0,
yyy,jjj,0,0,0,0,0,0,0,
zzz,hhh,0,0,0,0,0,0,0,
zzz,iii,0,0,0,4,0,0,0,
zzz,jjj,0,0,3,0,0,0,0,

pandasのMultiIndexを利用した場合

データ分析で良く使うpandasのMultiIndexを用いた場合、stack() unstack()を使えば、同様の処理がより柔軟に行えるようだ。ただし、ネストされたdictionaryをそのままDataFrameに入れてMultiIndexとして使うことはできず、以下のようなリフォーム処理が必要になる。つまり、キーをタプルとしてdictionaryを作成する。

reformed_bdata = { (key1,key3,key2) : [val3] for key1, val1 in bdata.items() for key2, val2 in val1.items() for key3, val3 in val2.items() }
bdf = pd.DataFrame(reformed_bdata)
print(bdf.stack().stack())

reformed_cdata = { (key1,key3,key2) : [val3] for key1, val1 in cdata.items() for key2, val2 in val1.items() for key3, val3 in val2.items() }
cdf = pd.DataFrame(reformed_cdata)
print(cdf.stack().stack())
           A   B   C   D   E   F   G
0 xxx hhh  1  10  19  28  37  46  55
      iii  2  11  20  29  38  47  56
      jjj  3  12  21  30  39  48  57
  yyy hhh  4  13  22  31  40  49  58
      iii  5  14  23  32  41  50  59
      jjj  6  15  24  33  42  51  60
  zzz hhh  7  16  25  34  43  52  61
      iii  8  17  26  35  44  53  62
      jjj  9  18  27  36  45  54  63

           A  B  C  D  E  F  G
0 xxx hhh  1  0  0  0  0  0  0
      iii  0  0  0  0  0  6  0
      jjj  0  0  0  0  0  0  7
  yyy hhh  0  0  0  0  5  0  0
      iii  0  2  0  0  0  0  0
      jjj  0  0  0  0  0  0  0
  zzz hhh  0  0  0  0  0  0  0
      iii  0  0  0  4  0  0  0
      jjj  0  0  3  0  0  0  0

setdefault

collectionsモジュールをインポートしなくても、setdefaultを使えば、キーの確認をせずにキーが存在するものとして値を扱える。

$ cat setdefault_test.py
a = {}  
a.setdefault("aaa",0)
a["aaa"] += 1
print(a)
           
b = {}  
b.setdefault("aaa",[])
b["aaa"].append(10)
print(b) 

$ python setdefault_test.py
{'aaa': 1}
{'aaa': [10]}

ファイルの最後の方に出現する行を取得する

大量のファイル、しかもサイズの大きいものを読み込んで、ある特定の小数の行だけを取り出したいような場合で、その特定の行がいつもファイルの最後に近い位置に出現することが期待できる場合、ファイルを先頭から読み込んで行って、特定の行に達するまで読み捨てていく方法では時間がかかるのは明白だ。seekを使って、最後の方だけを読み込むことで、最小限のファイルシーク時間で処理を済ませたい。最後の方でなくても、ファイル中のどの位置にあるかがある程度分かっていればseekで読み飛ばせば良い。

気をつけないといけないのは、ファイルの先頭から最後まで読み込んでいく場合は、その特定の行にいつか辿り着くはずであるが、ファイルの後方からあるサイズだけ戻ってから読み込む場合は、そのサイズよりも前にその特定の行が出現している場合がある。ファイルの後方から戻るサイズは最小限にしておき、その特定の行をミスした場合はそのサイズを倍にしていく。(特定の行が存在しない場合に備え、そのサイズをファイルサイズと比較して処理を抜ける必要がある)あと、すでに読み込んで検査した行を、サイズを大きくする度に重複して何度も検査してしまっているので、読み込んだサイズを確認して一度読み込んで検査した行に達した場合は、処理を抜けるようにする必要がある(ToDo)。あと、読み込んだブロックの最初と、次に読み込んだブロックの最後にまたがって、特定の行が出現する場合に対処する必要がある(ToDo)。

以下、簡単にベンチマーク。ファイルの後方から読み込む場合はあえて一回で特定の行が見つからないように、サイズを調整している。(実際には、同じファイルを何度も参照しているので、読み込む度にメモリのディスクキャッシュをpurgeするか、別のファイルを100個用意する必要があるが、今回は省略)

import os, time, sys

start = time.time()
for i in range(0,100):
    for l in open("log.txt").readlines():
        if l.rstrip() == "9990":
            sys.stdout.write("."),
            sys.stdout.flush()
            break
print ""
end = time.time()
print end-start

start = time.time()
for i in range(0,100):
    flag = True
    filesize = os.path.getsize("log.txt")
    f = open("log.txt")
    seek_size = -20
    while flag:
        f.seek(seek_size,os.SEEK_END)
        seeked_size = 0
        for l in f.read().split("\n"):
            if l.rstrip() == "9990":
                sys.stdout.write("."),
                sys.stdout.flush()
                flag = False
                break
            seeked_size += len(l)+1
            if seeked_size > seek_size / 2 * (-1):
                break
        seek_size *= 2
        if flag:
            sys.stdout.write("-"),
            sys.stdout.flush()
        if seek_size * (-1) > filesize:
            break
    f.close()
print ""
 
end = time.time()
print end-start
$ for i in $(seq 1 10000);do echo $i >> log.txt; done

$ wc -l log.txt 
   10000 log.txt

$ python --version
Python 2.7.10

$ python large_file_search.py 
....................................................................................................
0.270761966705
--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.--.
0.00701999664307

Python3では

上記はPython2での結果。Python3では以下のようにエラーとなる。Documentには、textで読み込んだファイルに対して、io.SEEK_ENDからシークすることはできない、とある。

$ python --version
Python 3.4.3

$ python large_file_search.py 
....................................................................................................
0.31100988388061523
Traceback (most recent call last):
  File "large_file_search.py", line 21, in <module>
    f.seek(seek_size,os.SEEK_END)
io.UnsupportedOperation: can't do nonzero end-relative seeks

7. Input and Output — Python v3.2.6 documentation

In text files (those opened without a b in the mode string), only seeks relative to the beginning of the file are allowed (the exception being seeking to the very file end with seek(0, 2)).

そのため、os.path.getsizeでファイルの全体サイズを取得したうえで、ファイルの先頭位置から所望の位置までio.SEEK_SETからシークすれば良い。

Perlでは

こんな感じ。SEEK_ENDから負のオフセット値を指定できる。

use strict;
use Time::HiRes qw( gettimeofday tv_interval );
       
my $fsize = -s "log.txt";
       
my $stime = [gettimeofday];
for(1..100){
    open(IN,"log.txt");
    while(<IN>){
        if( $_ == "9990" ){
            print ".";
            last;
        }   
    }  
    close(IN);
}   
my $elapsed = tv_interval($stime);
       
print "\n".int($elapsed * 1000)."msec\n";
       
my $stime = [gettimeofday];
for(1..100){
    open(IN,"log.txt");
    seek(IN,-200,2);
    while(<IN>){
        if( $_ == "9990" ){
            print ".";                                                                               
            last;
        }   
    }  
    close(IN);
}   
my $elapsed = tv_interval($stime);
       
print "\n".int($elapsed * 1000)."msec\n";
$ perl --version

This is perl 5, version 18, subversion 2 (v5.18.2) built for darwin-thread-multi-2level
(with 2 registered patches, see perl -V for more detail)

Copyright 1987-2013, Larry Wall

Perl may be copied only under the terms of either the Artistic License or the
GNU General Public License, which may be found in the Perl 5 source kit.

Complete documentation for Perl, including FAQ lists, should be found on
this system using "man perl" or "perldoc perl".  If you have access to the
Internet, point your browser at http://www.perl.org/, the Perl Home Page.

$ perl file_test.pl 
....................................................................................................
173msec
....................................................................................................
1msec

セーブ機能を追加しました@transatonce.appspot.com

transatonce

単語を翻訳するとき、こういった要求を満たせるサイトがあると良いと思って作っているものです。

  • いくつかの単語を一度に翻訳したい(英文の記事や論文を読み終わったあとでメモしておいた単語を at once に翻訳したい)
  • それらを一つのページで表示したい(ブラウザの履歴で戻って確認したり、たくさんブラウザのタブを開いたりしたくない)
  • 翻訳した後で必要ない単語は削除したい
  • 翻訳サイトを切り替えて同じ単語を翻訳したい(複数の翻訳を比較したい)
  • 翻訳する単語を同じページに追加して確認したい

それに加えて、単語帳のように翻訳した単語を記憶しておけると良いと思っているのですが、Google accountを使ったDatastoreはログインの手間があるのであまりやりたくない。(過去やっていましたがやめてしまいました) 少なくとも、前回翻訳した単語が思い出せることで、記憶が定着する効果がありそうな気がしますよね。

GAE Memcache

そこで、CookieとSessionをカジュアルに使って、通常は読み取り負荷軽減に使われるMemcacheに、IDと単語をひも付けて記録しておくよう実装しました。 もちろん、Cookieなので端末はおろかブラウザを変えただけで、それぞれが別のユーザだと認識されてしまいますが、それなりには有用です。 まあCookieが盗聴されると、Session ID(というかUser ID)が分かってしまうので、翻訳した単語がばれてしまいますがその程度です。

GAE DataStore

Memcacheはインメモリなので長くは持たないと思っていたのですが、思った以上にephemeralなようです。永続化するつもりも無いのですが、少なくとも24時間くらいは維持したいので、DataStore(NDB)を使うように修正しました。いちおうOpt-outとして、保存された内容を削除することができるようにしています。

HTML5 localStorage

ブラウザごとに保存した内容を表示するだけなら、サーバ側でやる意味が無いわけですね。良く考えれば、JavaScriptでもローカルのストレージを扱うことができるはず、ということでlocalStorageを使うことにしました。数MB保存できれば十分です*1。すると、Cookieも必要無いわけです。サーバ側でやる意味があるとすれば、同じ人物が翻訳した内容にどのような傾向があるか、とか、こういう単語を翻訳している人はこういう単語にも興味があるはず、とか繋がりを意識したサービスはありそうです。

Valgrind で Pthread の Debug

  • OS X Yosemite (10.10.5) でC(GNU99)でpthreadを用いたマルチスレッドプログラムを書いていて、なかなかerrorが取れなかったときに以下ツイートを起点にした議論を見て、ValgrindのHelgrind toolが使えそうということで使ってみた。

  • ValgrindはDarwinにも対応しており、ソースからコンパイルすればインストールできる。使い方は、コンパイル時に-gでソースレベルのデバッグ情報を付与し、valgrind --tool=helgrind ./a.outと実行するだけで良い。
==2575== Possible data race during write of size 1 at 0x5E205C0 by thread #1
==2575== Locks held: none
==2575==    at 0x4C30491: strcat (vg_replace_strmem.c:274)
...
==2575== 
==2575== This conflicts with a previous read of size 1 by thread #2
==2575== Locks held: 2, at addresses 0x605240 0x6051C0
==2575==    at 0x4C30590: strlen (vg_replace_strmem.c:412)
...
==2575==  Address 0x5e205c0 is 0 bytes inside a block of size 131,072 alloc'd
==2575==    at 0x4C2AF2A: realloc (vg_replace_malloc.c:692)
...
==2575==  Block was alloc'd by thread #1
  • 上記の省略した箇所には、具体的なプログラムの行数が出力される。ここで、以下のように別のtoolを試してみた方が良い、というメッセージが出力される。
valgrind: m_mallocfree.c:304 (get_bszB_as_is): Assertion 'bszB_lo == bszB_hi' failed.
valgrind: Heap block lo/hi size mismatch: lo = 131136, hi = 7875142615529697846.
This is probably caused by your program erroneously writing past the
end of a heap block and corrupting heap metadata.  If you fix any
invalid writes reported by Memcheck, this assertion failure will
probably go away.  Please try that before reporting this as a bug.
  • そこで--tool=memcheckとしてvalgrindを実行し、以下の出力を得る。
==2659== Thread 3:
==2659== Invalid write of size 1                                                                     
==2659==    at 0x4C2AC73: strcpy (vg_replace_strmem.c:458)
...
==2659==  Address 0x600d015 is 0 bytes after a block of size 9,045 alloc'd
==2659==    at 0x4C27BBD: malloc (vg_replace_malloc.c:296)
...
==2659== 
  • なんとなくmallocで割り当てたメモリよりも多く書き込んでいるのではないか、と当たりを付けてソースコードチェックすると、見事的中。reallocするかどうかの判定文の不等号から=が漏れていた点と、strcpyが\0を含めてコピーするのを見越して+1したサイズのメモリを確保しなければいけなかった点を発見して修正(その他にもhelgrindで多数検出し修正した)。

  • 良く分からないのは、OS X での実行結果。実は上記はLinuxで実行して確認した結果であり、LinuxではValgrindもプログラム実行でもエラーが検出・発生していないにも関わらず、OS XではValgrindでなぜかloop中で実施しているpthread_createがPossible data raceとして検出されてしまう。

# OS X
$ uname -a
Darwin nishidy-MBA.local 14.5.0 Darwin Kernel Version 14.5.0: Wed Jul 29 02:26:53 PDT 2015; root:xnu-2782.40.9~1/RELEASE_X86_64 x86_64

$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 7.0.2 (clang-700.1.81)
Target: x86_64-apple-darwin14.5.0
Thread model: posix

$ valgrind --version
valgrind-3.11.0

$ gcc -g -std=gnu99 -pthread -lpthread -O2 -Wall xxx.c
# Linux 
$ uname -a
Linux localhost.localdomain 3.19.8-100.fc20.x86_64 #1 SMP Tue May 12 17:08:50 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ gcc --version
gcc (GCC) 4.9.3
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ valgrind --version
valgrind-3.10.0

$ gcc -g -std=gnu99 -pthread -lpthread -O2 -Wall xxx.c

OS X でのデバッグ

  • gdbは標準でインストールされていないので、lldbを使う。