2015-11-26

Python 3.5 + Docker

Один из самых удобных и современных способов деплоя рабочего окружения является Docker. В предыдущих постах мы устанавливали свежий Python из исходников, но если у вас несколько серверов, да еще и с разными версиями ОС, то этот процесс может отличаться. К тому же это безумие в чистом виде. Как вариант предлагают использование virtualenv, но он тоже не чистая песочница, и использует часть библиотек окружения, что может иметь последствия. Короче, Docker - идеальная изоляция не в ущерб производительности, не зависящая от версии ОС и установленных в ней библиотек. На DockerHub уже есть готовые образы, часть из них официальные, часть от частных лиц. Мы возьмем официальный Python 3.5 и добавим нужные нам пакеты. Минимум телодвижений - и набор готов.

Будем использовать скрипт автоматизации, который называется Dockerfile. Для создания нужного образа достаточно будет выполнить команду в каталоге с Dockerfile:


docker build -t python35 .

А вот примерное содержимое этого конфигурационного файла:


FROM python:3.5
MAINTAINER Main Tainer maintainer@email.com

# Update and install needed packaged:
# libblas-dev and liblapack-dev for matplotlib, gfortran for scipy
RUN apt-get -y update && apt-get install -y libblas-dev liblapack-dev gfortran

# Install packages for DataScience
RUN pip3 install numpy scipy sklearn pandas matplotlib seaborn nltk ipython[all] jupyter

# Web frameworks
RUN pip3 install tornado flask flask-admin flask-login flask-restful
RUN pip3 install django django-bootstrap3 django-admin-bootstrapped

# Databases: psycopg2 (Postgres), pika (RabbitMQ), sqlalchemy (ORM)
RUN pip3 install psycopg2 sqlalchemy pymongo pika redis elasticsearch

# HTTP-requests http://docs.python-requests.org/en/latest/
RUN pip3 install requests requests_oauthlib

# Image manipulations https://pillow.readthedocs.org/en/3.0.x/
RUN pip3 install pillow

# Make C-modules http://cython.org/
RUN pip3 install cython

# Scheduler https://apscheduler.readthedocs.org/en/latest/
RUN pip3 install apscheduler

Вот и все, можно запускать!


docker run -ti python35 python

Python 3.5.0 (default, Nov 20 2015, 06:18:32)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Что-нибудь посодержательнее? Ок, прокинем в контейнер каталог с реальным кодом, например с реализацией алгоритма для вычисления количества шестизначных счастливых билетов, и выполним скрипт:


docker run -v /path/outside:/path/inside python35 python /path/inside/code.py

Ура, теперь то мы знаем, что их всего 55252.

2015-11-24

Python3.5 + PyQt5 - The Hard Way

Сегодня мы, в продолжение предыдущей статьи, установим библиотеку PyQt5 для собранного из исходников Python3.5. Для версии Python3, идущей в репозитории дистрибутива есть простой путь, и в общем случае лучше придерживаться его - он проще и дает аналогичный результат.


sudo apt-get install python3-pyqt5 pyqt5-dev-tools

Маководам тоже повезло, им также хватит одной строки (если стоит Homebrew, а куда ж без него?):


brew install pyqt5

Тех же, кто не ищет легких путей или кому нужна именно собственноручно собранная версия, милости прошу к прочтению.

Итак, поехали. Для начала, скачиваем подходящий установщик Qt5, мне нужен был для Linux x64, запускаем и устанавливаем, можно в каталог по умолчанию ~/Qt.

Качаем и распаковываем свежие версии PyQt5 и sip отсюда: http://sourceforge.net/projects/pyqt/files/. Помним, что мы установили python в локальный каталог ~/local. В каталоге с распакованным sip конфигурируем и устанавливаем:


python3 configure.py -d ~/local/lib/python3.5/site-packages/
make
make install

Для успешной сборки GUI модулей PyQt5 (типа PyQt5.QtWidgets) пришлось установить пакетик с исходниками mesa:


sudo apt-get install libgl1-mesa-dev

Теперь в каталоге с распакованным PyQt5 конфигурируем и устанавливаем:


