Python: уроки, справочник и задачи

Python: чтение больших файлов без лишнего расхода памяти

Раздел: Работа с файлами в Python · Обновлено 25.10.2026
Открытый жесткий диск компьютера
Фото: Evan-Amos, CC BY-SA 3.0

Пока файл весит несколько мегабайт, его можно прочитать как угодно, и f.read() отлично работает. Когда лог вырастает до 5 ГБ, а выгрузка из базы до 20 миллионов строк, тот же код съедает всю память или работает часами. Ниже собраны проверенные приемы, которые позволяют обрабатывать файлы любого размера при постоянном расходе памяти: построчное чтение, чтение блоками, генераторы, mmap и потоковая обработка CSV.

Почему read и readlines не подходят для больших файлов

Метод read() загружает весь файл в одну строку, а readlines() создает список из всех строк. В обоих случаях памяти нужно больше, чем весит файл: строка Python с кириллицей хранит каждый символ в 2 байтах, а каждый элемент списка добавляет еще около 50 байт служебных данных. Файл на 1 ГБ из коротких строк легко превращается в 3-5 ГБ занятой памяти.

СпособПамятьКогда использовать
f.read()весь файлмаленькие файлы, конфиги
f.readlines()весь файл и списокпочти никогда, лучше list(f) осознанно
for line in fодна строка и буфертекстовые файлы любого размера
f.readline()одна строкакогда нужно читать по одной строке вручную, например заголовок
f.read(size) в циклеодин блокбинарные файлы, файлы без переводов строк
mmapстраницы по требованиюпоиск и произвольный доступ к огромному файлу

Базовые режимы открытия файлов и разница между текстовым и бинарным режимом описаны в статье о чтении и записи файлов. Здесь сосредоточимся на объеме.

Построчное чтение файла

Файловый объект в Python сам является итератором по строкам. Цикл for читает данные буфером (обычно 8 КБ и больше) и выдает по одной строке, поэтому в памяти одновременно находится только текущая строка.

errors = 0
with open("access.log", encoding="utf-8") as f:
    for line in f:
        if " 500 " in line:
            errors += 1
print(errors)

У каждой строки, кроме, возможно, последней, есть символ перевода строки на конце. Убирайте его через line.rstrip("\n"). Вызов strip() без аргумента уберет и значимые пробелы в начале строки, что для данных с фиксированной шириной полей недопустимо.

Номера строк и ранний выход

with open("data.txt", encoding="utf-8") as f:
    for line_no, line in enumerate(f, start=1):
        if line.startswith("#"):
            continue
        if "STOP" in line:
            print(f"маркер найден в строке {line_no}")
            break

Ранний break важен: если ответ найден в начале файла, остальные гигабайты не читаются вовсе.

Цикл с readline

Метод readline() полезен, когда нужно прочитать первые строки отдельно (заголовок, версию формата), а потом обработать остальное. При достижении конца файла он возвращает пустую строку.

with open("export.txt", encoding="utf-8") as f:
    header = f.readline().rstrip("\n").split(";")
    while line := f.readline():
        values = line.rstrip("\n").split(";")
        record = dict(zip(header, values))

Смешивать for line in f и readline() в одном цикле можно в Python 3, но читается такой код хуже. Предпочтительнее взять заголовок через next(f), а дальше идти обычным for.

Чтение файла по частям (кусками)

Если в файле нет переводов строк (бинарные данные, минифицированный JSON, дамп) или строки огромные, читайте блоками фиксированного размера через read(size).

CHUNK = 1024 * 1024   # 1 МБ

with open("video.mp4", "rb") as f:
    while chunk := f.read(CHUNK):
        process(chunk)

Классический пример применения: контрольная сумма файла любого размера.

import hashlib

def sha256_file(path, chunk_size=1 << 20):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        while chunk := f.read(chunk_size):
            h.update(chunk)
    return h.hexdigest()

print(sha256_file("ubuntu.iso")[:16])   # -> первые 16 символов хеша

В Python 3.11+ то же самое делает одна функция hashlib.file_digest(f, "sha256"). Размер блока от 64 КБ до 4 МБ дает почти одинаковую скорость. Слишком маленький блок (например, 100 байт) замедляет чтение в десятки раз из-за накладных расходов на каждый вызов.

Проблема границ блоков

Блок фиксированного размера может разрезать строку или многобайтовый символ UTF-8 пополам. В текстовом режиме read(n) читает n символов, а не байтов, поэтому символы не режутся, но строки могут. Если обрабатываете текст блоками, переносите хвост незавершенной строки в следующий блок:

def lines_from_chunks(path, chunk_size=1 << 20):
    tail = ""
    with open(path, encoding="utf-8") as f:
        while chunk := f.read(chunk_size):
            chunk = tail + chunk
            *lines, tail = chunk.split("\n")
            yield from lines
    if tail:
        yield tail

В бинарном режиме для текста используйте инкрементальный декодер codecs.getincrementaldecoder("utf-8")(): он сохраняет недочитанные байты символа до следующего вызова.

