複数のファイルを読み書きする際にfileiputをしばしば使う。シンプルなコーディングにしやすいからだ。今回は複数のファイルを読み込む操作を行おう。
開発環境:
Python 3.9.2
参考情報:
指定したディレクトリ配下の「.log」を含む名前の複数ファイルからキーワードを含む行を抽出するシナリオとしよう。ちょっとした調査やトラブルシュートで発生するユースケースだ。まずは「指定したディレクトリ」が存在するのかpathlibで確認する。
import logging
import logging.config
import yaml
import fileinput as fi
from pathlib import Path
import os
import sys
(中略)
# ディレクトリを確認する関数
def check_dir(dirname):
workdir = Path(dirname)
if not workdir.is_dir(): # is_dirメソッドで判定する
try:
raise FileNotFoundError(f'{dirname} is not Found')
except FileNotFoundError as e:
logger.error(e)
sys.exit(1)
else:
logger.info(f'{dirname} check succeed.')
return workdir
上記の関数を呼び出した後にglobする。すると検索対象のファイル名は出来上がったイテレータから取り出せる。
# 検索対象ファイルを格納したディレクトリの確認
workdir = check_dir('./logs/')
# 検索対象ファイルの指定
targets = workdir.glob('*.log*')
>>> targets = workdir.glob('*.log*')
>>> list(targets) # イテレータをリストにすると・・・
[PosixPath('logs/logger.log'), PosixPath('logs/logger.log.1'), PosixPath('logs/logger.log.2'), PosixPath('logs/logger.log.3'), PosixPath('logs/logger.log.4')]
>>> list(targets) # イテレータを再度参照すると・・・
[]
>>>
検索対象ファイルが複数でも1個のファイルを扱うかのようにwith句で記述可能だ。検索対象ファイル名と、検索するキーワードを下記の関数に渡すと、結果のリストが戻り値として得られる。この戻り値はテキストファイルへ出力しても良いだろう。
# 対象ファイルからキーワードを検索する関数
def search_words(targets, file_hook, words):
results = []
with fi.input(targets, openhook=file_hook) as f:
for line in f:
matched = [line for word in words if word in line]
if matched:
results.extend([f'{fi.filename()}:\
{fi.lineno()}:{line}'])
if not results:
try:
raise ValueError('Results is not Found.')
except ValueError as e:
logger.error(e)
sys.exit(1)
else:
logger.info(f'Search Results: {len(results)}')
return results
しかしこの実装には課題が潜んでいる。もしも、個々のログファイルが肥大化しており、キーワードにマッチする行数も膨大だったらどうなるだろう。生成されるリストの情報はどこに展開されるのだろう。
>>> keywords = ['comp', 'YAML']
>>> results = search_words(*initialize(), keywords)
2021-05-22 13:17:25,466 -INFO- <stdin> : ./logs/ check succeed.
2021-05-22 13:17:25,510 -INFO- <stdin> : Search Results: 45
>>> len(results)
45 # 45行マッチした
>>> results.__sizeof__()
456 # このサンプルでは45行で456バイト、しかしファイアウォールのようなデバイスや認証サーバのアクセスログだとすると・・・
>>>
この課題はジェネレータ関数にすることで手軽に解決可能だ。
# 対象ファイルからキーワードを検索するジェネレータ関数
def search_words(targets, file_hook, words):
with fi.input(targets, openhook=file_hook) as f:
for line in f:
matched = [line for word in words if word in line]
if matched:
result = f'{fi.filename()}:{fi.lineno()}:{line}'
yield result
リストと同じくイテレータはforループで要素を取り出せる。
>>> results = search_words(*initialize(), keywords) # 関数をジェネレータ関数に書き換えて実行してみよう
2021-05-22 14:45:56,745 -INFO- <stdin> : ./logs/ check succeed.
>>> results.__sizeof__()
96 # このサンプルでは劇的な変化は感じられないが、もちろんサイズは小さくなる
>>>
>>> for line in results:
... print(line, end='') # マッチしたログは既に改行を含んでいるため改行を挿入しない
...
logs/logger.log:1:2021-05-17 00:45:27,361 -INFO- MainThread [LineNo.: 16] foo : completed!
logs/logger.log:2:2021-05-17 00:46:20,928 -INFO- MainThread [LineNo.: 27] foo : logdir=./logs YAML=./logging.yaml
logs/logger.log:5:2021-05-17 00:46:20,952 -INFO- MainThread [LineNo.: 16] foo : completed!
関連ファイル:
ロガーで使用しているYAMLはこの記事のYAMLに追記して完成させよう。
まとめ:
複数ファイルの読み書きにfileinputを活用しよう
リストの肥大化が予測される際は、イテレータを使ってみよう
※ジェネレータ関数で生成されたイテレータをジェネレータイテレータやジェネレータと呼ぶことがあるがこのエントリではイテレータに統一した。