Edited at

言語処理100本ノックに挑戦 / 第2章: UNIXコマンドの基礎

More than 1 year has passed since last update.


:muscle: 前書き

前回に続いて、言語処理100本ノックの第二章を回答していきます。本章では Unix コマンドと python コードを並行して学ぶことができ、ファイルI/Oやソートについて楽しく覚えられました。


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


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


UTF-8,LF の24行のテキストでした。内容は気温でソートされていて、同一の県名が複数回出てきたりします。


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
(以下略)


⚾ 10. 行数のカウント


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



コマンドでの確認


  • wc - print newline, word, and byte counts for each file


    • wc [OPTION]... [FILE]...

    • wc [OPTION]... --files0-from=F




bash


$ wc ./data/hightemp.txt
24 96 813 ./data/hightemp.txt


解1 open()とstr.count


answer

file_temp = open ('./data/hightemp.txt','r',encoding='utf-8')

text = file_temp.read()
file_temp.close()
print(text.count('\n'))
# 24


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


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



コマンドでの確認


  • sed - stream editor for filtering and transforming text


    • sed [OPTION]... {script-only-if-no-other-script} [input-file]...



  • tr - translate or delete characters


    • tr [OPTION]... SET1 [SET2]



  • expand - convert tabs to spaces


    • expand [OPTION]... [FILE]...


    • -t, --tabs=NUMBER


      • have tabs NUMBER characters apart, not 8






bash

$ sed 's/\t/ /g' ./data/hightemp.txt

高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
(以下略)
$ tr '\t' ' ' < ./data/hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
(以下略)
$ expand -t 1 ./data/hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
(以下略)


解1 replace


answer

file_temp = open ('./data/hightemp.txt','r',encoding='utf-8')

text = file_temp.read()
file_temp.close()
print(text.replace('\t',' '))



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


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



コマンドでの確認


  • cut - remove sections from each line of files


    • cut OPTION... [FILE]...


    • -f, --fields=LIST


      • select only these fields; also print any line that contains no delimiter character, unless the -s option is specified






bash

$ cut -f 1 ./data/hightemp.txt

高知県
埼玉県
(以下略)
$ cut -f 2 ./data/hightemp.txt
江川崎
熊谷
(以下略)


解1 愚直にループ


answer

file_temp = open ('./data/hightemp.txt','r',encoding='utf-8')

text = file_temp.read()
file_temp.close()

lst_col1 = []
lst_col2 = []
for line in text.split('\n'):
if len(line) > 0:
lst_col1.append(line.split('\t')[0])
lst_col2.append(line.split('\t')[1])

file_col1 = open ('./col1.txt','a',encoding='utf-8')
file_col2 = open ('./col2.txt','a',encoding='utf-8')

print(*lst_col1,sep='\n',file=file_col1)
print(*lst_col2,sep='\n',file=file_col2)
file_col1.close()
file_col2.close()



解2 Withを使ったファイルアクセス


answer

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:

text = file_temp.read()

lst_col1 = []
lst_col2 = []
for line in text.split('\n'):
if len(line) > 0:
lst_col1.append(line.split('\t')[0])
lst_col2.append(line.split('\t')[1])

with open ('./col1.txt','a',encoding='utf-8') as file_col1:
print(*lst_col1,sep='\n',file=file_col1)

with open ('./col2.txt','a',encoding='utf-8') as file_col2:
print(*lst_col2,sep='\n',file=file_col2)



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


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



コマンドでの確認


  • paste - merge lines of files


    • paste [OPTION]... [FILE]...


    • -d, --delimiters=LIST


      • reuse characters from LIST instead of TABs





  • command1とcommand2の結果をcommand3にファイルとして渡す


    • command3 <(command1) <(command2)




bash

$ paste -d '\t' <(cut -f 1 ./data/hightemp.txt) <(cut -f 2 ./data/hightemp.txt)

高知県 江川崎
埼玉県 熊谷
(以下略)


解1 zip



  • with open() を使ったファイルアクセス


answer

with open ('./col1.txt','r',encoding='utf-8') as col1_temp:

text_col1 = col1_temp.read()
with open ('./col2.txt','r',encoding='utf-8') as col2_temp:
text_col2 = col2_temp.read()

col1and2 = ''
for col1,col2 in zip(text_col1.split('\n'),text_col2.split('\n')):
col1and2 += col1 + '\t' + col2 + '\n'

with open ('./col1and2.txt','a',encoding='utf-8') as file_col1and2:
print(col1and2,file=file_col1and2)

# 高知県 江川崎
# 埼玉県 熊谷
# (以下略)



解2 with を纏める


  • 1行が長くなってしまったが、何度も with open() と書くよりは良さそう


answer

with open ('./col1.txt','r',encoding='utf-8') as col1_temp, open ('./col2.txt','r',encoding='utf-8') as col2_temp, open ('./col1and2.txt','a',encoding='utf-8') as file_col1and2:

col1and2 = ''
for col1,col2 in zip(col1_temp.read().split('\n'),col2_temp.read().split('\n')):
col1and2 += col1 + '\t' + col2 + '\n'

