言語処理100本ノック with Python(第2章・後編)


はじめに

自然言語処理と Python のトレーニングのため,東北大学の乾・岡崎研究室 Web ページにて公開されている言語処理100本ノックに挑戦していきます.その中で実装したコードや,抑えておくべきテクニック等々をメモしていく予定です.コードについてはGitHubでも公開していきます.

ここだけは Python だけではなく, UNIX コマンドの説明もちょいちょい挟めれば良いかなと思っています.
UNIX コマンドの細かいオプションなどについては man コマンドや,ITpro さんのウェブサイトなどで確認するとちゃんと勉強になると思います!

第2章: UNIXコマンドの基礎(再掲)

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

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

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

Python での回答

16.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 16.py

import sys


def split_file(filename, number_of_parts):
    with open(filename) as f:
        lines = f.readlines()

    if len(lines) % number_of_parts != 0:
        raise Exception("Undividable by N=%d" % number_of_parts)
    else:
        number_of_lines = len(lines) / number_of_parts

    for i in range(number_of_parts):
        with open("split%s.txt" % str(i), "w") as w:
            w.writelines(lines[number_of_lines * i: number_of_lines * (i + 1)])

if __name__ == '__main__':
    try:
        split_file(sys.argv[1], int(sys.argv[2]))
    except Exception as err:
        print("Error:", err)

Python 回答へのコメント

例外処理

N 分割する際に厄介なのは,元の行数が N で割り切れなかった場合です.
割り切れなかったときの対処は様々あると思いますが,今回は Errorraise してみようと思います.

通常の例外処理であれば,

try:
    # Error が発生しそうな処理
    # ex) a = 10/0 (ゼロ除算,ZeroDivisionError)とか
except [Error ]:
    # Error が発生したときにする処理
    # ex) print("Unable to divide by 0") とか
    # except [何かの Error] as err: と記述すると e にエラー内容が格納される
finally:
    # Error の発生によらず必ず行う処理

といった形で記述できます.except の部分で拾う Error の種別は指定しなくても動作しますが,できるだけ指定するよう推奨されています.なぜならあらゆる Error を拾って(無理やり)動作できるようにしてしまうので,バグの温床となってしまうからです.

さらに今回は自分で Exceptionraise しています.
もうちょっと頑張ると自作例外なんかも実装できたりするんですが,まだ Python の Class については理解不足なので,今回は既存の Exception を利用するに留めました.

Error を発生させるためには raise Exception("Error message") と記述します.すると強制的にその箇所で Error が発生します.
今回ではファイルの行数が自然数 N で割り切れなかった場合にエラーを発生させています.

if name == 'main':

僕も最初 if __name__ == '__main__': という記述を見て「?」となりました.
こう書くことでこのプログラムを直接呼び出したときに,この if 文内が実行されます.逆に他のプログラムから import などによって間接的に呼び出した場合,if 文の内部は実行されません.
Qiita 内の記事においては基本的にこの記述は省略していましたが,例外処理の関係上今回は併せて記述しておきます.

UNIX での回答

// 3分割すると 24 ÷ 3 = 8行ごとのファイルが生成される
$ split -l 8 hightemp.txt 
$ ls
xaa          xab          xac          hightemp.txt

$ cat xaa
高知県   江川崎   41  2013-08-12
(中略...)
山梨県   勝沼  40.5    2013-08-10

$ cat xab
埼玉県   越谷  40.4    2007-08-16
(中略...)
山形県   酒田  40.1    1978-08-03

$ cat xac
岐阜県   美濃  40  2007-08-16
(中略...)
愛知県   名古屋   39.9    1942-08-02

UNIX 回答へのコメント

本来 split は分割する行数を指定するので,N 分割というのを直接指定するのは難しそうです.コマンドライン上で除算させるテクニックも思い当たらなかったので,入力者には 24÷N を計算してもらってオプション指定してもらうことにしました.

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

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

Python での回答

17.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 17.py

import sys

filename = sys.argv[1]
prefectures = set()

