言語処理100本ノック第2章 解答と解説 Q14~Q16まで

こんにちは。福田直起です。

前回に引き続き、言語処理100本ノック2020を解いているアウトプットとして、「第2章 : UNIXコマンド」の10問のうち、Q14~Q16の3問を解説していきます。
他の問題の回答は、まとめページにリンクを貼ってあります。

解答・解説の前に

この記事の

  • 主な対象者と目的
  • 実行環境
  • 解答・解説を読む際の注意

は、以前の記事で説明していますので、解答・解説を読む前にこれらを知っておきたい、という方はそちらを参照していただくようお願いします。

また、長い解説は折りたたんであります。折りたたんである解説は「解説を読む」をクリックすると見ることができます。

実行環境

  • Ubuntu 20.04.4 LTS
  • Jupyter Notebook 4.10.0
    • IPython 8.3.0
  • Python 3.8.13
  • pandas 1.4.3

解答・解説

引き続き、アメリカで生まれた赤ちゃんのデータをタブ区切り形式で格納したファイルであるpopular-names.txtに対して、問題文で指示されたテキスト処理を行っていきます。
popular-names.txtのダウンロードの方法は以前の記事で解説しています。

Q14. 先頭からN行を出力

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

Python

解答コード

%%writefile data/q14.py
# このセルを実行すると、dataフォルダ内に.pyファイルが書き出される
# 次のセルでその.pyファイルにコマンドライン引数を与えて実行する

import typer

# typerでコマンドライン入力を引数に割り当てている
# typerは大文字をコマンドライン入力の割当先に使うとエラーになるので、小文字のnを使う
def output_head(n :int = typer.Argument(..., min=1)):
    with open("popular-names.txt") as file:
        lines = file.readlines()
        
    # nの値がファイルの行数より大きい場合、エラー
    if n > len(lines):
        typer.echo("[ERROR]入力された値が大きすぎます", err=True)
        raise typer.Abort()
        
    print(f'Pythonで先頭{n}行を出力:')
    for line in lines[:n]:
        print(line.replace("\n", ""))

if __name__ == '__main__':
    typer.run(output_head)

実行結果

Writing data/q14.py

解説

解説を読む 問題の条件を満たすために、Jupyter Notebookのマジックコマンドという機能と、Pythontyperというライブラリを使用しています。

Jupyter Notebookのマジックコマンドについて

Jupyter Notebookでは、UNIXコマンドなどのOSのコマンド以外にも、マジックコマンドという便利なコマンドを使える機能があります。
この解答に使った2つのコマンドを紹介します。

%%writefile 書き出し先のパス
このコマンドは、このコマンドを書いたセルの内容をそのまま.pyファイルとして書き出します。
今回は、以下のtyperライブラリを使ったコマンドラインアプリケーションとして実行するために、一旦書き出しています。

使用例:

%%writefile hello.py

print("Hello, World!")

%run 実行したいPythonコードのパス
このコマンドはPythonコードを実行します。
解答のように、コマンドライン引数やオプションを指定することができます。

使用例:

%run hello.py
Pythontyperライブラリについて

オプション解析処理を行うなど、コマンドラインアプリケーションを作りやすくしてくれるライブラリです。

Python3.6以降が必須です。
また、標準ライブラリではないので、
pip install typer
でインストールする必要があります。

以下に、今回使ったものを中心にいくつかtyperの便利な点を紹介します。

def output_head(n :int = typer.Argument(..., min=1)):
...の部分には初期値を指定することもできますが、今回は...を指定することで「初期値なし、入力必須」としています。
また、min=1で、最小値に1を指定しています。
これらの指定に違反した場合(型にintを指定しているので、整数以外が入力された場合も含む)はtyperがエラーメッセージを出してくれるので、便利です。

typer.echo(エラーメッセージ, err=True)
このように記述することで、エラーメッセージを出力することができます。
今回は、ファイルの行数をコマンドラインで指示された行数が上回った場合にエラーメッセージを出力するために使っています。
print()と異なる点としては、err=Trueオプションを付けることで標準エラー出力に出力できるので、エラーログを別ファイルに出力したい場合にも対応できます。

使用例:

typer.echo("[ERROR]入力された値が大きすぎます", err=True)

