無駄と文化

実用的ブログ

Python3 で言語処理100本ノック 2015 - 第2章

まさか続くとは。

乾・岡崎研究室が公開している 言語処理100本ノック 2015 に取り組んで行きます。
使用する言語は Python3 です。

第2章まで出来たんでまとめます。


第2章: UNIXコマンドの基礎

hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.
以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.

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
静岡県 天竜 40.6 1994-08-04
山梨県 勝沼 40.5 2013-08-10
埼玉県 越谷 40.4 2007-08-16
群馬県 上里見 40.3 1998-07-04
愛知県 愛西 40.3 1994-08-05
千葉県 牛久 40.2 2004-07-20
静岡県 佐久間 40.2 2001-07-24
愛媛県 宇和島 40.2 1927-07-22
山形県 酒田 40.1 1978-08-03
岐阜県 美濃 40 2007-08-16
群馬県 前橋 40 2001-07-24
千葉県 茂原 39.9 2013-08-11
埼玉県 鳩山 39.9 1997-07-05
大阪府 豊中 39.9 1994-08-08
山梨県 大月 39.9 1990-07-19
山形県 鶴岡 39.9 1978-08-03
愛知県 名古屋 39.9 1942-08-02

タブ区切り形式です。


以下の課題は基本的に Python でコーディングして結果の検算を UNIX コマンドで行います。

もう一つ、第2章に取り組むにあたって方針を立てます。 入力ファイルが巨大なものになってもメモリを圧迫せずに動作する というものです。
テキストファイルを一気に全て読み込むことは避けて、なるべく行毎に読み込むコードに落とし込みます。

10. 行数のカウント

行数をカウントせよ.確認にはwcコマンドを用いよ.

Python

import codecs

count = sum(1 for line in codecs.open("./src/hightemp.txt", "r", "utf-8"))

print(count)
# => 24

というわけでジェネレーター内包表記です。

つづいて UNIX コマンドを用いた場合、

Bash

cat ./src/hightemp.txt | wc -l
# => 24

いいですね。


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

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

Python

import codecs

for line in codecs.open("./src/hightemp.txt", "r", "utf-8"):
    print(line.replace("\t", " "), end="")
# =>
# 高知県 江川崎 41 2013-08-12
# 埼玉県 熊谷 40.9 2007-08-16
# ...

実質的には .replace() してるだけです。

Bash

cat ./src/hightemp.txt | sed -e 's/\t/ /g'
# =>
# 高知県 江川崎 41 2013-08-12
# 埼玉県 熊谷 40.9 2007-08-16
# ...

sed 好きなんですよね。


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

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

Python

import codecs

with codecs.open("./dest/col1.txt", "w", "utf-8") as f1 \
     , codecs.open("./dest/col2.txt", "w", "utf-8") as f2:
    for line in codecs.open("./src/hightemp.txt", "r", "utf-8"):
        cols = line.split("\t")

        f1.write(cols[0]+"\n")
        f2.write(cols[1]+"\n")

with 記法を使ってます。open() と with 記法を同時に使うと1行が長くなりがちなのがちょっと嫌ですね。

Bash

[ -d dest ] || mkdir dest

cat ./src/hightemp.txt | cut -f1 > dest/col1.txt
cat ./src/hightemp.txt | cut -f2 > dest/col2.txt

cut したものをリダイレクトするだけ。便利ですね。


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

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

Python

import codecs

with codecs.open("./dest/col1.txt", "r", "utf-8") as rf1 \
     , codecs.open("./dest/col2.txt", "r", "utf-8") as rf2 \
     , codecs.open("./dest/col1+2.txt", "w", "utf-8") as wf:
    for col1, col2 in zip(rf1, rf2):
        wf.write("{0}\t{1}\n".format(col1.strip(), col2.strip()))

面倒だー。ファイルを3つも開いて、さらにその中でループ回してるのでコードが非常にもっさりしてます。

Bash

[ -d dest ] || mkdir dest

paste ./dest/col1.txt ./dest/col2.txt > ./dest/col1+2.txt

一撃!UNIX コマンド便利!


14. 先頭からN行を出力

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

Python

import sys
import codecs
import itertools

count = int(sys.argv[1])

with codecs.open("./src/hightemp.txt", "r", "utf-8") as f:
    for line in itertools.islice(f, 0, count):
        print(line, end="")

itertools.islice() を使えばイテレーターを対象にしてスライスが可能なようで。

Bash

cat ./src/hightemp.txt | head -n $1

表示だけが目的ならこれで充分ですね。


15. 末尾のN行を出力

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

Python

import sys
import codecs
import itertools

count = int(sys.argv[1])
max_count = sum(1 for line in codecs.open("./src/hightemp.txt", "r", "utf-8"))

with codecs.open("./src/hightemp.txt", "r", "utf-8") as f:
    for line in itertools.islice(f, max_count - count, None):
        print(line, end="")

itertools.islice() は引数にマイナスの数を使えないようです。 f[-5:] みたいに書けると便利なんですが。

