たいちょーの雑記

ぼくが3日に一度くらい雑記をかくところ

第50回シェル芸勉強会に参加しました

参加しました

今回もリモート開催でした。毎度書きますけど、やっぱり問題の辛さに呻く声とかが聞こえなくて寂しいですね

YouTubeの配信はこちら

今回も時間中に解答したものと、これを書くと同時にPowerShellでの解答も書いていこうと思います。できるのか…俺に…?

Q1

un.txt.gzから、UNITY, UNKO, unity, unkoの数をそれぞれなるべく短時間で数えてください。 単語の途中に改行が入っているものも数えてください。

問題の意味は簡単に理解できるので、パッと思いつくような書き方で行くなら

$ zcat un.txt.gz | tr -d \\n | grep -io -e "UNKO" -e "UNITY" | sort | uniq -c | sort -rn

ところが、これでは時間がかかりすぎるということで、速度の改善が必要とのこと。最初はzgrepで改行を巻き込んだ検索をしようとしました

$ zgrep -oiP "(U\n?N\n?K\n?O\n?|U\n?N\n?I\n?T\n?Y\n?)" ./un.txt.gz | sort | uniq -c | sort -rn

これだとうまく引っかけられなかったので、長考。解答例では以下のように aだけの行を削ってから検索すればよいとのこと