python3 configure.py --destdir ~/local/lib/python3.5/site-packages/\
 --qmake ~/Qt/5.5/gcc_64/bin/qmake --disable QtPositioning
make
make install

Почему здесь фигурирует "--disable QtPositioning"? Потому что иначе не собирается, выбрасывая сообщение "qgeolocation.h: No such file or directory". Да и шут с ним. Вот собственно и всё, можно начинать писать код.


from PyQt5.QtWidgets import QApplication, QWidget
app = QApplication([])
w = QWidget()
w.resize(300, 200)
w.move(400, 400)
w.setWindowTitle('PyQt5 installed')
w.show()

Получите, распишитесь.

2015-11-19

Setup Python3.5 for DataScience

Цель - установить локальную версию свежего дистрибутива Python 3.5 и группу пакетов, полезных для ковыряния в данных. Платформа - Linux Mint 17.2 x64. Устанавливаем пакет с компиляторами на все случаи жизни, скачиваем с официального сайта архив с исходниками и распаковываем его, скажем, в каталог ~/Python-3.5.0. В терминале перемещаемся в него и начинаем.


sudo apt-get install build-essential git git-core xz-utils
wget https://www.python.org/ftp/python/3.5.0/Python-3.5.0.tar.xz
tar -xpJf Python-3.5.0.tar.xz
cd Python-3.5.0

Для начала нам нужно установить зависомости, без которых в дальнейшем не соберутся некоторые пакеты. Не буду приводить, какие для чего, оставлю для любознательных. Скажу лишь, что первая пара команд - для того, чтобы при компиляции Python не предупреждал нас о пропуске некоторых пакетов ввиду отсутствия библиотек. Это не повлияет на итоговый успех установки, но может всплыть позже.


sudo apt-get install tk-dev libsqlite3-dev libbz2-dev libfreetype6-dev
sudo apt-get install liblzma-dev libgdmb-dev libreadline-dev
sudo apt-get install lib32ncurses5-dev libpng12-dev libjpeg-dev tk
sudo apt-get install liblapack-dev libplas-dev gfortran libpq-dev

В среде Google Cloud в Ubuntu 15.04 пакеты libgdmb-dev и libplas-dev не находятся, возможно есть замена, но я не разбирался, не очень то и нужны.

Теперь собственно, сборка и проверка. Предварительно создадим каталог для локальной версии Python, у меня это ~/local.


./configure --prefix=$HOME/local
make -j4
make test
make install

У меня не установился модуль _ctypes, и непонятно, чего ему не хватило. Если знаете, в чем дело, напишите в комментариях. Стоит добавить в файл ~/.bashrc несколько строк, чтобы оболочка была в курсе, где наш новый Python и его библиотеки живут.


export PATH=$HOME/local/bin:$PATH
export LD_LIBRARY_PATH=$HOME/local/lib:$LD_LIBRARY_PATH
export C_INCLUDE_PATH=$HOME/local/include/:$C_INCLUDE_PATH

Теперь настало время для наших изыскательских (и просто ежедневно полезных) пакетов. Первая строка - mast have для исследователя, остальные - не обязательно, они просто хорошие :)


pip3 install numpy scipy sklearn pandas matplotlib seaborn ipython[all] jupyter
pip3 install tornado pillow psycopg2 sqlalchemy cython nltk
pip3 install requests requests_oauthlib elasticsearch
pip3 install django django-bootstrap3 django-admin-bootstrapped
pip3 install flask flask-admin flask-login apscheduler pymongo pika redis

С matplotlib могут возникнуть сложности, а именно - без выбрасывания ошибок не показывает окно с результатом. Чтобы этого не происходило мы и установили tk и собрали Python с tk-dev. Стоит проверить, что в файле с настройками matplotlib (у меня это '~/local/lib/python3.5/site-packages/matplotlib/mpl-data/matplotlibrc') выставлен верный параметр backend: TkAgg. Можно выставить его и прямо в коде. Также, можно попробовать установить matplotlib с github:


pip3 install git+https://github.com/matplotlib/matplotlib.git

Кроме того, при сборке matplotlib может ругнуться на отсутствие freetype, даже если установлен libfreetype6-dev. Такое я наблюдал, когда собирал Docker контейнер с основой на ubuntu:trusty. Помогло создание ссылки:


