周回遅れでIT業界デビューしたプロジェクトマネージャーのブログ

就職氷河期にモロにぶち当たり、人生で混迷を極めた末にIT業界に安寧を見出そうとしているアラフォーの活動日記です。

「言語処理100ノック2015」をPythonで練習(No.10~19)

f:id:sionff:20180119162019j:plain

www.cl.ecei.tohoku.ac.jp

第2章: UNIXコマンドの基礎(No.10~19)です。
ただし、Pythonのコード部分のみです。UNIXコマンドはやっていません。

以下を出来るだけ意識して書いています。

  • 内包表記を使う
  • 関数化する
  • 一般化する

今回は関数化を結構頑張りました。

そのため他の人の回答よりはコード量が多くなっていますが、全体としては各パートでやることが明確になり、処理も順番こになっているので後々手を入れたりしやすくなっていると思います。

あと、No.16を見て欲しいです。

x行をn分割したとき、余りが出てしまった場合の処理を考える必要があるのですが、qiitaなどでよく見る方法は「行数÷分割数の商を端数切り上げで処理」していますがNGだと思います。

例えば24行のとき、

  • 5分割の場合 > [5,5,5,5,4]になるのでOKに見える
  • 9分割の場合 > [3,3,3,3,3,3,3,3,0]となり9ファイル目に割り当てる行がなくなりNG

なので、端数を適宜割り振ってからリスト化して運用した方がいいと思います。

第2章まえがき

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

import codecs
fname = './input/hightemp.txt'

10. 行数のカウント

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

with codecs.open(fname, 'r', 'utf_8') as ht:
    print(len([ line for line in ht ]))
        
# 文字コードの問題でファイルが読み込めない場合があるので、
# 今回の設問の場合は、事前にcodecをインポートして対応することにしました。
# 参照:http://osksn2.hep.sci.osaka-u.ac.jp/~taku/osx/python/encoding.html

# 24

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

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

with codecs.open(fname, 'r', 'utf_8') as ht:
    for line in ht:
        ln = line.replace('\t', ' ')
        print(ln)

# 高知県 江川崎 41 2013-08-12
# 埼玉県 熊谷 40.9 2007-08-16
# 岐阜県 多治見 40.9 2007-08-16
# ...

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

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

def doc_read():
    """
    必要なカラムを抽出する処理。
    """
    with codecs.open(fname, 'r', 'utf_8') as ht:
        lines = [ line.strip('\n').split('\t') for line in ht ]
    return lines

def create_col(path, doc, num):
    """
    抽出したカラムをファイルに書き出す処理。
    """
    data = [ln[num] for ln in doc]
    doc_col = "\n".join(data)
    with codecs.open(path, 'w', 'utf_8') as cd:
        cd.write(doc_col)

create_col('./output/col1.txt', doc_read(), 0)
create_col('./output/col2.txt', doc_read(), 1)

# col1.txt
# 高知県
# 埼玉県
# 岐阜県
# ...

# col2.txt
# 江川崎
# 熊谷
# 多治見
# ...

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

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

def doc_read(path):
    """
    ドキュメントを読み込む処理。
    """
    cols = []
    for pt in path:
        with codecs.open(pt, 'r', 'utf_8') as ht:
            lines = [ line.strip('\n') for line in ht ]
        cols.append(lines)
    return merge_col(cols)

def merge_col(cols):
    """
    ドキュメントをマージする処理。
    """
    length = len(cols[0])
    for i in range(len(cols)):
        doc_lines = [ cols[0][i] + '\t' + cols[1][i] for i in range(length) ]
        doc = "\n".join(doc_lines)
    return create_txt(doc)

def create_txt(doc):
    """
    ドキュメントを書き出す処理。
    """
    with codecs.open('./output/merge.txt', 'w', 'utf_8') as cd:
        cd.write(doc)
    return None

read_path = ['./output/col1.txt', './output/col2.txt']
    
doc_read(read_path)

# merge.txt
# 高知県	江川崎
# 埼玉県	熊谷
# 岐阜県	多治見
# ...

# ここはあんまり上手く書けなかった……後日リベンジするかも。

14. 先頭からN行を出力

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

import re

def input_num():
    """
    数字を入力する処理。自然数以外の場合は再度入力を受け付ける。
    """
    while 1:
        num = input()
        if re.match('^[0-9]+$', num) and int(num) > 0:
            break
        else:
            continue
    return output_lines((int(num)), doc_read())

def doc_read():
    """
    ドキュメントを読み込む処理。
    """
    with codecs.open(fname, 'r', 'utf_8') as ht:
        lines = [ line for line in ht ]
    return lines

def output_lines(num, lines):
    """
    先頭からN行を出力する処理。
    """
    i = 0
    for line in lines:
        print("".join(line))
        i = i + 1
        if i >= num:
            break
        else:
            continue
                
input_num()

# 「3」を入力

# 高知県	江川崎	41	2013-08-12
# 埼玉県	熊谷	40.9	2007-08-16
# 岐阜県	多治見	40.9	2007-08-16

15. 末尾のN行を出力

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

import re