print(col1and2,file=file_col1and2)



⚾ 14. 先頭からN行を出力


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



コマンドでの確認


  • man:head - output the first part of files


    • head [OPTION]... [FILE]...


    • -n, --lines=[-]NUM


      • print the first NUM lines instead of the first 10; with the leading '-', print all but the last NUM lines of each file






bash

$ head -n 3 ./data/hightemp.txt

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


解1 引数の受取


answer

import sys

arg = int(sys.argv[1])
text = ''
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
for i in range(arg):
text += file_temp.readline()

print(text)



解2 argparse を利用した引数受取



  • argparse を利用することで型チェックやhelp設定ができる




answer

import argparse

parser = argparse.ArgumentParser(description='hightemp.txt の内容を指定行表示します')
parser.add_argument('integer', metavar='N', type=int, help='表示する行数を数字で入力')
args = parser.parse_args()

text = ''
with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
for i in range(args.integer):
text += file_temp.readline()

print(text)



bash

$ python .\Chapter02_Q14_A2.py --help

usage: Chapter02_Q14_A2.py [-h] N

hightemp.txt の内容を指定行表示します

positional arguments:
N 表示する行数を数字で入力

optional arguments:
-h, --help show this help message and exit

$ python .\Chapter02_Q14_A2.py abcd
usage: Chapter02_Q14_A2.py [-h] N
Chapter02_Q14_A2.py: error: argument N: invalid int value: 'abcd'

$ python .\Chapter02_Q14_A2.py 3
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
岐阜県 多治見 40.9 2007-08-16



⚾ 15. 末尾のN行を出力


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



コマンドでの確認


  • tail - output the last part of files


    • tail [OPTION]... [FILE]...


    • -n, --lines=[+]NUM


      • output the last NUM lines, instead of the last 10; or use -n +NUM to output starting with line NUM






bash

$ tail -n 3 ./data/hightemp.txt

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


解1 readlines() で読込、range で後半だけ出力

import argparse

parser = argparse.ArgumentParser(description='hightemp.txt の内容を末尾から指定行表示します')
parser.add_argument('integer', metavar='N', type=int, help='表示する行数を数字で入力')
args = parser.parse_args()

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
lines = file_temp.readlines()
text = ''
for linenumber in range(len(lines)-args.integer,len(lines)):
text += lines[linenumber]

print(text)


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


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



コマンドでの確認


  • split - split a file into pieces



    • split [OPTION]... [FILE [PREFIX]]



      • -l, --lines=NUMBER


        • put NUMBER lines/records per output file




      • --additional-suffix=SUFFIX


        • append an additional SUFFIX to file names








bash

$ split ./data/hightemp.txt -l 10 --additional-suffix=_split.txt

$ ll
-rwxrwxrwx 1 root root 169 Apr 18 21:22 xaa_split.txt*
-rwxrwxrwx 1 root root 174 Apr 18 21:22 xab_split.txt*
-rwxrwxrwx 1 root root 174 Apr 18 21:22 xac_split.txt*
-rwxrwxrwx 1 root root 161 Apr 18 21:22 xad_split.txt*
-rwxrwxrwx 1 root root 135 Apr 18 21:22 xae_split.txt*
$ $ cat xaa_split.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


解1 math ライブラリ


answer

import argparse

import math

parser = argparse.ArgumentParser(description='hightemp.txt の内容を引数個にファイル分割します')
parser.add_argument('integer', metavar='N', type=int, help='分割する個数を数字で入力')
args = parser.parse_args()

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_in:
lines = file_in.readlines()
file_suffix_no = 1
write_lines = math.ceil( len(lines) / args.integer )
for integer, line in enumerate(lines):
with open ('./split_' + str(file_suffix_no) + '.txt', 'a' ,encoding='utf-8') as file_out:
print(line,end='',file=file_out)
if ( (integer + 1) % write_lines == 0 ):
file_suffix_no += 1



bash

$ python Chapter02_Q16_A1.py 5

$ ll
-rwxrwxrwx 1 root root 169 Apr 18 21:50 split_1.txt*
-rwxrwxrwx 1 root root 174 Apr 18 21:50 split_2.txt*
-rwxrwxrwx 1 root root 174 Apr 18 21:50 split_3.txt*
-rwxrwxrwx 1 root root 161 Apr 18 21:50 split_4.txt*
-rwxrwxrwx 1 root root 135 Apr 18 21:50 split_5.txt*
$ cat split_1.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


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


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



コマンドでの確認


  • sort - sort lines of text files


    • sort [OPTION]... [FILE]...



  • uniq - report or omit repeated lines


    • uniq [OPTION]... [INPUT [OUTPUT]]




bash

$ cut -f 1 data/hightemp.txt | sort | uniq

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


解1 sort(と、リストの破壊操作)


answer

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:

text = file_temp.read()

lst_col1 = []
for line in text.split('\n'):
lst_col1.append(line.split('\t')[0])

