Справочник по Python · модуль 8 · исключения и файлы
Глава 8.11
Произвольный доступ:
seek() и tell()
До сих пор файл читался только от начала к концу. seek() позволяет перейти к любой позиции, а tell() — узнать текущую. В текстовом режиме у обоих есть жёсткие ограничения, и понять их проще всего через разницу между символами и байтами.
- Категория
- Файлы
- Уровень
- Средний → продвинутый
- Связанные темы
- Кодировки, байты, режимы открытия
- Зачем это знать
- Работать с частями файла, не читая его целиком
§1Что даёт произвольный доступ
Глава 8.4 разделила два способа работы с файлом. Все предыдущие главы использовали последовательный: данные шли подряд, от начала к концу. Произвольный доступ снимает это ограничение.
| Возможность | Зачем нужна |
|---|---|
| переход к любой позиции | прочитать середину файла, не читая начало |
| работа с частями большого файла | файл не помещается в память целиком |
| запись в середину | обновить запись, не переписывая файл |
| повторное чтение | вернуться в начало и прочитать заново |
Последняя строка — самая частая на практике: именно ради неё в главах 8.6 и 8.8 встречался вызов seek(0).
§2tell(): где мы сейчас
Метод возвращает текущую позицию — то место, откуда пойдёт следующее чтение или запись.
with open("demo.txt", "w", encoding="utf-8") as file: file.write("0123456789ABCDEFGHIJ") with open("demo.txt", encoding="utf-8") as file: print(file.tell()) file.read(3) print(file.tell())
0 3
Здесь всё совпало с интуицией только потому, что символы латинские. С кириллицей картина изменится — §7.
§3seek(): переход к позиции
seek(offset) перемещает позицию на указанное число байтов от начала файла.
with open("demo.txt", encoding="utf-8") as file: file.seek(10) print(file.read(5)) print(file.tell())
ABCDE 15
Метод возвращает новую позицию, поэтому print(file.seek(10)) напечатал бы 10. Часто этот результат просто игнорируют.
§4Параметр whence
Второй аргумент задаёт точку отсчёта.
whence | Отсчёт от | Пример |
|---|---|---|
0 (по умолчанию) | начала файла | seek(10) — на десятый байт |
1 | текущей позиции | seek(5, 1) — на пять байт вперёд |
2 | конца файла | seek(-10, 2) — за десять байт до конца |
Смещение может быть отрицательным — для whence=1 это движение назад, для whence=2 иначе и не бывает.
§5Ограничения текстового режима
Таблица выше описывает двоичный режим. В текстовом работает только часть возможностей.
with open("demo.txt", encoding="utf-8") as file: file.read(3) file.seek(5, 1)
io.UnsupportedOperation: can't do nonzero cur-relative seeks
| Вызов | Текстовый режим | Двоичный режим |
|---|---|---|
seek(n) | можно | можно |
seek(0, 1) | можно | можно |
seek(n, 1) | ошибка | можно |
seek(0, 2) | можно | можно |
seek(-n, 2) | ошибка | можно |
Разрешены только два безопасных случая: переход к позиции, полученной от tell(), и перемещение в конец файла нулевым смещением.
§6Почему так
Ограничение выглядит произвольным, но у него простая причина — кодировка. В UTF-8 символы занимают разное число байтов, и сдвиг «на пять байт вперёд» может попасть в середину символа.
"привет" в UTF-8:
п р и в е т
d0 bf d1 80 d0 b8 d0 b2 d0 b5 d1 82
↑
seek(1) — середина символа, декодировать нечем
Python не может проверить заранее, попадёт ли произвольное смещение на границу символа, поэтому просто запрещает такие переходы в текстовом режиме. В двоичном проверять нечего — там байты и есть единица работы.
§7Позиция в байтах, чтение в символах
Отсюда же главное недоразумение темы: read() считает символы, а tell() — байты.
with open("demo.txt", "w", encoding="utf-8") as file: file.write("привет мир") with open("demo.txt", encoding="utf-8") as file: file.read(3) print(file.tell())
6
- прочитано: три символа
- ещё не прочитано
Верхний ряд — символы, нижняя линейка — байты. Кириллическая буква в UTF-8 занимает два байта, пробел — один, поэтому обе шкалы расходятся с самого начала. read() отсчитывает деления по верхней шкале, tell() и seek() — по нижней.
Это то же расхождение, что в главе 5.4: три кириллических символа занимают шесть байтов. Вычислять позицию вручную по числу символов нельзя — для латиницы совпадёт, для кириллицы нет.
§8Правило работы с текстом
Единственный надёжный способ вернуться к месту в текстовом файле — сохранить позицию, полученную от tell().
with open("demo.txt", encoding="utf-8") as file: file.read(3) bookmark = file.tell() file.seek(0) file.seek(bookmark) print(file.read())
вет мир
Значение tell() в текстовом режиме — непрозрачное число: его не нужно понимать, складывать или уменьшать. Его только сохраняют и передают обратно в seek().
§9Двоичный режим без ограничений
В режиме "rb" доступны все формы seek().
with open("demo.txt", "w", encoding="utf-8") as file: file.write("0123456789ABCDEFGHIJ") with open("demo.txt", "rb") as file: file.seek(10) print(file.read(5)) file.seek(-4, 2) print(file.read())
b'ABCDE' b'GHIJ'
Поэтому произвольный доступ — в первую очередь инструмент для двоичных файлов и записей фиксированной длины, где позиция вычисляется арифметикой.
§10Размер файла через seek
Переход в конец с нулевым смещением возвращает размер файла в байтах — и это работает в обоих режимах.
with open("demo.txt", "rb") as file: size = file.seek(0, 2) print(size)
20
Приём удобен, когда файл уже открыт. Если он ещё не открыт, проще os.path.getsize(path) — функция из стандартной библиотеки.
§11Запись по позиции
seek() действует и на запись. Но запись не «вставляет» данные, а перезаписывает то, что было на этом месте.
with open("demo.txt", "w", encoding="utf-8") as file: file.write("0123456789") with open("demo.txt", "r+", encoding="utf-8") as file: file.seek(3) file.write("XX") print(open("demo.txt", encoding="utf-8").read())
012XX56789
Режим "r+" открывает файл на чтение и запись, не стирая содержимое, — в отличие от "w+" из главы 8.6. Вставить данные в середину без сдвига остального нельзя: для этого файл переписывают целиком.
§12Режим a игнорирует seek
Отдельная ловушка: в режиме дозаписи любая запись идёт в конец, куда бы ни поставили позицию.
with open("demo.txt", "a", encoding="utf-8") as file: file.seek(0) file.write("!") print(open("demo.txt", encoding="utf-8").read())
012XX56789!
Это не ошибка и не недоработка: режим "a" по определению означает «всегда в конец». Ошибки не будет — просто данные окажутся не там, где ожидалось.
§13Где это применяется
| Задача | Как используется |
|---|---|
| перечитать файл | seek(0) и повторное чтение |
| прочитать конец большого журнала | seek(-n, 2) в двоичном режиме |
| записи фиксированной длины | seek(номер * длина) |
| заголовок двоичного формата | прочитать первые байты и перейти к данным |
| обновить поле на месте | seek() и write() в режиме "r+" |
В обычной работе с текстом произвольный доступ нужен редко: файл либо читают целиком, либо построчно. Его область — двоичные форматы и структуры с известным устройством.
§14Частые ошибки
| Что написано | Что произойдёт | Как правильно |
|---|---|---|
seek(5, 1) в текстовом режиме | UnsupportedOperation | открыть в "rb" или сохранить tell() |
seek(-10, 2) в текстовом режиме | то же исключение | двоичный режим |
считать tell() числом символов | для кириллицы значение вдвое больше | это байты, и в тексте — непрозрачные |
| вычислять позицию арифметикой в тексте | попадание в середину символа | только значения из tell() |
seek() в режиме "a" перед записью | данные всё равно уйдут в конец | режим "r+" |
ждать вставки от write() после seek() | существующие данные будут перезаписаны | переписать файл целиком |
seek() без повторного чтения | ничего не изменится: метод только двигает позицию | после seek() вызвать read() |
§15Проверьте себя
В каких единицах работает seek()?
В байтах — в обоих режимах. При этом read() в текстовом режиме считает символы, и совпадать эти числа не обязаны.
Почему seek(5, 1) запрещён в текстовом режиме?
Произвольное смещение может попасть в середину многобайтового символа, а проверить это заранее Python не может.
Какие формы seek() разрешены в текстовом режиме?
Переход к абсолютной позиции, seek(0, 1) и seek(0, 2). Остальное — только в двоичном.
Как надёжно вернуться к месту в текстовом файле?
Сохранить значение tell() и передать его в seek(). Вычислять позицию самому нельзя.
Что делает write() после seek()?
Перезаписывает данные с этой позиции, а не вставляет. Существующие символы будут заменены.
Почему seek() не работает в режиме "a"?
Режим дозаписи всегда пишет в конец файла независимо от позиции. Ошибки при этом нет.
§16Шпаргалка
file.seek(0)file.seek(offset)pos = file.tell()file.seek(pos)file.seek(0, 2)file.seek(-n, 2) — только "rb"open(path, "r+")Итог главы
seek() переводит позицию в файле, tell() её возвращает, и оба работают в байтах. В текстовом режиме разрешены только абсолютный переход и нулевые смещения — потому что произвольный сдвиг может разорвать многобайтовый символ. Значение tell() для текста стоит считать непрозрачным: его сохраняют и возвращают в seek(), но не вычисляют. Запись после перехода перезаписывает данные, а в режиме "a" позиция вовсе игнорируется.