sort | uniq -c
下記のLinuxのコマンドの並びの意味は分かるだろうか?
sort | uniq -c
答えは、標準入力からデータを読み取って、どのような文字列の行が何個あるかを数えることである。パイプ(縦棒)は、左側のコマンドの標準出力を右側のコマンドの標準入力に渡す働きをしている。
しかし、Linux/Unixのsortは遅い。(大体100万個を超えると)
下記は1000万個(1e7=10**7)の1,2,3,4,5の文字からなる行を作って、 sort して、uniq -c している。1m28.381sとあるように約1分半かかっている。いろいろな考え方があるが、Linux/Unixの sort コマンドはやや遅い(Perl言語に実装されている sort コマンドより数倍遅い)。
なので、専用コマンドを作る。
また、Linux/Unixの sort コマンドの弱点として、英数字以外の入力に対してエラーを起こすことがあることである。そういうようなもろもろの問題を解決すべく、この記事の後半にあるようなコマンド freq で同じことをしてみた。4秒以内(3.679秒で)、1,2,3,4,5の文字がそれぞれ何個が数えている。
例. ツイッターで60万個くらいのアカウントを収集した場合の、HTTPステータスコードの様子
たとえば、ツイッターから60万個くらいのアカウントについて、インターネット経由で情報を取りだそうとして、各アカウントのページの状態について、HTTPステータスコードのデータが取得出来たとする。全てが "200 OK" とは限らない。ある時1週間くらい掛けて、データを取り出して、そのHTTPステータスコードを収集したら、下の図のようになった。左側1列目の数が、それより右側に書かれた文字列で表されたHTTPステータスコードの出現数を表している。
プログラム
ということで、何が何個あったか一覧表(度数分布表を作る)プログラムは下記の通り。
プログラムが走り始めて、何秒か待っている内に、いつ終わるのか/どこまで数えたのか、心配になることはよくあるから、Ctrl+Cを一度押したら何行まで数えたか表示する様にし、そして1秒待ってさらにその後4秒以内に再びCtrl+Cを押したら、それまでの結果を表示し、さらに再び1秒待ってその後4秒以内にCtrl+Cが押されたら、プログラムが初めて強制終了するような工夫をしてある。(SQLで select var, count(*) from tbl group by var とやっても、Ctrl+Cで途中で止めたら何も残らないから、困った人は多いことでしょう??) その他、どういうような順序で表示するか選べるようにもしてある。
#!/usr/bin/perl
use 5.011 ; use strict ; use warnings ; # Already confirmed that 5.001, 5.011, 5.018 is ok.
use List::Util qw{sum} ;
use Getopt::Std ;my %o ; getopts "1de:knrsuEKPSW%=" , \%o ;
use Term::ANSIColor qw/color :constants/ ; $Term::ANSIColor::AUTORESET = 1 ;
my %strcnt ; # 数える対象の文字列(各行から最後の改行文字列を取り除いたもの) に対して、数えた数を入れる。
my ($totalSum, $cumsum ) = ( 0 , 0 ) ; # 累和カウンタ
my $sep = "\t" ; # 出力用セパレータ
print STDERR "\$PID=$$\n" if $o{P} ; # <- 何をする??
my $intflg = 0 ;
my $header = <> if ($o{q/=/} );
& reading() ; ### 1. 読む
& output() ; ### 2. 出力する
exit(0) ;
# ここからは関数
sub sigint1 {
alarm 0 ;
$intflg = 0 ;
print STDERR YELLOW "Processing $.-th line. ", scalar localtime , "\n" ;
sleep 1 ;
$SIG{INT} = \&sigint2 ;
$SIG{ALRM} = sub { $SIG{INT} = sub { $intflg = 1 } } ;
alarm 4 ;
}
sub sigint2 {
alarm 0 ;
sleep 1 ;
$SIG{INT} = sub { print color('reset') ; die } ;
$SIG{ALRM} = sub { $SIG{INT} = \&sigint1 } ;
alarm 4 ;
print color('cyan') ;
&output ;
print color('reset') ;
}
sub reading {
$SIG{INT} = sub { $intflg = 1 } ;
while ( <> ) {
chomp ;
if ( $o{E} && $_ eq qq{} or $o{W} && /^\s*$/ ) { next ; } # -Eオプションの時は、空文字列を飛ばす -Wのときは空白文字行を飛ばす
if ( $o{e} && $_ eq qq{} ) { $_ = $o{e} } ; # -eオプションで、空文字をどの文字列に置き換えるか指定する。
$strcnt { $_ } ++ ;
if ( $intflg ) { &sigint1 } ;
}
}
sub output {
print "header:\t", $header if ( defined $header ) ;
my @k = sort keys %strcnt ;
if ( $o{d} ) { @k = grep { $strcnt { $_ } > 1 } @k } # -d オプションにより、2個以上のもののみを残す。
elsif ( $o{u} ) { @k = grep { $strcnt { $_ } == 1 } @k } # -u オプションにより、2個以上のもののみを残す。
if ( $o{n} ) { @k = sort { $strcnt{$a} <=> $strcnt{$b} } @k } # -n オプションによりコンテンツの数であらかじめ、ソートする
if ( $o{k} ) { @k = sort { $a cmp $b } @k } # -k オプションによりキー文字列であらかじめ、ソートする
if ( $o{K} ) { @k = sort { $a <=> $b } @k } # -k オプションによりキー文字列であらかじめ、ソートする
if ( $o{r} ) { @k = reverse @k } # r オプションで逆順ソート
if ( $o{'%'} ) { $totalSum = sum ( values %strcnt ) } ; # '%'オプションにより、まず比率を計算するための総和を計算。
for ( @k ) {
if ( $o{s} ) {
$cumsum += $strcnt { $_ } ;
if ( $o{'%'} ) { print sprintf "%5.2f%%$sep", 100.0*$cumsum / $totalSum }
print $cumsum, $sep ;
} ; # -s オプションにより、累和を表示。
if ( $o{'%'} ) { printf "%5.2f%%$sep", 100.0*$strcnt{$_} / $totalSum }
print +( $strcnt{$_}.$sep ) x ($o{1}?0:1) . $_ ; # -1オプションがあれば個数を表示しない。
print "\n" ;
}
if ( $o{S} ) { print sum ( values %strcnt ) , "\n" } # -S オプションがあれば、最後に合計の数を表示
}
sub VERSION_MESSAGE{ }
sub HELP_MESSAGE{
$0=~s|.*/|| ;
$ARGV[1] //= '' ;
if ( $ARGV[1] =~ m/opt/ ) {
while ( <DATA> ) {
print $_ if m/^\ +\-/ ;
}
exit 0
}
while(<DATA>){
s/\$0/$0/g ;
print $_ if s/^=head1//..s/^=cut//
}
exit 0 ;
}
__END__
=encoding utf8
=head1
コマンド
$0 datafile
$0 < datafile
cat datafile | $0
オプションに関して
-= ならば、ヘッダがあると見なして処理
-e ならで空文字列を -e に続く文字列で置換する。
-E ならば空行を数えない
-W ならば空白文字だけで構成された行を数えない
-n なら出現数で整列する -nr なら逆順にする
-k ならキー文字列で整列する -kr なら逆順にする
-K ならキー文字列を数と見なして整列する -Kr なら逆順にする
-1 なら個数を表示しない。出現したキー文字列のみ表示。
-s で累和を表示
-S で合計の行数を表示する。
-d オプションにより、2個以上のもののみを残す。
-u オプションにより、1個のもののみを残す。
その他:
Ctrl+C により、それまで数えた結果が、すぐに標準出力に書き出されて、終了する。
他、途中どこまで数えたか、1,2,5万、10,20,50万、100,200,500万行に達したことなど、標準エラー出力に出したいが、未実装。
=cut