ln -s /usr/include/freetype2/ft2build.h /usr/include/

import numpy as np
import matplotlib.pyplot as plt
import matplotlib

matplotlib.use("TkAgg")
plt.imshow(np.random.random((10, 10)))
plt.show()

Красивая калякамаляка получилась!

2015-11-13

Custom Search Engine + Python

Использование Custom Search Engine API от Google из Python мне показалось несколько запутанным, поэтому опишу это пошагово, возможно кому-нибудь пригодится. Для начала, нам понадобится библиотека API Client Library for Python, установить которую не представляет труда:


sudo pip install --upgrade google-api-python-client

Во вторых, нам будет нужен идентификатор CSE, который мы для себя создадим на официальной странице. Добавьте в список сайтов для поиска "google.com", а переключатель "Поиск изображений" установите в положение ВКЛ. После того, как сохраним результаты, нажатие кнопки "Идентификатор поисковой системы" покажет нам то, для чего мы прошли все эти мытарства. Это будет похоже на "01345678901234567890:qwer23tyu_i".

Теперь отправляемся в консоль разработчика, а именно в раздел "APIs & auth/APIs". Найдите в списке "Custom Search API" и подключите его.

Там же в консоли переходим в пункт "Credentials", жмем "Add credntials", выбираем "API Key", затем жмем "Server key", даем ему имя и сохраняем. Забираем ключик, он будет похож на "QWerTyQWerTyQWe-QWerTyQWerTyQWerTyQWerTy".

На этом с ключами всё, пишем код на Python. В примере мы получаем все уникальные изображения пляжа, которые может позволить этот API:


from apiclient.discovery import build

service = build('customsearch', 'v1', developerKey="QWerTyQWerTyQWe-QWerTyQWerTyQWerTyQWerTy")

start = 1

while True:

    res = service.cse().list(
        q='beach',
        cx='01345678901234567890:qwer23tyu_i',
        fileType="png,jpg",  # bmp, gif, png, jpg, svg, pdf, ...
        imgColorType="color",  # mono, gray, color
        imgSize="medium",  # icon, small, medium, large, xlarge, xxlarge, huge
        imgType="photo",  # clipart, face, lineart, news, photo
        safe="high",  # high, medium, off
        searchType="image",
        start=start
    ).execute()

    for img in res["items"]:
        print(img["link"])

    if "nextPage" not in res["queries"]:
        break

    start = res["queries"]["nextPage"][0]["startIndex"]

2015-11-09

Выбор инструмента для быстрых микросервисов

Как известно, идеальных технологий не существует. Есть популярные в силу ряда причин (первые на рынке, удачный PR, хорошие менеджеры по продажам, даже реально классный код), есть незаслуженно забытые, есть продвинутые, но не для "простых смертных". Подбирая инструмент для решения какой-то задачи, мы обычно опираемся на собственный опыт (если есть) и на мнения других людей, желательно экспертов в исследуемой области. Проходя этот путь поиска я каждый раз ищу совершенство и каждый раз убеждаюсь, что его нет. Даже самый могучий ЯП и растопырчатый фреймворк имеют критиков с вескими аргументами.

Для реализации микросервисов, работа которых в том, чтобы пропускать через себя множество однотипных заданий и делать это быстро, я остановился на Go. Перечитал и пересмотрел много всего. Как питонщику он мне импонирует простотой и наличием встроенных батареек для решения повседневных задач, таких как работа с CSV и JSON, обработка HTTP-запросов и т.п. Авторы не ставили перед собой цель написать что-то концептуально новое, но сделать простой и годный инструмент, на котором писать просто, и работает быстро. Оно еще и компилируется моментально, что в процессе разработки чрезвычайно важно.

Что еще понравилось - так это статическая линковка, благодаря которой никаких зависимостей при деплое. Сервер с демонами может выглядеть как каталог с кучей бинарных файлов, файлом настроек и скриптом старта screen. Утрирую конечно, но как аккуратно получается! Вобщем, понравилось. Несколько экспериментов даже выложил на github:

2015-09-08