Генераторы: конвейер обработки

Генераторы позволяют разбить обработку на маленькие шаги, при этом данные проходят через все шаги по одной записи и не накапливаются в памяти. Подробнее о механизме yield рассказано в статье про генераторы.

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip("\n")

def only_errors(lines):
    return (ln for ln in lines if " ERROR " in ln)

def parse(lines):
    for ln in lines:
        date, time, level, *msg = ln.split(" ", 3)
        yield {"date": date, "level": level, "msg": " ".join(msg)}

pipeline = parse(only_errors(read_lines("app.log")))
for rec in pipeline:
    print(rec["date"], rec["msg"][:60])

Каждая функция маленькая, тестируется отдельно, а потребление памяти не зависит от размера лога.

Обработка пачками

Вставлять в базу данных по одной строке медленно, а все сразу нельзя из-за памяти. Компромисс: пачки по несколько тысяч записей. В Python 3.12 для этого появилась функция itertools.batched:

import sqlite3
from itertools import batched

con = sqlite3.connect("logs.db")
con.execute("CREATE TABLE IF NOT EXISTS errors (date TEXT, level TEXT, msg TEXT)")

for batch in batched(parse(only_errors(read_lines("app.log"))), 5000):
    con.executemany("INSERT INTO errors VALUES (:date, :level, :msg)", batch)
    con.commit()
con.close()

Как посчитать количество строк в файле

Простой и экономный по памяти вариант:

with open("big.csv", encoding="utf-8") as f:
    count = sum(1 for _ in f)

Он декодирует каждую строку и создает объект строки, поэтому на гигабайтах заметно медленнее, чем подсчет байтов перевода строки в бинарном режиме:

def count_lines(path, chunk_size=1 << 20):
    count = 0
    last = b""
    with open(path, "rb") as f:
        while chunk := f.read(chunk_size):
            count += chunk.count(b"\n")
            last = chunk
    if last and not last.endswith(b"\n"):
        count += 1          # последняя строка без перевода строки
    return count

Сравнение способов подсчета строк на многогигабайтном файле:

СпособСкоростьПамять
len(f.readlines())медленнобольше размера файла
sum(1 for _ in f), текстовый режиммедленно, тратится время на декодированиенесколько МБ
sum(1 for _ in f), бинарный режимбыстрее, декодирования нетнесколько МБ
chunk.count(b"\n") блоками по 1 МБсамый быстрый, подсчет идет в коде на Cоколо 1 МБ

Точные цифры зависят от диска и длины строк, поэтому замеряйте на своих данных через модуль timeit.

Большие CSV-файлы

Модуль csv из стандартной библиотеки работает потоково: csv.reader и csv.DictReader читают файл по одной записи. Главное правило: открывать файл с newline="", иначе поля с переводами строк внутри кавычек разберутся неправильно.

import csv
from collections import defaultdict

totals = defaultdict(float)
with open("sales.csv", encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f, delimiter=";")
    for row in reader:
        totals[row["region"]] += float(row["amount"].replace(",", "."))

for region, total in sorted(totals.items(), key=lambda kv: -kv[1])[:5]:
    print(f"{region}: {total:,.0f}")

Память здесь зависит только от количества регионов, а не от числа строк. Группировка через defaultdict разобрана в статье про модуль collections, а для записи результата обратно в CSV подойдет csv.writer, который тоже работает построчно.

Pandas и параметр chunksize

Если нужна именно pandas, не загружайте файл целиком. Параметр chunksize возвращает итератор по кускам таблицы, а usecols и dtype сокращают память в разы:

import pandas as pd

parts = []
for chunk in pd.read_csv("sales.csv", sep=";", chunksize=500_000,
                         usecols=["region", "amount"],
                         dtype={"region": "category"}, decimal=","):
    parts.append(chunk.groupby("region", observed=True)["amount"].sum())

result = pd.concat(parts).groupby(level=0).sum().nlargest(5)
print(result)

mmap: файл как массив байтов

Модуль mmap отображает файл в адресное пространство процесса. Python видит файл как большой объект bytes, но операционная система подгружает с диска только те страницы, к которым вы обращаетесь. Это удобно для поиска и произвольного доступа: найти подстроку в файле на 10 ГБ, прочитать байты по смещению, применить регулярное выражение.

import mmap
import re

with open("dump.sql", "rb") as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
    pos = mm.find(b"CREATE TABLE users")
    print(pos)                       # -> смещение в байтах или -1

    if pos != -1:
        end = mm.find(b";", pos)
        print(mm[pos:end + 1].decode("utf-8"))

    emails = set(re.findall(rb"[\w.+-]+@[\w-]+\.[\w.]+", mm))
    print(len(emails))

Особенности, о которых стоит помнить:

Для последовательного чтения от начала до конца mmap не быстрее обычного построчного цикла. Его сила в поиске и случайном доступе.