raise typer.Abort()
プログラムを終了し、標準出力に”Aborted!"というメッセージを表示します。 異常終了であることが明示できます。

if __name__ == '__main__':
    typer.run(最初にしたい処理が書かれている関数名)

最後にこれを記述して、最初にしたい処理が書かれている関数を呼び出します。

参考: オプション解析モジュール typer を使いこなそう - Qiita

標準入力を受け取る別解

Pythonでは
N = input()
のように書くことで、実行時にプログラムを入力待ちにして、ユーザーからの入力結果を変数Nに格納することができます。

これを使った別解を、以下に記載しておきます。

import sys
import re

def output_head(N :int):
    with open("popular-names.txt") as file:
        lines = file.readlines()
        
    # nの値がファイルの行数より大きい場合、エラー
    if N > len(lines):
        raise ValueError
    
    for line in lines[:N]:
        print(line.replace("\n", ""))


# 標準入力を受け取る
try:
    N = input()
    
    # 標準入力が自然数でない場合はエラー
    if not re.match(r"^[1-9][0-9]*$", N): 
        raise ValueError
        
except ValueError:
    print("[ERROR]自然数を入力してください")
    sys.exit(1)

print("Pythonで先頭" + N + "行を出力:")
try:
    output_head(int(N))

except ValueError:
    print("[ERROR]入力された値が大きすぎます")
    sys.exit(1)

なお、入力が自然数かどうかの判定に今回は正規表現を用いています。 正規表現については第3章で詳しく解説する予定ですが、今知りたいという方には以下のPythonの公式ドキュメントが参考になります。

re --- 正規表現操作 — Python 3.8.10 ドキュメント

UNIXコマンド

解答コード

# Python関数の処理結果をUNIXコマンドと比較して確認

# 入力する自然数Nを定義
N = 5

%run data/q14.py $N

!echo
!echo "以下のUNIXコマンドで同様の処理をした結果:"
!echo "head -n" $N "popular-names.txt"
!head -n $N popular-names.txt

実行結果

Pythonで先頭5行を出力:
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880

以下のUNIXコマンドで同様の処理をした結果:
head -n 5 popular-names.txt
Mary F 7065 1880
Anna F 2604 1880
Emma F 2003 1880
Elizabeth F 1939 1880
Minnie F 1746 1880

解説

解説を読む

UNIXheadコマンドについて

head -n 行数 対象ファイル
を実行することで、ファイルの先頭から任意の行数を表示するコマンドです。
また、オプションなしにhead 対象ファイルとすることで、先頭10行を表示します。

使用例:

head -n 5 popular-names.txt
head popular-names.txt

次の問題で使用するtailと並んで、最も使うUNIXコマンドの一つです。

参考: 【 head 】コマンド/【 tail 】コマンド――長いメッセージやテキストファイルの先頭だけ/末尾だけを表示する:Linux基本コマンドTips(3) - @IT

Q15. 末尾のN行を出力

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

Python

解答コード

%%writefile data/q15.py
# このセルを実行すると、dataフォルダ内に.pyファイルが書き出される
# 次のセルでその.pyファイルにコマンドライン引数を与えて実行する

import typer

# typerでコマンドライン入力を引数に割り当てている
# typerは大文字をコマンドライン入力の割当先に使うとエラーになるので、小文字のnを使う
def output_tail(n :int = typer.Argument(..., min=1)):
    with open("popular-names.txt") as file:
        lines = file.readlines()
        
    # nの値がファイルの行数より大きい場合、エラー
    if n > len(lines):
        typer.echo("[ERROR]入力された値が大きすぎます", err=True)
        raise typer.Abort()
        
    print(f'Pythonで先頭{n}行を出力:')
    for line in lines[-n:]:
        print(line.replace("\n", ""))

if __name__ == '__main__':
    typer.run(output_tail)

実行結果

Writing data/q15.py

解説

Pythonコードでやっていることは、ほぼ上記のQ14と同じです。

UNIXコマンド

解答コード

# Python関数の処理結果をUNIXコマンドと比較して確認

# 入力する自然数Nを定義
N = 5

%run data/q15.py $N

!echo
!echo "以下のUNIXコマンドで同様の処理をした結果:"
!echo "tail -n" $N "popular-names.txt"
!tail -n $N popular-names.txt

実行結果

