第41回シェル芸勉強会 - Python ワンライナー版解答

第 41 回シェル芸勉強会の問題の Python ワンライナーでの解答です。
真面目なほうは 第41回シェル芸勉強会に参加しました。 - nogiro_iotaのメモ を参照してください。

けっこう改訂しています。(解説も追加する予定です。しました。)
改訂履歴を最下段に載せています。

まとめ

python3 -c 'import sys; [print(x[:-1]) for x in sorted(sys.stdin, key=lambda x:hoge)]'hoge をいじれば標準入力を好き勝手ソートできる。

全体に共通する解説

第 41 回シェル芸勉強会はソート回とのことでした。
Python でのソートは sort()sorted() の 2 種類です。この記事中の回答ではすべてsorted()を使用しています。sorted()は引数として iterable なオブジェクト (sys.stdin やリスト) を受け取って、ソート済みのリストを返すためワンライナーで扱い易いためです。(sort() はリスト型クラスのメソッドで、呼ぶとリスト内部をソートして返り値は None になります。)

printf デバッグ

Python は短絡評価してくれて print() は None を返すので、値を見たい部分で print(hoge) or hoge すると printf デバッグできます。

以下は A4-2 を例にしています。

$ python3 -c '[print(x.strip()) for x in sorted(open("ShellGeiData/vol.41/sjis", "r", encoding="cp932"), key=lambda x:print(int(x.split()[0])) or int(x.split()[0]))]'
123
31
20
2242
20 ほじぱんふんじこみ
31 こきたてひーひー
123 ずんごるももう
2242 うえってきたかるとらまん

問題と Python ワンライナー版解答

sorted() の key でソート基準を決める関数を渡せるのでけっこう簡単でした。

Q1

 次のファイルについて、2列目をキーにしてエクセルの横列の記号(A, B, ..., Z, AA, AB, ...のやつ)順に並べ替えてください。

$ cat excel
114514 B
1192296 AA
593195 CEZ
4120 TZ
999 QQQ
A1
$ cat ShellGeiData/vol.41/excel | python3 -c 'import sys; [print(x[:-1]) for x in sorted(sys.stdin, key=lambda x:("%%%"+x.split()[1])[-4:])]'
114514 B
593195 AA
4120 TZ
1192296 CEZ
999 QQQ

解説

解き方自体は前記事と同じです。
列番号は x.split()[1] で得られるので、前部分に"%%%"をつけて後ろ 4 文字 ([-4:]) を返すようにします。

Q2

 次のファイルのレコードを干支順にソートしてください。

$ cat eto_yomi
申 さる
子 ね
寅 とら
卯 う
巳 み
辰 たつ
丑 うし
酉 とり
戌 いぬ
亥 い
午 うま
未 ひつじ

ただし、次のファイルを補助に使って良いこととします。

$ cat eto
子丑寅卯辰巳午未申酉戌亥
A2
$ cat ShellGeiData/vol.41/eto_yomi | python3 -c 'import sys; [print(x[:-1]) for x in sorted(sys.stdin, key = lambda x:open("ShellGeiData/vol.41/eto", "r").read().index(x.split()[0]))]'
子 ね
丑 うし
寅 とら
卯 う
辰 たつ
巳 み
午 うま
未 ひつじ
申 さる
酉 とり
戌 いぬ
亥 い

解説

補助ファイルを文字列として持てば、その index でソートすればキレイにできます。

ただ上の回答だと、eto_yomi を 1 行読むたびに eto を読み直しているのでファイルが大きくなると遅くなりそうですね。。。
以下のように、先に変数に入れておけば良いですね。

cat ShellGeiData/vol.41/eto_yomi | python3 -c 'import sys; eto = open("ShellGeiData/vol.41/eto", "r").read(); [print(x[:-1]) for x in sorted(sys.stdin, key=lambda x:eto.index(x.split()[0]))]'

