はじめに
社内のメンバーを中心にした勉強会で言語処理100本ノックを解いているのですが、その解答コードや、解く過程で便利だなと思った小技のまとめです。自分で調べたり検証したりした内容が多いですが、他の勉強会メンバーが共有してくれた情報も入っています。
今回はUNIXコマンドの基礎についてまとめていきますが、先行する@moriwoさんや@segavvyさんの記事でかなり詳しい解説がすでに書かれているので、この記事での説明はさらっと控えめにしたいと思います。以下を読んで分からない点があれば、上のリンクからお二方の記事なども見てみるのがおすすめです。
シリーズ
環境
- macOS
- Python 3.8.1
- JupyterLab
コード
10. 行数のカウント
def count_lines():
with open('hightemp.txt') as file:
return len(file.readlines())
count_lines()
24
!wc -l hightemp.txt
24 hightemp.txt
UNIXコマンドの方が圧倒的に簡潔に書けました。ちなみにwc
の前についている!
は、JupyterLabやNotebookでUNIXコマンドを実行するときに使うものです(場合によって!
なしでも動作します)。
11. タブをスペースに置換
def replace_tabs():
with open('hightemp.txt') as file:
return file.read().replace('\t', ' ')
print(replace_tabs())
!cat hightemp.txt | sed $'s/\t/ /g'
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
岐阜県 多治見 40.9 2007-08-16
山形県 山形 40.8 1933-07-25
山梨県 甲府 40.7 2013-08-10
...
UNIXのsed
について、$
記号をつけないと\t
がタブ記号として認識されないという点には注意が必要かなと思いました。
12. 1列目をcol1.txtに,2列目をcol2.txtに保存
import pandas as pd
def separate_columns():
df = pd.read_csv('hightemp.txt', sep='\t', header=None)
df.iloc[:,0].to_csv('col1.txt', header=False, index=False)
df.iloc[:,1].to_csv('col2.txt', header=False, index=False)
separate_columns()
!cut -f 1 hightemp.txt > col1_unix.txt
!cut -f 2 hightemp.txt > col2_unix.txt
結果を!head col1.txt col2.txt
で確認すると、次のようになります。!head col1_unix.txt col2_unix.txt
とした場合も同様です。
==> col1.txt <==
高知県
埼玉県
...
==> col2.txt <==
江川崎
熊谷
...
13. col1.txtとcol2.txtをマージ
def merge_columns():
with open('col1.txt') as col1_file, open('col2.txt') as col2_file, \
open('merge.txt', mode='w') as new_file:
for col1_line, col2_line in zip(col1_file, col2_file):
new_file.write(f'{col1_line.rstrip()}\t{col2_line.rstrip()}\n')
merge_columns()
!paste col[1-2].txt > merge_unix.txt
高知県 江川崎
埼玉県 熊谷
岐阜県 多治見
山形県 山形
...
結果の確認については!head merge.txt
または!head merge_unix.txt
とすると、上のような出力が得られるはずです。
14. 先頭からN行を出力
def show_head():
n = int(input())
with open('hightemp.txt') as file:
for line in file.readlines()[:n]:
print(line.rstrip())
show_head()
!head -3 hightemp.txt
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16
岐阜県 多治見 40.9 2007-08-16
Pythonについては、関数からリストを返すことにしたい場合、以下のように書くのもありかと思います。
def show_head():
n = int(input())
with open('hightemp.txt') as file:
return [line for line in file.readlines()[:n]]
print(*show_head())
一方UNIXではPythonのようにコマンドラインで整数を受け取ることが難しいのですが、-
の後ろに整数を書くと何行分表示するか指定できます。応用的な使い方としては、たとえば12
の解答の最後で
!cat hightemp.txt | sed $'s/\t/ /g' | head -5
と書いて、最初の5行だけ表示することもできたりします。
15. 末尾のN行を出力
def show_tail():
n = int(input())
with open('hightemp.txt') as file:
return [line for line in file.readlines()[-n:]]
print(*show_tail())
!tail -3 hightemp.txt
山梨県 大月 39.9 1990-07-19
山形県 鶴岡 39.9 1978-08-03
愛知県 名古屋 39.9 1942-08-02
14とほぼ同じ。
16. ファイルをN分割する
import math
def split_file():
n = int(input())
with open('hightemp.txt') as file:
lines = file.readlines()
num = math.ceil(len(lines) / n)
for i in range(n):
with open('split{}.txt'.format(i + 1), mode='w') as new_file:
text = ''.join(lines[i * num:(i + 1) * num])
new_file.write(text)
split_file()
!split -n 5 -d hightemp.txt split_unix
==> split1.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
==> split5.txt <==
大阪府 豊中 39.9 1994-08-08
山梨県 大月 39.9 1990-07-19
山形県 鶴岡 39.9 1978-08-03
愛知県 名古屋 39.9 1942-08-02
上記は、Pythonの出力を!head split1.txt split5.txt
で確認したものです。
一方、上のUNIXコマンドは自分の環境だと動作せず、これだけはColab(Google Colaboratory)で試しました。Linuxでは一般に-n
コマンドが提供されているようなのですが(@IT)、デフォルトのmacOSでは使えないようです。
Colabで上記を実行するとsplit_unix00
からsplit_unix04
まで5つのファイルができたのですが、加えてそこに拡張子のtxt
をつけようとしたりすると、ちょっと面倒になりそうだと感じました。@moriwoさんの記事ではawk
なども使った実装例が紹介されていますが、自分はPythonを使ったほうが読みやすいコードになるのかな、と思いました。
17. 1列目の文字列の異なり
import pandas as pd
def get_chars_set():
df = pd.read_csv('hightemp.txt', sep='\t', header=None)
return set(df.iloc[:, 0])
print(get_chars_set())
{'千葉県', '埼玉県', '山形県', '和歌山県', '静岡県', '高知県', '大阪府', '岐阜県', '群馬県', '愛媛県', '山梨県', '愛知県'}
!sort -u col1_unix.txt
千葉県
埼玉県
大阪府
山形県
...
UNIXコマンドは並べ替えと重複除去の処理をパイプで繋いで、!sort col1_unix.txt | uniq
と書くこともできます。
18. 各行を3コラム目の数値の降順にソート
def sort_rows():
df = pd.read_csv('hightemp.txt', sep='\t', header=None)
df.rename(columns={0: 'Prefect', 1: 'City', 2: 'Temp', 3: 'Date'}, inplace=True)
df.sort_values(by='Temp', inplace=True)
return df
sort_rows()
Prefect City Temp Date
23 愛知県 名古屋 39.9 1942-08-02
21 山梨県 大月 39.9 1990-07-19
20 大阪府 豊中 39.9 1994-08-08
...
!sort hightemp.txt -k 3
愛知県 名古屋 39.9 1942-08-02
山形県 鶴岡 39.9 1978-08-03
山梨県 大月 39.9 1990-07-19
大阪府 豊中 39.9 1994-08-08
...
「逆順」という言葉の意味が、元の逆ということなのか降順ということなのか判然としませんが、後者の解釈で解いてみました。この問題については、UNIXコマンドだと短く書けるということを特に強く感じられますね。
19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる
def count_freq():
df = pd.read_csv('hightemp.txt', sep='\t', header=None)
return df[0].value_counts()
count_freq()
群馬県 3
山梨県 3
山形県 3
埼玉県 3
!cut -f 1 hightemp.txt | sort | uniq -c | sort -r
3 群馬県
3 山梨県
3 山形県
3 埼玉県
UNIXコマンドは少し長くなりましたが、まず1列目を切り出して(cut -f 1
)、次にその出現頻度を求め(sort | uniq -c
)、最後に出現頻度の逆順で並べる(sort -r
)、という流れが分かりやすくなるように書いてみました。
ただ、pandasのvalue_counts()
の優秀さも考えると、ここではPythonを使ったほうが分かりやすいかなという気がします。
まとめ
この章については以上ですが、間違いなどあったらコメントいただけると助かります。