言語処理100本ノック2020年版を解いてみた【第2章:UNIXコマンド 10〜14】

自身の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

まずはcatsedを組み合わせた方法です。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=Falseheader=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")

col1col2という各列を記録するリストを用意し、ファイルを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で実行するよりもコマンドでやったほうが処理が早いものもあるようなので、機械学習に用いるような大規模なデータではコマンドを覚えておいたほうが、効率的かもしれませんね。

まだまだ未熟なので、よりよい解答がありましたらぜひご指摘ください!!
よろしくお願いいたします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です