言語処理100本ノック第2章で学ぶUNIXコマンド

## はじめに

社内のメンバーを中心にした勉強会で[言語処理100本ノック](http://www.cl.ecei.tohoku.ac.jp/nlp100)を解いているのですが、その解答コードや、解く過程で便利だなと思った小技のまとめです。自分で調べたり検証したりした内容が多いですが、他の勉強会メンバーが共有してくれた情報も入っています。

今回はUNIXコマンドの基礎についてまとめていきますが、先行する[@moriwoさん](https://qiita.com/moriwo/items/9d2a73a75f543e2ea6af)や[@segavvyさん](https://qiita.com/segavvy/items/fb50ba8097d59475f760)の記事でかなり詳しい解説がすでに書かれているので、この記事での説明はさらっと控えめにしたいと思います。以下を読んで分からない点があれば、上のリンクからお二方の記事なども見てみるのがおすすめです。

## シリーズ

- [言語処理100本ノック第2章で学ぶUNIXコマンド](https://qiita.com/Yusuke196/items/35668859ea70bc9539f5)（この記事）
- [言語処理100本ノック第3章で学ぶ正規表現](https://qiita.com/Yusuke196/items/47203be382f60d653da7)
- [言語処理100本ノック第4章で学ぶ形態素解析](https://qiita.com/Yusuke196/items/b41e70b3a60cbac25294)

## 環境

- macOS
- Python 3.8.1
- JupyterLab

## コード

### 10. 行数のカウント

```py:Python
def count_lines():
    with open('hightemp.txt') as file:
        return len(file.readlines())

count_lines()
```

```:結果（Python）
24
```

```sh:UNIX
!wc -l hightemp.txt
```

```:結果（UNIX）
      24 hightemp.txt
```

UNIXコマンドの方が圧倒的に簡潔に書けました。ちなみに`wc`の前についている`!`は、JupyterLabやNotebookでUNIXコマンドを実行するときに使うものです（場合によって`!`なしでも動作します）。

### 11. タブをスペースに置換

```py:Python
def replace_tabs():
    with open('hightemp.txt') as file:
        return file.read().replace('\t', ' ')
    
print(replace_tabs())
```

```sh:UNIX
!cat hightemp.txt | sed $'s/\t/ /g'
```

```:結果（PythonとUNIX共通）
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
岐阜県 多治見 40.9 2007-08-16
山形県 山形 40.8 1933-07-25
山梨県 甲府 40.7 2013-08-10
...
```

UNIXの`sed`について、`$`記号をつけないと`\t`がタブ記号として認識されないという点には注意が必要かなと思いました。

### 12. 1列目をcol1.txtに，2列目をcol2.txtに保存

```py:Python
import pandas as pd

def separate_columns():
    df = pd.read_csv('hightemp.txt', sep='\t', header=None)
    df.iloc[:,0].to_csv('col1.txt', header=False, index=False)
    df.iloc[:,1].to_csv('col2.txt', header=False, index=False)

separate_columns()
```

```sh:UNIX
!cut -f 1 hightemp.txt > col1_unix.txt
!cut -f 2 hightemp.txt > col2_unix.txt
```

結果を`!head col1.txt col2.txt`で確認すると、次のようになります。`!head col1_unix.txt col2_unix.txt`とした場合も同様です。

```:結果（Python）
==> col1.txt <==
高知県
埼玉県
...
==> col2.txt <==
江川崎
熊谷
...
```

### 13. col1.txtとcol2.txtをマージ

```py:Python
def merge_columns():
    with open('col1.txt') as col1_file, open('col2.txt') as col2_file, \
         open('merge.txt', mode='w') as new_file:
        
        for col1_line, col2_line in zip(col1_file, col2_file):
            new_file.write(f'{col1_line.rstrip()}\t{col2_line.rstrip()}\n')

merge_columns()
```

```sh:UNIX
!paste col[1-2].txt > merge_unix.txt
```

```:結果（PythonとUNIX共通）
高知県	江川崎
埼玉県	熊谷
岐阜県	多治見
山形県	山形
...
```

結果の確認については`!head merge.txt`または`!head merge_unix.txt`とすると、上のような出力が得られるはずです。

### 14. 先頭からN行を出力

```py:Python
def show_head():
    n = int(input())

    with open('hightemp.txt') as file:
        for line in file.readlines()[:n]:
            print(line.rstrip())
    
show_head()
```

```sh:UNIX
!head -3 hightemp.txt
```

```:結果（PythonとUNIX共通）
高知県	江川崎	41	2013-08-12
埼玉県	熊谷	40.9	2007-08-16
岐阜県	多治見	40.9	2007-08-16
```

Pythonについては、関数からリストを返すことにしたい場合、以下のように書くのもありかと思います。

```py:Python
def show_head():
    n = int(input())

    with open('hightemp.txt') as file:
        return [line for line in file.readlines()[:n]]
    
print(*show_head())
```

一方UNIXではPythonのようにコマンドラインで整数を受け取ることが難しいのですが、`-`の後ろに整数を書くと何行分表示するか指定できます。応用的な使い方としては、たとえば`12`の解答の最後で

```sh:UNIX
!cat hightemp.txt | sed $'s/\t/ /g' | head -5
```

と書いて、最初の5行だけ表示することもできたりします。

### 15. 末尾のN行を出力

```py:Python
def show_tail():
    n = int(input())

    with open('hightemp.txt') as file:
        return [line for line in file.readlines()[-n:]]

print(*show_tail())
```

```sh:UNIX
!tail -3 hightemp.txt
```

```:結果（PythonとUNIX共通）
山梨県	大月	39.9	1990-07-19
山形県	鶴岡	39.9	1978-08-03
愛知県	名古屋	39.9	1942-08-02
```

14とほぼ同じ。

### 16. ファイルをN分割する

```py:Python
import math

def split_file():
    n = int(input())

    with open('hightemp.txt') as file:
        lines = file.readlines()
        num = math.ceil(len(lines) / n)
        for i in range(n):
            with open('split{}.txt'.format(i + 1), mode='w') as new_file:
                text = ''.join(lines[i * num:(i + 1) * num])
                new_file.write(text)

split_file()
```

```sh:UNIX
!split -n 5 -d hightemp.txt split_unix
```

```:結果（Python）
==> split1.txt <==
高知県	江川崎	41	2013-08-12
埼玉県	熊谷	40.9	2007-08-16
岐阜県	多治見	40.9	2007-08-16
山形県	山形	40.8	1933-07-25
山梨県	甲府	40.7	2013-08-10

==> split5.txt <==
大阪府	豊中	39.9	1994-08-08
山梨県	大月	39.9	1990-07-19
山形県	鶴岡	39.9	1978-08-03
愛知県	名古屋	39.9	1942-08-02
```

上記は、Pythonの出力を`!head split1.txt split5.txt`で確認したものです。

一方、上のUNIXコマンドは自分の環境だと動作せず、これだけはColab（Google Colaboratory）で試しました。Linuxでは一般に`-n`コマンドが提供されているようなのですが（[＠IT](https://www.atmarkit.co.jp/ait/articles/1711/24/news016.html)）、デフォルトのmacOSでは使えないようです。

Colabで上記を実行すると`split_unix00`から`split_unix04`まで5つのファイルができたのですが、加えてそこに拡張子の`txt`をつけようとしたりすると、ちょっと面倒になりそうだと感じました。[@moriwoさんの記事](https://qiita.com/moriwo/items/9d2a73a75f543e2ea6af#16-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92n%E5%88%86%E5%89%B2%E3%81%99%E3%82%8B)では`awk`なども使った実装例が紹介されていますが、自分はPythonを使ったほうが読みやすいコードになるのかな、と思いました。

### 17. １列目の文字列の異なり

```py:Python
import pandas as pd

def get_chars_set():
    df = pd.read_csv('hightemp.txt', sep='\t', header=None)
    return set(df.iloc[:, 0])

print(get_chars_set())
```

```:結果（Python）
{'千葉県', '埼玉県', '山形県', '和歌山県', '静岡県', '高知県', '大阪府', '岐阜県', '群馬県', '愛媛県', '山梨県', '愛知県'}
```

```sh:UNIX
!sort -u col1_unix.txt
```

```:結果（UNIX）
千葉県
埼玉県
大阪府
山形県
...
```

UNIXコマンドは並べ替えと重複除去の処理をパイプで繋いで、`!sort col1_unix.txt | uniq`と書くこともできます。

### 18. 各行を3コラム目の数値の降順にソート

```py:Python
def sort_rows():
    df = pd.read_csv('hightemp.txt', sep='\t', header=None)
    df.rename(columns={0: 'Prefect', 1: 'City', 2: 'Temp', 3: 'Date'}, inplace=True)
    df.sort_values(by='Temp', inplace=True)
    return df

sort_rows()
```

```:結果（Python）
   Prefect  City  Temp        Date
23     愛知県   名古屋  39.9  1942-08-02
21     山梨県    大月  39.9  1990-07-19
20     大阪府    豊中  39.9  1994-08-08
...
```

```sh:UNIX
!sort hightemp.txt -k 3
```

```:結果（UNIX）
愛知県	名古屋	39.9	1942-08-02
山形県	鶴岡	39.9	1978-08-03
山梨県	大月	39.9	1990-07-19
大阪府	豊中	39.9	1994-08-08
...
```

「逆順」という言葉の意味が、元の逆ということなのか降順ということなのか判然としませんが、後者の解釈で解いてみました。この問題については、UNIXコマンドだと短く書けるということを特に強く感じられますね。

### 19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる

```py:Python
def count_freq():
    df = pd.read_csv('hightemp.txt', sep='\t', header=None)
    return df[0].value_counts()
    
count_freq()
```

```:結果（Python）
群馬県     3
山梨県     3
山形県     3
埼玉県     3
```

```sh:UNIX
!cut -f 1 hightemp.txt | sort | uniq -c | sort -r
```

```:結果（UNIX）
   3 群馬県
   3 山梨県
   3 山形県
   3 埼玉県
```

UNIXコマンドは少し長くなりましたが、まず1列目を切り出して（`cut -f 1`）、次にその出現頻度を求め（`sort | uniq -c`）、最後に出現頻度の逆順で並べる（`sort -r`）、という流れが分かりやすくなるように書いてみました。

ただ、pandasの`value_counts()`の優秀さも考えると、ここではPythonを使ったほうが分かりやすいかなという気がします。

## まとめ

この章については以上ですが、間違いなどあったらコメントいただけると助かります。