Обработка данных. Часть 2#

Работа с файловой системой#

Пути к файлам#

В Windows пути:

  • Начинаются с имени диска (C:, D:, …)

  • Регистронезависимы (Hello.txt и hello.txt один файл)

  • Компоненты пути разделяются символом \

В POSIX системах пути:

  • Начинаются с корневой директории /

  • Регистрозависимы (Hello.txt и hello.txt разные файлы)

  • Компоненты пути разделяются символом /

Пути бывают:

  • Абсолютные (C:\python\python.exe, /usr/bin/python)

  • Относительные (..\python.exe, ../python)

  • Со специальными сокращениями %WINDIR%\notepad.exe, ~/file.txt

Работа с путями как со строками#

Пути к файлам с точки зрения системы это текстовые строки.

filepath = r"C:\Windows\notepad.exe"
filepath = "/usr/bin/nano"
  • Просто

  • Всегда работает

  • Неперенасимо

Работа с путями как с объектами#

from pathlib import Path
p = Path("/usr/lib/libm.a")
print(p.parts)
print(p.parents[0])
print(p.parents[1])
('/', 'usr', 'lib', 'libm.a')
/usr/lib
/usr
  • Сложнее

  • Иногда приходится приводить к строкам

  • Переносимо

Некоторые функции#

Основные модули для работы с путями:

import os
import os.path
import shutil

Текущая папка скрипта#

os.chdir("/usr/lib")
print(os.getcwd())
/usr/lib

Пути абсолютные и относительные#

p1 = os.path.relpath("/usr/lib/libm.a")
print(p1)
p2 = os.path.relpath("/usr/bin/bash")
print(p2)
p3 = os.path.abspath('../../usr/bin/bash')
print(p3)
libm.a
../bin/bash
/usr/bin/bash

Компоненты пути#

# Имя файла без пути
p4 = os.path.basename("/usr/lib/libm.a")
print(p4)

# Путь без имени
p5 = os.path.dirname("/usr/lib/libm.a")
print(p5)

# Компоненты пути
p6 = os.path.normpath("/usr/lib/libm.a").split(os.sep)
print(p6)

# Расширение файла (последнее)
_, p7 = os.path.splitext("/usr/lib/libm.a")
print(p7)
libm.a
/usr/lib
['', 'usr', 'lib', 'libm.a']
.a

Существование файлов и папок#

# Существование файла
s1 = os.path.isfile("/usr/lib/libm.a")
print(s1)

# Существование папки
s2 = os.path.isdir("/usr/lib")
print(s1)
True
True

Атрибуты файлов и папок#

# Время создания
t2 = os.path.getctime("/usr/lib/libm.a")
print(t2)

# Время изменения
t3 = os.path.getmtime("/usr/lib/libm.a")
print(t3)

# Размер в байтах
s = os.path.getsize("/usr/lib/libm.a")
print(s)
1724592400.795275
1722888052.0
98

Перемещение/копирование/удаление#

# Создание одной папки
os.mkdir("/tmp/test")

# Создание всех папок в пути
os.makedirs("/tmp/test/test/test")

# Копирование папки рекурсивно
shutil.copytree("/tmp/test", "/tmp/test2")

# Удаление папки (пустой)
os.rmdir("/tmp/test/test/test")

# Удаление папки с файлами рекурсивно (осторожно!)
shutil.rmtree("/tmp/test")
shutil.rmtree("/tmp/test2")

with open('/tmp/test.txt', 'w') as f:
    f.write('test')

# Перемещения файла или папки
shutil.move("/tmp/test.txt", "/tmp/test2.txt")

# Копирование файла
shutil.copy("/tmp/test2.txt", "/tmp/test3.txt")

# Удаление файла
os.remove("/tmp/test2.txt")
os.remove("/tmp/test3.txt")

Список файлов по маске#

Обход по маске — метод glob модуля glob.

import glob
import os

s1 = glob.glob("/usr/lib/*.a")
print(s1[0], '...', s1[-1])

s2 = glob.glob("/usr/lib/**/*.a")
print(s2[0], '...', s2[-1])
/usr/lib/libllvm_gtest_main.a ... /usr/lib/libQt6QmlTypeRegistrar.a
/usr/lib/tdbc1.1.7/libtdbcstub1.1.7.a ... /usr/lib/gprofng/libgp-collectorAPI.a