How to convert *.m4a to *.mp3 on Mac? The unix-way

Как сконвертировать файлы *.m4a в *.mp3 в MacOS? Первый способ - встроенный, с использованием iTunes, и хоть это и не unix-way, ради справедливости приведу и его.

В iTunes идем в меню iTunes -> Настройки -> Настройки импорта -> Импортер -> выбираем "Кодер MP3" (по умолчанию там "Кодер AAC"), сохраняем. Затем выбираем интересующие треки и в контекстном меню используем пункт "Создать версию в формате MP3". В каталоге с файлами *.m4a появятся и *.mp3, которые можно скопировать куда нужно, а затем удалить их через iTunes, чтобы в нем не остались битые ссылки.

А теперь как правильно. Копируем свежий ffmpeg, например, этот, распаковываем бинарник (мне для этого пригодился "The Unarchiver", он есть в App store, бесплатный, встраивается в Finder). Это всё, можно конвертировать. Открываем консоль, и запускаем что-то вроде этого:


/path/to/ffmpeg -i /path/to/file.m4a -acodec libmp3lame -ab 160k /path/to/file.mp3

Для конвертации всех файлов в текущем каталоге можно использовать такой скрипт:


for i in *.m4a; do /path/to/ffmpeg -i "$i" -acodec libmp3lame -ab 160k "${i%.*}.mp3"; done

Для удобства я добавил alias в файл .profile, очень удобно. Удачной конвертации!

2015-07-17

Быстрая переброска данных в удаленный Postgres

Передо мной стояла задача быстрой вставки большого количества JSON в Postgres на удаленном сервере. Обычное соединение к СУБД и использование "INSERT" нужной скорости не давало, даже с "executemany". Решением стал комплекс действий, состоящий из следующих этапов.

Первое - это компактификация пересылаемых данных (в моем случае это JSON). Для этого я использовал модуль dict_tools, который заменяет ключи в словаре с данными на уникальные и максимально короткие, возвращая словарь с заменами, который потом можно использовать для возвращения первоначальных имен.


from dict_tools import names_generator, names_replace
aliases = names_generator()
tweets = [{'text': 'Hello, world'}, {'text': 'Bye, chaos'}]
shorten, replaced = names_replace(tweets, aliases=aliases)

Кроме того, превращая словарь в JSON-строку использовал опции "separators", "encoding" и "ensure_ascii" - меньше пробелов и нормальные символы в UTF-8 вместо escape-последовательностей типа \u01010 для юникода:


def dumps(d):
    return json.dumps(d, separators=(',',':'), encoding='utf-8', ensure_ascii=False)

Второе - это отправка сжатых данных на сервер, где расположен Postgres. Для этого с одной стороны работает скрипт, использующий модуль requests и отправляющий данные. В словаре с замененными именами ключи и значения меняются местами, чтобы произвести обратную замену на принимающей стороне. Здесь "localhost:8888" просто для примера.


import requests
data = {
    'snames': dumps({v: k for k, v in replaced.items()}),
    'tweets': dumps(shorten)
}
r = requests.post('http://localhost:8888', data=data)
print(r.text)

На другой стороне висит минисервис на Tornado, принимающий присылаемые данные, возвращающий оригинальные ключи в JSON, размещающий данные построчно в CSV-файле и вызывающий команду Postgres "COPY".


import tornado.ioloop
import tornado.web
import json
import csv
import os
import psycopg2
from dict_tools import names_replace

class MainHandler(tornado.web.RequestHandler):
    def post(self, *args, **kwargs):

        tweets = json.loads(self.get_argument('tweets'))
        snames = json.loads(self.get_argument('snames'))
        tweets, replaced_names =  names_replace(tweets, repl=snames)

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dump.csv')

        fieldnames = ['id', 'src']
        with open(path, 'w') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
            writer.writeheader()
            for tweet in tweets:
                writer.writerow({
                    'id': tweet['id'],
                    'src': json.dumps(tweet, separators=(',',':'), encoding='utf-8', ensure_ascii=False).encode('utf-8')
                })

        con = psycopg2.connect(host='localhost', port=5432, user='XXX', password='XXX', database='XXX')
        cur = con.cursor()
        cur.execute("COPY dump_table(id, src) FROM '{}' WITH CSV HEADER".format(path))
        con.commit()
        con.close()

        self.write(u'Saved {} items'.format(len(tweets)))

