Работа с файлами в Python
Введение
В этой статье вы узнаете как организовать работать с файлами в Python 3.
Создайте файл
files.py
и копируйте туда код из примеров.
Запустить файл можно командой
python3 files.py
Создать
Создать файл можно командой
open
Опции:
r чтение
rb чтение в бинарном режиме
rt чтение в текстовом режиме
w только запись.
wb запись в бинарном режиме
wt запись в текстовом режиме
w+ запись и чтение
a запись в конец файла - сохранит данные, которые были в файле
b - это селектор бинарного режима
t - это селектор текстового режима
Любая опция с w перезапишет существующий файл - будьте внимательны!
Пример:
f = open("log.txt","w+")
Кодировка
docs.python.org/3/library/codecs.html#standard-encodings
import sys print(sys.getdefaultencoding())
utf-8
Открыть файл
Синтаксис:
open(path_to_file, mode, encoding)
По умолчанию используется кодировка utf-8
Чтобы открыть файл для чтения выполните
f = open("log.txt","r")
Если файл log.txt не существует, он не будет создан
raceback (most recent call last): File "files.py", line 1, in <module> f = open('log.txt', 'r') FileNotFoundError: [Errno 2] No such file or directory: 'log.txt'
Закрыть файл
Чтобы закрыть файл выполните
f.close()
Менеджер контекста
Предпочтительнее использовать менеджер контекста для работы с файлами
Как только вы выйдете из блока в котором открыт файл - он автоматически закроется
with open('text.txt', 'r') as f:
pass
print(f.closed)
True
Будем рассматривать примеры работы с файлами как с использованием менеджера контекста так и без него.
read(): чтение файла
Получать данные из файла можно с помощью read()
В неё можно передавать аргумент типа int и тогда будет возвращено
соответсвующее количество байт содераждания файла.
Прочитать содержимое файла
with open('sites.txt', 'r') as f:
f_contents = f.read()
print(f_contents)
www.urn.su www.heihei.ru www.topbicycle.ru
Рассмотрим файл sites.md который состоит из одной строки
topbicycle.ruheihei.rueht1.ru
>>> f = open('sites.md', mode='rt', encoding='utf-8') >>> f.read(3)
'top'
>>> f.read(7)
'bicycle'
>>> f.read(3)
'.ru'
Чтобы получить всё что осталось в файл нужно вызвать read() без аргументов.
>>> f.read()
'heihei.rueth1.ru'
Если вызвать read() ещё раз, вернётся пустая строка
>>> f.read()
''
В данный момент указатель стоит на конце файла, но его можно переместить с помощью seek()
seek(0): перемещение в начало файла
С помощью seek(0) можно поставить указатель в начало файла.
Перейдём в конец файла
sites.md
topbicycle.ruheihei.rueht1.ru
>>> f = open('sites.md', mode='rt', encoding='utf-8') >>> f.read(3)
top
>>> f.read()
'bicyleheihei.rueth1.ru'
>>> f.read()
''
Теперь с помощью seek() поставим указатель в начало
>>> f.seek(0)
0
>>> f.read(3)
'top'
Очистить файл
>>> f = open('sites.md', mode='w', encoding='utf-8') >>> f.write('')
0
>>> f.close() >>> exit()
readline(): построчное чтение
Метод readline() выводит содержимое построчно.
А метод readlines() выводит все строки в виде
списка
>>> f = open('sites.md', mode='rt', encoding='utf-8') >>> f.readline()
'www.topbicycle.ru\n'
>>> f.readline()
'www.heihei.ru\n'
>>> f.readline()
'eth1.ru'
>>> f.readline()
''
>>> f.seek(0)
0
>>> f.readlines()
['www.topbicycle.ru\n', 'www.heihei.ru\n', 'eth1.ru']
>>> f.close()
Все строки с символом перехода на новую строку - readlines
with open('sites.txt', 'r') as f:
f_contents = f.readlines()
print(f_contents)
['www.urn.su\n', 'www.heihei.ru\n', 'www.topbicycle.ru\n']
Строки по одной readline
with open('sites.txt', 'r') as f:
f_contents = f.readline()
print(f_contents)
f_contents = f.readline()
print(f_contents)
www.urn.su www.heihei.ru
Строки по одной без лишних переходов end=''
with open('sites.txt', 'r') as f:
f_contents = f.readline()
print(f_contents, end = '')
f_contents = f.readline()
print(f_contents, end = '')
www.urn.su www.heihei.ru
Цикл для построчного вывода
with open('sites.txt', 'r') as f:
for line in f:
print(line, end = '')
www.urn.su www.heihei.ru www.topbicycle.ru
В качестве альтернативы можно использовать sys.stdout.write()
import sys f = open(sys.argv[1], mode='rt', encoding='utf-8') for line in f: sys.stdout.write(line) f.close()
python files.py sites.txt
www.urn.su www.heihei.ru www.topbicycle.ru
Прочитать определённое количество символов
with open('sites.txt', 'r') as f:
f_contents = f.read(20)
print(f_contents)
www.urn.su www.heihe
Если выполнять эту команду последовательно - будут прочитаны следующие символы
with open('sites.txt', 'r') as f:
f_contents = f.read(20)
print(f_contents, end = '')
f_contents = f.read(20)
print(f_contents, end = '')
www.urn.su www.heihei.ru www.topbicycle.
Цикл для произвольного количества символов .read
with open('sites.txt', 'r') as f:
size_to_read = 10
f_contents = f.read(size_to_read)
while len(f_contents) > 0:
print(f_contents, end = '')
f_contents = f.read(size_to_read)
www.urn.su www.heihei.ru www.topbicycle.ru
Выражение f_contents = f.read(size_to_read) нужно для того, чтобы когда файл закончится и f.read(size_to_read) станет нулем len(f_contents) тоже станет нулем и цикл завершится
Имя файла
Пример программы, которая выводит на экран имя файла и режим, в котором он открыт
f = open('log.txt', 'r')
print(f.name)
print(f.mode)
f.close()
Если файл log.txt существует, то в терминале вы увидите
log.txt
r
Копирование файлов
Текстовые файлы
Их можно копировать построчно
with open('sites.txt', 'r') as rf:
with open('sites_copy.txt', 'w') as wf:
for line in rf:
wf.write(line)
cat sites_copy.txt
www.urn.su www.heihei.ru www.topbicycle.ru
Изображения
Их тоже можно копировать построчно, но открывать
и записывать нужно в побитовом режиме. То есть
нужно добавлять опцию b
Скачайте изображение велосипеда с сайта
TopBicycle.ru
или возьмите любую другую картинку
wget https://topbicycle.ru/b/img/stels_pilot_950_MD_26.jpg
ls
stels_pilot_950_MD_26.jpg
with open('stels_pilot_950_MD_26.jpg', 'rb') as rf:
with open('stels_pilot_950_MD_26_copy.jpg', 'wb') as wf:
for line in rf:
wf.write(line)
python3 files.py
ls
stels_pilot_950_MD_26.jpg stels_pilot_950_MD_26_copy.jpg
Более правильным подходом считается копирование не в построчном режиме а частями с фиксированным размером
with open('stels_pilot_950_MD_26.jpg', 'rb') as rf:
with open('stels_pilot_950_MD_26_copy.jpg', 'wb') as wf:
chunk_size = 4096
rf_chunk = rf.read(chunk_size)
while len(rf_chunk) > 0:
wf.write(rf_chunk)
rf_chunk = rf.read(chunk_size)
Записать файл
Чтобы очистить файл от старого содержимого и записать в него новое используется опция w (write)
with open('log.txt', 'w') as f:
f.write("some text")
Рассмотрим запись в файл в интерактивном режиме и без менеджера контекста
>>> f = open('sites.md', 'w') >>> f.write("topbicycle.ru")
13
>>> f.write("heihei.ru")
9
13 и 9 это число байт переданное в файл
>>> f.close() >>> exit() cat sites.md
cat sites.md www.topbicycle.ru www.heihei.ru eth1.ruwww.aviasales.ru www.booking.comwww.tutu.ru www.velodrive.ruwww.velosklad.ru%topbicycle.ruheihei.ru
ls -l
-rw-r--r-- 1 andrei users 25 Apr 30 07:16 sites.md
Конечный размер файла будет зависеть от опецарионной системы.
В
Windows
и
Linux
разные переносы строк, поэтому когда Python применяет свой
универсальный перенос строки
количество байт может увеличится на 1 а может остаться прежним.
write() возвращает количество байт, переданных в файл, а не фактический размер записанных данных.
Дописать в файл
Если нужно добавить новые данные к предыдущему содержимому без удаления - используется опция a (append)
with open('log.txt', 'a') as f:
f.write("some text")
Рассмотрим пример добавления данных в файл sites.md с помощью метода writelines()
www.topbicycle.ru www.heihei.ru www.eth1.ru
>>> f = open('sites.md', mode='at', encoding='utf-8') >>> f.writelines(['www.aviasales.ru\n', 'www.booking.com', 'www.tutu.ru\n', 'www.velodrive.ru', 'www.velosklad.ru']) >>> f.close() >>> exit()
cat sites.md
www.topbicycle.ru www.heihei.ru eth1.ruwww.aviasales.ru www.booking.comwww.tutu.ru www.velodrive.ruwww.velosklad.ru
Видно, что переносы строк появились только там, где из указали вручную.
Записать json в файл
import json
# нужно где-то взять json
r = urllib.request.urlopen('http://urn.su/api/v1/getjson')
rr = r.read()
rj = json.loads(rr)
with open('file.txt', 'w') as f:
json.dump(rj, f)
Удалить первые несколько строк файла
with open('log.txt', 'a') as fin:
data = fin.readlines()[1:]
with open('new.txt', 'w') as fout:
fout.writelines(data)
Запись вывода программы в файл
Если вы запускаете скрипт из терминала, воспользуйтесь перенаправлением
python script.py > script.log
В самом скрипте можно временно подменить стандартный вывод.
Допустим я делаю запрос к API
(Подробнее про REST API)
import sys … # Сохраним ссылку на оригинальный stdout original_stdout = sys.stdout with open("log.txt", "a") as f: sys.stdout = f print(resp.data) sys.stdout = original_stdout
Тоже самое, если приходит json и хочется записать его красиво
import sys import json … with open("log.txt", "a") as f: sys.stdout = f print(json.dumps(resp.data, indent=4)) sys.stdout = original_stdout
Последовательность Рекамана
import sys from itertools import count, islice def sequence(): """Generate Recaman's sequence.""" seen = set() a = 0 for n in count(1): yield a seen.add(a) c = a - n if c < 0 or c in seen: c = a + n a = c def write_sequence(filename, num): """Write Recaman's sequence to a text file.""" f = open(filename, mode='wt', encoding='utf-8') f.writelines( f"{r}\n" for r in islice(sequence(), num + 1)) f.close() if __name__ == '__main__': write_sequence(filename=sys.argv[1], num=int(sys.argv[2]))
python recaman.py recaman.dat 1000
cat recaman.dat
1 3 6 2 7 … 2687 3685 2686 3686
"""Read and print an integer series.""" import sys def read_series(filename): f = open(filename, mode='rt', encoding='utf-8') series = [] for line in f: a = int(line.strip()) series.append(a) f.close() return series def main(filename): series = read_series(filename) print(series) if __name__ == "__main__": filename = sys.argv[1] main(filename)
python series.py recaman.dat
[0, 1, 3, 6, 2, 7 … 3684, 2687, 3685, 2686, 3686]
Если в файле будут неподходящие данные ошибка будет показана в Traceback, а до закрытия файла дело не дойдёт.
echo badidea >> recaman.dat
python series.py recaman.dat
Traceback (most recent call last): File "/home/andrei/series.py", line 21, in <module> main(filename) File "/home/andrei/series.py", line 15, in main series = read_series(filename) File "/home/andrei/series.py", line 8, in read_series a = int(line.strip()) ValueError: invalid literal for int() with base 10: 'badidea'
Это серьёзная проблема, и обычно решается использованием
менеджера контекста
, но можно решить и добавлением
try
,
finally
Модифицируем функцию read_series() заодно добавив
генератор списка
def read_series(filename): try: f = open(filename, mode='rt', encoding='utf-8') return [int(line.strip()) for line in f] finally: f.close()
С with получается короче.
def read_series(filename): with open(filename, mode='rt', encoding='utf-8') as f: return [int(line.strip()) for line in f]
В write_sequence() из recaman.py тоже лучше применить with
def write_sequence(filename, num): """Write Recaman's sequence to a text file.""" with open(filename, mode='wt', encoding='utf-8') as f: f.writelines( f"{r}\n" for r in islice(sequence(), num + 1))
Пример работы с bytes
bmp.py
"""A module for dealing with BMP bitmap image files.""" def write_grayscale(filename, pixels): """Creates and writes a grayscale BMP file. Args: filename: The name of the BMP file to be created. pixels: A rectangular image stored as a sequence of rows. Each row must be an iterable series of integers in the range 0-255. Raises: ValueError: If any of the integer values are out of range. OSError: If the file couldn't be written. """ height = len(pixels) width = len(pixels[0]) with open(filename, 'wb') as bmp: # BMP Header bmp.write(b'BM') size_bookmark = bmp.tell() # The next four bytes hold the filesize as a 32-bit bmp.write(b'\x00\x00\x00\x00') # little-endian integer. Zero placeholder for now. bmp.write(b'\x00\x00') # Unused 16-bit integet - should be zero bmp.write(b'\x00\x00') # Unused 16-bit integet - should be zero pixel_offset_bookmark = bmp.tell() # The next four bytes hold the integer offset to the bmp.write(b'\x00\x00\x00\x00') # pixel data. Zero placeholder for now. # Image Header bmp.write(b'\x28\x00\x00\x00') # Image header size in bytes - 40 decimal bmp.write(_int32_to_bytes(width)) # Image width in pixels bmp.write(_int32_to_bytes(height)) # Image height in pixels bmp.write(b'\x01\x00') # Number of image planes bmp.write(b'\x08\x00') # Bits per pixel 8 for grayscale bmp.write(b'\x00\x00\x00\x00') # No compression bmp.write(b'\x00\x00\x00\x00') # Zero for uncompressed images bmp.write(b'\x00\x00\x00\x00') # Unused pixels per meter bmp.write(b'\x00\x00\x00\x00') # Unused pixels per meter bmp.write(b'\x00\x00\x00\x00') # Use whole color table bmp.write(b'\x00\x00\x00\x00') # All colors are important # Color palette - a linear grayscale for c in range(256): bmp.write(bytes((c, c, c, 0))) # Blue, Green, Red, Zero # Pixel data pixel_data_bookmark = bmp.tell() for row in reversed(pixels): # BMP files are bottom to top row_data = bytes(row) bmp.write(row_data) padding = b'\x00' * ((4 - (len(row) % 4)) % 4) # Pad row to multiple of four bytes bmp.write(padding) # End of file eof_bookmark = bmp.tell() # Fill in file size placeholder bmp.seek(size_bookmark) bmp.write(_int32_to_bytes(eof_bookmark)) # Fill in pixel offset placeholder bmp.seek(pixel_offset_bookmark) bmp.write(_int32_to_bytes(pixel_data_bookmark)) def _int32_to_bytes(i): """Convert an integer to four bytes in little-endian format.""" # &: Bitwise-and # >>: Right-shift return bytes( (i & 0xff, i >> 8 & 0xff, i >> 16 & 0xff, i >> 24 & 0xff) )
fractal.py
import math def mandel(real, imag): """The logarighm of number of iterations needed to determine whether a complex point is in the Mandelbrot set. Args: real: The real coordinate imag: The imaginary coordinate Returns: An integer in the range 1-255. """ x = 0 y = 0 for i in range(1, 257): if x*x + y*y > 4.0: break xt = real + x*x - y*y y = imag + 2.0 * x * y x = xt return int(math.log(i) * 256 / math.log(256)) -1 def mandelbrot(size_x, size_y): """Make an Mandelbrot set image. Args: size_x: Image width size_y: Image height Returns: A list of lists of integers in the range 0-255 """ return [[mandel((3.5 * x / size_x) - 2.5, (2.0 * y / size_y) - 1.0) for x in range(size_x)] for y in range(size_y)]
>>> import fractal >>> pixels = fractal.mandelbrot(448, 256) >>> import reprlib >>> reprlib.repr(pixels)
'[[31, 31, 31, 31, 31, 31, ...], [31, 31, 31, 31, 31, 31, ...], [31, 31, 31, 31, 31, 31, ...], [31, 31, 31, 31, 31, 31, ...], [31, 31, 31, 31, 31, 31, ...], [31, 31, 31, 31, 31, 31, ...], ...]'
>>> import bmp >>> bmp.write_grayscale("mandel.bmp", pixels)
Определение размеров bmp изображения
def dimensions(filename): """Determine the dimensions in pixels of a BMP image. Args: filename: The filename of a BMP file. Returns: A tuple containing two integers with the width and height in pixels. Raises: ValueError: If the file was not a BMP file. OSError: If there was a problem reading the file. """ with open(filename, 'rb') as f: magic = f.read(2) if magic != b'BM': raise ValueError(f"{filename} is not a BMP file") f.seek(18) width_bytes = f.read(4) height_bytes = f.read(4) return ( _bytes_to_int32(width_bytes), _bytes_to_int32(height_bytes)) def _bytes_to_int32(b): "Convert a bytes object containing four bytes into an integer." return b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)
>>> import bmp >>> bmp.dimensions("mandel.bmp")
(448, 256)
encode()
>>> s = "abcd" >>> len(s) 4 >>> s.encode() b'abcd' >>> s = "сайт" >>> len(s) 4 >>> s.encode() b'\xd1\x81\xd0\xb0\xd0\xb9\xd1\x82' >>> s = "сайт" >>> s.encode('iso-8859-5') b'\xe1\xd0\xd9\xe2' >>> s.encode('utf-8') b'\xd1\x81\xd0\xb0\xd0\xb9\xd1\x82'
Японские символы
>>> j = "平仮" >>> len(j) 2 >>> j.encode() b'\xe5\xb9\xb3\xe4\xbb\xae' >>> j.encode("SHIFT-JIS") b'\x95\xbd\x89\xbc'
Преобразовать в "кириллический" iso-8859-5 не получится
>>> j.encode('iso-8859-5') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/lib/python3.8/encodings/iso8859_5.py", line 12, in encode return codecs.charmap_encode(input,errors,encoding_table) UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-1: character maps to <undefined>
j.encode('iso-8859-5') # это тоже что и j.encode('iso-8859-5', errors='strict')
>>> j.encode('iso-8859-5', errors='ignore') b'' >>> j = "Japanese symbol 平仮" >>> j.encode('iso-8859-5', errors='ignore') b'Japanese symbol '
Определить кодировки файлов
Пример скрипта для определения кодировок файлов. О том как создать файлы в разных кодировках в Linux читайте здесь
python -m pip install python-magic
import magic def get_encoding(sample): blob = open(sample, 'rb').read() m = magic.open(magic.MAGIC_MIME_ENCODING) m.load() encoding = m.buffer(blob) return encoding files = ['utf-8-file', 'windows-1251-file', 'shift-jis-file'] for f in files: print(get_encoding(f))
utf-8 iso-8859-1 unknown-8bit
С определением SHIFT-JIS пока проблемы
Путь до файла
python -m pip install pathlib
import pathlib from pathlib import Path dir_path = pathlib.Path.cwd() print(dir_path)
/home/andrei/sandbox/python/file_path
Прочитать файл из другой директории
Предположим, что мы находимся в директории one проекта file_path:
file_path/ ├── one │ └── path.py └── two └── sites.txt
cat ../two/sites.txt
www.heihei.ru
Прочитать файл sites.txt с помощью Python поможет библиотека pathlib
import pathlib from pathlib import Path dir_path = pathlib.Path.cwd() path = Path(dir_path, "..", "two", "sites.txt") with open(path, "r") as f: sites = f.read() print(sites)
python path.py
www.heihei.ru
glob
Предположим, что мы находимся в директории glob_ex:
glob_ex/ └── sample_files ├── it.txt └── travel.txt
# glob_ex.py import glob files = glob.glob("./sample_files/*.txt") print(files)
python glob_ex.py
['./sample_files/it.txt', './sample_files/travel.txt']
# glob_ex.py import glob files = glob.glob("./sample_files/*.txt") for file in files: print(file) with open(file, "r") as f: lines = f.readlines() for line in lines: print(line)
python glob_ex.py
./sample_files/it.txt beget.com kaspersky.com ./sample_files/travel.txt aviasales.com booking.com