真・マイナンバーシェル芸
マイナンバー。それは12桁の数字で表現されるシークレットなアイデンティティ・・・アイデンティティがシークレットとはこれいかに???
これはTwitterでリプライをいただきながら進化していった物語である。Twitterでリプライしていただいた皆様、本当に感謝!
ということで、少し前にマイナンバーをひたすら列挙するシェル芸の話題が上がっていた。しかしながらこれは、純粋に0から9までの数字を12桁並べるという単純なものだった。
最近、どこかは忘れてしまったが「マイナンバーシェル芸はチェックデジットを考慮されてないやんけ」、というツッコミが目にとまった。チェックデジットって何?ってことでググると下記のページが見つかった。
Rubyを使って、チェックデジットを計算。不正なマイナンバーがどうかを判定いている。他の言語でも、このページを参考にしてチェックデジットの計算をされているようだ。なーるほど、12桁目の数字がチェックデジットってやつなのか。
では上記のページを参考にして、チェックデジットが適正なマイナンバーの一覧を出してみようか。シェルのワンライナー一撃で。
環境
- パソコンはMac
- OSX Yosemite
- HomebrewでGNU系ツールとmawkを導入
Linuxな人は、gseq
をseq
に、gsed
をsed
に、ghead
はhead
に読み替えること。
考え方その1
チェックデジットに適合したマイナンバーを列挙することを考える。 最初は下記の処理を考えた。
- 12桁の数字を順に列挙する
- 先頭11桁からチェックデジットを計算
- チェックデジットと12桁目が一致する行を出力
注意点
ここに出てくるワンライナーは負荷が高い。Ctrl-C
などで止める、もしくはプロセスの止め方を確認しておくことを忘れずに。
ワンライナー作成
では真のマイナンバーを列挙するワンライナーを作ろう。まずは12桁の数字を1桁ずつ空白で区切ったものを作成。それをmawkに渡してチェックデジットの計算をする方針で。
12桁の数字を1桁ずつ区切る処理をどうやって高速化するか?1桁ずつ区切る処理をGNU sedを使ってLANG=C gsed 's/./& /g'
とやっていたが、ここがどうしてもボトルネックになってしまう。ということでTwitteで助けを求めてみた。
【急募】123456789012 みたいな数字を 1 2 3 4 5 6 7 8 9 0 1 2 みたいに空白で分離を高速に処理する方法。今はGNU sedで LANG=C gsed 's/./& /g' とかやってますが・・・
— ぱぴろんちゃん😱🙀 (@papiron) 2016年5月1日
するとリプライが。
後は
— ゾンビプログラマ (@qeMkDrlaJwLLcQ) 2016年5月1日
$ echo 123456789012 | mawk NF=NF FS=
とかかなぁ
意味が分からなかったのでおたずねするとリプライが。ありがたや。
@papiron FS= で一文字づつ $1~$NF に割り当てて、NF=NF で $0 を再構築しているのです。OFS がデフォルトでスペース1個なのであの様になります
— ゾンビプログラマ (@qeMkDrlaJwLLcQ) 2016年5月1日
どのくらい処理速度が変わるかためしてみた。1000万行分の処理で計測。
$ time gseq -w 0 999999999999 | LANG=C gsed 's/./& /g' | ghead -n 10000000 > /dev/null real 0m29.489s user 0m34.109s sys 0m0.362s $ time gseq -w 0 999999999999 | mawk NF=NF FS= | ghead -n 10000000 > /dev/null real 0m5.763s user 0m10.311s sys 0m0.339s
GNU sedを使うより大幅な高速化が出来た。ありがたや。
ということで、真のマイナンバーを列挙するワンライナーがこちら。GNUのseqとmawkコマンドを利用。パイプでつなぐコマンドの数は減らせるが、わかりやすさ重視で。
$ gseq -w 0 999999999999 | mawk NF=NF FS= | mawk '{print $0,($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11}' | mawk '$13<=1{$13=0;print}$13>1{$13=11-$13;print}' | mawk '$12==$13{$13="";print}' | LANG=C gtr -d ' ' 000000000000 000000000019 000000000027 000000000035 000000000043 000000000051 000000000060 000000000078 000000000086 000000000094 ....
最初のgseqで12桁の数値を連続列挙。それをmawkに渡して、チェックデジットの計算と適合した行のみ抜き出す処理をしている。1桁ずつスペース区切りにする処理はmawk NF=NF FS=
で。
実はこの記事、Twitterで@ebanさんから指摘されるまで、チェックデジットの計算を間違っていたのだ。今は修正してある。だが、更に短くて高速な書き方のリプライが。
@papiron
— eban (@eban) 2016年5月1日
1つにまとめるとこんな感じですね。
mawk '{c=($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11}$12==(c<=1?0:(11-c))' FS=
ただ、{アクション}
の後ろに$12==(c<=1?0:(11-c))
書かれている意味が分からん・・・ナニコレ??リプライで意味を教えていただいた。
@papiron アクションと次のパターン$12==(c<=1?0:(11-c))は別物です。
— eban (@eban) 2016年5月2日
このパターンはチェックデジットが適正ならという判断なので、
真になれば$0がprintされます。
@papiron ebanさんの回答は、パターンーアクション文が2行あって、1行目はパターン無し、2行目はアクションなしかと(なので暗黙のアクション { print $0 } が実行される)。
— ot2sy39 (@ot2sy39) 2016年5月2日
そして更なる高速化が。ワンランナーも超シンプルに。
$ gseq -w 0 999999999999 | mawk '{c=($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11}$12==(c<=1?0:(11-c))' FS=
考え方その2
しかしその後、Twitterでこんなリプライが。なんで指摘されるまで気が付かなかったのか???更なる飛躍的な高速化が期待出来るではないか。
11桁の数値にチェックデジットを付加する方がいいんではなかろうか? https://t.co/CubtDEfZkd
— うた (@h_uta) 2016年5月3日
ということで新しい考え方がこちら。処理するデータ量が10分の1になるね。
- 11桁の数字を順に列挙する
- チェックデジットを計算
- 12桁目に計算したチェックデジットを付加
ワンライナー作成
そして出来たワンライナーがこちら。なんだかとってもシンプルになった。
$ gseq -w 0 99999999999 | mawk '{c=($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11;d=c<=1?0:(11-c);print $0 d}' FS=
どのくらいの処理速度なのか?最初の1億人分の出力時間を計測。考え方その1に比べて約10倍近い速度になっている。
$ time gseq -w 0 99999999999 | mawk '{c=($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11;d=c<=1?0:(11-c);print $0 d}' FS= | ghead -n 100000000 > /dev/null real 2m6.589s user 2m52.486s sys 0m3.472s
オマケ1
MacというかOSX標準装備のコマンドのみを利用したワンライナーがこちら。やっている内容は上記とほぼ同じ。処理速度はかなり遅くなる。
$ jot - 0 999999999999 | /usr/bin/awk '{c=($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11;d=c<=1?0:(11-c);print $0 d}' FS=
オマケ2
先日USP研究所様から、Personal Tukubaiが発売された。Open版に比べて処理速度が高速で、便利なコマンドが追加されている。
Personal Tukubaiのコマンドを使ったワンライナーがこちら。利用しているPersonal Tukubaiのコマンドは、yarr、loopx、lcalcだ。特にloopxとlcalcの高速さがポイント。
$ yes '<(seq 0 9)' | head -n 11 | yarr | sed 's/^/loopx /' | bash | lcalc '$0,$1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2' | lcalc '$[1:11],$12%11<=1?0:11-$12%11' | gtr -d ' '
このくらい計算式が複雑になってくると、mawkとlcalcで処理速度はあまり変わらないようである。ただ上記のように計算を2つに分ける場合はlcalcの方が並列化による速度向上効果が高かった。
$ time yes '<(seq 0 9)' | head -n 11 | yarr | sed 's/^/loopx /' | bash | lcalc '$0,$1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2' | lcalc '$[1:11],$12%11<=1?0:11-$12%11' | gtr -d ' ' | ghead -n 100000000 > /dev/null real 1m46.147s user 3m9.883s sys 0m4.496s
オマケ3
11桁の数字列挙する処理で、100000000000番台以降の場合は、Personal Tukubaiのloopxコマンドを使うより、GNU seqを使う方が処理速度が速い。桁を揃えるgseq -w
オプションが遅くなる原因。
$ time gseq 100000000000 999999999999 | ghead -n 100000000 > /dev/null real 0m2.856s user 0m3.041s sys 0m1.611s $ time yes '<(seq 0 9)' | head -n 11 | yarr | sed 's/^/loopx /' | bash | ghead -n 100000000 > /dev/null real 0m4.347s user 0m5.901s sys 0m1.592s
これは下記のTwtterつぶやきが参考になった。
@eban GNU seq(1)、整数かつ増分が 1 の場合は特別に最適化したコードが選択されるようになっていますね。数値を+1して文字列化するのでなく、文字列を直接操作していたり。
— ふみやす@シェルまおう(自称でない) (@satoh_fumiyasu) 2016年2月22日
おまけ4
@ebanさんと@kunst1080さんから下記のリプライをいただいた。チェックデジットの計算をawkのforループを使って計算する方法だ。この方が数式の表現により近い計算方法になる。なるほど。
この方法を適用してみたところ、速度が倍以上遅くなってしまった。forループなどの処理が速度面でボトルネックになってしまったのだろう。今回の場合はベタに数式を書いた方が処理が高速になった。色々と参考になる。
@papiron 実は半年前にこんなのを書きましたhttps://t.co/nPHiGnVwCK
— eban (@eban) 2016年5月3日
ぱぴろん氏がチェックディジット計算を最適化しようとしているみたいなので、文字列を分割せずに計算する方法はないかなぁと思ってちょっと遊んでみたの図。 pic.twitter.com/2wUB4jTVh2
— くんすと (@kunst1080) 2016年5月1日
;r=sum%11;if(r==0||r==1){cd=0}else{cd=11-r}print cd}'
— くんすと (@kunst1080) 2016年5月1日
@kunst1080さんからリプライいただいた方法の場合は、下記のように最初にsum=0を追加して毎行sumを初期化しておこう。
$ gseq -w 0 99999999999 | mawk '{sum=0;for(i=1;i<=11;i++){a=int($0/(10^(i-1)));b=a-int(a/10)*10;if(i<=6){sum+=b*(i+1)}else{sum+=b*(i-5)}};r=sum%11;if(r==0||r==1){cd=0}else{cd=11-r}print $0 cd}'
そして並列化へ
いろいろとやっていると、この記事のブコメに上田さんから並列化〜というコメントをいただいた。ああそうか並列化出来ちゃうね。ワンライナーは辛かったので、Gistにバックグラウンドで並列化するシェルスクリプトを載せておいた。パソコンに高負荷がかかり、危険を伴うので注意して実行すべし。バックグラウンド処理の速度計測するため、グループ化とかwaitとかを使っている。
そしたらGNU seqとmawkを使った場合で、xargsの-Pオプションを使った並列化についてリプライをいただいた。
@papiron 並列化、xargsの-Pオプションを使ってみてはどうでしょう
— ゾンビプログラマ (@qeMkDrlaJwLLcQ) 2016年5月6日
$ echo -n {0..9} | xargs -P4 -d' ' -I@ sh -c
@papiron あっと、
— ゾンビプログラマ (@qeMkDrlaJwLLcQ) 2016年5月6日
$ echo -n {0..9} | xargs -P4 -d' ' ...
は
$ seq 0 9 | xargs -P4 ...
でいいですね
おおお、結局ワンライナーで10並列書けたぞ。これならCtrl-Cですぐに停止も出来て安全!
$ seq 0 9 | xargs -P10 -I@ sh -c 'gseq $([ @ -eq 0 ] && echo -w) @0000000000 @9999999999 | mawk '\''{c=($1*6+$2*5+$3*4+$4*3+$5*2+$6*7+$7*6+$8*5+$9*4+$10*3+$11*2)%11;d=c<=1?0:(11-c);print $0 d}'\'' FS='