Bash

cat ./src/hightemp.txt | tail -n $1

先ほどと同様です。


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

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

Python

import sys
import codecs

def line_counts(max_count, n):
    quo = max_count // n
    rem = max_count % n

    return [quo+1] * rem + [quo] * (n - rem)

n = int(sys.argv[1])
max_count = sum(1 for line in codecs.open("./src/hightemp.txt", "r", "utf-8"))

with codecs.open("./src/hightemp.txt", "r", "utf-8") as rf:
    for i, line_count in enumerate(line_counts(max_count, n)):
        with codecs.open("./dest/split.{0}.txt".format(i), "w", "utf-8") as wf:
            for _ in range(line_count):
                wf.write(rf.readline())

line_counts() という関数を定義しています。
この関数は整数 max_count をN分割します。戻り値は整数のリストで、各要素は高々1しか差が無いように調整されます。

具体的に、

line_counts(13, 5)
# => [3, 3, 3, 2, 2]

この関数を使って、例えば入力ファイルの行数が13行で、それを5分割するなら、出力ファイルの行数は3行, 3行, 3行, 2行, 2行にするという方針です。

Bash

これの UNIX コマンドでの解き方が分からなかったんですよね。
split コマンドを素朴に使うと「N分割」ではなく「M行毎に分割」という感じになるので。

というわけで カンニングしました
これをシェルスクリプトで書きたくはないですね、個人的に。


17. 1列目の文字列の異なり

1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.

Python

import codecs

prefs = set(line.split("\t")[0] for line in codecs.open("./src/hightemp.txt", "r", "utf-8"))

print(prefs)
# =>
# {
#     '埼玉県', '千葉県', '群馬県', '山形県', '静岡県', '愛知県',
#     '高知県', '岐阜県', '山梨県', '愛媛県', '和歌山県', '大阪府'
# }

unique な集合を得るのが目的なので set を使いました。

Bash

cat ./src/hightemp.txt | cut -f1 | sort | uniq
# =>
# 愛知県
# 愛媛県
# 岐阜県
# 群馬県
# 高知県
# 埼玉県
# 山形県
# 山梨県
# 静岡県
# 千葉県
# 大阪府
# 和歌山県

パイプで繋ぐだけでデータを加工していけている感覚、いいですね。


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

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

Python

import codecs

sorted_lines = sorted(
    codecs.open("./src/hightemp.txt", "r", "utf-8"),
    key=lambda line: float(line.split("\t")[2]),
    reverse=True,
)

print("".join(sorted_lines))
# =>
# 高知県  江川崎  41      2013-08-12
# 埼玉県  熊谷    40.9    2007-08-16
# 岐阜県  多治見  40.9    2007-08-16
# 山形県  山形    40.8    1933-07-25
# 山梨県  甲府    40.7    2013-08-10
# ...

sorted() の機能をフル活用しています。

Bash

# -n オプション: 対象を数値としてソート
# -r オプション: 降順(逆順)ソート
# -k3 オプション: タブ区切りの3列目を比較対象として各順をソート
cat ./src/hightemp.txt | sort -nrk3
# =>
# 高知県  江川崎  41      2013-08-12
# 埼玉県  熊谷    40.9    2007-08-16
# 岐阜県  多治見  40.9    2007-08-16
# 山形県  山形    40.8    1933-07-25
# 山梨県  甲府    40.7    2013-08-10
# ...

sort コマンド便利ですね。タブ区切り形式と相性が良い。


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

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

Python

import codecs
from collections import Counter

pref_counter = Counter(line.split("\t")[0] for line in codecs.open("./src/hightemp.txt", "r", "utf-8"))

print(pref_counter.most_common())
# =>
# [
#     ('山形県', 3), ('埼玉県', 3), ('群馬県', 3), ('山梨県', 3), ('岐阜県', 2),
#     ('愛知県', 2), ('千葉県', 2), ('静岡県', 2), ('愛媛県', 1), ('高知県', 1),
#     ('大阪府', 1), ('和歌山県', 1)
# ]
# ...

collections.Counter を使います。そのために用意されてるモジュールなので。

Bash

cat ./src/hightemp.txt | cut -f1 | sort | uniq -c | sort -rk1
# =>
# 3 山梨県
# 3 山形県
# 3 埼玉県
# 3 群馬県
# 2 千葉県
# 2 静岡県
# 2 岐阜県
# 2 愛知県
# 1 和歌山県
# 1 大阪府
# 1 高知県
# 1 愛媛県

uniq コマンドに -c オプションを渡すことで要素の出現回数をカウントしてくれるようになるんですね。これはいい。
sort を2回しちゃってるところが若干気になりますが。


所感

全体的に UNIX コマンドの便利さを身体で分からせるための出題ですね。勉強になりました。


私からは以上です。


コード全部まとめ

回答 - 言語処理100本ノック 2015 - 第2章 · GitHub


その他の章の回答はこちらから

blog.mudatobunka.org