自身のQiita記事(こちら)の転載です。
東北大乾・岡崎研(現乾・鈴木研)で作成された、新人研修の一つであるプログラミング基礎勉強会の教材『言語処理100本ノック 2020年版』をPython(3.7)で解いた記事の第3弾です。
独学でPythonを勉強してきたので、間違いやもっと効率の良い方法があるかもしれません。
改善点を見つけた際はご指摘いただけると幸いです。
今回取り組んだ部分に関してはpandasというデータ分析ライブラリーを使わなかった場合と使った場合の2パターンご紹介しています。
これらの言語処理は将来的に機械学習に用いることが多いと思いますので、機械学習でも用いられるpandasを使った方が、より活用の幅が広がるかもしれません。
UNIXコマンドの実行環境はMacOSですので、WindowsやLinuxをお使いの方は適宜自分の環境に合わせたやり方を調べていただければと思います。
ソースコードはGitHubにも公開しています。
第2章: UNIXコマンド
popular-names.txtは,アメリカで生まれた赤ちゃんの「名前」「性別」「人数」「年」をタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,popular-names.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
10. 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
file_name = "popular-names.txt"
with open(file_name) as f:
lines = f.readlines()
print(len(lines))
ファイルの読み込みは通常open()
でファイルオブジェクトが開かれ、close()
メソッドで閉じる必要があります。
しかし、上記のようにwith
ブロックを使うと、ブロックの終了時に自動的に閉じられ、閉じ忘れを防止できます。
ファイルオブジェクトに対してreadlines()
メソッドでファイル全体を1行ずつリストとして読み込みます。読み込んだリストの大きさが行数となります。
続いてwc
による行数の確認を見てみましょう。
#!/bin/sh
wc popular-names.txt
コマンドラインツールでwc テキストファイル名
で指定したテキストファイルの行数 単語数 バイト数
が表示されます。最初に表示される行数がPythonプログラムと一致していれば問題ありません。
pandasを使うと以下のように書けます。
import pandas as pd
file_name = "popular-names.txt"
data_frame = pd.read_table(file_name, header=None)
print(len(data_frame))
pandasでは、read_table()
メソッドでタブ区切りにされたテキストファイルを行列として読み込むことができます。今回はデータの1行目から実データが入っており、データが何を表すかを示すヘッダーは含まれていないため、header=None
のオプションをつけています。
読み込んだあとは大きさを求めると行数がわかります。
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
file_name = "popular-names.txt"
with open(file_name) as f:
text = f.read()
print(text.replace("\t", " "))
今回はファイルオブジェクトに対してread()
メソッドを用いて読み込みを行いました。read()
メソッドは文書全体を1つのテキストデータとして読み込むメソッドです。
読み込んだあとはreplace()
メソッドでタブ文字を半角スペースに変換して出力しています。replace()
メソッドは第一引数に変換前の文字を、第二引数に変換後の文字を渡します。
続いてUNIXコマンドによる確認です。
#!/bin/sh
cat ./popular-names.txt | sed s/$'\t'/' '/g
echo "---"
cat ./popular-names.txt | tr '\t' ' '
echo "---"
expand -t 1 popular-names.txt
3種類の方法で確認しています。
cat
+ sed
まずはcat
とsed
を組み合わせた方法です。sed
の前にある|
で前のコマンドの処理結果を後ろのコマンドに渡しています。
cat
はファイルの中身を出力するコマンドです。ファイルの中身を出力することでsed
にファイルの内容を渡しています。
sed
は文字列に対していろいろな処理を行うコマンドです。今回はsed
コマンドの後ろで文字列を指定して置換しています。
文字列の指定はs/置換前の文字/置換後の文字/g
で指定しています。g
に関しては、これをなくすと一番最初に出現した置換対象のみを置換します。Macではタブを$'\t'
で表すというのを調べるのに一番苦戦したかもしれません…
cat
+ tr
tr
コマンドは読み込んだ文字列を変換・削除して出力するコマンドです。sed
よりやれることが限られます。
cat
から渡されたテキストデータに対して、第一引数の文字を対象として第二引数の文字に置換します。ここではtr $'\t' ' '
となっているので、テキストデータのタブ文字を半角スペースに変換となるわけですね。
expand
expand
は更に用途が限られて、タブ文字を空白に変換するコマンドです。オプションの-t
でタブ文字の数を指定し、引数にファイル名を指定することで変換してくれます。シンプルで分かりやすいですね!
pandasを使った場合は次のようになりました。
import pandas as pd
file_name = "popular-names.txt"
data_frame = pd.read_table(file_name, header=None)
data_frame.to_csv("output/11pd_ans.txt", sep=" ", index=False, header=None)
to_csv()
メソッドでファイルとして出力しています。ファイルを出力する際は、区切り文字として半角スペースを指定することでタブ文字から変換を行っています。入力ファイルと同様の形のデータにしようとすると、インデックスとヘッダーは不要なので、それぞれindex=False
とheader=None
を指定してファイルに含まれないようにしています。
12. 1列目をcol1.txtに,2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
file_name = "popular-names.txt"
output_file1 = "output/col1.txt"
output_file2 = "output/col2.txt"
output_files = [output_file1, output_file2]
col1 = []
col2 = []
with open(file_name) as rf:
for line in rf:
item1 = line.split()[0]
item2 = line.split()[1]
col1.append(item1)
col2.append(item2)
cols = [col1, col2]
for output_file, col in zip(output_files, cols):
with open(output_file, mode='w') as wf:
wf.write("\n".join(col) + "\n")
col1
・col2
という各列を記録するリストを用意し、ファイルを1行ずつ読み込んで1列目・2列目のみをそれぞれ記録していきます。
ファイル出力をまとめてできるよう各列のデータをリスト化しておきます。
最終的にzip
関数を用いて、1列ずつ1つのファイルとして出力します。
出力の際には改行で各要素を結合します。
最後の+ "\n"
はシェルスクリプトでコマンドとの差分を比較した際に余計な差分が出ないようにするためのものです。
確認のコマンドは以下のように実行しました。
#!/bin/bash
diff output/col1.txt <(cut -f 1 -d $'\t' popular-names.txt)
diff output/col2.txt <(cut -f 2 -d $'\t' popular-names.txt)
厳密に比較できそうだったので、diff
コマンドを用いて差分を取得しています。diff
コマンドの基本的な使い方は
$ diff 比較したいファイル1 比較したいファイル2
ですが、今回はファイルとコマンドの実行結果を比較するため<(コマンド)
でdiff
に渡しています。
本題のcut
ですが、こちらは列を指定してファイルの内容を分割するコマンドです。-f
で何列目かを指定、-d
で列の区切り文字を指定します。
今回も区切り文字は$'\t'
でタブ文字を指定しています。
pandasを用いた場合はより簡潔に書けます。
import pandas as pd
data_frame = pd.read_table("popular-names.txt", header=None)
data_frame[0].to_csv("output/col1pd.txt", index=False, header=None)
data_frame[1].to_csv("output/col2pd.txt", index=False, header=None)
すべてのデータをタブ区切りで読み込んで、読み込んだオブジェクト(DataFrameオブジェクト)の列を指定してファイル出力するだけです。
13. col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.
col1_file = "output/col1.txt"
col2_file = "output/col2.txt"
cols_file = "output/cols.txt"
col_files = [col1_file, col2_file]
cols = []
for file_name in col_files:
with open(file_name) as rf:
cols.append(rf.readlines())
output = ""
for col1, col2 in zip(cols[0], cols[1]):
output += col1.rstrip() + "\t" + col2.rstrip() + "\n"
with open(cols_file, mode='w') as wf:
wf.write(output)
今までの内容の組み合わせで処理しています。
12で作成したそれぞれの列のファイルを1行ずつ読み込んで、リストにまとめることでzip
関数で同じ行を同時に扱えるようにしています。
ループ内では取り出した行をそれぞれ末尾の改行をrstrip()
メソッドで取り除いた上で結合、最後に改行を加えて出力文字列に加えます。
最終的に文字列をファイル出力して処理終了です。
#!/bin/bash
diff output/cols.txt <(paste output/col1.txt output/col2.txt)
UNIXコマンドでのファイル同士の結合はpaste
コマンドを使います。
ファイルを引数で指定することで、列方向でファイル結合を行います。
結合文字を-d
オプションで指定することも可能ですが、デフォルトでタブ文字で結合されるので今回は特に指定しておりません。
ちなみに、行方向にファイル結合する場合はcat
コマンドを用います。
pandasでは下記のように作れます。
import pandas as pd
c1 = pd.read_table("output/col1pd.txt", header=None)
c2 = pd.read_table("output/col2pd.txt", header=None)
data_frame = pd.concat([c1, c2], axis=1)
data_frame.to_csv("output/colspd.txt", sep='\t', index=False, header=None)
こちらも普通に書くよりもだいぶ短くなりました。
要素の結合はconcat()
メソッドを使います。
引数に結合したい列を順番にリストで渡し、axis
オプションで結合方向を指定します。axis
オプションは0
が行方向でデフォルトなので、1
を指定して列方向としています。
14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
import sys
if len(sys.argv) != 2:
print("Set an argument N, for example '$ python 14.py 3'.")
sys.exit()
n = int(sys.argv[1])
file_name = "popular-names.txt"
with open(file_name) as rf:
for i in range(n):
print(rf.readline().rstrip())
コマンドライン引数を用いるためにsys
モジュールをインポートしています。
コマンドライン引数の取得はsys.argv
でリストとして取得できます。
引数の数が意図したものでない場合、指示文を出力してプログラムを終了する処理を入れてあります。
今回はファイル読み込みの中で処理を完結させました。
ファイルの全てのデータを読み込む必要はないと思い、ファイルオブジェクトから1行ずつ取得するreadline()
メソッドを使っています。
コマンドライン引数の数だけ先頭から1行ずつ出力していきます。
UNIXコマンドの方はといいますと
#!/bin/bash
if [ $# -ne 1 ]; then
echo "指定された引数は$#個です。" 1>&2
echo "実行するには数字を1個引数に指定してください。" 1>&2
exit 1
fi
diff <(head -n $1 popular-names.txt) <(python 14.py $1)
シェルスクリプトを実行する際に引数を取り、引数が指定された個数でなければ指示文を出し、そうでなければ先頭行から指定行数表示するhead
とPythonの出力の差分を調べます。
head
コマンドは先頭行から-n
オプションで指定した行数を表示します。
pandasでは
import sys
import pandas as pd
if len(sys.argv) != 2:
print("Set a argument N, for example '$ python 14.py 3'.")
sys.exit()
n = int(sys.argv[1])
data_frame = pd.read_table("popular-names.txt", header=None)
print(data_frame.head(n))
としました。head()
メソッドで行数を指定することで課題をクリアできます。
まとめ
この記事では言語処理100本ノック2020年版 第2章:UNIXコマンドの問題番号10〜14までを解いてみました。
正直PythonでのプログラムよりもUNIXコマンドの使い方に苦戦しました…
ただ、Pythonで実行するよりもコマンドでやったほうが処理が早いものもあるようなので、機械学習に用いるような大規模なデータではコマンドを覚えておいたほうが、効率的かもしれませんね。
まだまだ未熟なので、よりよい解答がありましたらぜひご指摘ください!!
よろしくお願いいたします。