Кодировки и битые байты

Большие файлы чаще содержат "мусор": обрезанную последнюю строку, смесь кодировок, случайные бинарные вставки. Одна плохая последовательность байтов на 3-м гигабайте роняет всю обработку с UnicodeDecodeError. Параметр errors управляет этой ситуацией:

Значение errorsПоведение
"strict" (по умолчанию)исключение на первом плохом байте
"replace"замена на символ U+FFFD, обработка продолжается
"ignore"плохие байты отбрасываются молча
"surrogateescape"байты сохраняются и восстанавливаются при записи
with open("legacy.log", encoding="utf-8", errors="replace") as f:
    bad = sum(1 for line in f if "�" in line)
print(f"строк с поврежденными символами: {bad}")

Если файл в cp1251, указывайте encoding="cp1251" явно. Если в начале может стоять BOM, используйте utf-8-sig. Когда нужен только поиск байтовой подстроки, надежнее и быстрее вообще не декодировать текст и работать в режиме "rb".

Память, сжатые файлы и последние строки

Напоследок три приема, которые регулярно нужны при работе с логами и выгрузками: замер памяти, чтение архивов на лету и быстрый доступ к концу файла.

Как проверить расход памяти

import tracemalloc

tracemalloc.start()
count = count_lines("big.csv")
current, peak = tracemalloc.get_traced_memory()
print(f"пик памяти: {peak / 1024 / 1024:.1f} МБ")
tracemalloc.stop()

Если пик растет вместе с размером файла, где-то данные накапливаются: в списке, в словаре или в строке через +=.

Чтение сжатых файлов без распаковки

Логи часто хранят в .gz. Модуль gzip распаковывает данные на лету, и построчное чтение работает так же, как с обычным файлом:

import gzip

with gzip.open("access.log.gz", "rt", encoding="utf-8") as f:
    hits = sum(1 for line in f if "GET /api/" in line)

Режим "rt" обязателен: без буквы t вы получите байты.

Последние строки огромного файла

Читать 10 ГБ ради последних 20 строк незачем. Перейдите в конец через seek и прочитайте хвост:

import os

def tail(path, n=20, block=64 * 1024):
    with open(path, "rb") as f:
        f.seek(0, os.SEEK_END)
        size = f.tell()
        data = b""
        while size > 0 and data.count(b"\n") <= n:
            step = min(block, size)
            size -= step
            f.seek(size)
            data = f.read(step) + data
    return [ln.decode("utf-8", "replace") for ln in data.splitlines()[-n:]]

Функция читает файл с конца блоками по 64 КБ и останавливается, как только набрано нужное число строк.

Типичные ошибки при обработке больших файлов

Даже при правильном построчном чтении память может расти, а обработка замедляться. Причина почти всегда в том, что происходит внутри цикла.

Если после всех оптимизаций обработка все равно медленная, замерьте, где тратится время, с помощью профилировщика cProfile. Часто оказывается, что узкое место вовсе не в чтении файла, а в преобразовании дат или обращениях к базе данных внутри цикла.

Чек-лист для больших файлов

  1. Не вызывайте read() и readlines() без аргументов на файлах неизвестного размера.
  2. Текст читайте циклом for line in f, бинарные данные блоками через read(size).
  3. Стройте обработку как цепочку генераторов, а результаты агрегируйте, а не накапливайте.
  4. Пишите в базу и файлы пачками.
  5. Для поиска по огромному файлу используйте mmap или бинарный режим.
  6. Всегда указывайте encoding, а для грязных данных еще и errors.
  7. Для JSON на гигабайты переходите на формат JSON Lines или потоковый парсер.

Частые вопросы

Что быстрее: построчное чтение или чтение блоками?

Для подсчета символов или поиска байтовой подстроки блоки в бинарном режиме быстрее в несколько раз. Если же каждую строку все равно нужно разобрать, разница почти исчезает, а построчный код проще и надежнее.

Поможет ли увеличить buffering в open?

Немного. Параметр buffering=1024*1024 уменьшает число системных вызовов, но буфер по умолчанию уже достаточно велик. Заметный выигрыш дают отказ от декодирования, обработка в бинарном режиме и уменьшение работы внутри цикла.

Как прочитать большой JSON-файл?

json.load всегда загружает документ целиком. Для огромных массивов используйте потоковый парсер (пакет ijson) или храните данные в формате JSON Lines. Приемы работы с этим форматом описаны в статье о том, как записать и прочитать JSON-файл.

Можно ли читать большой файл в несколько процессов?

Да: разделите файл на диапазоны байтов, каждый процесс делает seek на начало своего диапазона, пропускает неполную первую строку и читает до конца диапазона. Это оправдано, когда узкое место в процессоре (разбор, вычисления), а не в скорости диска.

Как быстро прочитать первые N строк файла?

Через itertools.islice: list(islice(f, 10)) вернет 10 строк и не тронет остальной файл. Это быстрее и безопаснее, чем f.readlines()[:10].