def input_num():
    """
    数字を入力する処理。自然数以外の場合は再度入力を受け付ける。
    """
    while 1:
        num = input()
        if re.match('^[0-9]+$', num) and int(num) > 0:
            break
        else:
            continue
    return output_lines(int(num), doc_reverse())

def doc_reverse():
    """
    ドキュメントを読み込む処理。末尾なので逆順を返すようにする。#14と見比べてみて下さい。
    """
    with codecs.open(fname, 'r', 'utf_8') as ht:
        lines = [ line for line in ht ]
    return lines[::-1]

def output_lines(num, lines):
    """
    先頭からN行を出力する処理。
    """
    i = 0
    for line in lines:
        print("".join(line))
        i = i + 1
        if i >= num:
            break
        else:
            continue

input_num()

# 「3」を入力

# 愛知県	名古屋	39.9	1942-08-02
# 山形県	鶴岡	39.9	1978-08-03
# 山梨県	大月	39.9	1990-07-19

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

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

import re

def input_num():
    """
    数字を入力する処理。自然数以外の場合は再度入力を受け付ける。
    """
    while 1:
        num = input()
        if re.match('^[0-9]+$', num) and int(num) > 0:
            break
        else:
            continue
    return doc_read(int(num))

def doc_read(num):
    """
    ドキュメントを読み込む処理。
    """
    with codecs.open(fname, 'r', 'utf_8') as ht:
        lines = [ line for line in ht ]
    return part_num(lines, len(lines), num)

def part_num(lines, length, num):
    """
    行数をできるだけ均等になるようにN分割しリストを返す処理。
    """
    split_num = [ (length + i) // num for i in range(num) ]
    return split_file(lines, list(reversed(split_num)))

# 整数をできるだけ均等になるようにN分割
# 参考:https://qiita.com/keisuke-nakata/items/c18cda4ded06d3159109

# リストの逆順を取得する
# 参考:https://www.python-izm.com/advanced/reverse/

def split_file(lines, numbers):
    """
    ドキュメントをN分割し、つどファイルを出力する処理。
    """
    n = 0
    counter = 0
    for num in numbers:
        ln = []
        for i in range(n, num+n):
            ln.append(lines[i])
        with codecs.open("./output/file_split{}.txt".format(counter), 'w', 'utf_8') as fs:
            fs.write("".join(ln))
        n = n + num
        counter = counter + 1

input_num()

# 「9」を入力
# file_split0.txt
# file_split1.txt
# file_split2.txt
# file_split3.txt
# file_split4.txt
# file_split5.txt
# file_split6.txt
# file_split7.txt
# file_split8.txt

# x行をn分割したとき、余りが出てしまった場合の処理を考える必要があります。
# qiitaでよく見る方法は行数÷分割数の商を端数切り上げで処理していますが、これはNGだと思います。
# 例えば本問のように24行のとき……
# 5分割の場合 > [5,5,5,5,4]になるのでOKに見える
# 9分割の場合 > [3,3,3,3,3,3,3,3,0]となり9ファイル目に割り当てる行がなくなりNG
# なので、端数を適宜割り振ってからリスト化して運用した方がいいと思います。

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

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

# 内包表記で回す場合
with codecs.open(fname, 'r', 'utf_8') as ht:
    print(set([ line.split('\t')[0] for line in ht ]))

# for文で回す場合
#lines = []
#with codecs.open(fname, 'r', 'utf_8') as ht:
#    for line in ht:
#        lines.append(line.split('\t')[0])
#
#print(set(lines))

# 可読性がわりと死にますが内包表記だと1行で済みます。

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

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

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

# ラムダ式
with codecs.open(fname, 'r', 'utf_8') as ht:
    lines = ht.readlines()
    lines.sort(key=lambda line: float(line.split('\t')[2]), reverse=True)

for line in lines:
    print(line)

# 高知県	江川崎	41	2013-08-12
# 埼玉県	熊谷	40.9	2007-08-16
# 岐阜県	多治見	40.9	2007-08-16
# ...

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

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

def read_file():
    """
    ドキュメントを読み込んで、1列目だけを抽出する処理。
    """
    with codecs.open(fname, 'r', 'utf_8') as ht:
        lines = [ line.split('\t')[0] for line in ht ]
    return create_dict(lines)

def create_dict(lines):
    """
    dict型に変換しつつ、1列目の文字列の出現頻度を求める処理。
    """
    words = {}
    for line in lines:
        if line in words:
            words[line] += 1
        else:
            words[line] = 1
    
    return sort_dict(words)

def sort_dict(words):
    """
    出現頻度を降順にソートする処理。
    """
    words_sorted = sorted(words.items(), key=lambda x: x[1], reverse=True)
    return output(words_sorted)

def output(words):
    """
    得られた結果を出力する処理。
    """
    for entry in words:
        print(entry)
    return None
        
read_file()

# ('埼玉県', 3)
# ('山形県', 3)
# ('山梨県', 3)
# ('群馬県', 3)
# ('岐阜県', 2)
# ('静岡県', 2)
# ('愛知県', 2)
# ('千葉県', 2)
# ('高知県', 1)
# ('和歌山県', 1)
# ('愛媛県', 1)
# ('大阪府', 1)