with open(filename) as f:
    line = f.readline()
    while line:
        prefectures.add(line.split()[0])
        line = f.readline()

for pref in prefectures:
    print(pref)

Python 回答へのコメント

set は重複を許さない集合なので,今回にはぴったり.

UNIX での回答

$ cut -f 1 hightemp.txt | sort | uniq
千葉県
埼玉県
大阪府
(中略...)
静岡県
高知県
和歌山県

UNIX 回答へのコメント

sortuniq を用いてとのことでしたが,邪魔なので cut も利用しました.
直感的には uniq だけでできそうな気がするんですが, uniq はソートされていることを前提として動作するので sort は必須です.

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

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

Python での回答

18.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 18.py

import sys

with open(sys.argv[1]) as f:
    lines = f.readlines()

for line in sorted(lines, key=lambda x: x.split()[2], reverse=True):
    print line,

Python 回答へのコメント

最後の for 文でソート表示は,以前第1章でも扱いました.そのときには無説明で使っちゃいましたが,今回ここでご説明を.

無名関数 lambda

特定の数字列をソートするのであれば簡単なのですが,今回は文字列の途中に存在する数値順にソートしなければなりません.そのため無名関数の lambda を用いてソートの基準を指定しています.
無名関数とか lambda だの聞くと何だか難しそうですが,1行にまとめること以外は今までにも出てきている関数とほとんど同じです.

# 2の3倍を計算する

# def による関数定義
def double(x):
    return x * 3    
print double(2) # 6

# lambda による関数定義
double = lambda x: x * 3
print double(2) #6

今回の実装で言えば,引数で受け取った文字列の3列目を返す関数を lambda で表現したということになります.
その場限りの関数をさくっと書けるので,上手く使いこなせればとても便利です.

sorted()

名前の通りリストをソートしてくれる関数です.返り値ではなく引数に受け取ったリストをダイレクトにソートするので,取り扱いには若干注意.
key でソートする基準,reverse で昇順降順を切り替えられます.

UNIX での回答

$ sort -r -k 3 hightemp.txt
高知県   江川崎   41  2013-08-12
岐阜県   多治見   40.9    2007-08-16
埼玉県   熊谷  40.9    2007-08-16
(中略...)
大阪府   豊中  39.9    1994-08-08
山梨県   大月  39.9    1990-07-19
山形県   鶴岡  39.9    1978-08-03
愛知県   名古屋   39.9    1942-08-02

UNIX 回答へのコメント

-r で降順,-k 3 で3行目を指定しています.

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

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

Python での回答

19.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 19.py

import sys
from collections import defaultdict

filename = sys.argv[1]
prefectures = defaultdict(int)

with open(filename) as f:
    line = f.readline()
    while line:
        prefectures[line.split()[0]] += 1
        line = f.readline()

for k, v in sorted(prefectures.items(), key=lambda x: x[1], reverse=True):
    print(k)

Python 回答へのコメント

defaultdict

辞書を操作している時に厄介なのは,初期値が存在しないためにまだ存在しないキーに対して dict[key] += 1 みたいな書き方ができないことですね.
それを解消してくれるのが defaultdict で,初期値を設定した形での変数宣言が可能となります.
今回は int と指定したので初期値は0となりますが,より複雑な形の初期値も設定できます.かなり重宝しています.

UNIX での回答

$ cut -f 1 hightemp.txt | sort | uniq -c | sort -r | cut -c 6-
群馬県
山梨県
山形県
(中略...)
高知県
愛媛県
大阪府

UNIX 回答へのコメント

  • cut -f 1 1列目(県名)を切り出します
  • sort ソートをしないと uniq が正常に動作しないのでソートします
  • uniq -c 重複を削除し,削除前の出現回数つきで重複のない行を出力します
  • sort -r 「出現回数の高い順」なので出現回数に基づき降順にソートします
  • cut -c 6- 出力結果の行頭に余計なスペースや文字列が入ってしまうので,6文字目以降を表示してスペースを削除しています

こちらもすでに出てきたコマンドの組合せです.

おわりに

第3章へと続きます.