Pythonで先頭5行を出力:
Benjamin M 13381 2018
Elijah M 12886 2018
Lucas M 12585 2018
Mason M 12435 2018
Logan M 12352 2018

以下のUNIXコマンドで同様の処理をした結果:
tail -n 5 popular-names.txt
Benjamin M 13381 2018
Elijah M 12886 2018
Lucas M 12585 2018
Mason M 12435 2018
Logan M 12352 2018

解説

解説を読む

UNIXtailコマンドについて

tail -n 行数 対象ファイル
を実行することで、ファイルの末尾から任意の行数を表示するコマンドです。
また、オプションなしにtail 対象ファイルとすることで、末尾10行を表示します。

使用例:

tail -n 5 popular-names.txt
tail popular-names.txt

参考: 【 head 】コマンド/【 tail 】コマンド――長いメッセージやテキストファイルの先頭だけ/末尾だけを表示する:Linux基本コマンドTips(3) - @IT

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

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

Python

解答コード

%%writefile data/q16.py
# このセルを実行すると、dataフォルダ内に.pyファイルが書き出される
# 次のセルでその.pyファイルにコマンドライン引数を与えて実行する

import typer
import numpy

# typerでコマンドライン入力を引数に割り当てている
# typerは大文字をコマンドライン入力の割当先に使うとエラーになるので、小文字のnを使う
def output_split(n :int = typer.Argument(..., min=2)) -> int:
    with open("popular-names.txt") as file:
        lines = file.readlines()
        
    # nの値がファイルの行数より大きい場合、エラー
    if n > len(lines):
        typer.echo("[ERROR]入力された値が大きすぎます", err=True)
        raise typer.Abort()
        
    for i, splited_lines in enumerate(numpy.array_split(lines, n), 1):
            
        with open(f'data/q16-{i}.txt', mode="w") as output:
            for line in splited_lines:
                output.write(line)
                
    typer.echo("ファイルの書き出しが完了しました")

if __name__ == '__main__':
    typer.run(output_split)

実行結果

Writing data/q16.py

解説

解説を読む

Pythonnumpyライブラリについて

ざっくり言うと、Pythonにおいて、標準ライブラリだと手間がかかる計算処理を簡単にしてくれるライブラリです。

どんなライブラリなのかを公式ドキュメントより引用しておくと、

NumPy is the fundamental package for scientific computing in Python.
(訳:NumPyはPythonにおける計算科学のための基本的なパッケージです)

とのことです。

今回はnumpy.array_split(target_list, n)というtarget_listn分割した配列を生成してくれるメソッドを使っています。

使用例:

import numpy

sample_list = [1, 2, 3, 4, 5, 6, 7, 8, 9]

print(numpy.array_split(sample_list, 3))

# 実行結果:[array([1, 2, 3]), array([4, 5, 6]), array([7, 8, 9])]


# 配列数が分割数の倍数でない場合の例
sample_list2 = [1, 2, 3, 4, 5, 6, 7]

print(numpy.array_split(sample_list2, 3))

# 実行結果:[array([1, 2, 3]), array([4, 5]), array([6, 7])]

参考: numpy.array_split — NumPy v1.23 Manual

UNIXコマンド

解答コード

# Python関数の処理結果をUNIXコマンドと比較して確認

# 入力する自然数Nを定義
N = 5

%run data/q16.py $N
!echo

!echo "上のセルのPythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:"

!echo "Pythonの最初の出力ファイルの行数と同じ行数ごとに分割する"
cat_first_split = !cat data/q16-1.txt | wc -l
first_split_len = cat_first_split[0]
!echo "Pythonの最初の出力ファイルの行数:" $first_split_len
!echo

UNIX_FILE_PREFIX="data/q16-unix-"

!echo "split --lines=$first_split_len --numeric-suffixes=1  --additional-suffix=.txt ./popular-names.txt" $UNIX_FILE_PREFIX
!split --lines=$first_split_len --numeric-suffixes=1  --additional-suffix=.txt ./popular-names.txt $UNIX_FILE_PREFIX

unix_output_file_names = !ls data/q16-unix-*
for i, file_name_unix in enumerate(sorted(unix_output_file_names), 1):
    file_name_python = "data/q16-" + str(i) + ".txt"
    
    !echo
    !echo "diff " $file_name_python $file_name_unix
    !diff $file_name_python $file_name_unix

