CHAPTER 5 ・ ファイルとテキストの実践 / Lesson 21
大きなファイルを少しずつ処理する
100万行でも、メモリを使い切らない。ジェネレータで、1行ずつ流しながら処理する考え方です。
このレッスンが終わったら
「ジェネレータは、値を1つずつ作って渡す仕組み。全部をためこまないので、大きなデータでも動く」と言える。
こんな場面、ありませんか?
システムが出力するログ(動作の記録)や、基幹システムの明細CSVは、1年分で数十万〜数百万行になることがあります。ここでは小さな app.log(40行)を使います。ERROR の行だけを数える、基礎編の書き方はこうでした。
2025-10-01 09:01:18 WARNING 金額が空欄の行があります(1件目) 2025-10-01 09:01:53 INFO 集計を終了しました(2件目) 2025-10-01 09:02:39 ERROR 日付を読み取れませんでした(3件目) 2025-10-01 09:03:19 INFO 集計を開始しました(4件目) 2025-10-01 09:03:48 INFO 集計を開始しました(5件目) 2025-10-01 09:04:08 WARNING 金額が空欄の行があります(6件目) 2025-10-01 09:05:31 ERROR 日付を読み取れませんでした(7件目) 2025-10-01 09:05:49 INFO 集計を終了しました(8件目) 2025-10-01 09:06:15 INFO 集計を終了しました(9件目) 2025-10-01 09:07:43 ERROR 日付を読み取れませんでした(10件目) 2025-10-01 09:08:06 INFO ファイルを読み込みました(11件目) 2025-10-01 09:08:16 ERROR 日付を読み取れませんでした(12件目) 2025-10-01 09:09:19 INFO ファイルを読み込みました(13件目) 2025-10-01 09:09:56 INFO 集計を開始しました(14件目) 2025-10-01 09:10:46 ERROR 日付を読み取れませんでした(15件目) 2025-10-01 09:12:11 INFO ファイルを読み込みました(16件目) 2025-10-01 09:12:48 ERROR 日付を読み取れませんでした(17件目) 2025-10-01 09:13:59 WARNING 金額が空欄の行があります(18件目) 2025-10-01 09:14:52 INFO ファイルを読み込みました(19件目) 2025-10-01 09:16:00 INFO 集計を開始しました(20件目) 2025-10-01 09:16:57 WARNING 金額が空欄の行があります(21件目) 2025-10-01 09:17:16 WARNING 金額が空欄の行があります(22件目) 2025-10-01 09:17:54 INFO 集計を終了しました(23件目) 2025-10-01 09:18:22 INFO 集計を開始しました(24件目) 2025-10-01 09:19:29 INFO 集計を開始しました(25件目) 2025-10-01 09:20:04 ERROR 日付を読み取れませんでした(26件目) 2025-10-01 09:20:17 INFO 集計を開始しました(27件目) 2025-10-01 09:21:45 INFO 集計を終了しました(28件目) 2025-10-01 09:22:09 INFO ファイルを読み込みました(29件目) 2025-10-01 09:22:55 INFO 集計を開始しました(30件目) 2025-10-01 09:23:01 INFO ファイルを読み込みました(31件目) 2025-10-01 09:23:47 ERROR 日付を読み取れませんでした(32件目) 2025-10-01 09:24:56 WARNING 金額が空欄の行があります(33件目) 2025-10-01 09:26:08 INFO ファイルを読み込みました(34件目) 2025-10-01 09:26:43 INFO 集計を開始しました(35件目) 2025-10-01 09:28:02 ERROR 日付を読み取れませんでした(36件目) 2025-10-01 09:28:51 ERROR 日付を読み取れませんでした(37件目) 2025-10-01 09:29:46 INFO 集計を開始しました(38件目) 2025-10-01 09:30:59 INFO 集計を開始しました(39件目) 2025-10-01 09:32:28 INFO ファイルを読み込みました(40件目)
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
40行なら、何の問題もありません。しかし readlines() は、ファイルの全部を、いったんメモリ(作業中のデータを置く机)の上に広げる書き方です。これが100万行になると、机に載りきらず、パソコンが固まったり、エラーで止まったりすることがあります。しかも、欲しいのは ERROR の件数だけで、ほかの行は数えたら捨ててよいのです。
「全部をためこまず、1つずつ流しながら処理する」。その仕組みが、この回の主役、ジェネレータです。
1つずつ流してみる
じつは、ファイルは for に渡すと、もともと1行ずつ読めます。readlines() を使わず、そのまま回してみましょう。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
結果は同じですが、この書き方では、一度に机の上にあるのは1行だけです。次に、この「ERROR の行だけを順に渡す」部分を、yield を使った関数にしてみます。return が値を1回返して終わるのに対し、yield は、値を1つ渡してそこで一時停止し、次に呼ばれたら続きから再開します。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
error_lines(...) を呼んでも、まだ1行も読まれていません。種類は generator です。next(g) と書くたびに、次の ERROR 行まで進んで、1行ぶんだけ渡されます。
仕組みを読む
リストに「ためる」やり方と、ジェネレータで「流す」やり方の違いを、図にします。
- 全部読む40行を、すべてリストに
- リストを絞るERROR の行だけ
- 数える机の上に全部ある
- 1行読む机の上は1行だけ
- ERROR か判定違えば捨てる
- 渡して、次へまた1行読む
先ほどの関数を、1行ずつ日本語にします。
def error_lines(path):「error_lines」という関数を作る(ファイルの場所を受け取る) with open(path, encoding="utf-8") as f:ファイルを開く for line in f:1行ずつ取り出して、「line」と呼ぶ if " ERROR " in line:ERROR の行なら yield line.rstrip()(末尾の改行を取って)その行を1つ渡し、次に呼ばれるまで一時停止日本語にすると、「ファイルを1行ずつ見て、ERROR の行を見つけるたびに1つ渡し、次の注文があるまで待つ」という関数です。return の代わりに yield を書くだけで、普通の関数が、ジェネレータになります。
リストを作って返す関数
def error_lines(path):
result = []
with open(path, encoding="utf-8") as f:
for line in f:
if " ERROR " in line:
result.append(line.rstrip())
return result1つずつ渡す関数(ジェネレータ)
def error_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
if " ERROR " in line:
yield line.rstrip()変えて、試す
① 動きを目で見る。 一時停止と再開は、print を入れると分かります。for の側と、関数の側の表示が、交互に出ます。
▶ 実行を押すと、ここに結果が出ます
② ジェネレータ式。 内包表記(Lesson 01)の [ ] を ( ) に替えると、リストを作らず、1つずつ流す「ジェネレータ式」になります。合計を出す sum に渡すときは、さらに ( ) を省けます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
③ 1回しか使えない。 ジェネレータは、一度流し終わると、空っぽになります。もう一度使いたいなら、作り直すか、list() でリストにしておきます。
▶ 実行を押すと、ここに結果が出ます
④ 先頭だけ取る、つなげる。 itertools(標準ライブラリ)の islice は「先頭から何個だけ」、chain は「複数の流れを1本につなぐ」道具です。islice で先頭の3件だけ取ると、4件目以降のファイルは読みません。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
⑤ pandas でも、少しずつ。 巨大なCSVを pandas で読むときは、chunksize を付けると、指定した行数ずつの表(DataFrame)を、順に渡してくれます。ここでは128行を50行ずつに分けます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
3回に分かれ(50行・50行・28行)、合計は②の結果と同じになります。
自分で書く
お題① app.log から、ERROR の行の件数を、ファイルを1行ずつ読む形(readlines を使わない)で表示してください。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
sum(1 for line in f if " ERROR " in line) と書けます。1行ずつ数えるなら、count = 0 から始めて、for と if で count = count + 1 でも構いません。
答えの例を見る
with open("app.log", encoding="utf-8") as f:
print(sum(1 for line in f if " ERROR " in line))お題② ERROR の行のうち、先頭の5件だけを表示してください。islice を使うと、5件見つかった時点で読むのをやめます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
errors = (line.rstrip() for line in f if " ERROR " in line) を作り、for line in islice(errors, 5): print(line) と書きます。
答えの例を見る
from itertools import islice
with open("app.log", encoding="utf-8") as f:
errors = (line.rstrip() for line in f if " ERROR " in line)
for line in islice(errors, 5):
print(line)お題③ sales_detail.csv の1行ごとに「数量 × 単価」を yield する関数 line_amounts を完成させ、その合計を表示してください。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
CSVの値は文字なので、yield int(row["数量"]) * int(row["単価"]) のように、int() に直してから掛けます。
答えの例を見る
import csv
def line_amounts(path):
with open(path, encoding="utf-8") as f:
for row in csv.DictReader(f):
yield int(row["数量"]) * int(row["単価"])
print(sum(line_amounts("sales_detail.csv")))仕事ならどう使う?
理解チェックsquares = (x * x for x in range(3)) を作り、list(squares) を2回続けて実行すると、2回目の結果は?
理解チェックとても大きなログファイルから、ERROR の行の件数だけを知りたいとき、いちばん向いているのは?