Perlで改行のない巨大な固定長のテキストファイルを処理しよう
固定長のテキストファイルになっているバカでっかいデータを処理しなくちゃいけなくなったのですが、データセットの区切りに改行が入ってなかったんですよ。
というわけで、read 関数を使って処理することにしました。
ファイルのサイズが小さければ、一気に変数に読み込んで substr を回して処理できて楽だったんですけどねぇ。
固定長のテキストファイルになっているバカでっかいデータを処理しなくちゃいけなくなったのですが、データセットの区切りに改行が入ってなかったんですよ。
というわけで、read 関数を使って処理することにしました。
ファイルのサイズが小さければ、一気に変数に読み込んで substr を回して処理できて楽だったんですけどねぇ。
PerlでDBI(DBD::ODBC)経由でデータベースにアクセスしたところ、検索結果をフェッチするところで
というエラーが出て止まってしまいました。
メッセージによると、LongTruncOk が設定されていないか LongReadLen が小さすぎたかのどちらか(もしくは両方)とのことなので、対処法を調べてみました。
Perl の DBI で SQL を使いまわす場合、バインド変数を使うといろいろ楽ちんなんですが、LIKE 検索をする場合の書き方をいつも忘れてるのでメモメモです φ(..)。
で、何に迷うかって、 “%” をどこに使うか(書くか)なんですが、結論から言うとバインド変数に含めてやればいいんですね。
#!/usr/bin/perl use strict; use warnings; use DBI; my $dbh = DBI->connect('dbi:ODBC:hogehoge', 'username', 'password') || die "DB connect error:".$DBI::errstr; my $sql = "select * from friends where friend like ?"; my $sth = $dbh->prepare($sql); $sth->execute("mukai%"); # 以下略
とまぁ、こんな感じで。
かなり前から使っている Windows の Webサーバー に ActivePerl を入れて使っているのですが、久しぶりにモジュールの新規インストールをしようとしたら、
ERROR: 401 Authorization Required
というエラーになってモジュールをインストールできなかったので、ちょっと調べてみました。
Perl でファイルハンドラが開いているか(利用可能な状態かどうか)をチェックする方法を探してみました。
やりかたは幾つかあるようなんですが、fileno 関数を使うのが1番手っ取り早いようです。
使い方のサンプルは以下のようになります。
#!/usr/bin/perl use strict; use warnings; open(FH, "<", "sample.txt"); if(fileno(FH)){ print "Opened filehandle\n"; }else{ print "Closed filehandle\n"; } close(FH);
娘に 「強度の高いパスワードを作ってよ」 と言われたので、ランダムにパスワードを作成するプログラムを Perl で書いてみました。
パスワード生成の仕様としては、アルファベットの大文字と小文字と数字と記号を含む、8文字のランダムな文字列を作成します。
#!/usr/bin/perl use strict; use warnings; my @str1 = qw(a b c d e f g h i j k m n o p q r s t u v w x y z); my @str2 = qw(A B C D E F G H J K L M N P Q R S T U V W X Y Z); my @str3 = qw(1 2 3 4 5 6 7 8 9); my @str4 = qw(+ - / = _); my @pattern = (); my $historyP = ""; my $cnt = 0; while($cnt < 4){ my $type = int(rand()*4); if($historyP =~ m/$type /){ next; }else{ $historyP .= $type." "; push(@pattern, $type); $cnt++; } } my $history1 = ""; my $history2 = ""; my $history3 = ""; my $history4 = ""; my $pass = ""; $cnt = 0; while(length($pass) < 8){ my $type = ""; if($cnt < 4){ $type = shift(@pattern); }else{ $type = int(rand()*4); } if($type == 0){ my ($idx, $word) = SelectWord(25, @str1); if($history1 =~ m/$idx /){ next; }else{ $history1 .= $idx." "; $pass .= $word; $cnt++; } }elsif($type == 1){ my ($idx, $word) = SelectWord(24, @str2); if($history2 =~ m/$idx /){ next; }else{ $history2 .= $idx." "; $pass .= $word; $cnt++; } }elsif($type == 2){ my ($idx, $word) = SelectWord(9, @str3); if($history3 =~ m/$idx /){ next; }else{ $history3 .= $idx." "; $pass .= $word; $cnt++; } }elsif($type == 3){ my ($idx, $word) = SelectWord(5, @str4); if($history4 =~ m/$idx /){ next; }else{ $history4 .= $idx." "; $pass .= $word; $cnt++; } }else{ print "out of index $type\n"; } } print "$pass\n"; exit; sub SelectWord{ my $len = shift@_; my @words = @_; my $idx = int(rand()*$len); my $word = $words[$idx]; return($idx, $word); }
Enjoy!
Wondows 上の ActivePerl から DBIモジュールを使って、ODBC 経由で DB に接続した場合、なぜかバインド変数を使用した SQL がエラーになることがあるんですよ。
例えば次のようなソースだと、execute するところでエラーになるんです。
#!/usr/bin/perl use strict; use warnings; use DBI; my $dbh = DBI->connect("dbi:ODBC:hoge","hoge","fuga"); my $sql = "select name from item where code=?"; my $sth = $dbh->prepare($sql); open(FH, "<", "item_codes.txt"); while(<FH>){ chomp; $sth->execute($_); while(my @data = $sth->fetchrow_array()){ # いろいろ処理 } } close(FH); $dbh->disconnect();
仕事で2つの日付の間の日数を計算しなくちゃいけなくなりました。
いつもならすぐにサブルーチンなんか書き始めるのですが、今回はなんとなく 「Date::Calc モジュールで出来るかも」 と思いまして、調べてみたら、やっぱり出来るじゃないですか!
というわけでサンプルのソースは以下の通りになります。
#!/usr/bin/perl use strict; use warnings; use Date::Calc qw(Delta_Days); my $Dd = Delta_Days(2012,1,1, 2012,1,3); print $Dd;
Delta_Days の第1引数から第3引数までが基準になる年月日で、第4引数から第6引数までが比較対象の年月日になります。
上記の例だと、2012年1月1日から2012年1月3日までの日数が変数 $Dd に格納されます。ちなみにこの場合の結果は2になります。
なお、実在しない日付を指定した場合はエラーになってそこで止まってしまうので、事前の日付存在チェックは必須となります。
参照リンク
・Date::Calc - search.cpan.org
Perl という言語はえらく親切で、他の言語ではエラーで止まるような場合でも 「こうしたいんでしょ?」 みたいな感じに気を利かせてくれて、処理を続行してくれたりします。
もちろんそれが有りがたい場合もあるわけですが、そうじゃない場合だってあるじゃないですか。で、そういう時に autodie プラグマを使うと、ちょっとだけ幸せになれそうです。
機能としては、Perl の動作を
レキシカルスコープ内の関数を、成功しなければ die するものに置き換える
autodie - perldoc.jp より引用
というように変えてくれます。
CSV(カンマ区切りのテキスト)なデータを処理するときに、何が厄介かって "(ダブルクォート)で囲まれたフィールドの中に存在する ,(カンマ)だったりします。例えばこんな感じ(↓)のヤツです。
日本,"9,222.52",2012/11/21
アメリカ,"12,836.89",2012/11/22
だったら、そのカンマを取り除いてしまえ! というわけで、プログラムを書いてみました。
#!/usr/bin/perl use strict; use warnings; my $qw = '"'; # 引用符を定義 my $dlm = ","; # 区切り文字を定義 open(RH, "<", "from.csv"); open(WH, ">", "to.csv"); while(<RH>){ print WH RemoveDelimiterInQuotes($_, $qw, $dlm); } close(WH); close(RH); exit; sub RemoveDelimiterInQuotes { my $str = shift; # 対象文字列 my $qw = shift; # 引用符 my $dlm = shift; # 区切り文字 my @substrs = ( $str =~ /$qw[^$qw]*$qw/g); for my $substr (@substrs){ my $target = $substr; $substr =~ s/$dlm//g; $str =~ s/\Q$target\E/$substr/; } return $str; }
from.csv を読み込んでダブルクォートに囲まれた部分のカンマを削除して、to.csv に書き出します。これでバッチリだね!
と、ここまで出来てから、な~んとなくもう一度 Web で検索してみたところ、標準モジュールの Text::ParseWords を使えばもっと簡単に出来たことが判明。あちゃ~。
perlでcsvファイルを読む(ダブルコーテーション内カンマを無視したい) | PerlのQ&A【OKWave】
折角なので Text::ParseWords を使って書き直したのがこちらになります。
#!/usr/bin/perl use strict; use warnings; use Text::ParseWords; my $dlm = ","; # 区切り文字を定義 open(RH, "<", "from.csv"); open(WH, ">", "to.csv"); while(<RH>){ chomp; my @parsed = parse_line($dlm, 1, $_); for(my $i=0; $i<=$#parsed; $i++){ $parsed[$i] =~ s/$dlm//g; } print WH join($dlm, @parsed)."\n"; } close(WH); close(RH); exit;
今回は処理内容を同じにするために、あえて to.csv として書き出していますが、parse_line を通した時点でデータはフィールドごとに分かれていますので、そのまま目的の処理をしてしまうのが正しい使い方でしょうね。