zgrep -vP "^a+$" *a/*50/un.txt.gz | tr -d \\n | grep -ioP "un(ko|ity)" | sort | uniq -c | sort -rn

ところがこれは

aaaaaaaaaaaaaaaaun
aaaaaaaaaaaaaaaaaa
koaaaaaaaaaaaaaaaa

というデータにぶつかったときに正しくなくなる。(参考)

それを踏まえてもう一度考える。

$ time zcat ./un.txt.gz| sed -E 's/a+/a/g' | tr -d \\n | grep -ioP "UN(KO|ITY)" | sort | uniq -c | sort -rn 
      3 unko
      3 unity
      2 UNKO
      2 UNITY
zcat ./un.txt.gz  5.39s user 0.83s system 61% cpu 10.102 total
sed -E 's/a+/a/g'  7.67s user 2.43s system 99% cpu 10.102 total
tr -d \\n  0.03s user 0.24s system 2% cpu 10.102 total
grep -ioP "UN(KO|ITY)"  1.23s user 0.00s system 12% cpu 10.190 total
sort  0.00s user 0.00s system 0% cpu 10.189 total
uniq -c  0.00s user 0.00s system 0% cpu 10.189 total
sort -rn  0.00s user 0.00s system 0% cpu 10.188 total

連続するaを1文字のaにしてから改行削除、grepで絞り込みをしてみた。これだと10秒ぐらい。もっと速い解答もTwitterにあるけど、僕のはこれとしておきます。

続いてPowerShellです。

$ (7z e ./un.txt.gz -so |%{ $_ -replace "a+","a" }| Get-Unique) -join "" | Select-String -AllMatches "UN(KO|ITY)" | %{$_.Matches.Value} | Group-Object -CaseSensitive

Count Name                      Group
----- ----                      -----
    2 UNITY                     {UNITY, UNITY}
    3 unko                      {unko, unko, unko}
    2 UNKO                      {UNKO, UNKO}
    3 unity                     {unity, unity, unity}

やることは同じです。PowerShellではgrepの代わりにSelect-Stringをよく使いますが、grep-oに相当する機能を単独で持ちません。Select-StringMatchInfoというオブジェクトを出力するので、そこからマッチした部分(MatchInfo.Matches.Value)を取り出すことでgrep -o相当のことができます。あとはGroup-Objectで数え上げればOKですね。

Q2

un.txt.gzでaのみが書いてある行の行数をなるべく早くカウントしてください。

とのこと。

$ zgrep -cP "^a+$" *a/*50/un.txt.gz
$ rg -cz "^a+$" *a/*50/un.txt.gz

このどちらかがまあ割と高速。解説することがないですね。

PowerShellでの解答も書いていこうと思います。

$ 7z e ./un.txt.gz -so | group

これで出るんじゃないかな・・・(手元じゃ終わらなかったのでわからない)

Q3

catコマンドを遅くする問題

cat ./un.txt > /dev/null

こう書くと、一瞬で終わるのだけど、このコマンドの前に何かを仕込んで遅くするというもの

$ function cat() { \cat $1 | \cat }
$ cat a > /dev/null

単純に二段重ねにしたら遅くなるよねって感じですね。解答例としては、/proc/sys/vm/drop_cachesをいじってやるというもの。勉強になるなあ

$ echo 3 | sudo tee /proc/sys/vm/drop_caches
$ cat a > /dev/null

Q4

1から1億までの数字をシャッフルして2列にしたデータ(ファイル名はaにしましょう)を作ってください。速いワンライナーを考えてください。

とのこと。最初に自分が書いたのがこれ

$ seq 1000000 | paste -d' ' - - | teip -f1 shuf | teip -f2 shuf > a'

xargsでは永遠に終わらないだろうなと思っていたので、paste、シャッフルは後続のteipに任せることに。これ結構いいと思ったんですが…

マシンがやられてしまった。物理ボタンぶち押して再起動しました。悲しい。

再起動中TLを見ているとshuf-iオプションというのがあるのを知ってへぇと思いました

$ shuf -i 1-10
8
3
7
2
9
10
1
6
4
5

PowerShellでの解答も書いていこうと思います。

$ 1..100000000 | Sort-Object { Get-Random } | %{[PSCustomObject]@{k=$_}} | Format-Wide -Column 2

横に並べるって言うのが中々大変でした。Format-Wideでそのようなことができるのですが、一度PSCustomObjectにして置く必要があるんですねこれが。あとこれも全然終わりません。悲しい。どうしたらいいんだ

一応、Get-Content-ReadCountを使えばファイルの中身を分割できるので、少しずつなら出力できますね。ただし、区切られた区間でのみのシャッフルになりますが

$ 1..100000000 > nums
$ Get-Content ./nums -ReadCount 10000 | %{ $_ | Sort-Object{Get-Random} } | %{[PSCustomObject]@{k=$_}} | Format-Wide -Column 2

この問題の出力は次の問題で使うので、aというファイルに書き出しておきます。

Q5

cat a | sort -k2,2n > b を出力内容を変えずに高速化する問題。

これは知識問題。気づけるかな?という問題でした。catから始めていますが、sortに直接渡した方がCPU使用率に差がでるとのこと。そういえばcatからおもむろに始めることが多いので、知らないうちにワンライナーが遅くなっちゃってるかもですね。勉強になる

Q6

出題時マシン再起動中でした(前問を試していたら死んだ)。問題を見る限り、teipが使えそうと思っていたらTLにもあったのでさすがteipだなあと思いました

復活して書いたのがこれです。

$ awk '$1%2==$2%2==1' a | teip -og \\d+ -- factor | awk 'NF==4' > c

前段では、両方とも奇数なカラムだけを取り出して前処理、次に-og \\d+で数値だけなカラムに対してfactorを適用します。あとは awkで切り出しですね。

PowerShellでは、なんだろう…teipが使えないので、ForEach-Objectfactorするとかかな…?

Q7

aに対して二つの操作をする問題。

  • 行頭に A, B, C, ... , Z, A, ...を付ける
  • 行頭のA-ZでGroupByする

これ、最中は同時にやるもんだと思っていたので以下の解答をしました

$ awk '{print $1"_"$2}' a | paste -d' ' -{,,,,,,,,,,,,,,,,,,,,,,,,,} | rs -T| tr -d _ | paste <(echo {A..Z} | fmt -1) -'

26個なのは決まっているので、pasteで横に26個並べてから rs -Tで転地。あとは行頭に A-Zを付けただけですね。まぁこれは題意とは違うのでダメなのですが・・・

Q8

Q7について、さらに各行の数字を小さい順にソートするという条件をつけてください。ans2というファイルに保存します。

Q5の解きなおしをしていたらマシンが死んだので再起動していました。本当に悲しい。

LT

今回はLTしました。以前のエントリで書いたワンライナーC#ocsと、カラム選択を短縮できるselの紹介です。

ocsの方はまぁ良かったんですが、selの発表中に、すでにselfという同じ感じのあるよというナイフを突きつけられてめちゃめちゃ焦りました。

一応冷静にselfselの比較をしていたんですが、selには入力と出力のデリミタを指定できる、sed -iみたいな上書きができるなどの優位点があります。

速度的にはチューニング後のselHaskell版のselfで大体同じ。若干selfの方が速いです。ただしこれは、selの入力デリミタを固定文字(スペース)としたときなので、正規表現で分割するときは負けてしまいます。

あとはselgo getするだけ、selfPythonの特定バージョン以上が必要。もしくは、ghcなどでHaskell版をコンパイルする必要があるなどが違いですかね。

正直 sel 1 2 3 みたいな使い方をしたいだけならどちらでもよいので、好きな方を使おうって感じですね。今回のことでパフォーマンスチューニングに興味も沸いたのでselを爆速にしてやりたいと思っております。

まとめ

今回もとても楽しかったです!あととっても疲れました!!Q8のあたりとかマシンもぼくも疲労困憊でした!次回も楽しみです!ありがとうございました!