if __name__ == "__main__":
    application = tornado.web.Application([(r"/", MainHandler)])
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

Такая схема у меня работает в несколько раз быстрее, чем вставка с удаленного сервера.

2015-07-15

Postgres + Python = Awesome

Чем больше работаю с Postgres, тем больше радует меня эта СУБД. Шикарная работа с JSON, позволяющая хранить структуры разнотипных данных в полях JSON и JSONB, и даже вешать на них индексы для быстрого поиска внутри них - находка для архивирования твитов и им подобных. Отличный набор функций для обработки текста, включая regexp. Типы-массивы (тоже с индексами). Переменных-таблиц нет, но их с успехом заменяют переменные-массивы или временные таблицы. Вот некоторые выражения, приводящие меня в щенячий восторг:


    -- Новая таблица по образцу выборки
    CREATE TABLE new_table AS SELECT field1, field2 FROM old_table LIMIT 10;

    -- Временная самоликвидирующаяся таблица
    CREATE TEMP TABLE IF NOT EXISTS new_table(id INT, data JSON) ON COMMIT DROP;

    -- Одно и то-же, во втором случае используется массив
    SELECT * FROM old_table WHERE id IN (1,2,3);
    SELECT * FROM old_table WHERE id = ANY('{1,2,3}');

    -- Создание массива в процессе группировки
    SELECT array_agg(text_id) FROM old_table WHERE id IN (1,2,3);

    -- JSON-массив JSON-объектов
    SELECT json_agg(t.jobj) FROM (
        SELECT json_build_object('text_id', id, 'record_id', _id) jobj
        FROM old_table
    ) t;

    -- Cоотношение твитов с вложенными чувствами и черствых:
    -- на выходе что-то вроде "{ "false" : 9115, "true" : 166 }"
    SELECT json_object_agg(t.ps, t.c) FROM (
        SELECT src#>>'{possibly_sensitive}' ps, COUNT(_id) c
        FROM tweet_archive
        WHERE src#>>'{possibly_sensitive}' = ANY('{false,true}') -- а тут массив с текстами
        GROUP BY src#>>'{possibly_sensitive}'
    ) t;

Для работы с Postgres используется Python и стандартная библиотека json. Для компактности и корректной работы с не-ASCII символами внутри JSON-полей Postgres при упаковке твитов используются такие параметры:

    json.dumps(tweet, separators=(',',':'), encoding='utf-8', ensure_ascii=False)

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


    CREATE OR REPLACE FUNCTION outer_procedure(p_limit INT) RETURNS VOID AS $$
        # Добавление пути для импорта нашего модуля
        import sys
        sys.path.insert(0, '/home/developer/plpython_func/')

        # Загрузка модуля
        import test_functions

        # На время разработки, пока модуль будет меняться,
        # его нужно насильно перезагружать, т.к. он кешируется
        reload(test_functions)

        # Собственно, запуск функции из модуля.
        # plpy - это объект для взаимодействия с БД
        test_functions.run(plpy, p_limit)

        return None
    $$ LANGUAGE plpythonu VOLATILE STRICT;

Внешняя процедура на Python (test_functions.py), которую мы загружаем:


    # -*- coding: utf-8 -*-

    import traceback

    def run(plpy, limit):

        # Готовим план выполнения
        sql = 'INSERT INTO new_table(id, text) VALUES($1, $2)'
        plan = plpy.prepare(sql, ["int", "text"])

        # Выполняем вставку внутри транзакции, ловим ошибки
        try:
            with plpy.subtransaction():
                plpy.execute(plan, [1, "text"])
        except plpy.SPIError as e:
            plpy.error("Error inside transaction: %s" % e.args)
        except:
            plpy.error(traceback.format_exc(10))

2015-06-23

Муки выбора

