![]() |
![]() |
![]() |
紫藤は長年スクリプト言語として awk と Lisp を使ってきました。 Perl というものがあり、一時はブームになっていたのも知っていたのですが、 どうもなじめなかったので、ほとんど使いませんでした。 最近、Python や Ruby といった新世代のスクリプト言語が広く使われるようになったので、 それらを試してみることにしました。
現在は Python を使っているので、Python よりの比較になっていることを念頭において読んでください。 また、この文書に批判的な意見もあるので、 そちらも参考にしてください。
お題は以前紹介した メディアにある画像ファイルをハードディスクに保存するスクリプトです。 実は我が家のパソコンはほとんどアルバムと化しており、メディアからハードディスクに移すスクリプトは FireFox 以外では私の家族に一番利用されているプログラムです。
01: #! perl 02: 03: use strict; 04: use File::Copy; 05: use File::Compare; 06: use File::Find; 07: use Cwd; 08: 09: ## global parameters 10: my $DOC_DIR = 'D:/doc'; 11: my $MEDIA ='G:/'; 12: my $PHOTO_VIEWER = 'D:/WBIN/linar160/linar.exe'; 13: 14: # getting the string of "year(NN)-month(NN)" 15: sub get_year_month{ 16: my ($m, $y) = (localtime)[4,5]; 17: sprintf ("%02d-%02d", $y-100, $m + 1); 18: } 19: 20: # getting the starting number of photoNN, the directory in which photos should be saved 21: # This function should be called when the program is in the month directory. 22: sub get_first_photo_dir_number{ 23: my @pdirs = (glob "photo[0-9][0-9]"); 24: @pdirs ? 1+ substr($pdirs[-1], -2) : 1; 25: } 26: 27: #move into the directory "$DOC_DIR/y-m" 28: sub move_into_dir_of_month{ 29: my $dir_of_month = &get_year_month; 30: unless ($DOC_DIR eq cwd){ 31: chdir $DOC_DIR or die "Cannot move to $DOC_DIR: $!"; 32: } 33: unless (-d $dir_of_month){ 34: mkdir $dir_of_month or die "cannot create $dir_of_month: $!"; 35: } 36: chdir $dir_of_month or die "Cannot move to $dir_of_month: $!"; 37: "$DOC_DIR/$dir_of_month" ; 38: } 39: 40: #archive photos in the media into the HD 41: # This function should be called when the program is in the month directory. 42: sub archive_photos{ 43: my $photo_dir_number = shift; 44: my %dhash; 45: find({ 46: wanted => sub{push @{$dhash{$File::Find::dir}}, $_ if -f}, 47: }, $MEDIA); 48: for my $dir_from (sort keys %dhash){ 49: my $n = @{$dhash{$dir_from}}; 50: my $i = 0; 51: my $dir_to = sprintf("photo%02d", $photo_dir_number++); 52: mkdir $dir_to or die "cannot create $dir_to: $!"; 53: print "\n$dir_from ==> $dir_to\n"; 54: for my $fname (@{$dhash{$dir_from}}){ 55: my $copy_from = "$dir_from/$fname"; 56: my $copy_to = "$dir_to/$fname"; 57: copy($copy_from, $copy_to) or die "cannot make a copy for $copy_from: $!"; 58: if(0 == compare($copy_from, $copy_to)){ 59: unlink $copy_from; 60: print ++$i, "/$n\r"; 61: }else{ 62: die "an error occurs during coping $copy_from"; 63: } 64: } 65: } 66: %dhash; 67: } 68: 69: #main 70: my $dir_of_month = &move_into_dir_of_month; 71: my $first_photo_dir_number = &get_first_photo_dir_number; 72: if(&archive_photos($first_photo_dir_number)){ 73: exec (sprintf "%s %s/photo%02d", $PHOTO_VIEWER, $dir_of_month, $first_photo_dir_number); 74: }else{ 75: print "No photos in the media!\nGive Return:"; 76: < STDIN> 77: }大体 80 行くらいのコードになります。 リストは Perl のハッシュ表の値にはなれないのでリストを値としたければ、 リストのリファレンスを使う必要があります。 印象としては、awk と sed を混ぜたものをその適応範囲外まで拡張しすぎたという感じです。 崩壊寸前のダムみたいな感じで、いまいち。 ただし、ライブラリは優秀で、実行速度は速いです。
今は python や ruby があるので、いまさら新たに perl を学ぶ必要は無いと思います。
スクリプト言語としての欠点を挙げると、
それから、Python には
対話モードがあり、個々の関数をテストできます。(注1)
この機能は大きめのプログラムを書くときに便利です。
main に相当する部分を、
if __name__=='__main__': のブロックに入れることによって、
Python によって直接読み込まれたとき以外は動作しないようにすることが出来ます。(注2) 3.2. 次に Python
Python で書くと次のようになります。旧版のはいけてないので書き直しました。
001: #! /usr/bin/env python
002:
003: r"""
004: script to achive photos in the removal media into HD
005: by T.Shido
006: June 26, 2007
007: """
008:
009: import os, os.path, filecmp, shutil, re, sys, operator
010: from datetime import date
011:
012: # global parameters
013: HD = '/home/pub/photos/'
014: MEDIA = '/media/usbdisk/'
015:
016: REG_FILE = re.compile(r"\.(gif|bmp|jpe?g|tiff?|wav|mov)$", re.I)
017: REG_DIR = re.compile(r'^photo[0-9][0-9]$')
018:
019: def n_photo_dir(d):
020: """return the max NN of photoNN in directory of the month"""
021:
022: return \
023: reduce(max, \
024: [ int(x[-2:]) for x in os.listdir(d) \
025: if (os.path.isdir(os.path.join(d,x)) and REG_DIR.match(x))], \
026: 0)
027:
028: def search_media(d):
029: """search Media and returns a hash
030: whose keys are directory name and the values are lists of photo files."""
031: def search_sub(d):
032: os.chdir(d)
033: ls_d = os.listdir(d)
034: ls_f = [x for x in ls_d if os.path.isfile(x) and REG_FILE.search(x)]
035: if ls_f:
036: h[d] = ls_f
037: for d1 in [os.path.join(d,x) for x in ls_d if os.path.isdir(x)]:
038: search_sub(d1)
039:
040: h={}
041: search_sub(d)
042: return h
043:
044: def move_photos(d0, d1):
045: r"""
046: Moveing photo files from media into HD,
047: """
048: dir_of_month = os.path.join(d1, date.today().strftime("%y-%m"))
049: h = search_media(d0)
050: total_files = reduce(operator.__add__, [len(v) for v in h.itervalues()], 0)
051:
052: if total_files==0:
053: print "No photos in the media: give return"
054: sys.stdin.readline()
055: sys.exit()
056:
057: if not os.path.isdir(dir_of_month):
058: os.mkdir(dir_of_month)
059:
060: i_dir = n_photo_dir(dir_of_month)
061: count=0
062:
063: for d, ls_files in h.iteritems():
064: i_dir += 1
065: d_to = os.path.join(dir_of_month, "photo%02d" % i_dir)
066: os.mkdir(d_to)
067:
068: for f in ls_files:
069: f_from=os.path.join(d,f)
070: f_to=os.path.join(d_to, f)
071: shutil.copyfile(f_from, f_to)
072: if not filecmp.cmp(f_from, f_to):
073: print f_from + " and " + f_to + " are not same!"
074: sys.exit()
075: os.remove(f_from)
076: count+=1
077: print "%d/%d\n" % (count, total_files),
078:
079: if __name__=='__main__':
080: move_photos(MEDIA, HD)
大体 80 行になります。長さは大体 Perl で書いたものと同じです。
ソースの見栄えはとても良く、
インデントでブロックを表現するというアイデアは成功していると思います。
煉瓦のような雰囲気できっちりとしています。コーディングの自由度が少ないので、
(1年前の自分も含む)誰が書いても同じようなコードになり、読み取るのは容易です。
そのため、コメントの量も少なくて済み、変数名をコメント代わりに使う必要もありません。
変数はデフォルトで局所変数となるので、 Perl のように my で宣言する必要はありません。
また、リストの内包表現は Lisp の
mapcar と remove-if-not が同時に出来るので便利です。
ライブラリは優秀で、実行速度は Perl より速い気がします。
があります。
個々の関数のテストは次のようにします。
項目 | Perl | Python | Ruby |
---|---|---|---|
書きやすさ | ○ | ○ | ○ |
読みやすさ | △ | ◎ | ○ |
ライブラリー | ○ | ○ | △ |
実行速度 | ○ | ○ | △ |
ドキュメント | ◎ | ○ | △ |
ユーザー数 | ◎ | ○ | △ |
現在の人気を無視して、言語そのもののよしあしを考えると、 Python と Ruby はほぼ互角でしょう。しかし、今のところ Python の方が、 実行速度が速く、ライブラリが豊富なので、とりあえず Python を使うことにします。 また、Python には対話モードがあるのも Lisp に慣れた人間にとってはありがたいです。 Ruby は今後の健闘に期待します。
Perl や Ruby は純粋なスクリプト言語で、(もちろんそれなりに大きなプログラムも書けるようにはなっているものの) プログラムを短くすることに主眼が置かれています。 一方、Python はスクリプト言語としてもつかえる大規模プログラム作成言語で、デバックの容易さに主眼が 置かれています。プログラミング言語は適応範囲が広いほど学ぶ価値があるので、 その意味で Python を学ぶことは Perl や Ruby を学ぶより有用だと思います。 (このことから google が Perl や Ruby でなく、 Python を使っている 理由が分かるような気がします。)
ここにある内容は一部Python 早めぐりと重複しています。 Python のポリシーについては A morality tale of Perl versus Python (和訳) やThe Zen of Python (和訳)を見てください。
例)要素が非負の実数の場合、その平方根を返す。 [-3,-2,-1,0,1,2,3] ⇒ [0.0, 1.0, 1.4142135623731, 1.73205080756888]
01: # Perl 5 02: my @ls0=(-3,-2,-1,0,1,2,3); 03: my @ls1=(); 04: for (@ls0){ 05: push @ls1, sqrt($_) if $_ >= 0; 06: } 07: print "$_\n" for (@ls1); 08: 09: # Ruby 10: p [-3,-2,-1,0,1,2,3].select{|x| x>=0}.map{|x| Math.sqrt(x)} 11: 12: # Python 13: import math 14: print [math.sqrt(x) for x in [-3,-2,-1,0,1,2,3] if x>=0]
例)累積機の生成。 (数nを取り、「数iを取ってnをiだけ増加させ、その増加した値を返す関数」を返すような関数)
01: # Perl 5 02: sub foo { 03: my ($n) = @_; 04: sub {$n += shift} 05: } 06: 07: # Python 08: class foo: 09: def __init__(self, n): 10: self.n = n 11: def __call__(self, i): 12: self.n += i 13: return self.n 14: 15: # Ruby 16: def foo (n) 17: lambda {|i| n += i } 18: end使用例
01: >>> a=foo(10) 02: >>> a(3) 03: 13 04: >>> a(5) 05: 18
[walk_dir.py]
001: #!/usr/bin/env python 002: # coding:shift_jis 003: 004: from __future__ import with_statement 005: import os, os.path, sys 006: 007: 008: def walk_dir(f, d, exp=''): 009: u'''ディレクトリを再帰的にたどって全てのファイルに関数 f を適用します''' 010: 011: for p0 in os.listdir(d): 012: p1 = os.path.join(d, p0) 013: if os.path.isfile(p1) and p1.endswith(exp): 014: f(p1) 015: elif os.path.isdir(p1): 016: walk_dir(f, p1, exp) 017: 018: 019: def head(fname): 020: u'''最初の 5 行を表示する''' 021: print '==== %s ====' % (fname,) 022: with file(fname) as f: 023: for i, line in enumerate(f): 024: if i==5: break 025: print line 026: 027: 028: # スクリプトが直接呼ばれたときに以下のブロックが実行されます。 029: if __name__=='__main__': 030: walk_dir(head, os.getcwd(), '.py')
[walk_dir.rb]
001: #! ruby 002: 003: 004: # walk directories recursively 005: # and apply proc if the element is a file 006: def walk_dir(proc, d=nil, exp='') 007: Dir.foreach(d){|s| 008: if s != '.' and s != '..' then 009: p=File.join(d,s) 010: case File.ftype(p) 011: when "file" 012: if(exp.length==0 or File.extname(p) == exp) then 013: proc.call(p) 014: end 015: when "directory" 016: walk_dir(proc, p, exp) 017: end 018: end 019: } 020: end 021: 022: # show first 5 lines of a file 023: def head(fname) 024: print "\n\n==== " + fname + " ===\n" 025: i=0 026: open(fname) do |f| 027: f.each{|line| 028: if i==10 then break end 029: print line 030: i+=1 031: } 032: end 033: end 034: 035: #----- 036: 037: walk_dir(lambda{|fname| head(fname)}, Dir.pwd, '.rb')
![]() |
![]() |
![]() |