Простой Python — страница 32 из 66

Некоторые базы данных не являются реляционными и не поддерживают SQL. Они были созданы для работы с очень крупными наборами данных, позволяют более гибко определять данные и поддерживают пользовательские операции с данными. Такие базы данных называют NoSQL (раньше это означало «не SQL», теперь же расшифровка звучит как «не только SQL»).

Семейство dbm

Форматы 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

memcached (http://memcached.org/) — это быстрый сервер кэширования, располагающийся в памяти и работающий по принципу «ключ — значение». Его часто размещают перед базой данных, также он может использоваться для хранения данных сессии веб-сервера. Вы можете загрузить версии для Linux, OS X (http://bit.ly/install-osx) и Windows (http://bit.ly/memcache-win). Если вы хотите попробовать запустить примеры, показанные в этом разделе, вам понадобятся сервер memcached и драйвер Python.

Существует множество драйверов Python, тот, что работает с Python 3, называется python3-memcached (https://github.com/eguven/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

Redis — это сервер структур данных. Как и в случае с memcached, все данные сервера Redis должны поместиться в память (хотя у нас имеется возможность сохранить все данные на диск). В отличие от memcached Redis может делать следующее:

• сохранять данные на диск для надежности в случае перезагрузки;

• хранить старые данные;

• предоставлять более сложные структуры данных, нежели строки.

Типы данных, используемые Redis, похожи на типы данных, используемые в Python, и сервер Redis может быть применен в качестве промежуточного решения для того, чтобы одно или несколько приложений делились данными друг с другом. Я нахожу это настолько полезным, что посвящу этому небольшой фрагмент этой книги.

Исходный код драйвера Python redis-py и тесты находятся на GitHub (https://github.com/andymccurdy/redis-py), вы также можете найти документацию по нему (http://bit.ly/redis-py-docs). Можно установить этот драйвер с помощью следующей команды:

$ pip install redis

Сам по себе сервер Redis (http://redis.io/) хорошо задокументирован. Если вы установите и запустите его на своем локальном компьютере, который имеет сетевое имя 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']

В главе 10 будет показано, как использовать списки 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, но если вы присваиваете пользователям увеличивающиеся числовые ID, биты помогут вам быстрее и компактнее решить эту задачу.

Начнем с создания последовательности битов для каждого дня. Для этой проверки мы используем всего три дня и несколько ID:

>>> days = ['2013-02-25', '2013-02-26', '2013-02-27']

>>> big_spender = 1089

>>> tire_kicker = 40459

>>> late_joiner = 550212

Каждая дата является отдельным ключом. Установим бит для конкретного пользователя в эту дату. Например, в первую дату (2013-02-25) у нас есть посещения от big_spender (ID 1089) и tire_kicker (ID 40459):

>>> conn.setbit(days[0], big_spender, 1)

0

>>> conn.setbit(days[0], tire_kicker, 1)

0

На следующий день big_spender вернулся:

>>> conn.setbit(days[1], big_spender, 1)

0

На следующий день у нас снова появился наш друг big_spender, а также новый человек, которого мы назвали late_joiner:

>>> conn.setbit(days[2], big_spender, 1)

0

>>> conn.setbit(days[2], late_joiner, 1)

0

Получим счетчик ежедневных посещений за эти три дня:

>>> for day in days:

…·····conn.bitcount(day)

2

1

2

Посещал ли сайт заданный пользователь в указанный день?

>>> conn.getbit(days[1], tire_kicker)

0

Значит, tire_kicker не посещал сайт во второй день.

Сколько пользователей посещает сайт каждый день?

>>> conn.bitop('and', 'everyday', *days)

68777

>>> conn.bitcount('everyday')

1

Угадайте с трех попыток, кто это:

>>> conn.getbit('everyday', big_spender)

1

Наконец, сколько уникальных пользователей посетили сайт за эти три дня?

>>> conn.bitop('or', 'alldays', *days)

68777

>>> conn.bitcount('alldays')

3

Кэши и истечение срока действия

У всех ключей 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() указывает, что действие ключа истекает в заданное время эпохи Unix. Это может оказаться полезным для того, чтобы кэш оставался свежим и чтобы ограничить сессии авторизации.

Прочие серверы NoSQL

Серверы NoSQL, перечисленные здесь, могут работать с данными, объем которых превышает объем доступной памяти, и многие из них требуют использования нескольких компьютеров. В табл. 8.6 показаны наиболее популярные серверы и их библиотеки Python.


Таблица 8.6. Базы данных NoSQL
СайтPython API
Cassandrapycassa
CouchDBcouchdb-python
HBasehappybase
Kyotokyotocabinet
MongoDBmongodb
Riakriak-python-client

Full-Text Databases