LoginSignup
2
2

More than 5 years have passed since last update.

Shellだけで文章のN-gramを求めたい

Last updated at Posted at 2017-02-07

参考
シェルの弱点を補おう!"まさに"なCLIツール、egzact

こちら、convaddlなどのコマンド集です。
データ処理ももちろんですが、"コマンド文字列"を作るために便利なコマンド、という感じだと思います。

やったこと

$ cat niwa.txt
にわにはにわにわとりがいる

$ cat niwa.txt | grep -o . | sed -n 'N;p;D;' | paste - - -d '' | sort | uniq -c | sort -k 1 -n
      1 いる
      1 がい
      1 とり
      1 には
      1 はに
      1 りが
      1 わと
      2 わに
      3 にわ

というわけで、「にわ」が三回含まれているみたいです :chicken:

N-gramと書きましたが、実際やりたいことのコアは上記参考ページのconvに相当することです。
多分awkとかでもっと普通に書けるとは思いますが、シェルっぽくパイプで色々繋いで書いてみました。
ぶっちゃけ、pastesedを色々いじっていて、面白いなーと思ったで、後学のために記します。

解説

pasteのところ

$ seq 10 | paste - - -d ' '
1 2
3 4
5 6
7 8
9 10

$ seq 10 | paste - - - -d ':'
1:2:3
4:5:6
7:8:9
10::

$ seq 20 | paste $(echo "${(r:8::- :)}") -d ','
1,2,3,4
5,6,7,8
9,10,11,12
13,14,15,16
17,18,19,20

paste コマンドは-が現れる毎に、標準入力から1行持ってくるみたいです。
なので-をN個書くことで、入力をN個ずつのグループに分けることが出来ます(便利)。

-dは挿入するデリミタを指定することが出来ます

sedのところ

sedは、単に文字列を置換するだけではなく、結構いろいろなことができるんですね。

sedに渡しているN;p;D;は、それぞれ

N   入力の次の行をパターンスペースに追加する。
p  現在のパターンスペースを出力する。
D  パターンスペースの最初の改行までを削除し、次の入力行を読み込まずに、その結果のパターンスペースでサイクルを開始する。

というような意味になりますsed man page

$ seq 5 | sed -n "N;p;D;" 

とすると、何が起きるのか考えてみた所

  1. patternスペースに1が読み込まれる
  2. Nで次の行が読み込まれ、パターンスペースは1 2となる(便宜上、スペースを改行と読み替えて下さい)
  3. pで、パターンスペースを出力する
  4. Dでパターンスペースから先頭の1行を削除する -> 2になる
  5. (おそらく)次の行の2を読みに行くが、1行目を処理したときのNですでに読み込まれているんで何もおきない
  6. Nで次の行が読み込まれ、パターンスペースは2 3となる

というような動きをしているのだと思います。1, 5 あたりはちょっと怪しい・・・
ですが、2. のところで、patternスペースが1 2となるのはこういう動きなんじゃないかな?と思います

結果としては

$ seq 5 | sed -n "N;p;D;"
1
2
2
3
3
4
4
5

となります。
なお、オプションの-nを外すと、最後にマッチングに失敗した行が出力されてしまうため最後にもう一回5が出力されます。

もうちょっと何が起こっているのかわかりやすくするために、pの前に=というコマンドを挟むと、こんな感じになります。
=は、現在の行数を表示するコマンドです。

$ seq 1000 1005 | sed -n "N;=;p;D;"
2
1000
1001
3
1001
1002
4
1002
1003
5
1003
1004
6
1004
1005

ふむふむ。

発展

では、3個ずつはどうすればいいでしょうか?

paste は簡単ですね

paste - - - 

sedは

sed -n "1N;N;p;D"

となります。Nは、数字の引数を取ることができて、マッチした行数のときのみそのコマンドが実行されます。
というわけで、上記コマンドは、1行目のときのパターンスペースに1行取り込み、以後はこの1Nは何もしません。
2行目以降の行はN、Dで1行ずつ出したり入れたりするので、結果的にパターンスペースには3行ずつ入った状態でpが呼ばれます。

同様に、4-gramならsed -n "1N;2N;N;p;D"という感じです。

convコマンドとして仕立てるには、pasteとsedそれぞれのコマンド文字列を、引数から生成する必要がありますが、規則性がはっきりしているので、これはそんなに難しくなさそう。

実験

# 3-gram
$ cat melos.txt | grep -o . | sed -n "1N;N;p;D" | paste - - - -d '' | sort | uniq -c L | sort -k 1 -r | head -10
     51 メロス
     33 ロスは
     29 った。
     24 。」「
     22 ている
     22 た。「
     21 のだ。
     19 。メロ
     17 スは、
     17 おまえ

感想

なんとなく思ったのは、今までパイプでコマンドをつなぐときは、文字列を加工またはフィルターするということが多かった気がします
sedを上手く使えば、むしろ増やすということが結構簡単にできるため、手数が増えそうだな、と思いました。

例えば、参考ページのdupl 2は以下の様に書けます

$ echo kuri | sed -n "p;p;"
kuri
kuri
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2