Символ маски

Значение

*

Любое число любых символов

?

Один любой символ

**

Обойти все папки рекурсивно

Обход файлов#

Рекурсивный обход файлов метод walk модуля os.

import os

for dirname, subdirs, files in os.walk("/usr/lib"):
    # dirname — имя текущей папки
    print(dirname)
    # subdirs — подпапки текущей папки
    print(subdirs[0], '...', subdirs[-1])
    # files — файлы в текущей папке
    print(files[0], '...', files[-1])
    break # останавливаем обход принудительно
/usr/lib
tc ... gssproxy
libQt5Charts.so.5.15 ... libjasper.so.7.0.0

Что еще нужно знать о путях#

  • Если путь не абсолютный, то он вычисляется от текущей папки программы.

  • Обход глубоко вложенных деревьев может занимать очень много времени.

  • Разные файловые системы (ФС) имеют различные особенности в частности атрибуты и альтернативные потоки.

  • В Windows и POSIX различная схема управления правами на доступ к файлам, а еще есть такие вещи, как SELinux и ACL.

  • Минимальный размер файла и папки ~ 4 КБ (зависит от ФС).

  • Можно получать уведомления об изменении файлов от ОС, но это платформозависимо (модули inotify и watchdog).

Работа с файлами#

В зависимости от того, с какими опциями открыт файл в Python он может быть или текстовым (из него будут читаться строки str) или двоичным (из него будут читаться байтовые строки bytes).

Структура текстового файла:

Hello world!
Привет мир!

Машинное представление:

00000000  48 65 6c 6c 6f 20 77 6f  72 6c 64 21 0a d0 9f d1  |Hello world!....|
00000010  80 d0 b8 d0 b2 d0 b5 d1  82 20 d0 bc d0 b8 d1 80  |......... ......|
00000020  21 0a                                             |!.|

Кодировки#

Кодировки бывают:

  • Однобайтовые (кодовые страницы, например, CP1251): один байт соответствует одному символу. Диапазон 0-127 одинаков для всех кодовых страниц и включает символы латинского алфавита, знакам препинания, цифры, основные знаки математических действий некоторые непечатные символы. Диапазон 127-255 свой для каждой кодировки.

  • Многобайтовые (Unicode)

    • С фиксированной шириной (например, UTF-32): 2, 4 или 8 байт на каждый символ.

    • С плавающей шириной (например, UTF-8, UTF-16): количество байтов на символ, зависит от символа.

Популярные кодировки:

  • ascii — текстовые файлы IBM, только латиница.

  • cp1251 — текстовые файлы в русской локализации Windows.

  • cp866 — текстовые файлы в русской локализации DOS.

  • utf8 — стандарт для современных Unix-like систем. Кодировка текстовых файлов в Python по умолчанию. Кодировка исходного кода на Python по умолчанию.

Еще Windows, Unix и Mac отличаются символами конца строки в текстовых файлах:

  • Windows: \n\r

  • Unix: \n

  • Mac: \r

Работа с текстовыми файлами#

Открытие и закрытие файла#

# Создание файла
with open('/tmp/test.txt', 'w') as f:
    f.write('test hello world')
    
# Открытие на чтение (текст)
fobj = open("/tmp/test.txt")

# Открытие на чтение в заданной кодировке (текст)
fobj = open("/tmp/test.txt", encoding="utf8")
fobj = open("/tmp/test.txt", encoding="utf8", newline="\n")

# Работа с файлом

# Закрытие
fobj.close()

Преимущества: простота.

Проблема: если при работе с файлом возникнет исключение, то файл может останется незакрытым до завершения работы программы.

Работа с файлом через with#

with open("/tmp/test.txt") as fobj:
    pass
    # Работа с файлом    

Преимущества: если при работе с файлом возникнет исключение, то он все равно будет закрыт.

Проблема: рост уровня вложенности.

Чтение файла#

fobj = open("/tmp/test.txt")

# Прочесть 5 байт
s = fobj.read(5)
print(s)

# Прочесть еще 5 байт
s = fobj.read(5)
print(s)

# Прочесть файл до конца
s = fobj.read()
print(s)

fobj.close()
test 
hello
 world

Чтение в список строк#

fobj = open("/tmp/test.txt")

# Прочесть файл 
sa = fobj.readlines()
# sa == ['Hello world!\n','Привет мир!\n']

