言語処理100本ノック2020(python)備忘録10-19

10

path = 'popular-names.txt'
f = open(path)
s = f.read()
f.close()
s_line_sep = s.splitlines()
wc = len(s_line_sep)
print(wc)

出力は
2780
splitlinesでそれぞれの行を要素とする配列s_line_sepを作り、その長さ(=要素数)をlen関数で数える。
split('\n')でもいいと思ったが、単に\rなどの文字でもsplitされる以外にもsplitlines()との差がある。
具体的にはsplitlines()だと最後に空行が含まれない一方で、split('\n')では最後に空行が含まれる差がある。
蛇足:f.close()で閉じるならば、対称性によりf.open()で開きたいものだが、得体の知れないfがそうできないのもわかる。

例えば、

s = 'test\ntest\n\ntest\n\n\n'
s2 = s.split('\n')
s3 = s.splitlines()
s4 = s.split()
print(s2)
print(s3)
print(s4)

とすると、出力は
split('\n')を用いたs2は
['test', 'test', '', 'test', '', '', '']
である一方で、
splitlines()を用いたs3は
['test', 'test', '', 'test', '', '']
であり、最後の空行の数が異なる。
個人的には、多くの場合splitlines()の方が便利そうだと復習時に気がつくが、かなりの間split('\n')と書いてしまった。
またさらに、split()の引数を書かなかった場合には
['test', 'test', 'test']
が出力され、"連続する空白文字"が無視される。
参考: docs.python.org


コマンドでは
$wc -l popular-names.txt

11

path = 'popular-names.txt'
with open(path) as f:
    s = f.read()
    s = s.replace('\t',' ')
print(s)

with open を使ってみた。
with open は、インデントの間openしたファイルにアクセスでき、インデントが戻ると自動的にcloseする。
上記だとprint(s)の直前に f.close()が行われるはず。
close()はs = f.read()の直後でも問題ないと思う。
文字列に対して、.replace('第一引数','第二引数')で、第一引数の文字列が第二引数の文字列に置き換えられる。

コマンドでは
$expand -t 1 popular-names.txt

出力は非常に長く、
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880

で始まり
Lucas M 12585 2018
Mason M 12435 2018
Logan M 12352 2018

で終わる。
最後の改行を取り除くべきかは不明。

12

まず最初に思いついたのは

read = 'popular-names.txt'
write = 'col1_2.txt'
write2= 'col2_2.txt'

with open(read) as r, open(write, 'w') as w, open(write2, 'w') as w2:
    for i in l.split()[0:2] for l in r:
        w.write(i[0]+'\n')
        w2.write(i[1]+'\n')

と交互にwrite()で書く手法である。

for文の中の l.split()[0:2] for l in r は、イテレータrの中のlそれぞれに対してl.split()をしたものの0番目と1番目[0:2]を取り出し、iとして扱っている。
そこでi[0]とi[1]をwとw2にそれぞれ代入している。
内包表記と呼ばれている([]で囲と"リスト"内包表記?)
毎回write()するのではなく、listなどで一度書き込むデータを作ってからwrite()する方がいいのかもしれない。
そうするとjoin関数を使えば、最後の'\n'がないデータとなる。

以上を考慮すると、

PATH_R = 'popular-names.txt'
PATH_W1 = 'col1_3.txt'
PATH_W2 = 'col2_3.txt'

with open(PATH_R) as r:
    read_data = r.read().splitlines()
names = []
sexes = []
for line in read_data:
    name, sex, unknown, year = line.split('\t')
    names.append(name)
    sexes.append(sex)
with open(PATH_W1, 'w') as w1:
    w1.write('\n'.join(names))
with open(PATH_W2, 'w') as w2:
    w2.write('\n'.join(sexes))

import time
time.time()
を用いて時間を計測したが、どちらも2〜3ミリ秒で終わった。

出力は
Mary
Anna
Emma

で始まるcol1.txtと
F
F
F

で始まるcol2.txt

コマンドでは
$cut -f 1 popular-names.txt >cut1_command.txt
$cut -f 2 popular-names.txt >cut2_command.txt

ちなみに最初に思いついた別解

path = 'popular-names.txt'
with open(path) as f:
    s = f.read()
    # s_line_sep = s.splitlines()
s_word_sep = s.split()
col1 = s_word_sep[::4]
col2 = s_word_sep[1::4]

for i in range(len(col1)):
    if i == 0:
        col1_w = col1[0]
        col2_w = col2[0]
    else:
        col1_w = col1_w + '\n' + col1[i]
        col2_w = col2_w + '\n' + col2[i]