実行結果

上のセルのPythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:
split --lines=556 --numeric-suffixes=1  --additional-suffix=.txt ./popular-names.txt data/q16-unix-

ファイルの書き出しが完了しました

上のセルのPythonの出力ファイルとUNIXコマンドで同様の処理をした結果を比較:
Pythonの最初の出力ファイルの行数と同じ行数ごとに分割する
Pythonの最初の出力ファイルの行数: 556

split --lines=556 --numeric-suffixes=1  --additional-suffix=.txt ./popular-names.txt data/q16-unix-

diff  data/q16-1.txt data/q16-unix-01.txt

diff  data/q16-2.txt data/q16-unix-02.txt

diff  data/q16-3.txt data/q16-unix-03.txt

diff  data/q16-4.txt data/q16-unix-04.txt

diff  data/q16-5.txt data/q16-unix-05.txt

解説

解説を読む UNIXコマンドは、Jupyter Notebookの仕様を生かして、Pythonのfor文の中でUNIXコマンドを実行しています。
(UNIXコマンドにもfor文を書く方法はあるのですが、Jupyter Notebookの仕様だとPythonのfor文を使うほうがわかりやすいためです)

UNIXsplitコマンドについて

オプションに従ってファイルを分割するコマンドです。
split オプション 分割するファイルまでのパス 分割ファイル名の接頭辞
のように使います。
今回使っているオプションを解説していきます。

--lines

分割する行数を指定します。

--numeric-suffixes

分割後のファイル名を指定するオプションの一つです。
分割後のファイルにつく連番がいくつから始まるかを指定します。
今回のファイル名の一つであるq16-unix-01.txtのうち、01の部分が01始まりになるようにしているということですね。

--additional-suffix

分割後のファイル名を指定するオプションの一つです。
分割後のファイルにつく接尾辞を指定します。
拡張子をつけたい場合に使うことが多いと思います。
今回のファイル名の一つであるq16-unix-01.txtのうち、.txtの部分を指定しているということですね。

使用例: 分割対象ファイルq16-sample.txtの内容

1行目
2行目
3行目
4行目
5行目
6行目

実行するコマンドと結果

split --lines=3 --numeric-suffixes=1  --additional-suffix=.txt ./q16-sample.txt splited-q16-sample-
# 1行目~3行目までの"splited-q16-sample-01.txt"と、4行目~6行目までの"splited-q16-sample-02.txt"に分割される。

# ファイルの行数が分割する行数の倍数でない場合の例
split --lines=4 --numeric-suffixes=1  --additional-suffix=.txt ./q16-sample.txt splited-q16-sample-
# 1行目~4行目までの"splited-q16-sample-01.txt"と、5行目~6行目までの"splited-q16-sample-02.txt"に分割される。

参考: 【 split 】コマンド――ファイルを分割する:Linux基本コマンドTips(162) - @IT

UNIXコマンドの結果をPythonの変数に格納する

変数名 = !UNIXコマンド と宣言ことで、UNIXコマンドの結果をPythonの変数に格納しています。
ここではsplitコマンドで書き出したファイル名のリストを取得するためにlsコマンドを使っています。

使用例:

pwd_result = !pwd
!echo "カレントディレクトリ:" $pwd_result

Q11で説明したように、変数名の前に$をつけることでUNIXコマンドで使用するために呼び出すことができます。

Pythonsorted関数について

sorted(ソートしたいリスト)
のように書くことで、項目をソートしたリストを返す関数です。

使用例:

sample_list = [1, 2, 3, 4, 5, 1, 3, 1]

print(sorted(sample_list))

ここでは単純に連番のファイル名のリストを番号順に並べたいだけなので、特にオプションを設定していません。

参考: ソート HOW TO — Python 3.8.10 ドキュメント

おわりに

以上、言語処理100本ノックの第2章のQ14~Q16について解説しました。
typerは非常に便利なライブラリなので、覚えておくといつか役に立つと思います。

他の問題の回答は、まとめページにリンクを貼ってあります。

また、ブレインズコンサルティングでは一緒に働いてくれる仲間を募集しています。 ご興味のある方は、ぜひ一度採用サイトをご覧ください。