fobj.close()

Преимущества: Можно обращаться к отдельным строкам.

Недостатки: Файл приходится целиком загружать в память.

Чтение файла построчно#

fobj = open("/tmp/test.txt")

for line in fobj:
    print(line)

fobj.close()
test hello world

Преимущества: В каждый момент времени в память загружена только одна строка.

Недостатки: Некоторые алгоритмы обработки файлов трудно реализовать.

Перемещение по файлу#

По любому файлу (текстовому или двоичному) можно перемещаться по номеру байта. (осторожнее с Unicode, если номер окажется посередине многобайтового символа — будет ошибка).

fobj = open("/tmp/test.txt")

s = fobj.read()
print(s)

#Получить текущее положение в файле (в байтах)
ps = fobj.tell()
print(ps)

#Перейти на заданное положение в файле (в байтах)
fobj.seek(13)

s = fobj.read(5)
print(s)

# Перейти в начало
fobj.seek(0)

s = fobj.read()
print(s)

fobj.close()
test hello world
16
rld
test hello world

Открытие файла на запись#

По умолчанию файлы открываются только на чтение.

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

Опция

Режим

Текущая позиция чтения/записи

пусто или r

Только чтение

начало

w

Запись, файл обрезается до пустого

начало

r+

Чтение и запись, файл не обрезается

начало

a

Запись, файл не обрезается

конец

a+

Чтение и запись, файл не обрезается

конец

Запись в файл#

fobj = open("/tmp/test.txt", "w")

wbc = fobj.write("Hello world!\n")
# wbc == 13

fobj.close()
fobj = open("/tmp/test.txt", "r+")

content = fobj.read()

wbc = fobj.write("Hello world!\n")
# wbc == 13
wt = fobj.tell()
# wt == 26

fobj.close()

Двоичные файлы#

Работа с двоичным файлом аналогична работе с текстовым, однако меняются опции открытия файла. И читать/писать придется объекты типа bytes, а не str.

Опция

Режим

Текущая позиция чтения/записи

b или rb

Только чтение

начало

wb

Запись, файл обрезается до пустого

начало

rb+

Чтение и запись, файл не обрезается

начало

ab

Запись, файл не обрезается

конец

ab+

Чтение и запись, файл не обрезается

конец

Чтение и запись#

fobj = open("/tmp/test.txt", "ab+")

ds = "Привет мир!\n"
dbytes = ds.encode('utf8')
print(dbytes)

wbc = fobj.write(dbytes)
print(wbc)

fobj.seek(0)

dbs = fobj.read(24)
print(dbs)

fobj.close()

os.remove("/tmp/test.txt") # Очистка
b'\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82 \xd0\xbc\xd0\xb8\xd1\x80!\n'
21
b'Hello world!\nHello world'

Чтение и запись Python объектов#

Самый простой способ сохранить структурированные данные в Python — воспользоваться модулем pickle стандартной библиотеки.

import pickle

sdata = {'keyA': [1, 2, 3],
         'keyB': ("строка", b"bytes"),
         'keyC': 'Тест' }

with open('/tmp/data.pickle', 'wb') as fobj:
    pickle.dump(sdata, fobj)


with open('/tmp/data.pickle', 'rb') as fobj:
    tdata = pickle.load(fobj)

print(tdata)

os.remove("/tmp/data.pickle") # Очистка
{'keyA': [1, 2, 3], 'keyB': ('строка', b'bytes'), 'keyC': 'Тест'}

Достоинства: Простота. Сохраняет практически любой Python объект.

Недостатки: Только для Python. Небезопасно с точки зрения обмена данными.

Чтение и запись числовых массивов#

Для работы с массивами чисел в заданном машинном формате в Python есть класс array.

import array

ar = array.array('f', [2.0, 4.5, 3.3])

with open('/tmp/array.dat', 'wb') as fobj:
    ar.tofile(fobj)

ar2 = array.array('f')

with  open('/tmp/array.dat', 'rb') as fobj:
    ar2.fromfile(fobj, 3)

print(ar2)

os.remove("/tmp/array.dat") # Очистка
array('f', [2.0, 4.5, 3.299999952316284])

Некоторые форматы#

Формат

C-тип

Python-тип

Размер элемента в байтах

b

signed char

int

1

B

unsigned char

int

1

l

signed long

int

4

L

unsigned long