path_w1 = 'col1.txt'
with open(path_w1, mode='w') as f_col1:
    f_col1.write(col1_w)
path_w2 = 'col2.txt'
with open(path_w2, mode='w') as f_col2:
    f_col2.write(col2_w)


ちなみにこの書き方だと、4〜6ミリ秒かかるため前2実装に比べて遅い。リストの延長に時間がかかっていると予想。
if文でi==0を判定しているが、復習時に空のリストに順に追加する方がいいと思った。これはどっちがいいんでしょうか。

13

まず最初に思いついたのは

col1_path = 'col1.txt'
col2_path = 'col2.txt'
merge_path = 'merge.txt'

with open(col1_path) as c1, open(col2_path) as c2, open(merge_path, 'w') as m:
    col1 = c1.read()
    col2 = c2.read()
    col1_sep = col1.split()
    col2_sep = col2.split()

    for i in range(len(col1_sep)):
        m.write(col1_sep[i] +'\t'+ col2_sep[i]+'\n')

であった。
出力は
Mary F
Anna F
Emma F

で始まる。

それぞれを読んで、分けて、1行ずつmにwriteしている。

コマンドでは
$paste col1.txt col2.txt > merge_command.txt

.join()を使って最後の改行を含めない処理は、以下の書き方がきれいだと思う。

with open('col1.txt') as c1:
    col1 = c1.read()
with open('col2.txt') as c2:
    col2 = c2.read()
col1_sep = col1.splitlines()
col2_sep = col2.splitlines()

merged = []
for col1_e, col2_e in zip(col1_sep, col2_sep):
    merged.append('\t'.join([col1_e, col2_e]))

with open('merged.txt', 'w') as m:
    m.write('\n'.join(merged))

14

import sys
args = sys.argv

n = int(args[1])
read_path = args[2]

with open(read_path) as r:
    for i in range(n):
        out = r.readline()
        if len(out) != 0:
            print(out,end='')
        else:
            break

出力は、10 col1.txtを与えた場合
Mary
Anna
Emma
Elizabeth
Minnie
Margaret
Ida
Alice
Bertha
Sarah

import sysによって
sys.argvが使えるようになり、
sys.argvにはコマンドライン引数が入る。
args[1]とargs[2]がそれぞれ行数とファイル名である。
print()においてend=''オプションを入れているのは改行がoutに含まれているからで美しくない。
readlines()で読み取ったものの先頭N要素を.join('\n')すればこれは解決すると思う。

全体を読み取ってから先頭N行を出力してもいいと思うが、こちらの方がファイルサイズにあまり影響しないと感じたためこの実装にした。

型のチェックなどはしていないが、一応ファイルより長い行数を指定しても大丈夫なようにした。

コマンドでは
$head -n 10 col1.txt

ちなみに、readline()にはhintという引数があるため、

with open(read_path) as r:
    print(r.readlines(n))

と書けばn行表示されるかと思ったが、hintは「バイト数もしくは文字数」の指定であり、行数ではないため不採用である。

15

import sys
args = sys.argv

n = int(args[1])
read_path = args[2]

with open(read_path) as r:
    r_content = r.read()
    r_content_split = r_content.split()
    
    print('\n'.join(r_content_split[-n:]))

出力は10 col1.txtを与えた場合
Liam
Noah
William
James
Oliver
Benjamin
Elijah
Lucas
Mason
Logan

スライスで-nと指定した場合、nが大きすぎる(=-nが小さすぎる)場合にも-1行目以前は出力しないため長さの比較などは行っていない。
そして配列で手に入るので\nで結合して出力している

コマンドでは
$tail -n 10 col1.txt

16

import sys
args = sys.argv

n = int(args[1])
read_path = args[2]

with open(read_path) as r:
    r_content = r.read()