Наверное, это свойство присуще многим. Я имею ввиду поиск нового и лучшего в той сфере, которая тебе интересна. Но иногда этот поиск и муки выбора просто выводят из равновесия. Знакомо ли вам страдание по поводу выбора нового языка программирования? Долгие размышления о том, насколько он перспективен, как широки сферы его применения, насколько востребован на рынке и сколько платят разработчикам. Сканирование рынка вакансий, изучение рейтинга github.com, чтение статей на habrahabr.ru, сравнительных постов типа "A vs B" и т.п. Написание мегапроектов типа "Hello world", решений "вот оно!" и через день - возвращение сомнений "а оно ли?" - и все начинается сначала.

И вроде бы зачем выбирать, выучи всё, что нравится. Но без хорошей практики профи не станешь, а для практики во всём жизни не хватит. А годы идут без остановки, и новые технологии появляются постоянно. И многие весьма интересны, как новые игрушки для ребенка - хочется все их попробовать, повертеть, разобраться как работают. Да сколько можно то!

Конечно, время от времени приходит какое-то просветление, быть может это включается рациональное мышление. Есть сфера, в которой немало опыта, и которая нравится, стоит сделать упор на нее - а остальные ниши оставить другим. Только червячок сомнений "а вдруг это тоже очень интересно, а ты упустишь" заставляет иногда пускаться в хоровод.

Пост философский, просто вдруг найдется родственная душа со схожими переживаниями, и сможет поделиться, как с этим справляется.

P.S. Сейчас основным языком разработки у меня Python, а из нового в любимчики выбился Go. А почему именно он - может быть опишу отдельно.

2015-02-26

PostgreSQL: как получить несколько выборок (роусетов)

Переводя проект с использования AzureSQL (MSSQL) на PostgreSQL я столкнулся с тем, что последний не умеет возвращать результаты нескольких запросов SELECT (так называемые роусеты, т.е. наборы строк). К примеру, некая процедура генерирует SELECT из нескольких таблиц, чтобы отобразить отчет по этой сборной солянке. Так вот, в отличии от MSSQL, PostgreSQL возвращает только последий из них, такая уж особенность. Странно, но факт.

Что делать? Мне в голову приходили три варианта.

Один из них - создать в процедуре временные таблицы с гарантированно уникальными именами, сложить наборы строк в них и вернуть их список. На клиенте соответственно понадобится сделать запросы к этим таблицам и при необходимости их уничтожить. Минусов у такого подхода полно, взять хотя бы увеличение количества запросов к БД и возможные коллизии в случае забывания удаления временных таблиц (хотя это решаемо).

Второй способ - генерировать бинарные строки специфического формата (к примеру, Паскалевскую строку) из всех выборок и вернуть один SELECT с этими бинарниками. Минус - сложность при создании и распаковке на клиенте, хотя и позволяет получить сразу все данные одним запросом, к тому же очень компактно.

И наконец, третий вариант, который работает в PostgreSQL благодаря поддержке типа данных JSON. Думаю, простой демонстрации кода будет достаточно, чтобы понять, как это работает:

SELECT json_agg(json_build_array(r.id, r.title))
FROM (SELECT id, title FROM queries LIMIT 3) r

Результат:

[
    [1, "Query 1"],
    [2, "Query 2"],
    [3, "Query 3"],
]

Запрос:

SELECT json_build_array(
    (SELECT json_agg(json_build_array(r.id, r.title))
    FROM (SELECT id, title FROM queries LIMIT 2) r),
    (SELECT json_agg(json_build_array(r.id, r.name))
    FROM (SELECT id, name FROM companies LIMIT 2 OFFSET 2) r)
);

Результат:

[
    [
        [1, "Query 1"],
        [2, "Query 2"]
    ],
    [
        [3, "Company 1"],
        [4, "Company 2"]
    ]
]

2015-01-30

Как узнать пароль в форме

Иногда такое случается - забыть пароль. А вот браузер его помнит. И даже заполняет форму звездочками. Но вот скопировать эти звездочки в виде текста не удается.

А вот решение. Открываем панель просмотра элементов, в Google Chrome это происходит по клавише F12. Находим элемент с паролем и меняем его атрибут "type" с "password" на "text":



Вот собственно, и всё. Не оставляйте ваши компьютеры в чужих руках, ведь для вышеописанных действий достаточно 10-15 секунд!