int

4

f

float

float

4

d

double

float

8

Подробная таблица форматов.

Достоинства: Простота. Переносимость.

Недостатки: Можно сохранять только массивы чисел.

Чтение и запись С-структур#

Для работы со структурами данных C в Python есть модуль struct.

import struct

st = struct.Struct('10sIIxBB')
data = st.pack(b"Hello",2,3,19,19)
# data == b'Hello\x00\x00\x00\x00\x00...

with open('/tmp/struct.dat', 'wb') as fobj:
    fobj.write(data)

with  open('/tmp/struct.dat', 'rb') as fobj:
    td = fobj.read() 

values = st.unpack(td)
print(values)
(b'Hello\x00\x00\x00\x00\x00', 2, 3, 19, 19)

Если структура данных сложная, но есть ее описание на С, то можно воспользоваться модулем cffi.

Некоторые форматы struct#

Управление порядком байт < — little-endian, > — big-endian.

Формат

C-тип

Python-тип

Размер элемента в байтах

x

1

b

signed char

int

1

B

unsigned char

int

1

i

int

int

4

I

unsigned int

int

4

f

float

float

4

d

double

float

8

s

char[]

bytes

1

Подробная таблица форматов.

Достоинства: Переносимость.

Недостатки: Нужно вручную описывать формат структур, думать о порядке байт и выравнивании.

Файлы отображенные в память#

Есть возможность работать с файлом, как с массивом байтов не загружая его в память полностью. Для этого используется отображения файла в память.

import mmap

fobj = open("/tmp/struct.dat", "r+b")

# Создаем отображенный файл:
# Первый аргумент - дескриптор заранее открытого файла
# Второй - начальное положение в байтах (можно отобразить не весь файл)
mapobj = mmap.mmap(fobj.fileno(), 0)

# Работаем с файлом как с массивом байтов
print(mapobj[:5])
mapobj[2:5] = b"***"

mapobj.close()
fobj.close()

with open("/tmp/struct.dat", "rb") as fobj:
    print(fobj.read())

os.remove("/tmp/struct.dat") # Очистка
b'Hello'
b'He***\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x00\x13\x13'

Также файл отображенный на память поддерживает стандартные методы для работы с файлами, напрмер read и write.

Псевдофайлы#

С точки зрения Python файлы это просто объекты имеющие определенные методы.

В Python есть классы объекты которых выглядят, как утки файлы, но файлами не являются. Например StringIO и BytesIO из модуля io стандартной библиотеки.

import io

fobj = io.StringIO()
fobj.write('First line.\n')
fobj.seek(5)

s = fobj.read()
print(s)
ss = fobj.getvalue()
print(ss)

fobj.close()
 line.

First line.

Сводная таблица методов файлов#

Метод

Действие

Возвращает

read()

Читает файл от текущей позиции до конца

str/bytes

read(n)

Читает n символов или байт

str/bytes

readlines()

Читает текстовый файл от текущей позиции до конца, разбивает результат по os.linesep

str/bytes

write(x)

Записывает в файл с текущей позиции str/bytes, возвращает число записанных символов / байт

int

tell()

Текущее положение в файле всегда в байтах

int

seek(n)

Перемещается на позицию n в файле всегда в байтах, возвращает позицию на которую удалось переместиться

int

flush()

Записывает сбрасывает буфер на диск

None

close()

Закрывает файл

None

Особые файлы#

При запуске скрипта автоматически открываются 3 файла доступных в модуле sys:

Файл

Тип

Смысл

sys.stdout

w

вывод в консоль

sys.stdout.buffer

wb

вывод в консоль

sys.stderr

w

сообщения об ошибках

sys.stderr.buffer

wb

сообщения об ошибках

sys.stdin

r

ввод с консоли

sys.stdin.buffer

rb

ввод с консоли

Такие операции как print, input сводятся к работе с этими файлами.

Что еще нужно знать о файлах#

  • Как правило read/write — слишком низкий уровень.

  • По умолчанию запись в файл производится не в момент write, а в момент, когда накопится достаточно данных для записи. Для того, чтобы выполнить запись немедленно есть метод flush.

  • Если файл больше десятка мегабайт, загружать его в память целиком — не очень хорошая идея.

  • При работе с двоичными файлами, помните, что есть byteorder.

  • Будьте осторожны с записью и перезаписью файлов.