r_content_split = r_content.split()
for i in range(n):
    write_path = "splited"+str(i)+".txt"
    with open(write_path, 'w') as w:
        w.write('\n'.join(r_content_split[i*len(r_content_split)//n:(i+1)*len(r_content_split)//n]))

分割したときに等分できない場合についての処理が書かれていなかった。
上記でも差が1なので妥協。
pathもインクリメンタルに生成。

pythonでは//で答えも整数の割り算が可能。
私の環境(mac)ではsplitコマンドがなかった。

17

read = 'popular-names.txt'

name_set = set()
with open(read) as r:
    for i in (l.split()[0] for l in r):
        name_set.add(i)
print('sorted set = ', sorted(name_set))

sortedにする必要は無いため、最後の表示はprint('set = ', name_set)でもよいが、出力が統一できるのでこれで出力した例を載せる。
出力は
['Abigail', 'Aiden', 'Alexander'
で始まり、
'Virginia', 'Walter', 'William']
で終わる。

set型を使うことで重複を自動的に除外。
setは本体をソートするのではなく、sortedで並び替えられたものを得る。本体であるname_setは変化しない。


open('popular-names.txt')は<_io.TextIOWrapper name='col1.txt' mode='r' encoding='UTF-8'>であり、直接イテレートできる。
参考:
io --- ストリームを扱うコアツール — Python 3.8.3 ドキュメント
そのため、

name_set = set()
for line in open('popular-names.txt'):
    name_set.add(line.split()[0])
print('sorted set = ', sorted(name_set))

でも動く。
コマンドは
$cut -f 1 popular-names.txt | sort | uniq 

18

read = 'popular-names.txt'

r_content_splitlines_split = []

with open(read) as r:
    r_content = r.read()
    r_content_splitlines = r_content.splitlines()
    for i in r_content_splitlines:
        r_content_splitlines_split.append(i.split())
    
    r_content_splitlines_split.sort(key=lambda x: int(x[2]), reverse=True)

write = '18_out.txt'
with open(write, 'w') as w:
    for i in r_content_splitlines_split:
        w.write('\t'.join(i) + '\n')

配列を作り、3番目の要素でソートしている。
3番目の要素でのソートは、.sortを用い、keyにラムダ式を使い、x[2]をintに変換したもので並び替えるように設定している。
また、降順でのソートであるため、reverse=Trueとしている。

出力は
Linda F 99689 1947
Linda F 96211 1948
James M 94757 1947

で始まり
Bertha F 1320 1880
Alice F 1308 1881
Sarah F 1288 1880

で終わる。

またちょっと工夫すると、

with open('popular-names.txt') as r:
    r_lines = r.read()
lines = r_lines.splitlines()
r_split = [line.split() for line in lines]
r_split.sort(key=lambda x: int(x[2]), reverse=True)
with open('18_out.txt', 'w') as w:
    w.write('\n'.join(['\t'.join(eles) for eles in r_split]))

と書ける。リスト内包表記をふんだんに使っている。
もちろんlinesと一度変数にしなくてもよいので、

with open('popular-names.txt') as r:
    r_split = [line.split() for line in r.read().splitlines()]
r_split.sort(key=lambda x: int(x[2]), reverse=True)
with open('18_out.txt', 'w') as w:
    w.write('\n'.join(['\t'.join(eles) for eles in r_split]))

でもいいのだがこのあたりが可読性的限界かな?

19

nameset = dict()
with open('popular-names.txt') as r:
    r_data = r.read()
r_data_splitlines = r_data.splitlines()
for i in r_data_splitlines:
    if i.split()[0] not in nameset:
        nameset[i.split()[0]] = 1
    else:
        nameset[i.split()[0]] += 1
print([eles[0] for eles in sorted(nameset.items(), key = lambda x: x[1], reverse=True)])

出力は
[('James', 118), ('William', 111), ('John', 108),
で始まり
('Crystal', 1), ('Rachel', 1), ('Lucas', 1)]
で終わる(しかし名前の順番については特に指定が無いため出現頻度が同一であれば順が入れ替わる場合もある?。

dict型の、人名をキーとして出現回数を値として扱う。
not in namesetによって存在しない場合には1を代入し、
存在する場合はすでに存在する値を1増やしている。
題意には含まれていないが、出現回数を見たい場合には
最後の出力を

print(sorted(nameset.items(), key = lambda x: x[1], reverse=True))

とすればよい。

collections( collections --- コンテナデータ型 — Python 3.8.3 ドキュメント )を使ってもいいと思う。
その場合は

import collections
with open('popular-names.txt') as r:
    r_data = r.read()
r_data_splitlines = r_data.splitlines()
counter = collections.Counter()
for line in r_data_splitlines:
    counter[line.split()[0]] += 1
print([eles[0] for eles in counter.most_common()])

で動作する。
dictを用いた実装では、if文で not in nameset と確かめなければ
KeyError: 'Mary'
とエラーが出てしまうが、collectionsのCounterでは、存在しない場合もcounter[line.split()[0]] += 1で1が代入され、便利。