先日,言語処理100本ノック2020が公開されました.私自身,自然言語処理を初めてから1年しか経っておらず,細かいことはよくわかっていませんが,技術力向上のために全ての問題を解いて公開していこうと思います.
すべてjupyter notebook上で実行するものとし,問題文の制約は都合よく破っていいものとします.
ソースコードはgithubにもあります.あります.
1章はこちら.
環境はPython3.8.2とUbuntu18.04です.
解説記事としてはこちらのほうがわかりやすいと思います.ぜひ著者の方には10章までの解説記事を書いていただきたいですね.
第2章: UNIXコマンド
popular-names.txtは,アメリカで生まれた赤ちゃんの「名前」「性別」「人数」「年」をタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,popular-names.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
必要なデータセットはここからダウンロードしてください.
ダウンロードしたファイルはdata
以下に置くものとします.
10. 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
with open('data/popular-names.txt') as f:
print(len(list(f)))
2780
ファイルオブジェクトの長さを求めるだけです.ファイルオブジェクトはイテレータなので,リストにしないといけません.
入力が十分大きいファイルである場合はメモリに乗らないようなこともあるかもしれませんが,そういう場合はfor文で回してカウントしてけばいいだけです.
wc -l < data/popular-names.txt
2780
wc
コマンドにlオプションを指定して行数を求めます.ファイル名を与えるといろいろ余分なものが表示されるので,標準入力から与えます.
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
with open('data/popular-names.txt') as f:
for line in f:
line = line.strip()
line = line.replace('\t', ' ')
print(line)
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880
Margaret F 1578 1880
Ida F 1472 1880
Alice F 1414 1880
Bertha F 1320 1880
Sarah F 1288 1880
ファイルオブジェクトをイテレータとして回して得られる各文字列は,末尾に改行文字がついているのでstrip
で取り除きます(rstrip('\n')
のほうが望ましい場合もあります).
タブをスペースに置換して出力するだけです.
strip
で改行文字を取り除かず,print(line, end='')
するという方法もあります.
awk '{gsub("\t", " ", $0); print $0}' data/popular-names.txt
perl -pe 's/\t/ /g' data/popular-names.txt
sed 's/\t/ /g' data/popular-names.txt
expand -t 1 data/popular-names.txt
tr '\t' ' ' < data/popular-names.txt
出力はPythonのものと同じなので省略します.(以後も同様です)
UNIXコマンドいろいろあってなかなか覚えられません.
12. 1列目をcol1.txtに,2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
with open('data/popular-names.txt') as f, \
open('result/col1.txt', 'w') as g, \
open('result/col2.txt', 'w') as h:
for line in f:
line = line.strip()
pref, city, _, _ = line.split('\t')
print(pref, file=g)
print(city, file=h)
素直に書きました.
Mary
Anna
Emma
Elizabeth
Minnie
Margaret
Ida
Alice
Bertha
Sarah
F
F
F
F
F
F
F
F
F
F
cut -f 1 data/popular-names.txt > col1.txt
cut -f 2 data/popular-names.txt > col2.txt
cut
コマンド使えば簡単です.awk '{print $1}'
でもいいと思います.
13. col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.
2つのファイルを別々にopenすればいいだけですが,せっかくなのでcontextlib.ExitStack
を使って任意の数のファイルを扱えるような実装にします.
コンテキストマネージャについては→https://docs.python.org/ja/3/library/stdtypes.html#typecontextmanager
from contextlib import ExitStack
files = ['result/col1.txt', 'result/col2.txt']
with ExitStack() as stack:
files = [stack.enter_context(open(filename)) for filename in files]
for lines in zip(*files):
x = [line.strip() for line in lines]
x = '\t'.join(x)
print(x)
Mary F
Anna F
Emma F
Elizabeth F
Minnie F
Margaret F
Ida F
Alice F
Bertha F
Sarah F
paste result/col1.txt result/col2.txt
pasteコマンドを使えば簡単ですね.
14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
個人的には標準入力とコマンドライン引数を受け取るのはargparse
とfileinput
でやるのが好きですが,今回はすべてのコードをjupyter notebook上で動かせるようにしたいので,コマンドライン引数は用いません.(問題文の「など」に優しさを感じる)
N = 5
with open('data/popular-names.txt') as f:
lst = range(N)
for _, line in zip(lst, f):
print(line, end='')
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880
head
コマンドで同様の処理ができますね.
head -n 5 data/popular-names.txt
15. 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
標準入力をすべてリストに入れて最後5つの要素を取り出してもいいとは思いますが,大きなファイルだとメモリに乗らないかもしれないので,キューを使っていきます.
from collections import deque
N = 5
queue = deque([], 5)
with open('data/popular-names.txt') as f:
for line in f:
queue.append(line)
for line in queue:
print(line, end='')
Benjamin M 13381 2018
Elijah M 12886 2018
Lucas M 12585 2018
Mason M 12435 2018
Logan M 12352 2018
tail
コマンドで同様の処理ができますね.
tail -n 5 data/popular-names.txt
16. ファイルをN分割する
自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.
ファイルを行単位でN分割する状況っていうのがあまりないからだと思うんですが,splitコマンドの実装によっては行でのN分割がなかったりします.GNU拡張だとあったりします.
split -d -nl/5 data/popular-names.txt result/shell5.
587 2348 11007 result/shell5.00
554 2216 11010 result/shell5.01
556 2224 11006 result/shell5.02
540 2160 11007 result/shell5.03
543 2172 10996 result/shell5.04
2780 11120 55026 total
このGNU拡張のコード( https://github.com/coreutils/coreutils/blob/master/src/split.c )と同じ挙動をするようにpythonでも実装してみました.
def split_string_list(N, lst):
chunk_size = sum([len(x) for x in lst]) // N
chunk_ends = [chunk_size * (n + 1) - 1 for n in range(N)]
i = 0
acc = 0
out = []
for chunk_end in chunk_ends:
tmp = []
while acc < chunk_end:
tmp.append(lst[i])
acc += len(lst[i])
i += 1
out.append(tmp)
return out
def split_file(N, filepath, outprefix):
with open(filepath) as f:
lst = list(f)
lst = split_string_list(N, lst)
for i, lines in enumerate(lst):
idx = str(i).zfill(2) # 手抜き
with open(outprefix + idx, 'w') as f:
f.write(''.join(lines))
split_file(5, 'data/popular-names.txt', 'result/python5.')
まずは全体の文字数を数えて,なるべく文字数がそろうように切る位置(chunk_ends
)を決めます.
そして,chunk_endsの各要素を超えるまで行を取っていき,超えたらファイルに出力します.
587 2348 11007 result/python5.00
554 2216 11010 result/python5.01
556 2224 11006 result/python5.02
540 2160 11007 result/python5.03
543 2172 10996 result/python5.04
2780 11120 55026 total
diff result/python5.00 result/shell5.00
diff result/python5.01 result/shell5.01
diff result/python5.02 result/shell5.02
diff result/python5.03 result/shell5.03
diff result/python5.04 result/shell5.04
同じ結果になりました.
17. 1列目の文字列の異なり
1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはcut, sort, uniqコマンドを用いよ.
names = set()
with open('data/popular-names.txt') as f:
for line in f:
name = line.split('\t')[0]
names.add(name)
names = sorted(names)
for name in names:
print(name)
Abigail
Aiden
Alexander
Alexis
Alice
Amanda
Amelia
Amy
Andrew
Angela
1列目を順に集合に追加していき,ソートして出力しています.(※pythonのバージョンは3.8.2です.)
cut -f 1 data/popular-names.txt | sort -s | uniq
1列目だけを取り出して,ソートして重複を取り除きます.sortを入れ忘れるとおかしなことになります.また,pythonに合わせて安定ソートとするために,sオプションをつけています.
18. 各行を3コラム目の数値の降順にソート
各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).
with open('data/popular-names.txt') as f:
lst = [line.strip() for line in f]
lst.sort(key = lambda x : -int(x.split('\t')[2]))
for line in lst[:10]:
print(line)
Linda F 99689 1947
Linda F 96211 1948
James M 94757 1947
Michael M 92704 1957
Robert M 91640 1947
Linda F 91016 1949
Michael M 90656 1956
Michael M 90517 1958
James M 88584 1948
Michael M 88528 1954
sort
関数のキーを指定してあげることで,ソートする基準を指定できます.
sort -nrsk 3 data/popular-names.txt
sort
コマンドだけでできます.簡単ですね.
19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる
各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.
collections.Counter
を使うといいです.
from collections import Counter
cnt = Counter()
with open('data/popular-names.txt') as f:
for line in f:
name = line.split('\t')[0]
cnt.update([name])
lst = cnt.most_common()
lst.sort(key=lambda x:(-x[1], x[0]))
for name, num in lst[:10]:
print(name)
James
William
John
Robert
Mary
Charles
Michael
Elizabeth
Joseph
Margaret
Counter
のオブジェクトにリストをそのまま渡すか,update()
で少しずつ渡していくかします.most_common()
で多い順に並べてくれます.
cut -f 1 data/popular-names.txt | sort | uniq -c | sort -nrsk1 | awk '{print $2}'
uniq
を取る際に-c
オプションを付けるといくつあるか数えてくれます.最後に個数でソートするとほしい結果が得られます.