言語処理100本ノック第2章: UNIXコマンドの俺の解答。その他の章はこちら。
10
省略
11. タブをスペースに置換
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上で標準出力の改行コード書き換えを抑制する方法はこの辺に書いてあったが、かなり面倒でトリッキーそう。
12~14
省略
15. 末尾のN行を出力
言語処理などのデータ処理においては、データが巨大な場合を想定して、全データをメモリ上に保持するようなことは避けるべき。ということでFIFOのキューを使って実装。
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分割せよ」と言われれば、なるべく行数が均等になるように分割するという意味かなと思う。なので、そのつもりで解答した。
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列目の文字列の異なり
1列目を取り出すのにsplitして最初のを取り出すのがコード行数的には楽だけど、2列目以降も切るところが無駄なので正規表現で1列目だけ取り出す。律儀すぎるよな(笑)
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に頼ることにした。
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
(略)
この出力と同じになるように作成:
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")