カルチャーとデータサイエンス

ビジネスの意思決定は無理でも、せめてカルチャーはデータサイエンスしたい

言語処理100本ノックをひたすら解く ~ No.10 ~ 19(UNIXコマンドの基礎)

はじめに

fyiwdwytm.hatenablog.com

↑の続きです。

当記事の目的

言語処理100本ノック 2015の問題10 ~ 19を実装します。

UNIXコマンドはほぼ素人なので、先人の知恵になるべく頼っていきます。。

参考元

回答の参考として、主に以下Qiitaの記事を参照しました。

qiita.com

qiita.com

準備

前回と同じく、実装はjupyter notebook上で行います。UNIXコマンドもnotebook上の記法(!を最初につけるマジックコマンド)で実装します。

また、入力ファイルとしてhightemp.txtを使用します。ipynbファイルと同じディレクトリにinputディレクトリを切り、その中から呼び出すこととします。同様に、出力を伴う問題のファイルはoutput配下に出力することとします。

hightemp.txt : 気象庁が公開している「歴代全国ランキング>観測史上の順位>最高気温の高い方から」を基に,手作業でタブ区切り形式に整形したものです.利用規約等はこちらのページを参照して下さい.- 言語処理100本ノック 2015

UNIXコマンドでわからないものはman <コマンド名>コマンドで逐一調べます。

実装

10. 行数のカウント

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

(解)python

def nlp010(filename):
    with open(filename) as f:
        result = len(f.readlines())
    return result

nlp010("./input/hightemp.txt")

(解)unix

!wc -l ./input/hightemp.txt

(注釈)

  • with open():ファイルを読み込みます。オプションはr: 読み込み、w:書き出し等ありますが、指定なしの場合は読み込みが設定されます。その他使い方はPythonでファイルの読み込み、書き込み(作成・追記) | note.nkmk.meを参照してください。
  • wcコマンド:指定したファイルの文字数をカウントします。-lオプションで文字ではなく行数をカウントします。コマンド名はword countの略でしょうかね。

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

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

(解)python

def nlp011(filename):
    with open(filename) as f:
        for line in f:
            print(line.replace("\t", " "), end=" ")

nlp011("./input/hightemp.txt")

(解)unix

#sed
!sed 's/    / /g' ./input/hightemp.txt

#tr
!tr '\t' ' ' < ./input/hightemp.txt

#expand
!expand -t 1 ./input/hightemp.txt

