Огромная ошибка — делать выводы, не имея необходимой информации.
Артур Конан Дойль
Активная программа работает с данными, которые хранятся в запоминающем устройстве с произвольным доступом (Random Access Memory, RAM). RAM — очень быстрая память, но дорогая и требующая постоянного питания: если питание пропадет, то все данные, которые в ней хранятся, будут утеряны. Жесткие диски медленнее оперативной памяти, но более емкие, стоят дешевле и могут хранить данные даже после того, как кто-то выдернет шнур питания. Поэтому много усилий при создании компьютерных систем было потрачено на поиск оптимального соотношения между хранением данных на диске и в оперативной памяти. Как программистам, нам важно постоянство: хранение и извлечение данных с использованием энергонезависимых носителей, таких как диски.
В этой главе мы рассмотрим разнообразные способы хранения данных, каждый из которых оптимизирован для разных целей: плоские файлы, структурированные файлы и базы данных. Операции с файлами, не касающиеся ввода-вывода, рассматриваются в главе 14.
Запись — это термин, обозначающий некоторые связанные между собой данные. Запись состоит из отдельных полей.
Самый простой пример постоянного хранилища — это старый добрый файл, который иногда называют еще плоским файлом. Он хорошо работает в том случае, когда у данных очень простая структура и вы полностью записываете их на диск или считываете с него. Такой подход годится для простых текстовых данных.
В этом формате каждое поле записи имеет фиксированную длину и при необходимости дополняется до требуемой длины (как правило, пробелами) так, чтобы все записи имели одинаковый размер. Программист может использовать функцию seek() для перемещения по файлу, для записи или только для чтения необходимых записей и полей.
Для простых текстовых файлов единственным уровнем организации является строка. Но иногда вам может понадобиться более структурированный файл, чтобы сохранить данные из программы для дальнейшего использования или отправить их другой программе.
Существует множество форматов, и у каждого есть свои особенности.
•Разделитель (separator или delimiter) — такие символы, как табуляция ('\t'), запятая (','), вертикальная черточка ('|'). Это пример CSV — формата со значениями, разделенными запятой.
• Символы '<' и '>' в окружении тегов. Примеры включают в себя XML и HTML.
• Знаки препинания. Примером является JavaScript Object Notation (JSON).
• Выделение пробелами. Примером является YAML (аббревиатура расшифровывается как YAML Ain’t Markup Language — «YAML — не язык разметки»).
• Другие файлы, например конфигурационные.
Каждый из этих форматов структурированных файлов может быть считан и записан с помощью как минимум одного модуля Python.
Файлы с разделителями часто используются в качестве формата обмена данными для электронных таблиц и баз данных. Вы можете считать файл CSV вручную, по одной строке за раз, разделяя каждую строку на поля, расставляя запятые и добавляя результат в структуру данных, такую как список или словарь. Но лучшим решением будет использовать стандартный модуль csv, поскольку парсинг этих файлов может оказаться сложнее, чем вы думаете. Ознакомьтесь с важными характеристиками файлов CSV, о которых нужно помнить:
• некоторые имеют альтернативные разделители вместо запятой: самыми популярными являются '|' и '\t';
• некоторые имеют escape-последовательности. Если символ-разделитель встречается внутри поля, все поле может быть окружено кавычками или же ему будет предшествовать escape-последовательность;
• некоторые имеют разные символы конца строк. В Unix используется '\n', в Microsoft — '\r\n'. Apple раньше применяла символ '\r', но теперь перешла на использование '\n';
• некоторые в первой строке могут иметь названия столбцов.
Сначала мы посмотрим, как читать и записывать список строк, каждая из которых содержит список столбцов:
>>> import csv
>>> villains = [
... ['Doctor', 'No'],
... ['Rosa', 'Klebb'],
... ['Mister', 'Big'],
... ['Auric', 'Goldfinger'],
['Ernst', 'Blofeld'],
... ]
>>> with open('villains', 'wt') as fout: # менеджер контекста
... csvout = csv.writer(fout)
... csvout.writerows(villains)
Этот код создает пять записей:
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld
Теперь попробуем считать их обратно:
>>> import csv
>>> with open('villains', 'rt') as fin: # менеджер контекста
... cin = csv.reader(fin)
... villains = [row for row in cin] # здесь используется включение списка
...
>>> print(villains)
[['Doctor', 'No'], ['Rosa', 'Klebb'], ['Mister', 'Big'],
['Auric', 'Goldfinger'], ['Ernst', 'Blofeld']]
Мы воспользовались структурой, созданной функцией reader(). Она услужливо создала в объекте cin ряды, которые мы можем извлечь с помощью цикла for.
Используя функции reader() и writer() с их стандартными опциями, мы получим столбцы, разделенные запятыми, и ряды, разделенные символами перевода строки.
Данные могут иметь формат списка словарей, а не списка списков. Снова считаем файл villains, на этот раз используя новую функцию DictReader() и указывая имена столбцов:
>>> import csv
>>> with open('villains', 'rt') as fin:
... cin = csv.DictReader(fin, fieldnames=['first', 'last'])
... villains = [row for row in cin]
...
>>> print(villains)
[OrderedDict([('first', 'Doctor'), ('last', 'No')]),
OrderedDict([('first', 'Rosa'), ('last', 'Klebb')]),
OrderedDict([('first', 'Mister'), ('last', 'Big')]),
OrderedDict([('first', 'Auric'), ('last', 'Goldfinger')]),
OrderedDict([('first', 'Ernst'), ('last', 'Blofeld')])]
Словарь OrderedDict используется для обеспечения совместимости с версиями Python ниже 3.6, в которых словари сохраняли порядок элементов по умолчанию.
Перепишем CSV-файл с помощью новой функции DictWriter(). Мы также вызовем функцию writeheader(), чтобы записать начальную строку, содержащую имена столбцов, в CSV-файл:
import csv
villains = [
{'first': 'Doctor', 'last': 'No'},
{'first': 'Rosa', 'last': 'Klebb'},
{'first': 'Mister', 'last': 'Big'},
{'first': 'Auric', 'last': 'Goldfinger'},
{'first': 'Ernst', 'last': 'Blofeld'},
]
with open('villains', 'wt') as fout:
cout = csv.DictWriter(fout, ['first', 'last'])
cout.writeheader()
cout.writerows(villains)
Этот код создает файл villains.csv со строкой заголовка (пример 16.1).
Пример 16.1. villains.csv
first,last
Doctor,No
Rosa,Klebb
Mister,Big
Auric,Goldfinger
Ernst,Blofeld
Теперь считаем его обратно. Опуская аргумент fieldnames в вызове DictReader(), мы указываем функции использовать значения первой строки файла (first,last) как имена столбцов и соответствующие ключи словаря:
>>> import csv
>>> with open('villains', 'rt') as fin:
... cin = csv.DictReader(fin)
... villains = [row for row in cin]
...
>>> print(villains)
[OrderedDict([('first', 'Doctor'), ('last', 'No')]),
OrderedDict([('first', 'Rosa'), ('last', 'Klebb')]),
OrderedDict([('first', 'Mister'), ('last', 'Big')]),
OrderedDict([('first', 'Auric'), ('last', 'Goldfinger')]),
OrderedDict([('first', 'Ernst'), ('last', 'Blofeld')])]
Файлы с разделителями отображают только два измерения: ряды (строки) и столбцы (поля внутри строк). Если вы хотите обмениваться структурами данных между программами, вам нужен способ кодирования иерархий, последовательностей, множеств и других структур в виде текста.
XML является самым известным форматом разметки, который можно применять в этом случае. Для разделения данных он использует теги, как показано в следующем примере (файл menu.xml):
<?xml version="1.0"?>
<menu>
<breakfast hours="7-11">
<item price="$6.00">breakfast burritos</item>
<item price="$4.00">pancakes</item>
</breakfast>
<lunch hours="11-3">
<item price="$5.00">hamburger</item>
</lunch>
<dinner hours="3-10">
<item price="8.00">spaghetti</item>
</dinner>
</menu>
Рассмотрим основные характеристики формата XML.
• Теги начинаются с символа <. В этом примере использованы теги menu, breakfast, lunch, dinner и item.
• Пробелы игнорируются.
• Обычно контент размещается после начального тега, такого как <menu>. Имеется и соответствующий конечный тег, такой как </menu>.
• Теги могут быть вложены в другие теги на любой глубине. В этом примере теги item являются потомками тегов breakfast, lunch и dinner, которые, в свою очередь, являются потомками тега menu.
• Внутри начального тега могут встретиться опциональные атрибуты. В этом примере price является опциональным атрибутом тега item.
• Теги могут содержать значения. В этом примере каждый тег item имеет значение pancakes для второго элемента тега breakfast.
• Если у тега с именем thing нет значений или потомков, он может быть оформлен как единственный тег путем включения прямого слеша прямо перед закрывающей угловой скобкой (<thing/>), вместо того чтобы использовать начальный и конечный теги <thing> и </thing>.
• Место размещения данных — атрибутов, значений или тегов-потомков — является в какой-то мере произвольным. Например, мы могли бы написать последний тег item как <itemprice="$8.00"food="spaghetti"/>.
XML часто используется в каналах данных и сообщениях, у него есть такие подформаты, как RSS и Atom. В некоторых отраслях существует множество специализированных форматов XML, например в сфере в финансов ().
Сверхгибкость формата XML вдохновила многих людей на создание библиотек для Python, каждая из которых отличается от других подходом и возможностями.
Самый простой способ проанализировать XML в Python — использовать стандартный модуль ElementTree. Рассмотрим небольшую программу, которая анализирует файл menu.xml и выводит на экран некоторые теги и атрибуты:
>>> import xml.etree.ElementTree as et
>>> tree = et.ElementTree(file='menu.xml')
>>> root = tree.getroot()
>>> root.tag
'menu'
>>> for child in root:
... print('tag:', child.tag, 'attributes:', child.attrib)
... for grandchild in child:
... print('\ttag:', grandchild.tag, 'attributes:', grandchild.attrib)
...
tag: breakfast attributes: {'hours': '7-11'}
tag: item attributes: {'price': '$6.00'}
tag: item attributes: {'price': '$4.00'}
tag: lunch attributes: {'hours': '11-3'}
tag: item attributes: {'price': '$5.00'}
tag: dinner attributes: {'hours': '3-10'}
tag: item attributes: {'price': '8.00'}
>>> len(root) # количество разделов меню
3
>>> len(root[0]) # количество блюд для завтрака
2
Для каждого элемента вложенных списков tag — это строка тега, а attrib — это словарь его атрибутов. Библиотека ElementTree имеет множество других способов поиска данных, организованных в формате XML, модификации этих данных и даже записи XML-файлов. Все детали изложены в документации библиотеки ElementTree ().
Среди других библиотек Python для работы с XML можно отметить следующие:
•xml.dom. The Document Object Model (DOM) (знакомая разработчикам на JavaScript) представляет веб-документы в виде иерархических структур. Этот модуль загружает XML-файл в память целиком и позволяет получать доступ ко всем его частям;
•xml.sax. Simple API for XML, или SAX, разбирает XML на ходу, поэтому не загружает в память сразу весь документ. Она может быть хорошим выбором, если нужно обработать очень большие потоки XML.
Вы можете использовать любой формат, описанный в этой главе, чтобы сохранять объекты в файлы и снова их читать. Однако при этом существует вероятность получить проблемы с безопасностью.
Например, в следующем фрагменте XML-файла со страницы «Википедии» об атаках billion laughs () (это разновидность атаки «отказ в обслуживании») определяется десять вложенных сущностей, каждая из которых расширяет более низкий уровень в десять раз, порождая в сумме один миллиард сущностей:
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
Плохая новость: атака подорвет работоспособность всех XML-библиотек, упомянутых в предыдущем подразделе. На ресурсе Defused XML () эта и другие атаки перечислены наряду с уязвимостями библиотек Python. Перейдя по ссылке, вы увидите, как изменять настройки многих библиотек так, чтобы избежать подобных проблем. Вы также можете использовать библиотеку defusedxml в качестве внешнего интерфейса безопасности для других библиотек:
>>> # небезопасно:
>>> from xml.etree.ElementTree import parse
>>> et = parse(xmlfile)
>>> # безопасно:
>>> from defusedxml.ElementTree import parse
>>> et = parse(xmlfile)
Стандартный сайт Python также имеет свою собственную страницу об уязвимостях XML.
Огромные объемы данных сохраняются в формате гипертекстового языка разметки — Hypertext Markup Language (HTML). Это основной формат документов в Интернете. Проблема заключается в том, что значительная часть этих документов не соответствует правилам формата HTML, что затрудняет анализ. Кроме того, большая часть HTML предназначена для форматирования выводимой информации, а не для обмена данными. Поскольку эта глава предназначена для описания относительно хорошо определенных форматов данных, я вынес рассмотрение HTML в главу 18.
JavaScript Object Notation (JSON) (/) стал очень популярным форматом обмена данными, вышедшим за пределы языка JavaScript. Формат JSON является частью языка JavaScript и часто содержит легальный с точки зрения Python синтаксис. Он прекрасно подходит Python, что делает его хорошим выбором для обмена данными между программами. Вы увидите множество примеров JSON для веб-разработки в главе 18.
В отличие от XML, для которого написано множество модулей, для JSON существует всего один модуль с простым именем json. Эта программа кодирует (выгружает) данные в строку JSON и декодирует (загружает) строку JSON обратно. В следующем примере мы создадим структуру данных, содержащую данные из более раннего примера XML:
>>> menu = \
... {
... "breakfast": {
... "hours": "7-11",
... "items": {
... "breakfast burritos": "$6.00",
... "pancakes": "$4.00"
... }
... },
... "lunch" : {
... "hours": "11-3",
... "items": {
... "hamburger": "$5.00"
... }
... },
... "dinner": {
... "hours": "3-10",
... "items": {
... "spaghetti": "$8.00"
... }
... }
... }
.
Далее закодируем структуру данных menu в строку JSON menu_json с помощью функции dumps():
>>> import json
>>> menu_json = json.dumps(menu)
>>> menu_json
'{"dinner": {"items": {"spaghetti": "$8.00"}, "hours": "3-10"},
"lunch": {"items": {"hamburger": "$5.00"}, "hours": "11-3"},
"breakfast": {"items": {"breakfast burritos": "$6.00", "pancakes":
"$4.00"}, "hours": "7-11"}}'
А теперь превратим строку JSON menu_json обратно в структуру данных menu2 с помощью функции loads():
>>> menu2 = json.loads(menu_json)
>>> menu2
{'breakfast': {'items': {'breakfast burritos': '$6.00', 'pancakes':
'$4.00'}, 'hours': '7-11'}, 'lunch': {'items': {'hamburger': '$5.00'},
'hours': '11-3'}, 'dinner': {'items': {'spaghetti': '$8.00'}, 'hours': '3-10'}}
menu и menu2 являются словарями с одинаковыми ключами и значениями.
Вы можете получить исключение, пытаясь закодировать или декодировать некоторые объекты, например datetime (этот вопрос детально рассматривается в главе 13), как показано здесь:
>>> import datetime
>>> import json
>>> now = datetime.datetime.utcnow()
>>> now
datetime.datetime(2013, 2, 22, 3, 49, 27, 483336)
>>> json.dumps(now)
Traceback (most recent call last):
# ... (опустили стек вызовов, чтобы спасти деревья)
TypeError: datetime.datetime(2013, 2, 22, 3, 49, 27, 483336) is not JSON serializable
>>>
Это может случиться, поскольку стандарт JSON не определяет типы даты или времени — он ожидает, что вы укажете ему, как с ними работать. Вы можете преобразовать формат datetime в то, что JSON понимает, например в строку или значение времени epoch (см. главу 13):
>>> now_str = str(now)
>>> json.dumps(now_str)
'"2013-02-22 03:49:27.483336"'
>>> from time import mktime
>>> now_epoch = int(mktime(now.timetuple()))
>>> json.dumps(now_epoch)
'1361526567'
Если значение datetime встретится между обычно сконвертированными типами данных, может быть неудобно выполнять такие особые преобразования. Вы можете изменить способ кодирования JSON с помощью наследования, описанного в главе 10. Документация JSON для Python () содержит пример такого переопределения для комплексных чисел, что также заставляет JSON притвориться мертвым. Напишем переопределение для datetime:
>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> class DTEncoder(json.JSONEncoder):
... def default(self, obj):
... # isinstance() checks the type of obj
... if isinstance(obj, datetime.datetime):
... return int(mktime(obj.timetuple()))
... # else it's something the normal decoder knows:
... return json.JSONEncoder.default(self, obj)
...
>>> json.dumps(now, cls=DTEncoder)
'1361526567'
Новый класс DTEncoder является подклассом, или классом-потомком, класса JSONEncoder. Нам нужно лишь переопределить его метод default(), добавив обработку datetime. Наследование гарантирует, что все остальное будет обработано родительским классом.
Функция isinstance() проверяет, является ли объект obj объектом класса datetime.datetime. Поскольку в Python все является объектом, функция isinstance() работает везде:
>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> type(now)
<class 'datetime.datetime'>
>>> isinstance(now, datetime.datetime)
True
>>> type(234)
<class 'int'>
>>> isinstance(234, int)
True
>>> type('hey')
<class 'str'>
>>> isinstance('hey', str)
True
При работе с JSON и другими форматами структурированного текста вы можете загрузить файл в память и разместить его в структуре данных, не зная о самих структурах заранее. Затем вы можете пройтись по структурам, используя функцию isinstance() и методы, соответствующие типу, чтобы проверить значения структур. Например, если один из элементов является словарем, вы можете извлечь его содержимое с помощью функций keys(), values() и items().
После того как вы сделали это сложным способом, сообщу вам, что существует более простой способ преобразовать объекты типа datetime в JSON:
>>> import datetime
>>> import json
>>> now = datetime.datetime.utcnow()
>>> json.dumps(now, default=str)
'"2019-04-17 21:54:43.617337"'
Инструкция default=str указывает функции json.dumps() применить функцию преобразования str() к тем типам данных, которые она не понимает. Это сработает, поскольку в определении класса datetime.datetime присутствует метод __str__().
Как и JSON, YAML (/) имеет ключи и значения, но обрабатывает большее количество типов данных, включая дату и время. Стандартная библиотека Python не содержит модулей, работающих с YAML, поэтому вам нужно установить стороннюю библиотеку yaml (). Функция load() преобразует строку в формате YAML к данным Python, а функция dump() предназначена для противоположного действия.
Следующий YAML-файл, mcintyre.yaml, содержит информацию о канадском поэте Джеймсе Макинтайре и два его стихотворения:
name:
first: James
last: McIntyre
dates:
birth: 1828-05-25
death: 1906-03-31
details:
bearded: true
themes: [cheese, Canada]
books:
url:
poems:
- title: 'Motto'
text: |
Politeness, perseverance and pluck,
To their possessor will bring good luck.
- title: 'Canadian Charms'
text: |
Here industry is not in vain,
For we have bounteous crops of grain,
And you behold on every field
Of grass and roots abundant yield,
But after all the greatest charm
Is the snug home upon the farm,
And stone walls now keep cattle warm.
Такие значения, как true, false, on и off, преобразуются в булевы переменные. Целые числа и строки преобразуются в их эквиваленты в Python. Для остального синтаксиса создаются списки и словари:
>>> import yaml
>>> with open('mcintyre.yaml', 'rt') as fin:
>>> text = fin.read()
>>> data = yaml.load(text)
>>> data['details']
{'themes': ['cheese', 'Canada'], 'bearded': True}
>>> len(data['poems'])
2
Создаваемые структуры данных совпадают со структурами YAML-файла, которые в данном случае имеют глубину более одного уровня. Вы можете получить заголовок второго стихотворения с помощью следующей ссылки:
>>> data['poems'][1]['title']
'Canadian Charms'
PyYAML может загружать объекты Python из строк, а это опасно. Используйте метод safe_load() вместо метода load(), если импортируете данные в формате YAML, которым не доверяете. А лучше всегда используйте метод safe_load(). Прочтите статью Неда Батчелдера War is peace (), чтобы узнать о том, как незащищенная загрузка YAML скомпрометировала платформу Ruby on Rails.
Теперь, когда вы прочитали все предыдущие разделы, я расскажу вам, что существует сторонний пакет, который позволяет импортировать, экспортировать и изменять табличные данные в форматах CSV, JSON или YAML, а также данные в Microsoft Excel, Pandas DataFrame и некоторые другие. Вы можете установить его привычным способом (pipinstalltablib), а также заглянуть в документацию (/).
Сейчас самое время познакомиться с Pandas (/) — библиотекой Python для структурированных данных. Это отличный инструмент для решения реальных проблем с данными. Она позволяет:
• читать и записывать данные во множестве текстовых и бинарных форматов, таких как:
• текст, поля которого разделены запятыми (CSV), символами табуляции (TSV) или другими символами;
• текст фиксированной длины;
• Excel;
• JSON;
• таблицы HTML;
• SQL;
• HDF5;
• и др. ();
• группировать, разбивать, объединять, разделять, сортировать, выбирать и помечать;
• преобразовывать типы данных;
• изменять размер или форму;
• обрабатывать случаи, когда данные отсутствуют;
• генерировать случайные значения;
• управлять временными последовательностями.
Функции чтения возвращают объект типа DataFrame (). Это является стандартным представлением для двумерных данных (которые делятся на строки и столбцы) в Pandas. Объект этого типа похож на электронную таблицу или таблицу реляционной базы данных. Его одномерный младший брат называется Series ().
В примере 16.2 показывается простое приложение, которое считывает данные из нашего файла villains.csv из примера 16.1.
Пример 16.2. Читаем данные в формате CSV с помощью Pandas
>>> import pandas
>>>
>>> data = pandas.read_csv('villains.csv')
>>> print(data)
first last
0 Doctor No
1 Rosa Klebb
2 Mister Big
3 Auric Goldfinger
4 Ernst Blofeld
Переменная data имеет тип DataFrame: у этого типа данных возможностей больше, чем у простого словаря Python. Он особенно полезен для обработки большого количества чисел с помощью NumPy, а также для подготовки данных для машинного обучения.
Обратитесь к разделам Getting Started () документации Pandas, чтобы узнать подробнее о ее особенностях, и к разделу 10 Minutes to Pandas () для того, чтобы увидеть рабочие примеры.
Воспользуемся Pandas для того, чтобы создать небольшой календарь — список, содержащий первый день первых трех месяцев 2019 года:
>>> import pandas
>>> dates = pandas.date_range('2019-01-01', periods=3, freq='MS')
>>> dates
DatetimeIndex(['2019-01-01', '2019-02-01', '2019-03-01'],
dtype='datetime64[ns]', freq='MS')
Создать такой календарь можно было бы и с помощью функций даты и времени, которые мы рассмотрели в главе 13. Но это намного сложнее, особенно отладка (дата и время добавляют работы). Pandas также позволяет обрабатывать множество особых деталей даты и времени (), например бизнес-месяцы и годы.
Мы еще поговорим о Pandas, когда речь пойдет о картах (см. подраздел «Geopandas» на с. 498) и научных приложениях (см. раздел «Pandas» на с. 516).
Большинство программ предлагают различные параметры или настройки. Динамические настройки могут быть переданы как аргументы программы, но долговременные настройки должны где-то храниться. Соблазн на скорую руку определить собственный формат конфигурационного файла очень силен, но вы должны устоять. Как правило, это бывает и неточно, и не так уж быстро. Вам нужно обслуживать как программу-писатель, так и программу-читатель (которая иногда называется парсером). Существуют хорошие альтернативы, которые вы можете добавить в свою программу, включая те, что были показаны в предыдущих подразделах.
Здесь мы используем стандартный модуль configparser, который обрабатывает файлы с расширением .ini, характерные для Windows. Такие файлы имеют разделы с определениями ключ=значение. Так выглядит минимальный файл settings.cfg:
[english]
greeting = Hello
[french]
greeting = Bonjour
[files]
home = /usr/local
# simple interpolation:
bin = %(home)s/bin
А так выглядит код, который позволяет считать его и разместить в структурах данных:
>>> import configparser
>>> cfg = configparser.ConfigParser()
>>> cfg.read('settings.cfg')
['settings.cfg']
>>> cfg
<configparser.ConfigParser object at 0x1006be4d0>
>>> cfg['french']
<Section: french>
>>> cfg['french']['greeting']
'Bonjour'
>>> cfg['files']['bin']
'/usr/local/bin'
Доступны и другие опции, в том числе более мощная интерполяция. Обратитесь к документации configparser (). Если вам нужно более двух уровней вложенности, попробуйте использовать YAML или JSON.
Некоторые файловые форматы были разработаны для хранения определенных структур данных и не являются ни реляционными базами данных, ни базами данных NoSQL. В следующих подразделах рассказывается о некоторых из них.
Заполненные пробелами бинарные файлы и управление памятью. Такие файлы похожи на заполненные пробелами текстовые файлы, но содержимое может быть бинарным, а в качестве заполнителя может использоваться байт \x00. Каждая запись имеет фиксированный размер, как и каждое поле внутри записи. Это позволяет легче искать нужные записи и поля с помощью функции seek(). Каждая операция с данными выполняется вручную, поэтому такой подход должен применяться только в очень низкоуровневых (близких к «железу») ситуациях.
Данные в таком формате могут быть размещены в ОЗУ с помощью стандартной библиотеки mmap. Взгляните на примеры (/) и стандартную документацию ().
Электронные таблицы, в частности Microsoft Excel, — это широко распространенный бинарный формат данных. Если вы можете сохранить свою таблицу в CSV-файл, то можете считать его с помощью стандартного модуля csv, который был описан ранее.
Это распространяется на бинарный файл xls: для его считывания и записи можно использовать стороннюю библиотеку xlrd (/) или tablib (она упоминалась ранее в подразделе «Tablib» на с. 338).
HDF5 () — это бинарный формат данных, предназначенный для хранения многомерных или иерархических числовых данных. Обычно он используется в научных целях, где быстрый случайный доступ к крупным наборам данных (от гигабайтов до терабайтов) является распространенным требованием. Несмотря на то что HDF5 в некоторых случаях мог бы стать хорошей альтернативой базам данных, по каким-то причинам этот формат практически неизвестен в современном мире. Он лучше всего подходит для приложений вида WORM (write once — read many — «запиши однажды — считай много раз»), которые не нуждаются в защите от конфликтующих записей. Вам могут быть полезными следующие модули:
•h5py — интерфейс низкого уровня с широкими возможностями. Прочтите его документацию (/) и код ();
•PyTables — интерфейс немного более высокого уровня, имеющий некоторые особенности, характерные для баз данных. Прочтите его документацию (/) и код (/).
Оба этих формата рассматриваются в главе 22 с точки зрения применения в научных приложениях, написанных на Python. Здесь я упоминаю об HDF5 затем, чтобы у вас был под рукой нестандартный вариант на случай, когда вам нужно сохранять и высчитывать крупные объемы данных. Хорошим примером использования этого формата является Million Song Dataset () с записями песен в форматах HDF5 и SQLite.
У формата HDF5 недавно появился последователь, который позволяет хранить как плотные, так и разреженные массивы — TileDB (/). Установите интерфейс Python () (он включает в себя и саму библиотеку TileDB), запустив команду pipinstalltiledb. Эта библиотека предназначена для работы с научными данными и приложениями.
Реляционным базам данных всего около 40 лет, но в компьютерном мире они используются повсеместно. Вам практически наверняка придется поработать с ними. В эти моменты вы сможете оценить следующие их преимущества.
• Доступ к данным возможен для нескольких пользователей одновременно.
• Действует защита от повреждения данных пользователями.
• Существуют эффективные методы сохранения и считывания данных.
• Данные определены схемами и имеют ограничения.
• Объединения позволяют найти отношения между различными типами данных.
• Декларативный (в противоположность императивному) язык запросов SQL (Structured Query Language).
Такие базы данных называются реляционными, поскольку они показывают отношения между различными типами данных в форме прямоугольных таблиц. Например, в нашем более раннем примере меню есть отношение между каждым элементом и его ценой.
Таблица представляет собой прямоугольную сетку столбцов (полей данных) и строк (отдельных записей), похожую на электронную таблицу. Пересечение строки и столбца называется ячейкой. Чтобы создать таблицу, необходимо указать ее имя и порядок, имена и типы ее столбцов. Каждая строка имеет одинаковые столбцы, хотя столбец может быть определен так, чтобы в ячейках отсутствовали данные (null). В примере с меню вы могли бы создать таблицу, содержащую по одной строке для каждого продаваемого элемента. Каждый элемент имеет одинаковые столбцы, включая и тот, который хранит цену.
Первичным ключом таблицы является столбец или группа столбцов. Значения ключа должны быть уникальными — таким образом предотвращается ввод одинаковых данных в таблицу. Этот ключ индексируется для быстрого поиска во время выполнения запроса. Работа индекса немного похожа на работу алфавитного указателя, который позволяет быстро найти определенный ряд.
Каждая таблица находится внутри родительской базы данных, как файлы в каталоге. Два уровня иерархии позволяют немного лучше организовывать данные.
Да, словосочетание «база данных» используется в разных значениях: называет и сервер, и хранилище таблиц, и сами данные. Если вам нужно говорить обо всех них одновременно, можно использовать термины «сервер базы данных», «база данных» и «данные».
Если вы хотите найти строки по определенному неключевому значению, определите для столбца вторичный индекс. В противном случае база данных должна будет выполнить сканирование таблицы — поиск нужного значения перебором всех строк.
Таблицы могут быть связаны друг с другом с помощью внешних ключей, и значения столбцов могут быть ограничены этими ключами.
SQL не является API или протоколом. Это декларативный язык: вы говорите, что вам нужно, а не как это сделать. SQL — универсальный язык реляционных баз данных. Запросы SQL являются текстовыми строками: клиент отсылает их серверу базы данных, а тот определяет, что с ними делать дальше.
Существует несколько стандартов определения SQL, но все поставщики баз данных добавили свои собственные настройки и расширения, что привело к появлению множества диалектов SQL. Если вы храните данные в реляционной базе данных, SQL дает вам некоторую переносимость данных. Однако наличие диалектов и операционных различий может усложнить перенос данных в другую базу.
Есть две основные категории утверждений SQL.
•DDL (Data Definition Language — язык определения данных). Обрабатывает создание, удаление, ограничения и разрешения для таблиц, баз данных и пользователей.
•DML (Data Manipulation Language — язык манипулирования данными). Обрабатывает добавление данных, их выборку, обновление и удаление.
В табл. 16.1 перечислены основные команды SQL DDL.
Таблица 16.1. Основные команды SQL DDL
Операция | Шаблон SQL | Пример SQL |
Создание базы данных | CREATE DATABASE имя_базы | CREATE DATABASE d |
Выбор текущей базы данных | USE имя_базы | USE d |
Удаление базы данных и ее таблиц | DROP DATABASE имя_базы | DROP DATABASE d |
Создание таблицы | CREATE TABLE имя_таблицы (описания_столбцов) | CREATE TABLE t (id INT, count INT) |
Удаление таблицы | DROP TABLE имя_таблицы | DROP TABLE t |
Удаление всех строк таблицы | TRUNCATE TABLE имя_таблицы | TRUNCATE TABLE t |
Почему все пишется БОЛЬШИМИ БУКВАМИ? Язык SQL не зависит от регистра, но по традиции (не спрашивайте меня почему) ключевые слова ВЫКРИКИВАЮТСЯ, чтобы их можно было отличить от имен столбцов.
Основные операции DML реляционной базы данных можно запомнить с помощью акронима CRUD:
•Create — создание с помощью оператора SQL INSERT;
• Read — чтение с помощью SELECT;
• Update — обновление с помощью UPDATE;
•Delete — удаление с помощью DELETE.
В табл. 16.2 показаны команды, доступные SQL DML.
Таблица 16.2. Основные команды SQL DML
Операция | Шаблон SQL | Пример SQL |
Добавление строки | INSERT INTO имя_таблицы VALUES(…) | INSERT INTO t VALUES(7, 40) |
Выборка всех строк и столбцов | SELECT * FROM имя_таблицы SELECT * FROM t | SELECT * FROM t |
Выборка всех строк и некоторых столбцов | SELECT cols FROM имя_таблицы | SELECT id, count FROM t |
Выборка некоторых строк и некоторых столбцов | SELECT cols FROM имя_таблицы WHERE условие | SELECT id, count from t WHERE count > 5 AND id = 9 |
Изменение некоторых строк в столбце | UPDATE имя_таблицы SET col = значение WHERE условие | UPDATE t SET count = 3 WHERE id = 5 |
Удаление некоторых строк | DELETE FROM имя_таблицы WHERE условие | DELETE FROM t WHERE count <= 10 OR id = 16 |
Программный интерфейс приложения (Application Programming Interface, API) — это набор функций, которые вы можете вызвать, чтобы получить доступ к какой-либо услуге. DB-API () — это стандартный API в Python, предназначенный для получения доступа к реляционным базам данных. С его помощью вы можете написать одну программу, которая работает с несколькими видами реляционных баз данных, вместо того чтобы писать несколько программ для работы с каждым видом баз данных по отдельности. Этот API похож на JDBC в Java или dbi в Perl.
Рассмотрим его основные функции:
•connect() — создание соединения с базой данных. Этот вызов может включать в себя такие аргументы, как имя пользователя, пароль, адреса сервера и пр.;
• cursor() — создание объекта курсора, предназначенного для работы с запросами;
• execute() и executemany() — запуск одной или нескольких команд SQL;
•fetchone(), fetchmany() и fetchall() — получение результатов работы функции execute().
Модули базы данных в Python, которые будут рассмотрены в следующих подразделах, соответствуют DB-API, но часто имеют некоторые расширения или разницу в деталях.
SQLite (/) — это хорошая легкая реляционная база данных с открытым исходным кодом. Она реализована как стандартная библиотека Python и хранит базы данных в обычных файлах. Эти файлы можно переносить в другие машины и операционные системы, что делает SQLite портативным решением для простых приложений реляционных баз данных. У нее не так много возможностей, как у MySQL или PostgreSQL, но она поддерживает SQL и позволяет нескольким пользователям работать с ней одновременно. Браузеры, смартфоны и другие операционные системы используют SQLite как встроенную базу данных.
Работа начинается с вызова connect() для установки соединения с локальным файлом базы данных, который вы хотите создать или использовать. Этот файл эквивалентен похожей на каталог базе данных, которая хранит таблицы на других серверах. С помощью специальной строки ':memory:' можно создать базу данных только в памяти — это быстро и удобно для тестирования, но данные будут потеряны при завершении программы или выключении компьютера.
Для следующего примера создадим базу данных enterprise.db и таблицу zoo, чтобы управлять нашим процветающим бизнесом по содержанию придорожного контактного зоопарка. В таблице будут следующие столбцы:
•critter — строка переменной длины, наш первичный ключ;
• count — целочисленное количество единиц используемого инвентаря для этого животного;
• damages — сумма, выраженная в долларах, наших убытков из-за взаимодействий людей с животными:
>>> import sqlite3
>>> conn = sqlite3.connect('enterprise.db')
>>> curs = conn.cursor()
>>> curs.execute('''CREATE TABLE zoo
(critter VARCHAR(20) PRIMARY KEY,
count INT,
damages FLOAT)''')
<sqlite3.Cursor object at 0x1006a22d0>
Тройные кавычки в Python очень полезны при создании длинных строк, таких как запросы SQL.
Теперь добавим в зоопарк несколько животных:
>>> curs.execute('INSERT INTO zoo VALUES("duck", 5, 0.0)')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.execute('INSERT INTO zoo VALUES("bear", 2, 1000.0)')
<sqlite3.Cursor object at 0x1006a22d0>
Существует более безопасный способ добавить данные — использовать заполнитель:
>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES(?, ?, ?)'
>>> curs.execute(ins, ('weasel', 1, 2000.0))
<sqlite3.Cursor object at 0x1006a22d0>
На этот раз мы использовали в запросе три вопросительных знака, чтобы показать, что мы планируем вставить три значения, а затем передать эти значения в виде кортежа в функцию execute(). Заполнители помогают нам справляться с утомительными деталями, например с расстановкой кавычек. Они защищают от внедрений SQL-кода — внешней атаки, распространенной в Сети, которая внедряет в систему вредные команды SQL (такие атаки часто происходят в Интернете).
Теперь проверим, сможем ли мы вывести всех наших животных снова:
>>> curs.execute('SELECT * FROM zoo')
<sqlite3.Cursor object at 0x1006a22d0>
>>> rows = curs.fetchall()
>>> print(rows)
[('duck', 5, 0.0), ('bear', 2, 1000.0), ('weasel', 1, 2000.0)]
Получим их еще раз, но упорядочим список по количеству животных:
>>> curs.execute('SELECT * from zoo ORDER BY count')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('weasel', 1, 2000.0), ('bear', 2, 1000.0), ('duck', 5, 0.0)]
Эй, мы хотели получить список в нисходящем порядке:
>>> curs.execute('SELECT * from zoo ORDER BY count DESC')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('duck', 5, 0.0), ('bear', 2, 1000.0), ('weasel', 1, 2000.0)]
Какие животные обходятся нам дороже всего?
>>> curs.execute('''SELECT * FROM zoo WHERE
... damages = (SELECT MAX(damages) FROM zoo)''')
<sqlite3.Cursor object at 0x1006a22d0>
>>> curs.fetchall()
[('weasel', 1, 2000.0)]
Вы бы подумали, что это медведи. Лучше всегда проверять фактические данные.
Перед тем как попрощаться с SQLite, следует очиститься. Если мы открывали соединение и курсор, их нужно закрыть после того, как работа будет закончена:
>>> curs.close()
>>> conn.close()
MySQL (/) — очень популярная реляционная база данных с открытым исходным кодом. В отличие от SQLite она является настоящим сервером, поэтому клиенты могут получать к ней доступ с разных устройств всей сети.
В табл. 16.3 перечислены драйверы, которые вы можете использовать для того, чтобы получить доступ к MySQL из Python. За более подробной информацией обо всех драйверах MySQL для Python обратитесь к энциклопедии на python.org ().
Таблица 16.3. Драйверы MySQL
Название | Ссылка | Пакет PyPi | Импортировать как | Примечание |
mysqlclient |
| mysql-connector-python | MySQLdb |
|
MySQL Connector |
| mysql-connector-python | mysql.connector |
|
PYMySQL | / | pymysql | pymysql |
|
oursql | / | oursql | oursql | Требует наличия клиентской библиотеки MySQL C |
PostgreSQL (/) — полнофункциональная реляционная база данных с открытым исходным кодом, гораздо более продвинутая, чем MySQL. В табл. 16.4 показаны драйверы Python, которые можно использовать для того, чтобы получить к ней доступ.
Таблица 16.4. Драйверы PostgreSQL
Название | Ссылка | Пакет PyPi | Импортировать как | Примечание |
psycopg2 |
| psycopg2 | psycopg2 | Необходим pg_config из клиентских инструментов PostgreSQL |
py-postgresq |
| py-postgresq | py-postgresq |
|
Самым популярным драйвером является psycopg2, но для его установки требуется наличие клиентских библиотек PostgreSQL.
SQL не для всех реляционных баз данных одинаков, а DB-API дает вам ограниченный набор возможностей. Каждая база данных реализует определенный диалект, отражая свои особенности и философию. Многие библиотеки пытаются тем или иным способом компенсировать эти различия. Самая популярная библиотека для работы с разными базами данных — SQLAlchemy (/).
Она не является стандартной, тем не менее широко известна и многими используется. Вы можете установить ее в свою систему с помощью следующей команды:
$ pip install sqlalchemy
Использовать SQLAlchemy можно на нескольких уровнях.
• Самый низкий уровень управляет пулами соединений с базами данных, выполняет команды SQL и возвращает результат. Это ближе всего к DB-API.
• Следующий уровень — язык выражений SQL, который позволяет вам выражать запросы более Python-ориентированным способом.
• Самый высокий уровень — это ORM (Object Relational Model — объектно-реляционная модель), который использует язык выражений SQL Expression Language и связывает код приложения с реляционными структурами данных.
По мере углубления в материал вы поймете, что означают эти термины. SQLAlchemy работает с драйверами базы данных, задокументированными в предыдущих подразделах. Вам не нужно импортировать драйвер — он будет определен с помощью строки соединения, которую вы предоставите SQLAlchemy. Эта строка выглядит примерно так:
диалект + драйвер :// пользователь : пароль @ хост : порт / имя_базы
В нее нужно поместить следующие значения:
•диалект — тип базы данных;
• драйвер — драйвер, который вы хотите использовать для этой базы данных;
• пользователь и пароль — строки аутентификации для этой базы данных;
• хост и порт — расположение сервера базы данных (значение port нужно указывать только в том случае, если вы используете нестандартный порт);
•имя_базы — имя базы данных, к которой нужно подключиться.
В табл. 16.5 перечислены диалекты и драйверы.
Таблица 16.5. Соединение с SQLAlchemy
Диалект | Драйвер |
sqlite | pysqlite (можно опустить) |
mysql | Mysqlconnector |
mysql | Pymysql |
mysql | Oursql |
postgresql | psycopg2 |
postgresql | Pypostgresql |
Почитайте более подробно о диалектах SQLAlchemy для MySQL (), SQLite (), PostgreSQL () и других баз данных (/).
Уровень движка. Сначала обратимся к самому низкому уровню SQLAlchemy, возможности которого почти не отличаются от функций DB-API.
Попробуем поработать с SQLite, поскольку его поддержка уже встроена в Python. Строка соединения для SQLite опускает значения параметров хост, порт, имя_пользователя и пароль. Имя имя_базы информирует SQLite о том, какой файл использовать для хранения вашей базы данных. Если вы опустите параметр имя_базы, SQLite создаст базу данных в памяти. Если имя_базы начинается со слеша /, значит, это абсолютное имя файла на вашем компьютере (как в Linux и macOS). В противном случае это относительное имя текущего каталога.
Следующие сегменты являются частью одной программы, которую мы разделили для удобства объяснения.
Для начала нужно импортировать все, что нам понадобится. Следующая строка является примером импортирования псевдонима, который позволяет использовать строку sa для того, чтобы ссылаться на методы SQLAlchemy. Я делаю это в основном потому, что sa написать гораздо проще, чем sqlalchemy:
>>> import sqlalchemy as sa
Соединимся с базой данных и создадим хранилище в памяти (строка аргументов 'sqlite:///:memory:' также сработает):
>>> conn = sa.create_engine('sqlite://')
Создадим таблицу, которая называется zoo и имеет три столбца:
>>> conn.execute('''CREATE TABLE zoo
... (critter VARCHAR(20) PRIMARY KEY,
... count INT,
... damages FLOAT)''')
<sqlalchemy.engine.result.ResultProxy object at 0x1017efb10>
Вызов conn.execute() возвращает объект SQLAlchemy, который называется ResultProxy. Скоро вы увидите, что с ним можно сделать.
Кстати, если раньше вы никогда не создавали базы данных, примите мои поздравления. Можете вычеркнуть этот пункт из своего списка дел, которые в жизни обязательно нужно реализовать.
Далее вставьте три набора данных в новую пустую таблицу:
>>> ins = 'INSERT INTO zoo (critter, count, damages) VALUES (?, ?, ?)'
>>> conn.execute(ins, 'duck', 10, 0.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017efb50>
>>> conn.execute(ins, 'bear', 2, 1000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef090>
>>> conn.execute(ins, 'weasel', 1, 2000.0)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef450>
Сделайте выборку того, что только что разместили в базе:
>>> rows = conn.execute('SELECT * FROM zoo')
В SQLAlchemy rows не является списком — это специальный объект ResultProxy, который мы не можем отобразить непосредственно:
>>> print(rows)
<sqlalchemy.engine.result.ResultProxy object at 0x1017ef9d0>
Однако вы можете итерировать по нему, как по списку, и получать по одному ряду за раз:
>>> for row in rows:
... print(row)
...
('duck', 10, 0.0)
('bear', 2, 1000.0)
('weasel', 1, 2000.0)
Этот пример очень похож на тот, в котором использовался SQLite DB-API. Единственное преимущество подобного подхода состоит в том, что нам не нужно импортировать драйвер — SQLAlchemy сам определит драйвер на основе строки соединения. Простое изменение строки соединения позволит перенести код на базу данных другого типа. Еще один плюс SQLAlchemy заключается в наличии пула соединений, о котором вы можете прочитать в документации ().
Язык выражений SQL. Следующий уровень SQLAlchemy — язык выражений SQL. Он предоставляет функции, которые позволяют создать SQL для разных операций. Язык выражений обрабатывает большее количество различий в диалектах, чем низкоуровневый слой движка, и может оказаться полезным промежуточным решением для приложений, работающих с реляционными базами данных.
Рассмотрим создание и наполнение таблицы zoo. И снова все последующие фрагменты принадлежат одной программе.
Импортирование и подключение не изменяются:
>>> import sqlalchemy as sa
>>> conn = sa.create_engine('sqlite://')
Для того чтобы определить таблицу zoo, вместо SQL начнем использовать язык выражений:
>>> meta = sa.MetaData()
>>> zoo = sa.Table('zoo', meta,
... sa.Column('critter', sa.String, primary_key=True),
... sa.Column('count', sa.Integer),
... sa.Column('damages', sa.Float)
... )
>>> meta.create_all(conn)
Обратите внимание на круглые скобки в операции, которая занимает несколько строк в предыдущем примере. Структура метода Table() совпадает со структурой таблицы. Поскольку наша таблица содержит три столбца, в методе Table()тоже три вызова метода Column().
zoo представляет собой некий волшебный объект, который соединяет мир баз данных SQL и мир структур данных Python.
Запишите в таблицу данные с помощью новых функций языка выражений:
... conn.execute(zoo.insert(('bear', 2, 1000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017ea910>
>>> conn.execute(zoo.insert(('weasel', 1, 2000.0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017eab10>
>>> conn.execute(zoo.insert(('duck', 10, 0)))
<sqlalchemy.engine.result.ResultProxy object at 0x1017eac50>
Далее создадим оператор SELECT. Функция zoo.select() делает выборку всего, что содержится в таблице, представленной объектом zoo, как это сделала бы инструкция SELECT*FROMzoo в простом SQL:
>>> result = conn.execute(zoo.select())
Наконец, получим результат:
>>> rows = result.fetchall()
>>> print(rows)
[('bear', 2, 1000.0), ('weasel', 1, 2000.0), ('duck', 10, 0.0)]
Object-Relational Mapper (ORM). В предыдущем подразделе объект zoo был промежуточным звеном между SQL и Python. В самом верхнем слое SQLAlchemy объектно-реляционное отображение (Object-Relational Mapper, ORM) использует язык выражений SQL, но старается сделать реальные механизмы базы данных невидимыми. Вы определяете классы, а ORM обрабатывает способ, с помощью которого они получают данные из базы данных и возвращают их обратно. Основная идея, на которой базируется сложный термин «объектно-реляционное отображение», заключается в том, что вы можете ссылаться на объекты в своем коде и придерживаться таким образом принципов работы с Python, но при этом использовать реляционную базу данных.
Мы определим класс Zoo и свяжем его с ORM. На этот раз мы укажем SQLite использовать файл zoo.db так, чтобы мы могли убедиться в работе ORM.
Как и в предыдущих двух статьях, следующие фрагменты являются частью одной программы, разделенной пояснениями. Не переживайте, если чего-то не поймете. В документации к SQLAlchemy содержатся все необходимые подробности — работа с SQLALchemy может оказаться довольно сложной.
Я просто хочу, чтобы вы поняли, как много придется поработать, и чтобы сами могли решить, какой из подходов, рассмотренных в этой главе, подходит вам больше других.
Импорт остается неизменным, но в этот раз нам нужно кое-что еще:
>>> import sqlalchemy as sa
>>> from sqlalchemy.ext.declarative import declarative_base
Вот так создается соединение:
>>> conn = sa.create_engine('sqlite:///zoo.db')
Теперь мы начинаем работать с SQLAlchemy ORM. Определяем класс Zoo и связываем его атрибуты со столбцами таблицы:
>>> Base = declarative_base()
>>> class Zoo(Base):
... __tablename__ = 'zoo'
... critter = sa.Column('critter', sa.String, primary_key=True)
... count = sa.Column('count', sa.Integer)
... damages = sa.Column('damages', sa.Float)
... def __init__(self, critter, count, damages):
... self.critter = critter
... self.count = count
... self.damages = damages
... def __repr__(self):
... return "<Zoo({}, {}, {})>".format(self.critter, self.count,
... self.damages)
Следующая строка как по волшебству создает базу данных и таблицу:
>>> Base.metadata.create_all(conn)
Добавить данные в таблицу можно путем создания объектов Python. ORM управляет данными изнутри:
>>> first = ('duck', 10, 0.0)
>>> second = Zoo('bear', 2, 1000.0)
>>> third = Zoo('weasel', 1, 2000.0)
>>> first
<Zoo(duck, 10, 0.0)>
Далее мы указываем ORM перенести нас в страну SQL. Создаем сессию для коммуникации с базой данных:
>>> from sqlalchemy.orm import sessionmaker
>>> Session = sessionmaker(bind=conn)
>>> session = Session()
Внутри сессии записываем три созданных нами объекта в базу данных. Функция add() добавляет один объект, а функция add_all() добавляет список:
>>> session.add(first)
>>> session.add_all([second, third])
Наконец, нам нужно завершить сессию:
>>> session.commit()
Сработало? Файл zoo.db был создан в текущем каталоге. Вы можете использовать программу командной строки sqlite3, чтобы в этом убедиться:
$ sqlite3 zoo.db
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .tables
zoo
sqlite> select * from zoo;
duck|10|0.0
bear|2|1000.0
weasel|1|2000.0
Целью этого статьи было показать, что такое ORM и как оно работает на высоком уровне. Автор SQLAlchemy написал полное руководство к нему (). После прочтения статьи определитесь, какой из следующих уровней наиболее подходит для ваших нужд:
• простой DB-API, показанный ранее в этой главе в подразделе «SQLite»;
• движок SQLAlchemy;
• язык выражений SQLAlchemy;
• SQLAlchemy ORM.
Естественным выбором выглядит ORM, что позволит избежать всех сложностей SQL. Стоит ли им пользоваться? Некоторые люди считают, что ORM следует избегать, а другие полагают, что критика незаслуженна. Кто бы ни был прав, ORM — это абстракция, а все абстракции в какой-то момент разрушаются — они допускают утечки памяти (/). Если ORM не делает того, что вам нужно, вы должны понять, как он работает, а затем разобраться, как это исправить с помощью SQL. Перефразируя интернет-мем: «Столкнувшись с проблемой, некоторые люди думают: “Точно, обращусь-ка я к ORM!” Теперь у них две проблемы».
Используйте ORM для простых приложений или приложений, которые довольно точно отображают данные в таблицах базы данных. Но если приложение кажется простым, то вам, возможно, стоит использовать простой SQL (или язык выражений SQL).
Если вы ищете инструменты Python, которые позволяют работать со многими базами данных и имеют больше возможностей, чем простой DB-API, но меньше, чем SQLAlchemy, рассмотрите следующие варианты:
•dataset (/). Его девиз — «Базы данных для ленивых». Он создан на основе SQLAlchemy и предоставляет простой ORM для хранилищ SQL, JSON и CSV;
•records (/). Его разработчики считают, что это «SQL для людей». Он поддерживает только запросы SQL, используя SQLAlchemy для обработки проблем с диалектами SQL, пулами соединений и т.д., и интегрируется с tablib (упоминается в подразделе «Tablib» на с. 338), что позволяет экспортировать данные в форматах CSV, JSON и многих других.
Реляционные таблицы имеют форму прямоугольника, но поступающие данные могут иметь разную форму и их размещение требует больших усилий. Это похоже на проблему квадратного колышка и круглой дырки.
Отдельные нереляционные базы данных позволяют более гибко определять данные, работают с очень крупными наборами и поддерживают пользовательские операции с данными. Такие базы данных называют NoSQL (раньше это означало «не SQL», теперь же расшифровка звучит как «не только SQL»).
Простейшей вариацией баз данных NoSQL являются хранилища ключей-значений. О некоторых из них, которые попали в рейтинг популярности (), поговорим в следующих подразделах.
Форматы dbm существовали задолго до того, как появился NoSQL. Они представляют собой простые хранилища, работающие по принципу «ключ — значение». Их часто встраивают в приложения вроде браузеров, чтобы поддерживать различные настройки. База данных dbm похожа на обычный словарь в следующих отношениях:
• вы присваиваете значение ключу, и оно автоматически сохраняется в базе данных на диске;
• вы можете получить ключ по его значению.
Рассмотрим простой пример. Второй аргумент следующего метода open() может принимать значения 'r' для чтения, 'w' для записи и 'c' для того и другого, создавая файл, если его не существует:
>>> import dbm
>>> db = dbm.open('definitions', 'c')
Для того чтобы создать пары «ключ — значение», просто присвойте значение ключу, как если бы вы работали со словарем:
>>> db['mustard'] = 'yellow'
>>> db['ketchup'] = 'red'
>>> db['pesto'] = 'green'
Приостановимся и посмотрим, что мы уже имеем:
>>> len(db)
3
>>> db['pesto']
b'green'
Теперь закроем файл и откроем его снова, чтобы убедиться, действительно ли наши данные были сохранены:
>>> db.close()
>>> db = dbm.open('definitions', 'r')
>>> db['mustard']
b'yellow'
Ключи и значения сохраняются как байты. Вы не можете итерировать по объектам базы данных db, но можете получить количество ключей с помощью функции len(). Обратите внимание на то, что функции get() и setdefault() работают точно так же, как и для словарей.
memcached (/) — это быстрый сервер кэширования, располагающийся в памяти и работающий по принципу «ключ — значение». Часто его размещают перед базой данных или используют для хранения данных сессии веб-сервера. Вы можете загрузить версии для Linux, macOS () и Windows (). Если хотите попробовать запустить примеры, показанные в этом подразделе, вам понадобятся запущенный сервер memcached и драйвер Python.
Существует множество драйверов Python. Тот, что работает с Python 3, называется python3-memcached (). Установить его можно с помощью такой команды:
$ pip install python-memcached
Чтобы его использовать, подключитесь к серверу memcached, после чего вы сможете:
• устанавливать и получать значения ключей;
• увеличивать и уменьшать значения;
• удалять ключи.
Ключи и значения, хранимые в базе, неустойчивы и могут исчезать. Это происходит из-за того, что memcached является сервером кэша, а не базой данных. Он избегает ситуаций, когда у него заканчивается память, стирая старые данные.
Вы можете подключиться к нескольким серверам memcached одновременно. В следующем примере мы беседуем с одним сервером на том же компьютере:
>>> import memcache
>>> db = memcache.Client(['127.0.0.1:11211'])
>>> db.set('marco', 'polo')
True
>>> db.get('marco')
'polo'
>>> db.set('ducks', 0)
True
>>> db.get('ducks')
0
>>> db.incr('ducks', 2)
2
>>> db.get('ducks')
2
Redis — это сервер структур данных. Он работает с ключами и их значениями, но значения имеют гораздо больше возможностей, чем в других хранилищах. Как и в случае с memcached, все данные сервера Redis должны поместиться в память (хотя у нас имеется возможность сохранить все данные на диск). В отличие от memcached Redis может делать следующее:
• сохранять данные на диск для надежности в случае перезагрузки;
• хранить старые данные;
• предоставлять более сложные, по сравнению со строками, структуры данных.
Типы данных Redis близки к типам данных Python, и сервер Redis может быть полезным посредником для обмена данными между приложениями. Мне это кажется настолько важным, что я посвящу данной теме небольшой фрагмент книги.
Исходный код драйвера Python redis-py и тесты находятся на GitHub (), документация по нему находится по адресу . Драйвер устанавливается с помощью следующей команды:
$ pip install redis
Сам по себе сервер Redis (/) хорошо задокументирован. Если вы установите и запустите его на своем локальном компьютере, который имеет сетевое имя localhost, вы сможете запустить программы, описанные в следующих подразделах.
Строки. Ключ, имеющий одно значение, является строкой Redis. Простые типы данных Python автоматически преобразуются. Подключимся к серверу Redis, расположенному на определенных хосте (по умолчанию localhost) и порте (по умолчанию 6379):
>>> import redis
>>> conn = redis.Redis()
Строки redis.Redis('localhost') или redis.Redis('localhost',6379) дадут тот же результат.
Перечислим все ключи (которых пока нет):
>>> conn.keys('*')
[]
Создадим простую строку (с ключом 'secret'), целое число (с ключом 'carats') и число с плавающей точкой (с ключом 'fever'):
>>> conn.set('secret', 'ni!')
True
>>> conn.set('carats', 24)
True
>>> conn.set('fever', '101.5')
True
Получим значения согласно заданным ключам:
>>> conn.get('secret')
b'ni!'
>>> conn.get('carats')
b'24'
>>> conn.get('fever')
b'101.5'
Метод setnx() устанавливает значение, но только если ключа не существует:
>>> conn.setnx('secret', 'icky-icky-icky-ptang-zoop-boing!')
False
Метод не сработал, поскольку мы уже определили ключ 'secret':
>>> conn.get('secret')
b'ni!'
Метод getset() возвращает старое значение и одновременно устанавливает новое:
>>> conn.getset('secret', 'icky-icky-icky-ptang-zoop-boing!')
b'ni!'
Не будем сильно забегать вперед. Это сработало?
>>> conn.get('secret')
b'icky-icky-icky-ptang-zoop-boing!'
Теперь мы получим подстроку с помощью метода getrange() (как и в Python, смещение 0 означает начало списка, −1 — конец):
>>> conn.getrange('secret', -6, -1)
b'boing!'
Заменим подстроку с помощью метода setrange() (используя смещение, которое начинается с нуля):
>>> conn.setrange('secret', 0, 'ICKY')
32
>>> conn.get('secret')
b'ICKY-icky-icky-ptang-zoop-boing!'
Далее установим значения сразу нескольких ключей с помощью метода mset():
>>> conn.mset({'pie': 'cherry', 'cordial': 'sherry'})
True
Получим более одного значения с помощью метода mget():
>>> conn.mget(['fever', 'carats'])
[b'101.5', b'24']
Удалим ключ с помощью метода delete():
>>> conn.delete('fever')
True
Выполним инкремент с помощью команд incr() и incrbyfloat() и декремент с помощью команды decr():
>>> conn.incr('carats')
25
>>> conn.incr('carats', 10)
35
>>> conn.decr('carats')
34
>>> conn.decr('carats', 15)
19
>>> conn.set('fever', '101.5')
True
>>> conn.incrbyfloat('fever')
102.5
>>> conn.incrbyfloat('fever', 0.5)
103.0
Команды decrbyfloat() не существует. Используйте отрицательный инкремент, чтобы уменьшить значение ключа fever:
>>> conn.incrbyfloat('fever', -2.0)
101.0
Списки. Списки Redis могут содержать только строки. Список создается, когда вы добавляете первые данные. Добавим данные в начало списка с помощью метода lpush():
>>> conn.lpush('zoo', 'bear')
1
Добавим в начало списка более одного элемента:
>>> conn.lpush('zoo', 'alligator', 'duck')
3
Добавим один элемент до или после другого с помощью метода linsert():
>>> conn.linsert('zoo', 'before', 'bear', 'beaver')
4
>>> conn.linsert('zoo', 'after', 'bear', 'cassowary')
5
Добавим элемент, указав смещение для него, с помощью метода lset() (список уже должен существовать):
>>> conn.lset('zoo', 2, 'marmoset')
True
Добавим элемент в конец с помощью метода rpush():
>>> conn.rpush('zoo', 'yak')
6
Получим элемент по заданному смещению с помощью метода lindex():
>>> conn.lindex('zoo', 3)
b'bear'
Получим все элементы, находящиеся в диапазоне смещений, с помощью метода lrange() (можно использовать любой индекс от 0 до –1):
>>> conn.lrange('zoo', 0, 2)
[b'duck', b'alligator', b'marmoset']
Обрежем список с помощью метода ltrim(), сохранив только элементы в заданном диапазоне:
>>> conn.ltrim('zoo', 1, 4)
True
Получим диапазон значений (можно использовать любой индекс от 0 до –1) с помощью метода lrange():
>>> conn.lrange('zoo', 0, -1)
[b'alligator', b'marmoset', b'bear', b'cassowary']
В главе 15 показано, как использовать списки Redis и механизм публикации — подписки, чтобы реализовать очереди задач.
Хеши. Хеши Redis похожи на словари в Python, но содержат только строки, поэтому мы можем создать только одномерный словарь. Рассмотрим примеры, в которых создается и изменяется хеш с именем song.
Установим в хеше song значения полей do и re одновременно с помощью метода hmset():
>>> conn.hmset('song', {'do': 'a deer', 're': 'about a deer'})
True
Установим значение одного поля хеша с помощью метода hset():
>>> conn.hset('song', 'mi', 'a note to follow re')
1
Получим значение одного поля с помощью метода hget():
>>> conn.hget('song', 'mi')
b'a note to follow re'
Получим значение нескольких полей с помощью метода hmget():
>>> conn.hmget('song', 're', 'do')
[b'about a deer', b'a deer']
Получим ключи всех полей хеша с помощью метода hkeys():
>>> conn.hkeys('song')
[b'do', b're', b'mi']
Получим значения всех полей хеша с помощью метода hvals():
>>> conn.hvals('song')
[b'a deer', b'about a deer', b'a note to follow re']
Получим количество полей хеша с помощью функции hlen():
>>> conn.hlen('song')
3
Получим ключи и значения всех полей хеша с помощью метода hgetall():
>>> conn.hgetall('song')
{b'do': b'a deer', b're': b'about a deer', b'mi': b'a note to follow re'}
Создадим поле, если его ключ не существует, с помощью метода hsetnx():
>>> conn.hsetnx('song', 'fa', 'a note that rhymes with la')
1
Множества. Множества Redis похожи на множества Python, в чем вы сможете убедиться в следующих примерах.
Добавим одно или несколько значений множества:
>>> conn.sadd('zoo', 'duck', 'goat', 'turkey')
3
Получим количество значений множества:
>>> conn.scard('zoo')
3
Получим все значения множества:
>>> conn.smembers('zoo')
{b'duck', b'goat', b'turkey'}
Удалим значение из множества:
>>> conn.srem('zoo', 'turkey')
True
Создадим второе множество, чтобы продемонстрировать некоторые операции:
>>> conn.sadd('better_zoo', 'tiger', 'wolf', 'duck')
0
Пересечение множеств (получение общих членов) zoo и better_zoo:
>>> conn.sinter('zoo', 'better_zoo')
{b'duck'}
Выполним пересечение множеств zoo и better_zoo и сохраним результат в множестве fowl_zoo:
>>> conn.sinterstore('fowl_zoo', 'zoo', 'better_zoo')
1
Есть кто живой?
>>> conn.smembers('fowl_zoo')
{b'duck'}
Выполним объединение (всех членов) множеств zoo и better_zoo:
>>> conn.sunion('zoo', 'better_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}
Сохраним результат этого пересечения в множестве fabulous_zoo:
>>> conn.sunionstore('fabulous_zoo', 'zoo', 'better_zoo')
4
>>> conn.smembers('fabulous_zoo')
{b'duck', b'goat', b'wolf', b'tiger'}
Какие элементы присутствуют в множестве zoo и отсутствуют в множестве better_zoo? Используйте метод sdiff(), чтобы получить разницу множеств, и метод sdiffstore(), чтобы сохранить ее в множестве zoo_sale:
>>> conn.sdiff('zoo', 'better_zoo')
{b'goat'}
>>> conn.sdiffstore('zoo_sale', 'zoo', 'better_zoo')
1
>>> conn.smembers('zoo_sale')
{b'goat'}
Упорядоченные множества. Один из самых гибких типов данных Redis — это упорядоченное множество, или zset. Он представляет собой набор уникальных значений, но с каждым значением связан счетчик с плавающей точкой. Вы можете получить доступ к элементу по его значению или по счетчику. Упорядоченные множества применяются как:
• списки лидеров;
• списки вторичных индексов;
• временные ряды, где отметки времени используются как счетчик.
Мы рассмотрим последний вариант применения, отслеживая логины пользователей с помощью временных меток. Мы будем использовать значение времени epoch (подробнее об этом — в главе 10), которое возвращает функция time():
>>> import time
>>> now = time.time()
>>> now
1361857057.576483
Добавим первого гостя (он немного нервничает):
>>> conn.zadd('logins', 'smeagol', now)
1
Пять минут спустя добавим второго гостя:
>>> conn.zadd('logins', 'sauron', now+(5*60))
1
Через два часа:
>>> conn.zadd('logins', 'bilbo', now+(2*60*60))
1
Еще один гость не торопился и пришел спустя сутки:
>>> conn.zadd('logins', 'treebeard', now+(24*60*60))
1
Каким по счету пришел bilbo?
>>> conn.zrank('logins', 'bilbo')
2
Когда это было?
>>> conn.zscore('logins', 'bilbo')
1361864257.576483
Посмотрим, каким по счету пришел каждый гость:
>>> conn.zrange('logins', 0, -1)
[b'smeagol', b'sauron', b'bilbo', b'treebeard']
И когда:
>>> conn.zrange('logins', 0, -1, withscores=True)
[(b'smeagol', 1361857057.576483), (b'sauron', 1361857357.576483),
(b'bilbo', 1361864257.576483), (b'treebeard', 1361943457.576483)]
Кэши и истечение срока действия. У всех ключей Redis есть время жизни, или дата истечения срока действия. По умолчанию этот срок длится вечно. Мы можем использовать функцию expire(), чтобы указать Redis, как долго хранить заданный ключ. Значением является количество секунд:
>>> import time
>>> key = 'now you see it'
>>> conn.set(key, 'but not for long')
True
>>> conn.expire(key, 5)
True
>>> conn.ttl(key)
5
>>> conn.get(key)
b'but not for long'
>>> time.sleep(6)
>>> conn.get(key)
>>>
Команда expireat() указывает, что действие ключа истекает в заданное время epoch Unix. Истечение срока действия ключа полезно для поддержания свежести кэшей и ограничения количества сеансов входа в систему. Рассмотрим аналогию: в холодильнике, расположенном за стойками с молоком в вашем продуктовом магазине, работники избавляются от галлонов молока, когда у тех истекает срок годности.
Документоориентированные базы данных — это базы данных формата NoSQL, которые хранят данные с разными полями. В сравнении с реляционной таблицей (прямоугольной формы, с одинаковыми столбцами для каждой строки) данные, хранящиеся в этих таблицах, являются ragged — могут содержать разные поля (столбцы) в каждой строке, а также могут иметь вложенные поля. Можно обрабатывать такие данные с помощью словарей и списков или же сохранять их в файлах JSON. Для сохранения таких данных в реляционную таблицу нужно определить все возможные столбцы и использовать значения null для отсутствующих данных.
ODM может расшифровываться как Object Data Manager или Object Document Mapper. ODM является документоориентированным аналогом ORM — реляционных баз данных. Некоторые популярные () документоориентированные базы данных и инструменты (драйверы и ODM) перечислены в табл. 16.6.
Таблица 16.6. Документоориентированные базы данных
База данных | Python API |
Mongo (/) | tools () |
DynamoDB (/) | boto3 () |
CouchDB (/) | couchdb () |
PostgreSQL может выполнять некоторые задачи, которые выполняют и документоориентированные базы данных. Его расширения позволяют избежать определенных минусов реляционных баз данных, но сохраняют при этом такие особенности, как транзакции, валидация данных и внешние ключи: 1) многомерные массивы () позволяют хранить более одного значения в ячейке таблицы; 2) jsonb () позволяет сохранить в ячейке данные в формате JSON с полным индексированием и запросами.
Данные временных рядов могут быть собраны в фиксированные (например, метрики производительности компьютера) или случайные интервалы времени. Это привело к появлению множества методов их хранения (). Методы, поддерживаемые Python, перечислены в табл. 16.7.
Таблица 16.7. Временные базы данных
База данных | Python API |
InfluxDB (/) | influx-client (/) |
kdb+ (/) | PyQ (/) |
Prometheus (/) | prometheus_client () |
TimescaleDB (/) | PostgreSQL-клиенты |
OpenTSDB (/) | potsdb (/) |
PyStore () | PyStore (/) |
Последний вид данных, для которых нужна собственная база данных, представляет собой графы: узлы (данные), соединенные ребрами (отношениями). Отдельный пользователь Twitter может быть узлом с ребрами, ведущими к другим пользователям, на которых подписан он или которые подписаны на него.
Графовые данные стали появляться чаще по мере роста соцсетей, где связи так же важны, как и содержимое. Некоторые популярные () графовые базы данных приведены в табл. 16.8.
Таблица 16.8. Графовые базы данных
База данных | Python API |
Neo4J (/) | py2neo (/) |
OrientDB (/) | pyorient () |
ArangoDB (/) | pyArango () |
Серверы NoSQL, перечисленные здесь, могут работать с данными, объем которых превышает объем доступной памяти, — многие из них требуют использования нескольких компьютеров. В табл. 16.9 показаны наиболее популярные серверы и их библиотеки Python.
Таблица 16.9. Базы данных NoSQL
База данных | Python API |
Cassandra (/) | pycassa () |
CouchDB (/) | couchdb-python () |
HBase (/) | happybase () |
Kyoto (/) | kyotocabinet (/) |
MongoDB (/) | mongodb (/) |
Pilosa (/) | python-pilosa () |
Riak (/) | riak-python-client () |
Наконец, существует особая категория баз данных для полнотекстового поиска. Они индексируют все, поэтому вы легко можете найти то стихотворение, в котором говорится о ветряных мельницах и гигантских головках сыра. Популярные примеры таких баз данных с открытым исходным кодом и их Python API представлены в табл. 16.10.
Таблица 16.10. Полнотекстовые базы данных
Сайт | Python API |
Lucene (/) | pylucene (/) |
Solr (/) | SolPython () |
ElasticSearch (/) | elasticsearch (/) |
Sphinx (/) | sphinxapi (/ source/browse/trunk/api/sphinxapi.py) |
Xapian (/) | xappy (/) |
Whoosh () | Написан на Python, уже содержит API |
В предыдущей главе рассматривался вопрос выполнения разных фрагментов кода за разные промежутки времени (конкурентность). Следующая глава посвящена перемещению данных в пространстве (сети) — его можно использовать для написания конкурентного кода и для многого другого.
16.1. Сохраните следующие несколько строк в файл books.csv. Обратите внимание на то, что, если поля разделены запятыми, вам нужно заключить в кавычки поле, содержащее запятую:
author,book
J R R Tolkien,The Hobbit
Lynne Truss,"Eats, Shoots & Leaves"
16.2. Используйте модуль csv и его метод DictReader, чтобы считать содержимое файла books.csv в переменную books. Выведите на экран значения переменной books. Обработал ли метод DictReader кавычки и запятые в заголовке второй книги?
16.3. Создайте CSV-файл books.csv и запишите его в следующие строки:
title,author,year
The Weirdstone of Brisingamen,Alan Garner,1960
Perdido Street Station,China Miéville,2000
Thud!,Terry Pratchett,2005
The Spellman Files,Lisa Lutz,2007
Small Gods,Terry Pratchett,1992
16.4. Используйте модуль sqlite3, чтобы создать базу данных SQLite books.db и таблицу books, содержащую следующие поля: title (текст), author (текст) и year (целое число).
16.5. Считайте данные из файла books.csv и добавьте их в таблицу book.
16.6. Считайте и выведите на экран столбец title таблицы book в алфавитном порядке.
16.7. Считайте и выведите на экран все столбцы таблицы book в порядке публикации.
16.8. Используйте модуль sqlalchemy, чтобы подключиться к базе данных sqlite3books.db, которую вы создали в упражнении 16.4. Как и в упражнении 16.6, считайте и выведите на экран столбец title таблицы book в алфавитном порядке.
16.9. Установите сервер Redis и библиотеку Python redis (с помощью команды pipinstallredis) на свой компьютер. Создайте хеш redis с именем test, содержащий поля count(1) и name('FesterBestertester'). Выведите все поля хеша test.
16.10. Увеличьте поле count хеша test и выведите его на экран.
Однако на данный момент XML не поддерживается.