こんにちは。福田直起です。
前回に引き続き、言語処理100本ノック2020を解いているアウトプットとして、「第2章 : UNIXコマンド」の10問のうち、Q14~Q16の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のマジックコマンドについて
Jupyter Notebookでは、UNIXコマンドなどのOSのコマンド以外にも、マジックコマンドという便利なコマンドを使える機能があります。
この解答に使った2つのコマンドを紹介します。
%%writefile 書き出し先のパス
このコマンドは、このコマンドを書いたセルの内容をそのまま.pyファイルとして書き出します。
今回は、以下のtyperライブラリを使ったコマンドラインアプリケーションとして実行するために、一旦書き出しています。
使用例:
%%writefile hello.py
print("Hello, World!")
%run 実行したいPythonコードのパス
このコマンドはPythonコードを実行します。
解答のように、コマンドライン引数やオプションを指定することができます。
使用例:
%run hello.py
Pythonのtyperライブラリについて
オプション解析処理を行うなど、コマンドラインアプリケーションを作りやすくしてくれるライブラリです。
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
解説
解説を読む
UNIXのheadコマンドについて
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
解説
解説を読む
UNIXのtailコマンドについて
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
解説
解説を読む
Pythonのnumpyライブラリについて
ざっくり言うと、Pythonにおいて、標準ライブラリだと手間がかかる計算処理を簡単にしてくれるライブラリです。
どんなライブラリなのかを公式ドキュメントより引用しておくと、
NumPy is the fundamental package for scientific computing in Python.
(訳:NumPyはPythonにおける計算科学のための基本的なパッケージです)
とのことです。
今回はnumpy.array_split(target_list, n)というtarget_listをn分割した配列を生成してくれるメソッドを使っています。
使用例:
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のsplitコマンドについて
オプションに従ってファイルを分割するコマンドです。
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コマンドで使用するために呼び出すことができます。
Pythonのsorted関数について
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は非常に便利なライブラリなので、覚えておくといつか役に立つと思います。
他の問題の回答は、まとめページにリンクを貼ってあります。
また、ブレインズコンサルティングでは一緒に働いてくれる仲間を募集しています。 ご興味のある方は、ぜひ一度採用サイトをご覧ください。