Q3

 次のファイルのレコードを数字(第一フィールドの計算結果)が小さい順に並べてください。

$ cat kim_calc
1+2+4 金正日
4*3 金正男
3-1-5 金日成
495/3 金正恩
0x1F 金正哲
A3
$ cat ShellGeiData/vol.41/kim_calc | python3 -c 'import sys;[print(x.strip()) for x in sorted(sys.stdin, key=lambda x:eval(x.split()[0]))]'
3-1-5 金日成
1+2+4 金正日
4*3 金正男
0x1F 金正哲
495/3 金正恩

解説

eval() がつよい。

Q4

 次のファイルはシフトJISのテキストですが、これを1) 辞書順、2) 数字の小さい順、にソートしてください。出力もシフトJISとします。

$ cat sjis | nkf -g
Shift_JIS
$ cat sjis | nkf -wLux
123 ずんごるももう
31 こきたてひーひー
9 ほじぱんふんじこみ
2242 たまもとやろう
A4-1
$ python3 -c 'import codecs;[print(x.strip()) for x in sorted(codecs.open("ShellGeiData/vol.41/sjis", "r", "cp932"))]'
123 ずんごるももう
20 ほじぱんふんじこみ
2242 うえってきたかるとらまん
31 こきたてひーひー

解説

PythonSJIS のファイルを読み込むには codecs.open() を使うか open()encoding を渡します。

A4-2
$ python3 -c '[print(x.strip()) for x in sorted(open("ShellGeiData/vol.41/sjis", "r", encoding="cp932"), key=lambda x:int(x.split()[0]))]'
20 ほじぱんふんじこみ
31 こきたてひーひー
123 ずんごるももう
2242 うえってきたかるとらまん

解説

Python では全角の数字を int にキャストできるようです。すごい。

Q5

 サイズの小さい順にソートしてください。

$ cat size 
2GB
1.2GB
40000MB
1000000000kB
0.4GB
410MB
A5
$ cat ShellGeiData/vol.41/size | python3 -c 'import sys; d = {"k":1,"m":2,"g":3}; [print(x.strip()) for x in sorted(sys.stdin, key=lambda x:eval("".join([j.replace(i,"*1000" * d[i]) if i in j else "" for j in [x[:-2].lower()] for i in d.keys()])))]'
0.4GB
410MB
1.2GB
2GB
40000MB
1000000000kB

解説

SI 接頭辞をディクショナリで定義して (d = {"k":1,"m":2,"g":3})、その値の数 "*1000" をつけたものを eval() しています。

想像しにくいように思うので、 eval() で計算する前の文字列を出力したものを以下に貼ります。(改行していますが cat()])))]' までコピペで動きます。)

$ cat ShellGeiData/vol.41/size | python3 -c 'import sys; d = {"k":1,"m":2,"g":3}; [print(x.strip()) for x in sorted(sys.stdin, key=lambda x:
print("".join([j.replace(i,"*1000" * d[i]) if i in j else "" for j in [x[:-2].lower()] for i in d.keys()])) or 
 eval("".join([j.replace(i,"*1000" * d[i]) if i in j else "" for j in [x[:-2].lower()] for i in d.keys()])))]'
2*1000*1000*1000
1.2*1000*1000*1000
40000*1000*1000
1000000000*1000
0.4*1000*1000*1000
410*1000*1000
0.4GB
410MB
1.2GB
2GB
40000MB
1000000000kB

途中で出てくる for j in [x[:-2].lower()] はリスト内包表記内で変数を使う方法です。

Q6 (省略)

bash 内部コマンドは Python ワンライナーから呼べないため省略します。

Q7

 次のローマ数字をソートしてください。