(注釈)

  • replace(): 第一引数に置換前文字列、第二引数に置換後文字列を指定して文字を置き換えます。ここで指定した\tはタブのエスケープ文字です。
  • sedコマンド: 基本的な使い方はsed 's/<置換前文字列>/<置換後文字列/g>' <ファイルPATH>でファイルに含まれる文字を置換します。他の使い方はsedコマンドで覚えておきたい使い方12個(+3個) | 俺的備忘録 〜なんかいろいろ〜が大変参考になりました。
  • macではUNIXにてタブのエスケープ文字\tに対応していないようで、jupyter notebook上では変換に失敗してしまいます... ターミナルでの実行時はctrl + V入力後にタブキーを入力することでタブを挿入することができます。jupyter notebok上での解放が分かるかた、アドバイスお待ちしています..
  • tr コマンド: tr '置換前文字列' '置換後文字列'で文字を置き換えます。<はパイプラインで、ここではtxtファイルをtr`コマンドの標準入力に入れていることを指します。
  • expand コマンド: タブを空白に変換するコマンドです。 -t <数値>オプションで、タブを指定した数値分の空白(1で半角)に置き換えます。

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

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

(解)python

# python
def nlp012(input: str, col: int) -> str:
    with open(input, "r") as f:
        data = list(l.split('\t') for l in f)
    
    with open("./output/nlp012/col{}_p.txt".format(str(col+1)), "w") as f:
        for i in data:
            f.write(i[col] + '\n')

nlp012("./input/hightemp.txt", 0)
nlp012("./input/hightemp.txt", 1)

(解)unix

!cut -f 1 ./input/hightemp.txt > ./output/nlp012/col1_u.txt
!cut -f 2 ./input/hightemp.txt > ./output/nlp012/col2_u.txt

(注釈)

  • pythonの方は、入力ファイル名と列番号を指定して、該当列のファイルを作成する関数を実装しました。
  • cutコマンド: テキストファイルを横方向に分割するコマンドです。-f <数値>オプションによって、区切り文字に分割された列の何番目の項目を取得するかを設定します。区切り文字は-dオプションで指定できますが、デフォルトは空白かタブ?のようです。

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

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

(解)python

def nlp013(input1:str, input2:str) -> str:
        with open(input1, "r") as f1:
            data1 = f1.readlines()
    
        with open(input2, "r") as f2:
            data2 = f2.readlines()
            
        result = [i.strip()+"\t"+j.strip()+"\n" for i, j in zip(data1, data2)]
        
        with open('./output/nlp013/col12_p.txt', 'w') as f:
            for i in result:
                f.write(i)
            
nlp013("./output/nlp012/col1_p.txt", "./output/nlp012/col2_p.txt")

(解)unix

!paste ./output/nlp012/col1_u.txt ./output/nlp012/col2_u.txt > ./output/nlp013/col12_u.txt

(注釈)

  • pythonのコードでは2つのファイルを引数に入れる関数を実装しました。col1.txtcol2.txtを内包表記で結合し、resultに格納した後ファイルを書き込みます。
  • pasteコマンドは2つのファイルを列方向に結合します。リダイレクトしてcol12_u.txtファイルに出力しています。

14. 先頭からN行を出力

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

(解)python

def nlp014(filename: str):
    n = int(input("自然数nを指定"))
    with open(filename) as f:
        for i, line in enumerate(f):
            if i >= n: 
                break
            print(line.strip())

nlp014("./input/hightemp.txt")

(解)unix

!head -n 5 ./input/hightemp.txt

(注釈)

  • 今回Jupyter Notebookにて実装しているため、自然数Nをコマンドライン引数ではなく標準入力で受け取るようにしています。input()で受け取ることが可能です。
  • メモリ節約のため、一行ずつ読み込み・処理する形式(readlines()などを使用しない)をとっています。
  • headコマンドは-nオプションで指定した行数分だけファイルを出力します。

15. 末尾のN行を出力

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

(解)python

def nlp015(filename: str):
    n = int(input("自然数nを指定"))
    with open(filename) as f:
        lines = f.readlines()
        
        for line in lines[-n:]:
            print(line.strip())

nlp015("./input/hightemp.txt")

(解)unix

!tail -n 5 ./input/hightemp.txt

(注釈)

    1. とは異なり、一度全てのファイルをreadlines()で読み込む実装方針をとっています。
  • tailコマンドはheadコマンドの対となるコマンドです。

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

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

(解)python

def nlp016(filename: str):
    n = int(input("自然数nを指定"))
    with open(filename) as f:
        lines = f.readlines()
        
    line = math.ceil(len(lines) / n)
    outputs = [lines[idx:idx+line] for idx in range(0,len(lines), line)]
    
    for i, output in enumerate(outputs):
            with open('./output/nlp016/output{}_p.txt'.format(i), 'w') as f:
                for j in output:
                    f.write(j)
            
    
nlp016("./input/hightemp.txt")

(解)unix

!split -l $(expr $(wc -l ./input/hightemp.txt | awk '{print $1}') / 5) ./input/hightemp.txt ./output/nlp016/output

(注釈)

  • pythonのコードでは、まず受け取った自然数nを1ファイルあたりの行数linesに変換しています。math.ceil関数で小数点以下は切り上げます。
  • outputsの内包表記の処理が少しややこしいかもしれません。nで指定した行数分でグルーピングして2次元のリストを作成しています。下記の簡単な実装例(26字の英文字を6文字置きに区切る)も参考にしてみてください。
# a~z26字のリストを6文字おきに区切る
list = [chr(i) for i in range(97, 97+26)] #a~zの文字列を生成
output = [list[i:i+6] for i in range(0, 26, 6)] #range(0, 26, 6): 0~26の範囲で6個置きに数値を出力

print(output)
# [['a', 'b', 'c', 'd', 'e', 'f'], ['g', 'h', 'i', 'j', 'k', 'l'], ['m', 'n', 'o', 'p', 'q', 'r'], ['s', 't', 'u', 'v', 'w', 'x'], ['y', 'z']]を出力
  • unixコマンドはかなり複雑になっています。splitコマンドのオプション-lは分割数ではなく1ファイルあたりの行数を入力するため、その計算が必要です。まずinputファイルの行数をwcコマンドでカウントします。出力は24 hightemp.txtのように行数とファイル名が同時に表示されてしまうので、awkコマンドで結果をパースし行数のみを取得します。最後に、式評価を行うexprコマンドで行数を自然数で分割し、1ファイルあたりの行数を取得します。
  • splitコマンド、macでは連番ファイル名を指定できず、○○a, ○○b, ...という名称で出力されます。

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

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

(解)python

def nlp017(filename: str):
    with open("./input/hightemp.txt") as f:
        data = list(l.split('\t') for l in f)
        result = set()
        for i in data:
            result.add(i[0])
            
    return result
            
    
nlp017("./input/hightemp.txt")

(解)unix

!cut -f 1 ./input/hightemp.txt | sort| uniq 

(注釈)

  • pythonのコードでは、まずファイルを行列区切りの2次元リストに格納し、次にリストの1列目のみをset型に格納しています。set型は06. 集合でも使用しました。
  • unixコマンドでは、3つのコマンドを使用し、それぞれの出力をパイプラインで他のコマンドにわたすように実装しています。cutコマンドではファイルの一列目のみを取得し、sortコマンドで並び替えた後、uniqコマンドで重複を削除します。uniqコマンドを使用するには予め並び替えが必要になります。

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

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

(解)python

def nlp018(filename: str):
    import sys
    with open("./input/hightemp.txt") as f:
        data = f.readlines()
        
        for line in sorted(l , key=lambda l: l.split('\t')[2], reverse=True):
            print(line.strip())
    
nlp018("./input/hightemp.txt")

(解)unix

!sort -k 3 -r ./input/hightemp.txt 

(注釈)

  • sorted(): 引数にリストを指定し、ソートされたリストを返却します。引数keyではソートする対象を指定しますが、ここではラムダ式(無名関数)を使用しています。リストを渡して3列目を返却する無名関数をキーに指定しています。
  • sortコマンドは-kオプションで何列目をキーにするかを指定できます。-rオプションは降順で返却することを指定します。

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

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

(解)python

def nlp019(filename: str):
    result = {}
    import sys
    with open("./input/hightemp.txt") as f:
        data = list(l.split('\t') for l in f)
        
        for line in data:
            result[line[0]] = result.get(line[0], 0) + 1
    
        for key, value in sorted(result.items(), key=lambda kv: kv[1], reverse=True):
            print("{} {}".format(value, key))
    
nlp019("./input/hightemp.txt")

(解)unix

!cut -f 1 ./input/hightemp.txt | sort | uniq -c | sort -k 1 -r

(注釈)

  • pythonのコードは18. と似ていますが、途中の結果をリストではなく辞書に格納しています。辞書resultにはkeyに一列目の値、valueに出現回数を格納しています
  • uniqコマンドは、-cオプションをつけることで重複を除いた文字列だけでなく、各文字列の出現頻度を出力することが可能です。

Todo

  • 続き(No.20~以降)

おわりに

UNIXだけではなく、pythonの記法(内包表記、lamdaなど)のよい復習になりました。

次回に続きます。