lst_col1.sort()
# ['千葉県', '千葉県', '和歌山県', '埼玉県', '埼玉県', '埼玉県', '大阪府', '山形県', '山形県', '山形県', '山梨県', '山梨県', '山梨県', '岐阜県', '岐阜県', '愛媛県', '愛知県', '愛知県', '群馬県', '群馬県', '群馬県', '静岡県', '静岡県', '高知県']
for value in lst_col1: #
if lst_col1.count(value) > 1: # ココ
lst_col1.remove(value) #

for value in lst_col1:
print(value)
# 千葉県
# 和歌山県
# 埼玉県
# 大阪府
# 山形県
# 山梨県
# 山梨県
# 岐阜県
# 愛媛県
# 愛知県


想定外に * 山梨県 × 2 が現れました。* mohikanz の先輩方に相談したところ。 for x in list ループ内で list.remove() することでリスト破壊し、ループが想定どおりに回っていませんでした。検証コード。


answer

for index, value in enumerate(lst_col1):

print('target:{} / index:{} / count:{}'.format(value,index,lst_col1.count(value)))
# remove-next してしまうので、処理対象が 1 つ漏れる
if lst_col1.count(value) > 1:
lst_col1.remove(value)
# target:千葉県 / index:0 / count:2
# target:和歌山県 / index:1 / count:1
# target:埼玉県 / index:2 / count:3
# target:埼玉県 / index:3 / count:2
# target:山形県 / index:4 / count:3
# target:山形県 / index:5 / count:2
# target:山梨県 / index:6 / count:3
# target:岐阜県 / index:7 / count:2
# target:愛媛県 / index:8 / count:1
# target:愛知県 / index:9 / count:2
# target:群馬県 / index:10 / count:3
# target:群馬県 / index:11 / count:2
# target:静岡県 / index:12 / count:2

そのため、元の配列を remove していくのではなく、別の配列に append していくカタチに書き換えました。


answer

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:

text = file_temp.read()

lst_col1 = []
lst_answer = []
for line in text.split('\n'):
lst_col1.append(line.split('\t')[0])

lst_col1.sort()

for value in lst_col1:
if lst_answer.count(value) == 0:
lst_answer.append(value)

for value in lst_answer:
print(value)



解2 set() で重複除去


answer

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:

text = file_temp.read()

lst_col1 = []
lst_answer = []
for line in text.split('\n'):
lst_col1.append(line.split('\t')[0])

lst_col1.sort()
lst_answer = set(lst_col1) #dict型

for value in lst_answer:
print(value)



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


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



コマンドでの確認


  • sort - sort lines of text files


    • sort [OPTION]... [FILE]...


    • -r, --reverse


      • reverse the result of comparisons




    • -k, --key=KEYDEF


      • sort via a key; KEYDEF gives location and type




    • -n, --numeric-sort


      • compare according to string numerical value






bash

$ sort -k 3,3 -n -rr ./data/hightemp.txt

高知県 江川崎 41 2013-08-12
岐阜県 多治見 40.9 2007-08-16
埼玉県 熊谷 40.9 2007-08-16
(中略)
埼玉県 鳩山 39.9 1997-07-05
千葉県 茂原 39.9 2013-08-11


解1 sorted() を使ったソート


answer

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:

lines = file_temp.readlines()

lines2 = []
lines3 = []
lines4 = []
for line in lines:
lines2.append(line.split('\t'))

for line2 in lines2:
lines3.append([float(line2[2])] + line2 )

lines4 = sorted(lines3, reverse=True)
for line4 in lines4:
del line4[0] # 1列目を消す
print ('\t'.join(line4), end='')
# 高知県 江川崎 41 2013-08-12
# 岐阜県 多治見 40.9 2007-08-16
# 埼玉県 熊谷 40.9 2007-08-16
# (中略)
# 埼玉県 鳩山 39.9 1997-07-05
# 千葉県 茂原 39.9 2013-08-11



解2 ラムダ式でコンパクトに纏める


answer

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:

lines = file_temp.readlines()

lines2 = []
for line in lines:
lines2.append(line.split('\t'))

key_temp = lambda x: float(x[2]) #
# ラムダ式を使わない場合はこう
# def key_tmp ( x ):
# return float(x[2])

for line in sorted( lines2, key=key_tmp, reverse=True):
print ('\t'.join(line), end='')



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


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



コマンドでの確認


bash

$  cut -f 1 ./data/hightemp.txt | sort | uniq -c | sort -r

3 群馬県
3 山梨県
(中略)
1 大阪府
1 和歌山県


解1 ラムダ関数でソートキー取得


  • No17とほぼ同じ

  • ラムダ無名関数を利用しソートキー取得


answer

import itemgetter

with open ('./data/hightemp.txt','r',encoding='utf-8') as file_temp:
text = file_temp.read()

lst_col1 = []
lst_col2 = []
for line in text.split('\n'):
lst_col1.append(line.split('\t')[0])

lst_col1.sort()
for value in set(lst_col1):
if value != '': #空行処理
lst_col2.append([lst_col1.count(value)] + [value])

lst_col2.sort(key=lambda x:x[0]) # [0]順に昇順ソート
lst_col2.refverse() # ひっくり返して降順

for value in lst_col2:
print(value)