$ cat roman
IV
XI
LXXXIX
IX
XLIII
XX
VIII
A7
$ cat ShellGeiData/vol.41/roman | python3 -c 'import sys;d={"I":1,"V":5,"X":10,"L":50};[print(x[:-1]) for x in sorted(sys.stdin, key=lambda x:eval("+".join([[a[j]+"*-1" if j < len(a) - 1 and a[j] < a[j+1] else a[j] for j in range(len(a))] for a in [[str(d[i]) for i in x[:-1]]]][0])))]'
IV
VIII
IX
XI
XX
XLIII
LXXXIX

解説

A5 と大きく変わっていません。

IVXL などの減算則の部分に "*-1" をつける処理 ([a[j]+"*-1" if j < len(a) - 1 and a[j] < a[j+1] else a[j] for j in range(len(a))]) が主な違いです。

Q8

 次のファイルを辞書順にソートしてください。ただし、濁点がついているものが先に来るようにしてください。できる人はワンライナー中で「かきくけこがぎぐげご」の文字を使わないでください。

$ cat gagigugego 
かき氷
ぎ・おなら吸い込み隊
きつねうどん
ぐりこもりなが事件
きききりん
がきの使い
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ
がきの使い
かき氷
ぎ・おなら吸い込み隊
きききりん
きつねうどん
ぐりこもりなが事件
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ
A8
$ cat ShellGeiData/vol.41/gagigugego | python3 -c 'import sys; from unicodedata import normalize; c=u"\U00003099"; [print(x[:-1]) for x in sorted(sys.stdin, key=lambda x:["".join(c in a and a.insert(a.index(c), u"\U00000000") or a) for a in [[i for i in normalize("NFD",x)]]])]'
がきの使い
かき氷
ぎ・おなら吸い込み隊
きききりん
きつねうどん
ぐりこもりなが事件
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ

解説

Python だと unicodedata.normalize()Unicode 正規化できます。

そのままソートすると期待どおりにソートされないので、濁点 (u"\U00003099") の前に小さな値 (u"\U00000000") を追加してソートしています。

改訂

  • A8. a[a.index(c):]+[u"\U00000000"] + a[:a.index(c)] -> a.insert(a.index(c), u"\U00000000")
  • A2. [j for j in [i for i in open("ShellGeiData/vol.41/eto", "r")][0]] -> [i for i in open("ShellGeiData/vol.41/eto", "r")][0]
  • A1.lambda x:("%%%"+x[1])[:-4] -> lambda x:("%%%"+x[1])[-4:]

eban on Twitter: "最後は[-4:]じゃないでしょうか?… "

  • 全体. sorted([i for i in sys.stdin], key= -> sorted(sys.stdin, key=
  • まとめを追加
  • A2. [i for i in open("ShellGeiData/vol.41/eto", "r")][0] -> open("ShellGeiData/vol.41/eto", "r").read()
  • A5. SI接頭辞をディクショナリで定義して汎用性を上げました
    • 前.
      cat ShellGeiData/vol.41/size | python3 -c 'import sys;[print(x.strip()) for x in sorted(sys.stdin, key=lambda x:eval(x[:-2].replace("k","*1000").replace("M","*1000"*2).replace("G","*1000"*3)))]'
    • 後.
      cat ShellGeiData/vol.41/size | python3 -c 'import sys; d = {"k":1,"m":2,"g":3}; [print(x.strip()) for x in sorted(sys.stdin, key=lambda x:eval("".join([j.replace(i,"*1000" * d[i]) if i in j else "" for j in [x[:-2].lower()] for i in d.keys()])))]'
  • A7. 減算則が適用されるのが、最初にある場合のみになっていたのを修正しました。 (LXXXIX が 89 ではなく 91 に計算されていた。)
    • 前.
      eval("+".join([[a[0]+"*-1"]+a[1:] if a[0] < a[1] else a for a in [[str(d[i]) for i in x[:-1]]]][0])) or x
    • 後.
      eval("+".join([[a[j]+"*-1" if j < len(a) - 1 and a[j] < a[j+1] else a[j] for j in range(len(a))] for a in [[str(d[i]) for i in x[:-1]]]][0]))
  • 解説を追加