言語処理100本ノック俺の解答 第2章

[言語処理100本ノック第2章: UNIXコマンド](https://nlp100.github.io/ja/ch02.html)の俺の解答。[その他の章はこちら](https://qiita.com/sangoro/items/9bb6b5c39e756818cce2)。

# 10
省略

# 11. タブをスペースに置換
```Python
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('in_filename')
parser.add_argument('out_filename')
args = parser.parse_args()
f_i = open(args.in_filename)
f_o = open(args.out_filename, "w", newline='\n')

for s in f_i:
    s = s.rstrip().replace('\t', ' ')
    f_o.write(s+'\n')
```
print()ではなくわざわざファイルにwrite()で書き出しているのは理由がある。Windows上で実行しているので、標準出力に\nを書き出すと勝手に\r\nに書き換えられてしまう。一方でpopular-names.txtの改行コードは\nなので、sed等の結果との答え合わせ時に差分が出てしまう。それを防ぐために出力ファイルをnewline='\n'で開き、write()で書き出している。
Windows上で標準出力の改行コード書き換えを抑制する方法は[この辺](https://stackoverflow.com/questions/49709309/prevent-python-prints-automatic-newline-conversion-to-crlf-on-windows)に書いてあったが、かなり面倒でトリッキーそう。

# 12～14
省略

# 15. 末尾のN行を出力
言語処理などのデータ処理においては、データが巨大な場合を想定して、全データをメモリ上に保持するようなことは避けるべき。ということでFIFOのキューを使って実装。

```Python
import argparse
import sys
import queue

parser = argparse.ArgumentParser()
parser.add_argument('-n', help="number of lines")
parser.add_argument('-o', help="output filename")
args = parser.parse_args()

n_int = int(args.n)
q = queue.SimpleQueue()
for l in sys.stdin:
    q.put(l.rstrip())
    if q.qsize() > n_int:
        q.get()

f_o = open(args.o, "w", newline='\n')
for i in range(0, n_int):
    f_o.write(q.get()+'\n')
```
# 16. ファイルをN分割する
この問題は曖昧ですね。普通「行単位でN分割せよ」と言われれば、なるべく行数が均等になるように分割するという意味かなと思う。なので、そのつもりで解答した。

```Python
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-i', help="input filename")
parser.add_argument('-n', help="number of files")
parser.add_argument('-o', help="output filename prefix")
args = parser.parse_args()

lines = 0
with open(args.i) as f_i:
    for l in f_i:
        lines += 1    

n_int = int(args.n)

i = 0
current_line = 0
f_i = open(args.i)
while i < n_int:
    remaining_lines = lines - current_line
    i_lines = remaining_lines // (n_int - i)
    if remaining_lines % (n_int - i) > 0:
        i_lines += 1
    with open("{}{:02d}".format(args.o, i), "w", newline='\n') as f_o:
        for j in range(0, i_lines):
            s = f_i.readline().rstrip()
            f_o.write(s + '\n')
    i += 1
    current_line += i_lines
```
さっきの問題のところで「全データをメモリ上に保持するようなことは避けるべき」と言った手前、それを頑なに守るために、まず全体の行数を数えるために1回、次にファイルを分割するために1回、計2回入力ファイルを開く羽目になっている。
さて、「同様の処理をsplitコマンドで実現せよ」とあるが、splitって、なるべく行数が均等になるように分割するという機能は無くないですか？普通にsplit -n l/Nってやると、ファイルサイズがなるべく均等になるように分割される。とりあえず、Nが2780の約数の場合に限定して、

```
$ split -l `expr 2780 / N` -d popular-names.txt popular-names-split/
```
なんてやってみましたけど…
ファイルサイズをなるべく均等にするプログラムを作ろうとすると、ちょっと面倒そう。

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

1列目を取り出すのにsplitして最初のを取り出すのがコード行数的には楽だけど、２列目以降も切るところが無駄なので正規表現で１列目だけ取り出す。律儀すぎるよな（笑）

```Python
import argparse
import sys
import re

parser = argparse.ArgumentParser()
parser.add_argument('-o', help="output filename")
args = parser.parse_args()

words = set()
regex = re.compile(r'^([^\t]+)\t')
for s in sys.stdin:
    words.add(regex.match(s).group(1))

f_o = open(args.o, "w", newline='\n')
for w in sorted(words):
    f_o.write(w+'\n')
```

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

ここはPandasに頼ることにした。
```Python
import argparse
import sys
import pandas as pd

parser = argparse.ArgumentParser()
parser.add_argument('-o', help="output filename")
args = parser.parse_args()

df = pd.read_csv(sys.stdin, delimiter='\t',
                 names=["name", "sex", "number", "year"])
df_sorted = df.sort_values(by="number")

df_sorted.to_csv(args.o, sep='\t', header=None, index=None, 
                 line_terminator='\n')
```
# 19. 各行の1コラム目の文字列の出現頻度を求め，出現頻度の高い順に並べる
先にUNIXでの作り方から。最初、
```
$ cut -f 1 popular-names.txt | sort | uniq -c | sort -nr
```
というのを考えたが、これだと名前も降順になってしまう。名前は昇順になるように以下のようにしてみた：
```
$ cut -f 1 popular-names.txt | sort | uniq -c | sort -k 1nr
```
これなら、以下のようになる：
```
（略）
     26 Emily
     26 Jennifer
     26 Linda
     26 Sarah
     25 Jacob
     25 Jessica
     24 Betty
     24 Mildred
     24 Susan
（略）
```
この出力と同じになるように作成：
```Python
import argparse
import sys
import re

parser = argparse.ArgumentParser()
parser.add_argument('-o', help="output filename")
args = parser.parse_args()

d = {}
regex = re.compile(r'^([^\t]+)\t')
for s in sys.stdin:
    w = regex.match(s).group(1)
    if not w in d:
        d[w] = 0
    d[w] += 1

sortedD = sorted(d.items())
sortedD = sorted(sortedD, key=(lambda x:x[1]), reverse=True)

f_o = open(args.o, "w", newline='\n')
for w in sortedD:
    f_o.write(f"{w[1]:>7d} {w[0]}\n")
```