言語処理100本ノックに挑戦 / 第2章: UNIXコマンドの基礎

# :muscle: 前書き
[前回](https://qiita.com/saitotak/items/73852ecb7cc4b10b42a5)に続いて、言語処理100本ノックの第二章を回答していきます。本章では Unix コマンドと python コードを並行して学ぶことができ、ファイルI/Oやソートについて楽しく覚えられました。

# :muscle: 第2章: UNIXコマンドの基礎
> [hightemp.txt](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/hightemp.txt) は，日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである．以下の処理を行うプログラムを作成し，hightemp.txtを入力ファイルとして実行せよ．さらに，同様の処理をUNIXコマンドでも実行し，プログラムの実行結果を確認せよ．

UTF-8,LF の24行のテキストでした。内容は気温でソートされていて、同一の県名が複数回出てきたりします。

```text:hightemp.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
和歌山県	かつらぎ	40.6	1994-08-08
(以下略)
```
## ⚾ 10. 行数のカウント
> 行数をカウントせよ．確認にはwcコマンドを用いよ．

### コマンドでの確認
- wc - print newline, word, and byte counts for each file
    - `wc [OPTION]... [FILE]...`
    - `wc [OPTION]... --files0-from=F`

```bash:bash

$ wc ./data/hightemp.txt
24  96 813 ./data/hightemp.txt
```

### 解1 open()とstr.count
- `open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)` でファイル開く
    - [2. 組み込み関数 — open](https://docs.python.jp/3/library/functions.html?highlight=open#open)
- `str.count(sub[, start[, end]])` で改行 `\n` をカウント
    - [4. 組み込み型 — str.count](https://docs.python.jp/3/library/stdtypes.html#str.count)

```python:answer
file_temp = open ('./data/hightemp.txt','r',encoding='utf-8')
text = file_temp.read()
file_temp.close()
print(text.count('\n'))
# 24
```

## ⚾ 11. タブをスペースに置換
> タブ1文字につきスペース1文字に置換せよ．確認にはsedコマンド，trコマンド，もしくはexpandコマンドを用いよ．

### コマンドでの確認
- sed - stream editor for filtering and transforming text
    - `sed [OPTION]... {script-only-if-no-other-script} [input-file]...`
- tr - translate or delete characters
    - `tr [OPTION]... SET1 [SET2]`
- expand - convert tabs to spaces
    - `expand [OPTION]... [FILE]...`
    - `-t, --tabs=NUMBER`
        -  `have tabs NUMBER characters apart, not 8`

```bash:bash
$ sed 's/\t/ /g' ./data/hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
(以下略)
$ tr '\t' ' ' < ./data/hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
(以下略)
$ expand -t 1 ./data/hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
(以下略)
```

### 解1 replace
```python:answer
file_temp = open ('./data/hightemp.txt','r',encoding='utf-8')
text = file_temp.read()
file_temp.close()
print(text.replace('\t',' '))

```

## ⚾ 12. 1列目をcol1.txtに，2列目をcol2.txtに保存
> 各行の1列目だけを抜き出したものをcol1.txtに，2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ．確認にはcutコマンドを用いよ．

### コマンドでの確認
- cut - remove sections from each line of files
    - `cut OPTION... [FILE]...`
    - `-f, --fields=LIST`
        - `select only these fields;  also print any line that contains no delimiter character, unless the -s option is specified`

```bash:bash
$ cut -f 1 ./data/hightemp.txt
高知県
埼玉県
(以下略)
$ cut -f 2 ./data/hightemp.txt
江川崎
熊谷
(以下略)
```

### 解1 愚直にループ
- `print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)` の `a` 追記モード、 `sep='\n'` で改行指定で書き込み
    - [2. 組み込み関数 — Python 3.6.5 ドキュメント](https://docs.python.jp/3/library/functions.html#print)

```python:answer
file_temp = open ('./data/hightemp.txt','r',encoding='utf-8')
text = file_temp.read()
file_temp.close()

lst_col1 = []
lst_col2 = []
for line in text.split('\n'):
    if len(line) > 0:
        lst_col1.append(line.split('\t')[0])
        lst_col2.append(line.split('\t')[1])

file_col1 = open ('./col1.txt','a',encoding='utf-8')
file_col2 = open ('./col2.txt','a',encoding='utf-8')

print(*lst_col1,sep='\n',file=file_col1)
print(*lst_col2,sep='\n',file=file_col2)
file_col1.close()
file_col2.close()
```

### 解2 Withを使ったファイルアクセス
- `with open()` を使ったファイルアクセス
    - [8. 複合文 (compound statement) — with 文](https://docs.python.jp/3/reference/compound_stmts.html#with)
    - こちらのほうがclose忘れがなくて良さそう

```python:answer
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    text = file_temp.read()

lst_col1 = []
lst_col2 = []
for line in text.split('\n'):
    if len(line) > 0:
        lst_col1.append(line.split('\t')[0])
        lst_col2.append(line.split('\t')[1])

with open ('./col1.txt','a',encoding='utf-8') as file_col1:
    print(*lst_col1,sep='\n',file=file_col1)

with open ('./col2.txt','a',encoding='utf-8') as file_col2:
    print(*lst_col2,sep='\n',file=file_col2)

```

## ⚾ 13. col1.txtとcol2.txtをマージ
> 12で作ったcol1.txtとcol2.txtを結合し，元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ．確認にはpasteコマンドを用いよ．

### コマンドでの確認
- paste - merge lines of files
    - `paste [OPTION]... [FILE]...`
    - `-d, --delimiters=LIST`
        - `reuse characters from LIST instead of TABs`
- command1とcommand2の結果をcommand3にファイルとして渡す
    - `command3 <(command1) <(command2)`

```bash:bash
$ paste -d '\t' <(cut -f 1 ./data/hightemp.txt) <(cut -f 2 ./data/hightemp.txt)
高知県  江川崎
埼玉県  熊谷
(以下略)
```

### 解1 zip
- `with open()` を使ったファイルアクセス

```python:answer
with open ('./col1.txt','r',encoding='utf-8') as col1_temp:
    text_col1 = col1_temp.read()
with open ('./col2.txt','r',encoding='utf-8') as col2_temp:
    text_col2 = col2_temp.read()

col1and2 = ''
for col1,col2 in zip(text_col1.split('\n'),text_col2.split('\n')):
    col1and2 += col1 + '\t' + col2 + '\n'

with open ('./col1and2.txt','a',encoding='utf-8') as file_col1and2:
    print(col1and2,file=file_col1and2)

# 高知県	江川崎
# 埼玉県	熊谷
# (以下略)
```

### 解2 with を纏める
- 1行が長くなってしまったが、何度も `with open()` と書くよりは良さそう

```python:answer
with open ('./col1.txt','r',encoding='utf-8') as col1_temp, open ('./col2.txt','r',encoding='utf-8') as col2_temp, open ('./col1and2.txt','a',encoding='utf-8') as file_col1and2:
    col1and2 = ''
    for col1,col2 in zip(col1_temp.read().split('\n'),col2_temp.read().split('\n')):
        col1and2 += col1 + '\t' + col2 + '\n'
    
    print(col1and2,file=file_col1and2)

```


## ⚾ 14. 先頭からN行を出力
> 自然数Nをコマンドライン引数などの手段で受け取り，入力のうち先頭のN行だけを表示せよ．確認にはheadコマンドを用いよ．

### コマンドでの確認
- man:head - output the first part of files
    - `head [OPTION]... [FILE]...`
    - `-n, --lines=[-]NUM`
        - `print the first NUM lines instead of the first 10; with the leading '-', print all but the last NUM lines of each file`

```bash:bash
$ head -n 3 ./data/hightemp.txt
高知県  江川崎  41      2013-08-12
埼玉県  熊谷    40.9    2007-08-16
岐阜県  多治見  40.9    2007-08-16
```
### 解1 引数の受取
- `sys.argv` で引数を受取
    - [29.1. sys — システムパラメータと関数](https://docs.python.jp/3/library/sys.html#sys.argv)

```python:answer
import sys

arg = int(sys.argv[1])
text = ''
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    for i in range(arg):
        text += file_temp.readline()

print(text)
```

### 解2 argparse を利用した引数受取
- `argparse` を利用することで型チェックやhelp設定ができる
    - [16.4. argparse](https://docs.python.jp/3/library/argparse.html#module-argparse)

```python:answer
import argparse

parser = argparse.ArgumentParser(description='hightemp.txt の内容を指定行表示します')
parser.add_argument('integer', metavar='N', type=int, help='表示する行数を数字で入力')
args = parser.parse_args()

text = ''
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    for i in range(args.integer):
        text += file_temp.readline()

print(text)
```

```bash:bash
$ python .\Chapter02_Q14_A2.py --help
usage: Chapter02_Q14_A2.py [-h] N

hightemp.txt の内容を指定行表示します

positional arguments:
  N           表示する行数を数字で入力

optional arguments:
  -h, --help  show this help message and exit

$ python .\Chapter02_Q14_A2.py abcd
usage: Chapter02_Q14_A2.py [-h] N
Chapter02_Q14_A2.py: error: argument N: invalid int value: 'abcd'

$ python .\Chapter02_Q14_A2.py 3
高知県  江川崎  41      2013-08-12
埼玉県  熊谷    40.9    2007-08-16
岐阜県  多治見  40.9    2007-08-16
```

## ⚾ 15. 末尾のN行を出力
> 自然数Nをコマンドライン引数などの手段で受け取り，入力のうち末尾のN行だけを表示せよ．確認にはtailコマンドを用いよ．

### コマンドでの確認
- tail - output the last part of files
    - `tail [OPTION]... [FILE]...`
    - `-n, --lines=[+]NUM`
        - `output the last NUM lines, instead of the last 10; or use -n +NUM to output starting with line NUM`

```bash:bash
$ tail -n 3 ./data/hightemp.txt
山梨県  大月    39.9    1990-07-19
山形県  鶴岡    39.9    1978-08-03
愛知県  名古屋  39.9    1942-08-02
```

### 解1 readlines() で読込、range で後半だけ出力

```python
import argparse

parser = argparse.ArgumentParser(description='hightemp.txt の内容を末尾から指定行表示します')
parser.add_argument('integer', metavar='N', type=int, help='表示する行数を数字で入力')
args = parser.parse_args()

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    lines = file_temp.readlines()
    text = ''
    for linenumber in range(len(lines)-args.integer,len(lines)):
        text += lines[linenumber]

print(text)
```

## ⚾ 16. ファイルをN分割する
> 自然数Nをコマンドライン引数などの手段で受け取り，入力のファイルを行単位でN分割せよ．同様の処理をsplitコマンドで実現せよ．

### コマンドでの確認
- split - split a file into pieces
    - `split [OPTION]... [FILE [PREFIX]]`
        - `-l, --lines=NUMBER`
            - `put NUMBER lines/records per output file`
        - `--additional-suffix=SUFFIX`
            - `append an additional SUFFIX to file names`

```bash:bash
$ split ./data/hightemp.txt -l 10 --additional-suffix=_split.txt
$ ll
-rwxrwxrwx 1 root root  169 Apr 18 21:22 xaa_split.txt*
-rwxrwxrwx 1 root root  174 Apr 18 21:22 xab_split.txt*
-rwxrwxrwx 1 root root  174 Apr 18 21:22 xac_split.txt*
-rwxrwxrwx 1 root root  161 Apr 18 21:22 xad_split.txt*
-rwxrwxrwx 1 root root  135 Apr 18 21:22 xae_split.txt*
$ $ cat xaa_split.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
```

### 解1 math ライブラリ
- `math.ceil(x)` でファイル行数/引数 の切り上げ
    - [9.2. math — celi](https://docs.python.jp/3/library/math.html#math.ceil)
- `print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)` の `end=''` 指定で改行しないように制御
    - [2. 組み込み関数 — print](https://docs.python.jp/3/library/functions.html#print)

```python:answer
import argparse
import math

parser = argparse.ArgumentParser(description='hightemp.txt の内容を引数個にファイル分割します')
parser.add_argument('integer', metavar='N', type=int, help='分割する個数を数字で入力')
args = parser.parse_args()

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_in:
    lines = file_in.readlines()
    file_suffix_no = 1
    write_lines = math.ceil( len(lines) / args.integer )
    for integer, line in enumerate(lines):
        with open ('./split_' + str(file_suffix_no) + '.txt', 'a' ,encoding='utf-8') as file_out:
            print(line,end='',file=file_out)
        if ( (integer + 1) % write_lines == 0 ):
            file_suffix_no += 1
```

```bash:bash
$ python Chapter02_Q16_A1.py 5
$ ll
-rwxrwxrwx 1 root root  169 Apr 18 21:50 split_1.txt*
-rwxrwxrwx 1 root root  174 Apr 18 21:50 split_2.txt*
-rwxrwxrwx 1 root root  174 Apr 18 21:50 split_3.txt*
-rwxrwxrwx 1 root root  161 Apr 18 21:50 split_4.txt*
-rwxrwxrwx 1 root root  135 Apr 18 21:50 split_5.txt*
$ cat split_1.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
```

## ⚾ 17. １列目の文字列の異なり
> 1列目の文字列の種類（異なる文字列の集合）を求めよ．確認にはsort, uniqコマンドを用いよ．

### コマンドでの確認
- sort - sort lines of text files
    - `sort [OPTION]... [FILE]...`
- uniq - report or omit repeated lines
    - `uniq [OPTION]... [INPUT [OUTPUT]]`

```bash:bash
$ cut -f 1 data/hightemp.txt | sort | uniq
千葉県
和歌山県
埼玉県
大阪府
山形県
山梨県
岐阜県
愛媛県
愛知県
群馬県
静岡県
高知県
```

### 解1 sort（と、リストの破壊操作）
- `sort(*, key=None, reverse=False)` を使ってソート、重複は削除
    - [4. 組み込み型 — sort](https://docs.python.jp/3/library/stdtypes.html?#list.sort)

```python:answer
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    text = file_temp.read()

lst_col1 = []
for line in text.split('\n'):
    lst_col1.append(line.split('\t')[0])

lst_col1.sort()
# ['千葉県', '千葉県', '和歌山県', '埼玉県', '埼玉県', '埼玉県', '大阪府', '山形県', '山形県', '山形県', '山梨県', '山梨県', '山梨県', '岐阜県', '岐阜県', '愛媛県', '愛知県', '愛知県', '群馬県', '群馬県', '群馬県', '静岡県', '静岡県', '高知県']
for value in lst_col1:              #
     if lst_col1.count(value) > 1:  # ココ
        lst_col1.remove(value)      # 

for value in lst_col1:
    print(value)
# 千葉県
# 和歌山県
# 埼玉県
# 大阪府
# 山形県
# 山梨県
# 山梨県
# 岐阜県
# 愛媛県
# 愛知県
```

想定外に * 山梨県 × 2 が現れました。* mohikanz の先輩方に相談したところ。 `for x in list` ループ内で `list.remove()` することでリスト破壊し、ループが想定どおりに回っていませんでした。検証コード。

```python:answer
for index, value in enumerate(lst_col1):
     print('target:{} / index:{} / count:{}'.format(value,index,lst_col1.count(value)))
     # remove-next してしまうので、処理対象が 1 つ漏れる
     if lst_col1.count(value) > 1:
        lst_col1.remove(value) 
# target:千葉県 / index:0 / count:2
# target:和歌山県 / index:1 / count:1
# target:埼玉県 / index:2 / count:3
# target:埼玉県 / index:3 / count:2
# target:山形県 / index:4 / count:3
# target:山形県 / index:5 / count:2
# target:山梨県 / index:6 / count:3
# target:岐阜県 / index:7 / count:2
# target:愛媛県 / index:8 / count:1
# target:愛知県 / index:9 / count:2
# target:群馬県 / index:10 / count:3
# target:群馬県 / index:11 / count:2
# target:静岡県 / index:12 / count:2
```

そのため、元の配列を `remove` していくのではなく、別の配列に `append` していくカタチに書き換えました。

```python:answer
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    text = file_temp.read()

lst_col1 = []
lst_answer = []
for line in text.split('\n'):
    lst_col1.append(line.split('\t')[0])

lst_col1.sort()

for value in lst_col1:
    if lst_answer.count(value) == 0:
       lst_answer.append(value)

for value in lst_answer:
    print(value)

```

### 解2 set() で重複除去
- `sclass set([iterable])` を使って重複除去
    - [4. 組み込み型 — set（集合）型](https://docs.python.jp/3/library/stdtypes.html#set-types-set-frozenset)

```python:answer
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    text = file_temp.read()

lst_col1 = []
lst_answer = []
for line in text.split('\n'):
    lst_col1.append(line.split('\t')[0])

lst_col1.sort()
lst_answer = set(lst_col1) #dict型

for value in lst_answer:
    print(value)
```

## ⚾ 18. 各行を3コラム目の数値の降順にソート
> 各行を3コラム目の数値の逆順で整列せよ（注意: 各行の内容は変更せずに並び替えよ）．確認にはsortコマンドを用いよ（この問題はコマンドで実行した時の結果と合わなくてもよい）．


### コマンドでの確認
- sort - sort lines of text files
    - `sort [OPTION]... [FILE]...`
    - `-r, --reverse`
        - `reverse the result of comparisons`
    - `-k, --key=KEYDEF`
        - `sort via a key; KEYDEF gives location and type`
    - `-n, --numeric-sort`
        - `compare according to string numerical value`

```bash:bash
$ sort -k 3,3 -n -rr ./data/hightemp.txt
高知県  江川崎  41      2013-08-12
岐阜県  多治見  40.9    2007-08-16
埼玉県  熊谷    40.9    2007-08-16
（中略）
埼玉県  鳩山    39.9    1997-07-05
千葉県  茂原    39.9    2013-08-11
```

### 解1 sorted() を使ったソート
- `sorted(iterable, *, key=None, reverse=False)` で配列の逆順ソートをする
    - [2. 組み込み関数 — sorted](https://docs.python.jp/3/library/functions.html?highlight=sorted#sorted)

```python:answer
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    lines = file_temp.readlines()

lines2 = []
lines3 = [] 
lines4 = []
for line in lines:
    lines2.append(line.split('\t'))

for  line2 in lines2:
    lines3.append([float(line2[2])] + line2 )

lines4 = sorted(lines3, reverse=True)
for line4 in lines4:
    del line4[0]  # 1列目を消す
    print ('\t'.join(line4), end='')
# 高知県  江川崎  41      2013-08-12
# 岐阜県  多治見  40.9    2007-08-16
# 埼玉県  熊谷    40.9    2007-08-16
# (中略)
# 埼玉県  鳩山    39.9    1997-07-05
# 千葉県  茂原    39.9    2013-08-11
```

### 解2 ラムダ式でコンパクトに纏める
- `ラムダ (lambda)` で関数を作成、`sorted()` の2つめの引数 `key=None` に関数を渡す
    - [6. 式 (expression) — ラムダ (lambda)](https://docs.python.jp/3/reference/expressions.html#lambda)
    - [関数型プログラミング HOWTO — 小さな関数とラムダ式](https://docs.python.jp/3/howto/functional.html#small-functions-and-the-lambda-expression)
- ついに出たラムダ、ソートキーの取得に使えるのか

```python:answer
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    lines = file_temp.readlines()

lines2 = []
for line in lines:
    lines2.append(line.split('\t'))

key_temp = lambda x: float(x[2]) # 
# ラムダ式を使わない場合はこう
# def key_tmp ( x ):
#    return float(x[2])

for line in sorted( lines2, key=key_tmp, reverse=True):
    print ('\t'.join(line), end='')

```

## ⚾ 19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる
> 各行の1列目の文字列の出現頻度を求め，その高い順に並べて表示せよ．確認にはcut, uniq, sortコマンドを用いよ．

### コマンドでの確認
```bash:bash
$  cut -f 1 ./data/hightemp.txt | sort | uniq -c | sort -r
      3 群馬県
      3 山梨県
(中略)
      1 大阪府
      1 和歌山県
```
### 解1 ラムダ関数でソートキー取得
- No17とほぼ同じ
- ラムダ無名関数を利用しソートキー取得

```python:answer
import itemgetter
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
    text = file_temp.read()

lst_col1 = []
lst_col2 = []
for line in text.split('\n'):
    lst_col1.append(line.split('\t')[0])

lst_col1.sort()
for value in set(lst_col1):
    if value != '': #空行処理
        lst_col2.append([lst_col1.count(value)]  + [value])

lst_col2.sort(key=lambda x:x[0]) # [0]順に昇順ソート
lst_col2.refverse() # ひっくり返して降順

for value in lst_col2:
    print(value)

```