CHAPTER 5 ・ ファイルとテキストの実践 / Lesson 21

大きなファイルを少しずつ処理する

100万行でも、メモリを使い切らない。ジェネレータで、1行ずつ流しながら処理する考え方です。

目安 20分 ・ インストール不要

このレッスンが終わったら

「ジェネレータは、値を1つずつ作って渡す仕組み。全部をためこまないので、大きなデータでも動く」と言える。

STEP 1

こんな場面、ありませんか?

システムが出力するログ(動作の記録)や、基幹システムの明細CSVは、1年分で数十万〜数百万行になることがあります。ここでは小さな app.log(40行)を使います。ERROR の行だけを数える、基礎編の書き方はこうでした。

app.logサンプルファイル
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件目)
基礎編の書き方:全部の行をリストに読み込んでから、数えるサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

40行なら、何の問題もありません。しかし readlines() は、ファイルの全部を、いったんメモリ(作業中のデータを置く机)の上に広げる書き方です。これが100万行になると、机に載りきらず、パソコンが固まったり、エラーで止まったりすることがあります。しかも、欲しいのは ERROR の件数だけで、ほかの行は数えたら捨ててよいのです。

「全部をためこまず、1つずつ流しながら処理する」。その仕組みが、この回の主役、ジェネレータです。

STEP 2

1つずつ流してみる

じつは、ファイルは for に渡すと、もともと1行ずつ読めます。readlines() を使わず、そのまま回してみましょう。

for で1行ずつ読んで、ERROR だけ数えるサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

結果は同じですが、この書き方では、一度に机の上にあるのは1行だけです。次に、この「ERROR の行だけを順に渡す」部分を、yield を使った関数にしてみます。return が値を1回返して終わるのに対し、yield は、値を1つ渡してそこで一時停止し、次に呼ばれたら続きから再開します。

yield で、ERROR の行を1つずつ渡す関数サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

error_lines(...) を呼んでも、まだ1行も読まれていません。種類は generator です。next(g) と書くたびに、次の ERROR 行まで進んで、1行ぶんだけ渡されます。

STEP 3

仕組みを読む

リストに「ためる」やり方と、ジェネレータで「流す」やり方の違いを、図にします。

  1. 全部読む40行を、すべてリストに
  2. リストを絞るERROR の行だけ
  3. 数える机の上に全部ある
ためる方式(readlines):行数が増えるほど、机がいっぱいになる
  1. 1行読む机の上は1行だけ
  2. ERROR か判定違えば捨てる
  3. 渡して、次へまた1行読む
流す方式(ジェネレータ):何行あっても、机の上は1行ぶん

先ほどの関数を、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 result

1つずつ渡す関数(ジェネレータ)

def error_lines(path):
  with open(path, encoding="utf-8") as f:
      for line in f:
          if " ERROR " in line:
              yield line.rstrip()
result を用意して append して return する、という3つの手間がなくなります。
STEP 4

変えて、試す

① 動きを目で見る。 一時停止と再開は、print を入れると分かります。for の側と、関数の側の表示が、交互に出ます。

yield のたびに、一時停止する様子コードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

② ジェネレータ式。 内包表記(Lesson 01)の [ ] を ( ) に替えると、リストを作らず、1つずつ流す「ジェネレータ式」になります。合計を出す sum に渡すときは、さらに ( ) を省けます。

売上明細の金額合計を、ためこまずに出すサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

③ 1回しか使えない。 ジェネレータは、一度流し終わると、空っぽになります。もう一度使いたいなら、作り直すか、list() でリストにしておきます。

2回目は、何も出てこないコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

④ 先頭だけ取る、つなげる。 itertools(標準ライブラリ)の islice は「先頭から何個だけ」、chain は「複数の流れを1本につなぐ」道具です。islice で先頭の3件だけ取ると、4件目以降のファイルは読みません。

islice で先頭3件、chain で2つのログをつなぐサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

⑤ pandas でも、少しずつ。 巨大なCSVを pandas で読むときは、chunksize を付けると、指定した行数ずつの表(DataFrame)を、順に渡してくれます。ここでは128行を50行ずつに分けます。

read_csv(chunksize=50) で、50行ずつ処理するpandas・サンプルファイル付き
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

3回に分かれ(50行・50行・28行)、合計は②の結果と同じになります。

STEP 5

自分で書く

お題① app.log から、ERROR の行の件数を、ファイルを1行ずつ読む形(readlines を使わない)で表示してください。

お題①:ERROR 行の件数サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

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件見つかった時点で読むのをやめます。

お題②:ERROR 行の先頭5件サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

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 を完成させ、その合計を表示してください。

お題③:金額を1つずつ渡す関数サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

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")))
STEP 6

仕事ならどう使う?

理解チェックsquares = (x * x for x in range(3)) を作り、list(squares) を2回続けて実行すると、2回目の結果は?

理解チェックとても大きなログファイルから、ERROR の行の件数だけを知りたいとき、いちばん向いているのは?