20091124

Скрипт для создания статического значка Flickr

Делюсь маленьким и несовершенным скриптом, который, надеюсь, кому-то всё же окажется полезен. Скрипт генерирует картинку-бейджик с последними фотками на Flickr. Зачем он нужен? Официальные виджеты Flickr основаны на Javascript и на флэше, их не везде можно вставлять. А статическую картинку можно куда угодно вставлять, хоть в ЖЖ, хоть на форумах...

В общем, скрипт состоит, на самом деле, из двух. Первый, вспомогательный, на питоне, flickrlatest.py находит и печатает ссылки на миниатюры N последних фоток заданного пользователя. Для работы нужно иметь ключ API, который скрипт считывает из файла ~/.flickr.apikey.

Второй скрипт, основной, обычный скрипт на bash, скачивает нужные картинки и объединяет их Graphics Magick-ом (при желании легко заменить на Image Magick, но, как я недавно узнал, GM быстрее, используется и на самом фликере, так что решил переходить потихоньку на GM).

Использование второго скрипта:

mkflickrbadge.sh пользователь геометрия файл-куда-сохранить.png


Несколько примеров.

$ mkflickrbadge.sh arboreus 3x2 arboreus-3x2.png


Flickr badge 3×2


$ mkflickrbadge.sh dobrych 5x1 dobrych-5x1.png


Flickr badge 5×1


Можно использовать NSID вместо имени пользователя:

$ mkflickrbadge.sh 7333287@N07 2x2 marjina-2x2.png


Flickr badge 2×2


Надеюсь, идея понятна. Скачать скрипты можно c битбакета (архив.zip). Распаковать, сделать скрипты исполняемыми, положить где-нибудь в PATH. Как добавить в crontab — умолчу.

P.S. Отмечу в качестве альтернативы скрипту — вебсервис http://www.flickriver.com/badge/create.

20091028

Переименование переменных и слияние изменений в Darcs

Ныне к традиционным холиварам, вроде vi против emacs, прибавился ещё hg (Mercurial) против git. И то, и другое — распределённые системы управления версиями (DVCS). В чём их преимущество перед старыми централизованными системами и как пользоваться новыми давно уже написано. Впрочем, выбор этими двумя системами не ограничивается, отдельные маньяки успешно пользуются и другими системами. А среди альтернативных систем совершенно особняком стоит darcs.

Почитал я тут руководство по darcs, и обнаружил что есть у него одна удивительная возможность, которой, насколько мне известно, у его более популярных собратьев нет. А именно, поддержка замен в управляемых файлах. Например, можно переименовать в одной ветке функцию или переменную, в другой ветке делать другие изменения, затрагивающие эти же строки, а потом совершенно волшебным способом автоматически объединить изменения обеих веток. И ручное слияние изменений не потребуется. Возможность настолько необычная, что захотелось поделиться.

Основное отличие darcs от собратьев: он отслеживает не состояние каталога с файлами (и историю его изменений), а хранит сами изменения — патчи. А уж состояние рабочего каталога определяется просто как результат применения всех накопленных изменений-патчей. Всякое такое изменение обратимо, а некоторые можно безболезненно переставлять местами (и это очень облегчает слияния).

В случае обычных DVCS, каждое изменение определяется разницей двух состояний каталога. Чтобы объединить такие изменения, нужен их общий «предок», к которому изменения можно применить. В darcs изменение не обязательно должно определяться разницей между двумя состояниями каталога. Это позвляет создавать разные типы изменений, и автор может определить что именно изменение делает (семантически). В том числе есть и такой вид изменений: замена слов в файле (token replace patch).

Покажу, как это работает, а вы уж сами судите, насколько это круто :-)

Завязка



Итак, создадим вначале исходный репозиторий и поместим в него простую программку. Тут отличия между darcs и hg или git минимальны:
$ mkdir repo-0
$ cd repo-0
repo-0$ darcs init
repo-0$ cat > hello.py
def hello(what):
print "Hello %s" % what

hello("World")
^D

repo-0$ darcs add hello.py
repo-0$ darcs record -m 'initial commit' hello.py
Recording changes in "hello.py":

addfile ./hello.py
Shall I record this change? (1/2) [ynWsfvplxdaqjk], or ? for help: y
hunk ./hello.py 1
+def hello(what):
+ print "Hello %s" % what
+
+hello("World")
Shall I record this change? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'initial commit'


А теперь создадим две ветки. В каждой ветке сделаем свои изменения. В одной (A) изменим название переменной what на name, а в другой (B) переименуем и перепишем функцию hello().

Внезапно!



Клонируем исходный репозиторий:
repo-0$ cd ..
$ darcs get repo-0 repo-A
Copying patches, to get lazy repository hit ctrl-C...
Finished getting.

И переименовываем в этой ветке переменную. Только хитрость, мы хотим не просто сделать замену слов в файле, а мы хотим явно указать darcs-у, что это именно замена слов. Поэтому вместо текстового редактора выполняем такую вот команду:
$ cd repo-A
repo-A$ darcs replace what name hello.py

Убеждаемся, что программка изменилась:
repo-A$ cat hello.py
def hello(name):
print "Hello %s" % name

hello("World")

И записываем изменения в репозиторий:
repo-A$ darcs record -m 'renamed: what to name' hello.py
Recording changes in "hello.py":

replace ./hello.py [A-Za-z_0-9] what name
Shall I record this change? (1/1) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'renamed: what to name'


Тем временем...



Параллельно создаём другую ветку и как-нибудь меняем функцию hello:
repo-A$ cd ..
$ darcs get repo-0 repo-B
Copying patches, to get lazy repository hit ctrl-C...
Finished getting.
$ cd repo-B
repo-B$ cat > hello.py
def hello(what):
if len(what) > 6:
print "Hello %s" % what
else:
print "Hi %s" % what

hello("World")
^D

Изменения настолько серьёзны, что старое имя функции уже не подходит. Переименовываем её с помощью darcs replace:
repo-B$ darcs replace hello greet hello.py

И записываем изменения:
repo-B$ darcs record -m 'changed hello and renamed to greet' hello.py
Recording changes in "hello.py":

hunk ./hello.py 2
- print "Hello %s" % what
+ if len(what) > 6:
+ print "Hello %s" % what
+ else:
+ print "Hi %s" % what
Shall I record this change? (1/2) [ynWsfvplxdaqjk], or ? for help: y
replace ./hello.py [A-Za-z_0-9] hello greet
Shall I record this change? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished recording patch 'changed hello and renamed to greet'


Кровавый финал



А теперь возвращаемся в исходный репозиторий и объединяем изменения:
repo-B$ cd ../repo-0
repo-0$ darcs pull ../repo-A ../repo-B
Wed Oct 28 17:21:41 CET 2009 me@example.com
* changed hello and renamed to greet
Shall I pull this patch? (1/2) [ynWsfvplxdaqjk], or ? for help: y
Wed Oct 28 17:12:12 CET 2009 me@example.com
* renamed: what to name
Shall I pull this patch? (2/2) [ynWsfvplxdaqjk], or ? for help: y
Finished pulling and applying.

И что же мы видим?
repo-0$ cat hello.py
def greet(name):
if len(name) > 6:
print "Hello %s" % name
else:
print "Hi %s" % name

greet("World")

Изменения объединились правильно. Система управления версиями оказалась достаточно умной, чтобы применить изменения в нужном порядке (вначале переписать функцию, а уж потом переименовать все случаи использования переменной).

Я впечатлён.

20091016

Микросоветы

Всё чаще в твиттер
одной строкой пост целый
пишу на память.

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

Приёмы работы
LaTeX и вёрстка
Программирование
Находки (всякие программки)

1. Приёмы работы:

  • Чтобы не закрывать Firefox, когда закрывается последняя вкладка по Ctrl-W: идём в about:config, находим browser.tabs.closeWindowWithLastTab, ставим false. Проверено на FF 3.5.
  • OpenOffice: чтобы запретить разрыв слова (т.е. запретить перенос), вставляем нечитаемый символ U+2060 (Zero-width WORD JOINER). Символ можно найти, запустив gucharmap. Надо в .XCompose добавить...
  • Чтобы использовать новый, сжимающий раза в два лучше, видео-кодировщик Theora 1.1, нужно взять саму новую библиотечку (уже есть в Debian unstable), и, главное, ffmpeg2theora версии не ниже 0.25. На сайте разработчиков есть и бинарные сборки.
  • Принудительное отключение подсветки ЖК-дисплея: xset dpms force off. Отсюда.
  • Банальность. Удаление пустых строк sed-ом: sed '/^\s*$/d'.
  • Редактируя диаграммы Graphviz в Vim, быстрый просмотр по :make можно сделать так: :set makeprg=dot\ -Tpng\ %\ \\\|display\ png:- errorformat='' autowrite. Подставить название используемой программы (dot, neato, fdp, ...).
  • Создание паролей (если нет KeePassX): cat /dev/urandom | tr -d -c 'a-zA-Z0-9' | fold -w 8 | head -1
  • Поиск и удаление дубликатов файлов: fdupes в командной строке, fslint — утилита с графическим интерфейсом.
  • В Debian можно заменить файл пакета, не пересобирая пакет. Поможет dpkg-divert.
  • sudo -i имитирует логин под рутом (даёт #). Бывает полезно (раньше sudo su - иногда пользовался).
  • Как создавать картинки предварительного просмотра видеофайлов:
    ffmpeg -itsoffset -1 -i видеофайл.avi -vcodec mjpeg -vframes 1 -an -y -f rawvideo -s 320x240 картинка.jpg ; done
    Как создавать картинки из PDF:
    convert -thumbnail 300x300 документ.pdf[0] -gravity center -extent 300x300 картинка.png


2. LaTeX и вёрстка:

  • Рекомендуемая минимальная ширина полей, чтобы документ можно было печатать и на A4, и на Letter — А4, слева и справа 20 мм, сверху и снизу 33 мм. RFC 2346.
  • Чтобы избежать разрыва страницы в LaTeX, можно поместить фрагмент текста в окружение samepage. Это частый вопрос.
  • Отступы элементов списка в LaTeX можно настроить, если использовать окружение list вместо itemize. Пример.
  • Чтобы добавить межабзацный пробел, \setlength{\parskip}{10pt plus 1pt minus 1pt}. Особенно полезно в наборе без абзацного отступа. Отсюда.
  • Чтобы выравнять картинку и текст справа от неё по вертикали, по середине, повозившись, сделал себе макрос \sidebyside{}{}:
    \newsavebox{\leftbox}\newlength{\leftboxheight}\newcommand{\sidebyside}[2]{\sbox{\leftbox}{#1}\settoheight{\leftboxheight}{\usebox{\leftbox}}\usebox{\leftbox}\raisebox{0.5\leftboxheight}{#2}}
    Смотрите пример использования.
  • Чтобы автоматически закрывать окружения LaTeX, пользователи Vim могут поставить плагин tex_autoclose. Использование: в режиме вставки Ctrl+\, затем c.
  • Разрезать на страницы и «склеивать» PDF-документы можно с помощью pdftk. Объединить два файла в один:
    pdftk первый.pdf второй.pdf cat output новый.pdf


3. Программирование:

  • В Python, примитивное транспонирование списка пар в пару списков:
    unzip = lambda pairs: zip(*pairs)
    @vlasovskikh подсказал, что для больших списков izip будет быстрее (проверили, так).
  • Занятное и доходчивое объяснение «что такое продолжения» на 11-й минуте видеопрезентации Swarm-dpl.
  • Быстро создавать графический интерфейс для научных программок позволяет библиотечка TraitsUI (Python). Пока не пробовал, но прочитал урок по TraitsUI.
  • Говорят, Intel готовит Concurrent Collections и для Хаскеля.
  • Я же пока проснулся и прочитал про со-процедуры на Си и устройство Даффа. Впечатлился.
  • Хотите полюбоваться, как можно добавлять побочные вычисления «наследованием» типов? Вот, пожалуйста, в этом примере (на Хаскеле). Хотя это, конечно не Java.
  • Учился использвать монадные трансформеры (бррр!) — оказалось несложно. В результате получился такой пример использования StateT поверх IO. Может кому пригодится.
  • Мелкое копирование словарей в Python — грабли.


4. Находки (всякие программки):

  • Atrack — анонимный открытый битторент трекер для Google App Engine. Всего 246 строк кода.
  • Sweet Home 3D — программа для планирования интерьера. Можно рисовать планы комнат, расставлять мебель, крутить по всякому. Сделана красиво.
  • fuse-zip — файловая система FUSE для монтирования zip-архивов. Быстрая, легко собирается по make, умеет писать в архив. Использование:
    fuse-zip архив.zip /точка/монтирования
    Есть также avfs, которая монтирует любые архивы, но не пишет и не такая удобная. Её использовать так:
    mountavfs ; ls ~/.avfs/полный/путь/к/архиву.zip#/файл/в/архиве
    В Debian нужно предварительно добавить пользователя в группу fuse.
  • Python(x,y) — дистрибутив Python для научных работников, для Windows и Ubuntu. Все инструменты и библиотечки «из коробки».
  • В дополнение к своему однострочнику antiodt нашёл ещё хороший конвертер ODT в Markdown odt2txt.py.
  • Дружественный к Гному вариант Xmonad — Bluetile. Раз попробовал, и две недели им пользовался.
  • Попробовал gitit. Самая простая вики для совместной работы над математическими текстами (вместе с jsMath из коробки). Хранилище — git или darcs.
  • TxtSushi — утилитки, позволяющие выполнять SQL-запросы по простому текстовому (CSV, TSV) файлу.


Ух-ты, а немало получилось.

20090922

Как нарисовать стрелку в Inkscape

Очень люблю Inkscape, и часто в нём рисую разные схемы. А для того, чтобы рисовать схемы, нужны стрелки. Готового инструмента «стрелка» в Инкскейпе нет*, поэтому творю из подручных материалов сообразно вкусу и потребностям. В общем-то, минутное дело, умеючи...

Я подумал, что кому-то, возможно, будет интересно увидеть, как можно самому нарисовать практически любую стрелку. Предлагаю небольшой видеоурок, где показываю как нарисовать стрелку попроще и стрелку позатейливей:



Это мой первый скринкаст. Прошу строго не судить. Кино немое, просто с титрами.

* В инкскейпе есть маркеры конца и начала линии. Красить их нельзя, они всегда чёрные. Впрочем, подсказывают из зала, можно — создав стрелку, надо «оконтурить обводку», получить два контура (древко и острие), вручную их подправить-подравнять, и получится полноценная контурная стрелка. Я пока остаюсь приверженцем своего способа создания контурных стрелок. Впрочем, смотрите на другой способ сами (подал идею и сделал видео freedomfidaj).

20090918

(Новичковые) ужасы Хаскеля

Я — начинающий программист на Хаскеле, и пока я ещё помню всё, чем он страшен. И это хочу записать. Сразу скажу, когда я приступал к Хаскелю, я ещё не знал практически ничего о функциональном программировании, поэтому одновременно с языком, нужно было осваивать новые идеи и образ мысли. И вообще-то это было здорово. А у страха, как известно, глаза велики. В общем, я думаю, эта заметка может быть полезна и другим начинающим. В ней я укажу на пять непонятных мне вначале, но относительно несложных вещей, поняв которые хотя бы приблизительно, освоить язык мне было гораздо легче.

1. Ламбда-функции


— Но мы называем его лембас или путевой хлеб, он подкрепляет лучше, чем любая пища людей, и он гораздо вкуснее.


Вообще-то, именно поэтому я и выучил Хаскель. Мне было просто любопытно, что означают все эти лямбды. Очень помогла в самом начале статья Пола Худака Conception, evolution, and application of functional programming languages (PDF также здесь). Возможно, есть введения и получше, но я начинал с него.

Ламбда-выражение — самая суть «функций» — это выражение вида
\lambda x \;.\; \text{expression with $x$}
Значением этого выражения является пока ещё безымянная функция одного аргумента (x), что-то с ним вычисляющая (а именно, выражение справа от точки). С лямбда-функциями связана серьёзная математическая теория, но с точки зрения программирования можно считать \lambda ключевым словом для определения функций. Действительно, когда я и до этого уже пользовался лямбдами в Питоне (и почти во всех других современных языках они тоже есть). В Питоне они выглядят вот так:
lambda x: expression with x
Ими было очень удобно пользоваться в filter() и reduce(). И вообще, почти везде, где в качестве аргумента требуется имя функции. Однако у лямбда-функций нет имён, и именно поэтому их ещё называют анонимными (безымянными) функциями. В пайтоне иногда я давал им имена прямо на лету:
add_42 = lambda x: x + 42
Теперь имя add_42 указывает на функцию. Точно такой же результат можно было получить, записав определение функции как обычно:
def add_42(x):
return x+42
А что же насчёт Хаскеля? Да почти то же самое. Символ \ заменяет \lambda, -> служит вместо точки. Всё вместе записывается так:
\x -> выражение с x
И мы даже можем давать имена таким безымянным функциям, так же как и в Питоне:
add_42 = \x -> x + 42
Согласитесь, очень похоже.

Однако тут есть одна тонкость. Как только я начал читать о Хаскеле, я увидел лямбда-выражения, которые поначалу казались немного странными:
\x -> \y -> выражение с x и y
Что означают все эти «стрелочки»? Ответ оказался очень прост и очень полезен в дальнейшем освоении языка.

В Хаскеле все функции являются функциями одного аргумента. Поначалу это может показаться ограничением, но на деле это очень удобная и практичная идея. Любую функцию n аргументов можно представить как функцию одного аргумента, возвращающую другую функцию n–1 аргементов. И по науке это азывается каррированием. Эта идея, в частности, позволяет передавать функции только часть аргументов.

Узнав об этом, мы теперь можем читать любые выражение с множеством «стрелок»:
\x -> (\y -> выражение с x и y)
Значением такого выражения будет фукнция, берущая один аргумент и производящая другую функцию, которая берёт ещё один аргумент. Такое выражение в целом ведёт себя как функция двух аргументов. Например, мы можем вычислить такую функцию двух аргументов в интерпретаторе Хаскеля ghci:
ghci> (\x -> \y -> x + y ) 3 4
7
Конечно, есть более краткий способ записи функций двух аргументов (обратите внимание, что список аргументов брать в скобки совсем не нужно):
ghci> (\x y -> x + y) 3 4
7
Однако знать, что на самом деле все функции нескольких аргументов являются функциями одного аргумента очень полезно. Например, это помогает читать описания типов функций. Например, тип функции map выглядит так:
map :: (a -> b) -> [a] -> [b]
Я обычно читаю это следующим образом: «функция map принимает два аргумента, первый — функцию преобразующую a в b, второй — список элементов типа a, а возвращает список элементов типа b». Но иногда гораздо естественнее записать тот же самый тип так:
map :: (a -> b) -> ([a] -> [b])
«Функция, которая берёт функцию, преобразующую a в b, и возвращает функцию, преобразующую список a в список b».

Даже эти простые понятия о лямбда-функиях были уже достаточны, чтобы начать пользоваться Хаскелем и понять большинство примеров и объяснений.

2. Знак равенства


— Ну что, если тут нет смысла, — сказал Король, — тогда у нас гора с плеч: нам незачем пытаться его найти! Сэкономим кучу работы! И все же...


По-моему, знак равенства (=) — самый важный символ в Хаскеле. Понять его важно для понимания языка. И мне кажется, смысл равенства недостаточно подчёркивается во всевозможных учебниках. Например, это единственное ключевое слово, отсутствующее в списке ключевых слов Хаскеля в его вики.

В отличии от большинства императивных языков, где = означает присваивание (то есть действие), в Хаскеле он означает, что левая часть равна правой (то есть описывает свойство).

Дополнение: в комментариях уточняют, «равно» в Хаскеле — связывание имени слева с определением справа.


«Равна» — не значит «становится». Это означает, что нечто равно чему-то ещё. Всегда. Как в математике. a = b в Хаскеле означает, что a равно b по определению, a эквивалентно b.

Таким образом, = в Хаскеле служит для записи определений. «Равно» может определять самые разные вещи, но определяет их статично. Оно не зависит от порядка выполнения операций. На него можно положиться.

Пользователям функциональных языком это покажется слишком уж очевидным, но именно в смысле знака равенства самое важное изменение для тех, кто раньше пользовался императивными языками. Теперь, кстати, мы можем давать имена нашим безымянным функциям:
add = \x -> \y -> x + y
Признаю, что читается это плохо, поэтому в большинстве случаев функции в Хаскеле определяются так:
add x y = x + y
Но и это по-прежнему определение функции add.

3. Классы типов


Significant benefits arise from sharing a common type system, a common toolset, and so forth. These technical advantages translate into important practical benefits such as enabling groups with moderately differing needs to share a language rather than having to apply a number of specialized languages. — приписывается Б. Страуструпу


Система типов в Хаскеле просто прекрасна. В ней очень легко и естественно выражаются многие идеи. И возможно, именно классы типов — это наименее чуждая концепция для тех, кто приходит в Хаскель из процедурного и объектно-ориентированного мира. Во всяком случае, мне так показалось. Однако классы типов — это совсем не то же самое, что классы в Си++ или в Джаве. Гораздо больше они похожи на абстрактные шаблоны классов в Си++, потому что классы типов
  • определяют только абстрактный интерфейс
  • позволяют создавать несколько независимых реализаций интерфейса (таким образом, для любого типа можно определить экземпляр класса, если предоставить реализацию его методов)
  • полиморфны по своей природе и поддерживают наследование
  • не могут иметь переменных состояния

Как только мы привыкнем, что типы классов — это не классы Си++, а абстрактные интерфейсы, и экземпляры классов это не «объекты», а конкретные реализации абстрактных интерфейсов, Хаскель сразу станет привычным и уютным.

Я очень советую почитать вики-статью OOP vs type classes, которая гораздо более детально сравнивает объектно-ориентированный подход и классово-типовой.


4. Монады


И так как всякое настоящее состояние простой субстанции, естественно, есть следствие ее предыдущего состояния, то настоящее ее чревато будущим, — Лейбниц, «Монадология»


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

Но вот что я понял: изучать абстрактную математику совсем не обязательно, чтобы монады использовать, а они и правда очень изящная программистская техника. Вначале они казались мне немного странными, но понять раз и навсегда монады гораздо легче, чем запоминать (и правильно применять!) бесчисленные шаблоны ОО-проектирования. Монады логичней.

Поскольку тьюториалов по монадом огромное множество, я не буду их здесь повторять и ожидаю, что вы их уже прочитал парочку. Что же не так с монадами? Для человека, привыкшему к императивным языка, испорченному годами объектно-ориентированного мышления монады кажутся странными. Они выглядят как абстрактный класс-контейнер с загадочным методом >>=:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
fail :: String -> m a
Хорошо, если return — конструктор, то почему такое чудное имя? Если это класс-контейнер, то как из него что-либо извлечь? И какой смысл применять функцию внутри контейнера (а именно это делает метод >>=, называемый также операцией связывания), если мы не можем вытащить результат из этого контейнера?

Отвечу вначале на последний вопрос. Зачем нужно связывание (>>=)? Монады являются и одновременно не являются контейнерами. Они — обёртки, упаковки для вычислений, а не для значений (return). Однако они обёртывают вычисления не для того, чтобы их было удобнее хранить в монадных коробочках, а чтобы их можно было удобнее соединять друг с другом. Вместо «коробочек» представьте обыкновенные кирпичи, которые ровно кладутся друг к другу. Это, кстати, похоже на шаблон Adapter в ОО-проектировании. Каждая монада определяет какой-то способ передавать результат от одного вычисления к другому и реализует стандартный интерфейс, чтобы этот способ использовать (>>=). И что бы ни случилось, результат всегда останется в той же монаде (даже, если произойдёт сбой, fail).

Самая простая программистская аналогия монадам, которую я придумал, это конвееры (pipes) в командной оболочке Unix. Монады обеспечивают однонаправленный конвеер для вычислений. То же самое делают и конвееры в Unix. Например:
$ seq 1 5 | awk '{print $1 " " $1*$1*$1 ;}'
1 12 83 274 645 125
seq создаёт список целых чисел. awk вычислят куб каждого из них. Что здесь замечательного? У нас есть две слабо связанные друг с другом программы, которым мы можем легко указать работать вместе. Поток текста создаваемый программой слева попадает по конвееру в программу справа, которая может читать этот поток, что-то с ним делать и создавать уже новый текстовый поток. Текстовый поток — общий формат для результата вычислений, | — операция, связывающая их воедино.

Монады очень похожи. >>= берёт внутреннее вычисление из монады слева и подставляет его в вычисление справа, которое всегда должно создавать ещё одну монаду того же типа.

Как вы уже, наверное, знаете, списки и тип Maybe в Хаскеле — монады. Например, пусть у нас есть простое вычисление, которое возвращает пару из числа и его куба обратно в монаду (return):
\x -> return (x, x^3)
тогда мы можем взять список и направить его «по конвееру» в это вычисление:
ghci> [1,2,3,4,5] >>= \x -> return (x,x^3)
[(1,1),(2,8),(3,27),(4,64),(5,125)]
Заметьте, что мы получили список пар. Это та же самая исходная монада (то есть список). Однако если мы возмьём значение Maybe и направим его в то же вычисление, на выходе у нас будет та же сама монада Maybe:
ghci> Just 5 >>= \x -> return (x,x^3)
Just (5,125)
Таким образом, мы можем создать конвеер из двух вычислений, и поведение этого конвеера зависит от контекста (т.е. от того, какая монада используется), а не от самих вычислений. В отличии от юниксовых конвееров, монады строго типизованы, и сама система типов заботится о том, чтобы выход одной монады был совместим с входом другой. И в отличии от юниксовых конвееров, мы можем задавать наши собственные правила связывания (>>=). Например, такие: «не делать более 42 вычислений подряд» или «посмотреть на входное значение, сделать то или это». Классы монад содержат в себе подобные правила, как соединять вычисления.
Дополнение: в комментариях подсказывают, что гораздо более подробно и более строго аналогия между юниксовым конвеером и монадами разобрана в статье Monadic i/o and UNIX shell programming.
Теперь, я надеюсь, вы понимаете монады не хуже меня (не обязательно полностью). Хочу обсудить несколько мнемонических правил. Почему return называется return?

В большинстве языков return возвращает результат вычисления из функции. В Хаскеле же он конструктор для монад. Это очень странно. Однако посмотрим как работает >>=: эта операция извлекает значение из монады слева, а затем связывает его с аргументом функции справа (отсюда, кстати, и другое название метода — bind). А функция справа должна вернуть значение обратно в монаду, чтобы можно было передать эстафетную палочку дальше следующей операции >>=. Это первое мнемоническое правило: return — возвращает вычисленное значения обратно в монаду.

Вторая мнемоника. Функция верхнего уровня любой программы на Хаскеле выполняется в монаде IO (тип функции mainIO ()). Эта монада позволяет выполнять ввод-вывод и вообще любые последовательные действия. Таким образом, монадный код выполняется на самом верхнем уровне программы, и именно он вызывает любой «чистый» код по мере необходимости, а не наоборот. Таким образом, любое «чистое» значение, если не отбрасывается, то рано или поздно возвращается в монаду её вызвавшую.

Надеюсь, что после этих объяснений имя return для монадного конструктора больше не кажется таким уж странным. Я, однако, не утверждаю, что мои объяснения 100% технически верны.

Следующий вопрос бывшего ОО-программиста, как вытащить значение вычисления из монады?. Начнём с того, что монады преднамеренно спроектированы именно так, что это не всегда возможно. Например, нельзя извлечь чистое значение из монады IO. Если дана такая «односторонняя» монада, то всё, что можно с ней делать — передавать внутреннее значение по монадному конвееру дальше. В Хаскеле разработан специальный синтаксис с ключевым словом do, которые делает такую многократную передачу монады по конвееру очень похожей на последовательную императивную программу. Следующие две программы делают одно и то же. Первая записана с do-нотацией:
main :: IO ()
main = do
name <- getLine
putStrLn ("Hi, " ++ name)
а вторая явно использует >>=:
main :: IO ()
main = getLine >>= \name -> putStrLn ("Hi, " ++ name)
Эквивалентная программа на Питоне:
from sys import stdin, stdout

if __name__ == "__main__":
name = stdin.readline()
stdout.write("Hi, " + name)
Однако иногда вытащить чистое значение из монадного вычисления можно. Это не предусмотрено общим монадным интерфейсом, поэтому разработчик монады должен специально предусмотреть возможность извлекать значения наружу. Например, можно извлекать значения из монады Maybe, используя функцию fromMaybe:
ghci> fromMaybe 0 $ Just 3
3
ghci> fromMaybe 0 $ Nothing
0


Заключение по монадам

Итак, связывание (>>=) позволяет объединять различные монадные вычисления вместе. Почти везде, где есть цепь вычислений, монады очень подходят. Конкретные реализации монад могут содержать разные правила комбинирования вычислений. Имя метода return сбивает с толку начинающих, это метод возвращает результа вычисления обратно в монаду, а не из монады. В общем, когда я понял эти простые идеи, это сильно помогло.

5. Страшные слова


Я знаю только то, что ничего не знаю.


Даже спустя месяцы после того, как я начал учить Хаскель, умея написать какие-то полезные программы на нём, я вижу вокруг, в мире Хаскеля, ещё много понятий, о которых не знаю ничего или имею только очень смутное представление. Я называю такие понятия «страшными словами». И я вижу, что есть люди, которые создают и используют библиотеки, воплощающие эти понятия в жизнь.

Надо признать, Хаскель остаётся испытательной площадкой для исследователей. И это одновременно и хорошо, и плохо. Это хорошо, потому что даёт чувство, что передний край науки и технологии очень близок, и можно при желании пользоваться преимуществами новых подходов. При желании. И одновременно это плохо, потому что иногда, когда хочется использовать новую изящную библиотеку, оказывается, что она активно использует незнакомые и не совсем понятные идеи, и нужно быть готовым такие идеи осваивать.

Например, есть современная XML-библиотека HXT. Она очень много использует стрелки. Стрелки — более универсальные комбинаторы вычислений, чем монады, но мне потребовалось гораздо больше, чем один день, чтобы их более-менее понять. Строго говоря, стрелки не являются частью языка, но они — понятие, которое применяют пользователи этого языка. И таких примеров немало. Хотя тем, кому стрелки осваивать не хочется, можно пользоваться более традиционной и активно поддерживаемой XML-библиотекой HaXml.

Я думаю, важно не бояться «страшных слов». К счастью, основополагающие идеи хорошо описаны. Как правило, есть статьи их очень детально объясняющие. Я сам решил осваивать такие идеи по мере необходимости. Это обещает быть и увлекательным, и одновременно посильным.

Заключение



Я перечислил пять простых идей, освоив которые, мне стало легче привыкнуть к Хаскелю. Лямбды — это просто способ записи функций, и функции нескольких аргументов можно всегда записать как функцию одного, возвращающую другую функцию. Типы классов очень похожи на абстрактные полиморфные интерфейсы в объектно-ориентированном подходе. Монады — стандартизованный способ соединять вычисления вместе. А страшные слова — просто страшные слова. Без них можно жить, но скучно.

Надеюсь, мои заметки будут полезны и ещё кому-нибудь.

Эта статья есть также по-английски. This post is also available in English.

20090908

Ещё одна библиотека комбинаторного парсинга

Не так давно я писал о библиотеке pyparsing для комбинаторного парсинга в Python. В комментариях появилась ссылка на ещё одну библиотеку, о которой я вначале не знал, а именно на funcparserlib, написанную Андреем Власовских.

В общем-то, я посмотрел на новую библитечку, и она мне тоже понравилась. Подкупает сравнительная простота самой библиотеки, ясные исходники и подробно написанные руководства — понять как работает библиотека нетрудно. Правда, при чтении документации нужно быть знакомым с нотацией типов, принятой в Haskell (ArgType -> ResultType). Общее впечатление: имеющиеся в funcparserlib комбинаторы практически полностью ортогональны друг другу, записываются кратко, места на экране занимают мало, называются понятно.

Комбинаторов в библиотеке всего-то: some, a, many, finished, maybe, skip, oneplus, forward_decl, +, | и >>. Действие большинства комбинаторов угадывается из названия. Рабочие лошадки: some(функция-предикат) берёт токен, удовлетворяющий условию, a(токен) берёт токен, указанный в аргументе, many(парсер), maybe(парсер), oneplus(парсер) — множественное, возможное или хотя бы однократное срабатывание парсера, skip(парсер) отбрасывает всё найденное парсером. Плюс (+) последовательно применяет два парсера, «или» (|) пробует альтернативные варианты, >> подставляет результат парсера в функцию и создаёт новый парсер (очень полезный комбинатор!). Ещё есть tuple для группировки.

Для пробы я решил написать пример разбора того же файлового формата, что и в заметке о pyparsing. Мне кажется, что наиболее эффективно применять funcparserlib с предварительным лексическим анализом (разбиением текста на токены-лексемы). В принципе, готовый токенизатор — часть стандартной библиотеки Python, поэтому это не проблема. Удобная обёртка приведена в руководстве к funcparserlib. Однако мне хотелось написать пример в том же стиле, что и для pyparsing, поэтому в примере ниже я разбираю текст на уровне отдельных символов.

Полный текст примера с тестами кладу в pastebin. Далее пояснения.

Половина кода пришлась на разбор чисел. Вообще, по-моему, в такого рода библиотеках было бы хорошо класть готовые парсеры для чисел, дат, e-mail и тому подобных обыденных вещей где-нибудь в разделе contrib... Впрочем, написать разбор чисел было даже занятно.

Чтение знака числа. Если ни плюса, ни минуса нет, подразумеваю плюс. Волшебный комбинатор >> позволяет обработать результат и сразу подготовить ответ, или -1, или +1:
sign = maybe(a("-")|a("+")) >> (lambda c: c == "-" and -1 or +1)

Целые числа состоят из знака и последовательности цифр. Так и запишем:
digits = many(some(lambda c: c.isdigit()))int_num = sign + (digits >> to_int) >> mk_int

Здесь я пользуюсь опять комбинатором >> и двумя вспомогательными функциями. Одна превращает последовательность символов-цифр в число (to_int), другая умножает результат на -1, если необходимо (mk_int). Эти функции вспомогательные, можно пропустить:
powers = lambda digs: zip(digs,xrange(len(digs)-1,-1,-1))add_digit = lambda acc, dp: acc+int(dp[0])*10**dp[1]to_int = lambda digs: reduce(add_digit, powers(digs), 0)mk_int = lambda (s,i): s*i

Аналогично строю и парсер для рациональных чисел, только прибавляю ещё и дробь, и, соответственно, ещё две вспомогательных функции:
to_frac = lambda digs: to_int(digs)*1.0/10**len(digs)mk_frac = lambda (s,i,f): s*(i+f)frac_num = sign + (digits >> to_int) + (skip(maybe(a("."))) + (digits >> to_frac)) >> mk_frac

Вот и всё. Использовать примерно так:
>>> frac_num.parse("-1.25")-1.25

Второй половиной кода оказалось написание парсеров для аналогов Literal и Word из pyparsing. Это необходимо, потому что без токенизации приходится распознавать цепочки токенов. Дополнительно создал парсер ws для пропуска пробелов:
pack = lambda cs: ''.join(cs)literal = lambda s: reduce(lambda a,b: a+b, map(a,s)) >> packword = lambda p: oneplus(some(p)) >> packws = skip(many(some(lambda c: c.isspace())))

После этих определений собственно парсер выбранного формата укладывается в три выражения:
varname = word(lambda c: c.isalpha())var = (ws + varname + ws + frac_num + ws)custom_format = skip(literal("Inspection")) + \ws + skip(literal("#")) + \ws + int_num + \ws + skip(literal("SHOULD")) + \ws + skip(literal("Ref. Sys")) + \ws + int_num + \many(var)

Смотрим на результат:
$ python test.py < input.txt (2, 1, [('X', 28.749300000000002), ('Y', 78.995999999999995), ('Z', -1.0014000000000001)])


В общем, впечатления хорошие. Документация у библиотеки приличная. Использовать приятно. Только одно досадное неудобство было связано с тем, что setup.py install --prefix=... из дистрибутивного тарбола не сработал как надо. Впрочем, библиотека такая маленькая, что можно положить все три её файла прямо в свой проект, без общесистемной установки.

Тонкости: нужно быть осторожным, не помещая универсально успешный парсер внутрь many, чтобы избежать вечного цикла. Короче, нельзя внутрь many помещать many, maybe и pure. Подробнее — см. FAQ.

См. также заметку про pyparsing.

Дополнение: слайды презентации funcparserlib на DevConf 2010:

20090904

Как сделать видеофайл из GIF-а и добавить поля к видео

Для того, чтобы из анимированного GIFа сделать видеофайл, я недавно использовал gifsicle (чтобы разоптимизировать GIF и разбить на кадры) и ffmpeg (чтобы сделать из кадров видео):
gifsicle -U --explode "input.gif"
for f in *.gif.* ; do mv "$f" "$f.gif" ; done
ffmpeg -r 25 -i "input.gif.%03d.gif" -sameq -s 320x240 output.flv

Если нужно добавить чёрных полей (до нужного размера), действую примерно так (в данном случае, хочу получить 320×240):
ffmpeg -i input.file -s 320x180 -padtop 30 -padbottom 30 output.file

Дополнение: с новыми версиями ffmpeg (например, 0.6.90), поля к видео добавляются с помощью видеофильтра pad:
ffmpeg -i input.file -vf "scale=320:180,pad=320:240:0:30" output.file

Я не использую для разделения на кадры ImageMagick (convert), потому что мне кажется, что gifsicle работает быстрее и требует меньше памяти.

(in English)

20090903

Как пометить пакеты в Aptitude, чтобы потом удалить

Очень полезная возможность в aptitude — пользовательские метки для выбранных пакетов.

Например, нужно поставить какой-то набор пакетов, чтобы собрать программу X из исходников, а потом нужно эти пакеты удалить. При установке помечаем выбранные пакеты какой-то своей меткой (builddeps в моём примере):
$ sudo aptitude install --add-user-tag builddeps libчто-то-dev libчто-то-ещё-dev ...

А потом, когда эти пакеты больше не требуются, их удаляем, выбрав по той же метке:
$ sudo aptitude purge '?user-tag(builddeps)'

Поисковый шаблон ?user-tag(метка) можно использовать совместно со всеми другими поисковыми шаблонами. Присваивать метки можно не только при установке (install), но и во многих других операциях.

Как видно из примера, особенно эта возможность полезна для самостоятельной сборки пакетов и программ из исходников.

P.S. Не помню, есть ли --add-user-tag в Ubuntu, но в Debian Lenny (aptitude-0.4.11) точно есть.

This post in English

20090828

Скорое обновление RuNIX.org

Наконец собрался и подготовил обновление планеты русских блогов о *NIX RuNIX.org. Дело в том, что скрипт Planet Planet, на котором аггрегатор работает сейчас уже давно не обновляется, а на смену ему пришёл Planet Venus. Вот на него и переходим.

Грядущие изменения, большие и маленькие:
  • Другой, на мой вкус более аккуратный и человечный, шаблон.
  • Список блогов теперь можно сворачивать (будет такой маааленький треугольник в заголовке списка). По наведению на название блога из списка, открывается меню недавних из этого блога.
  • Появилась возможность навигации с клавиатуры (для тех, кто читает планету на сайте). Переход между записями по клавишам j и k можно включить в боковой врезке.
  • Если в фиде определена картинка пользователя (Channel image или Icon), планета может её использовать. RSS 2.0 из ЖЖ, например, такие картинки отдаёт (соответственно, у некоторых авторов автоматически появятся портреты). Естественно, присылать нам свои фотки 64×64 по-прежнему можно.
  • После перехода на Planet Venus, мы теперь можем пропускать заметки с будущими датами и изменения atom:updated (Помните, когда порой вдруг валились кучей старые посты какого-нибудь блога, как правило на blogspot? Вот чтобы так не было, и нужно). Должно стать лучше.
  • Шаблон теперь на XSLT, поэтому появилась определённая свобода в обработке записей (я в XSLT не силён, но думаю, справимся). Можно будет что-то исправлять или фильтровать. Дополнительно теперь есть возможность подключать фильтры-плагины.
На сайт runix.org изменения попадут после того, как GQ установит на сервере новый скрипт. Посмотреть одним глазком на новую планету уже можно здесь — для сравнения сохранил как было.

Кто не заметил ссылку — новая планет будет выглядеть так.

Приветствуются замечания по делу (если по вёрстске — лучше сразу патч к CSS, если считаете, что что-то нужно фильтровать-менять — поделюсь XSLT, обсудим). Если что-то из ряда вон и лучше ничего не трогать — кричите!

Есть некоторые задумки (не факт, что скоро сделаю):
  • добавить какой-нибудь ticket-tracker, чтобы заявки и жалобы быстрее собирать и все могли их видеть и комментировать;
  • сделать javascript-овый флажок «пожаловаться» рядом с каждой записью, чтобы вовремя замечать неподходящий или неинтересный материал (я не всегда успеваю такой заметить, а написать письмо читателям обычно более лениво, чем ткнуть в кнопку);
Помощь и пожелания принимаются.

P.S. Да, забыл предупредить. Допускаю, что при обновлении скрипта некоторые записи пройдут повторно. Прошу извинить за неудобство.

20090827

Декоративная табличка в LaTeX

В TeXblog появилась заметка, как делать декоративные таблички в LaTeX. Вот такие:

Fancy tables with LateX and Tikz

Табличка набрана как обычно, но внутри «узла» окружения tikzpicture, а фон и раскрашенные шапки — средствами Tikz на фоне. Исходник примера — в TeX blog.

Кстати, давно хочу написать про PGF/Tikz. Что-нибудь интересно?

20090826

Не видно букв в японском PDF?

Бывают такие PDF, родом из Японии, в которых, если попытаться открыть их в Evince или XPDF — букв вообще не видно, а в Adobe Reader-е вместо букв видны только точки. В свойствах документа список встроенных шрифтов вообще выглядит пустым. Google Docs же такие PDF открывает, что интересно. И открыв такие PDF в Google Docs, можно увидеть, что в них всё таки есть кое-что и латиницей. Только латиница эта — из японских шрифтов (квадратная и широкая).

Оказывается, для отображения этих PDF нужно поставить кое-какие дополнительные пакеты. Для Evince — нужно поставить рекомендуемый пакет poppler-data*. Для XPDF — нужно поставить пакет xpdf-japanese*. И только после этого мы действительно сможем нормально смотреть такие PDF-файлы.

* названия пакетов даны для Debian/Ubuntu.

Старая флэшка монтируется только для чтения?

Уже не раз столкнулся: пользуясь ГНОМом, вставляешь какую-нибудь старую флэшку или карточку памяти в кард-ридер, она вроде как обычно автоматом подключается, но права доступа какие-то чудные (скажем, только для чтения). А другие карточки и флэшки вставляешь — всё нормально.

Догадался, что дело в том, что когда-то ещё на старой машине я настраивал параметры монтирования для каждого носителя отдельно. С тех пор домашний каталог благополучно переезжал из системы в систему, и настройки ГНОМа переезжали вместе с ним. А вот новые группы и пользователи не всегда точно соответствовали тому, что было раньше.

В результате, при попытке вставить какой-нибудь старый внешний носитель, ГНОМ использует настройки рассчитанные на совсем другую машину (другие группы и другую принадлежность пользователя к ним). Естественно, такие настройки gnome-mount надо просто удалить (ну или поправить), вот только найти их в гномовских закромах не так-то просто.

Мой способ: вставляю проблемный носитель и выполняю blkid. Например,
$ blkid 
/dev/sdb1: SEC_TYPE="msdos" LABEL="PALM-CARD" UUID="15F4-492C" TYPE="vfat"

Запоминаю UUID и запускаю редактор реестра gconf-editor. Нахожу /system/storage/volumes/_org_freedesktop_Hal_devices_volume_uuid_15F4_492C и там сбрасываю установленный ключ mount_options. Отсоединяю носитель и подключаю опять. Пока что все проблемы с монтирование старых внешних носителей разрешались именно таким образом.

20090825

Как отслеживать изменения файлов в скриптах

Задача: отслеживать в скриптах изменения файла или файлов и выполнять какие-нибудь команды, когда файлы изменяются.

Пример: пересобирать документ LaTeX при изменении (сохранении) одного из исходных файлов.

Решение: остлеживать изменения можно с помощью утилит inotify-tools. Одна утилита, inotifywait ждёт указанных изменений и после этого завершается с тем или иным кодом возврата. Если произошло ожидаемое событие, код возврата 0 (успех). Именно inotifywait и используется в моём примере ниже. Другая утилита, inotifywatch, наблюдает за файлами и собирает информацию об изменениях, на выходе выводит табличку того, что заметила. Примеры применения этой утилиты смотрите на сайте inotify-tools, там же есть и дополнительные примеры использования inotifywait.

Пример использования: в данном случае я предполагаю, что все исходные файлы документа LaTeX лежат в текущем каталоге, а для сборки достаточно использовать pdflatex и bibtex. Вечный цикл: ждём любых изменений файлов текста или библиографии (первая команда цикла), в случае успеха (обнаруженных изменений) исполняем все нужные команды сборки документа (вторая команда цикла).
while true ; do \
inotifywait *.tex *.bib \
&& ( pdflatex -interaction=nonstopmode mypaper && \
bibtex mypaper && \
pdflatex -interaction=nonstopmode mypaper ) \
done

Естественно, применять можно для чего угодно, не только для LaTeX.

P.S. Вариант запуска LaTeX с опцией -interaction=nonstopmode позволяет с одной стороны избежать запроса интерактивного ввода в случае ошибки компиляции, а с другой стороны, позволяет эти ошибки компиляции всё же увидеть.

P.P.S. Рецепт работает только в линуксе. Для *BSD есть библиотечка pnotify и kqueue.

20090820

Автоматические отступы в XML

Для просмотра какого-нибудь XML часто нужно автоматически его отформатировать (чтобы отступы слева соответствовали вложенности элементов). Особенно это полезно, когда весь исходный XML записан в одну большую строку. Такие файлы — это нечитаемая каша, которую, однако, легко привести в порядок.

Первый способ — используем XSLT
Есть у меня файл с вот таким XSL-преобразованием:
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml"/>
<xsl:param name="indent-increment" select="' '" />

<xsl:template match="*">
<xsl:param name="indent" select="'&#xA;'"/>

<xsl:value-of select="$indent"/>
<xsl:copy>
<xsl:copy-of select="@*" />
<xsl:apply-templates>
<xsl:with-param name="indent"
select="concat($indent, $indent-increment)"/>
</xsl:apply-templates>
<xsl:value-of select="$indent"/>
</xsl:copy>
</xsl:template>

<xsl:template match="comment()|processing-instruction()">
<xsl:copy />
</xsl:template>

<!-- WARNING: this is dangerous. Handle with care -->
<xsl:template match="text()[normalize-space(.)='']"/>

</xsl:stylesheet>

Код взял здесь (предложил Николай Григорьев). Там ещё несколько вариантов есть.

В дополнение к XSL-файлу есть у меня ещё и скрипт-однострочник, который это преобразование применяет. Я выполняю XSL с помощью любимого мной xmlstarlet. Это программка с интерфейсом командной строки для работы с XML.
#!/bin/sh
# указать правильный путь к файлу с преобразованием!
xmlstarlet tr ~/bin/indent-xml.xsl

Пользуюсь этим скриптом так:
$ xmlindent < исходный.xml | view -

И всё, можно читать любой XML с правильными отступами. И подсветкой синтаксиса (view — это Vim!). Кроме xmlstarlet есть и другие XSLT-процессоры. На память приходит xsltproc и библиотечки для разных языков программирования. Вот, например, однострочник на Python.

Второй способ — используем xmllint
В пакете libxml2-utils есть программка для проверки и форматирования XML — xmllint. Для форматирования использовать так:
$ xmllint --format исходный.xml

Так даже проще.

Третий способ — xmlindent
xmlindent — отдельная утилита, написанная на чистом Си. Говорят, работает и с задачей справляется.

По теме:
Редактирование HTML и XML в Vim (добавил про HTML Entities)
Выделение HTML-тегов, строк и блоков кода в Vim
Vim в терминале: сохранение отступов вставленного текста

20090720

Как ускорить или замедлить видеоролик

Иногда нужно замедлить (растянуть) видеоролик, чтобы он игрался как будто в режиме замедленного воспроизведения, а иногда нужно наоборот, показать «на перемотке» слишком длинный ролик, выбросить часть кадров и ускорить воспроизведение. О том как это сделать — сия заметка.

Изменить частоту кадров в видеопотоке позволяет программа yuvfps из пакета mjpegtools. Как и большинство утилит пакета она принимает и отдаёт видеопоток в формате YUV4MPEG. И ffmpeg, и mencoder тоже умеют работать с YUV4MPEG (и умеют читать и писать всякие другие форматы). Я приведу пример использования ffmpeg.

Итак, чтобы ускорить видео, нужно взять исходный файл и посмотреть, какая в нём частота кадров
$ ffmpeg -i normal.ogg
FFmpeg version SVN-r13582, Copyright (c) 2000-2008 Fabrice Bellard, et al.
...
Input #0, ogg, from 'normal.ogg':
Duration: 00:00:10.49, start: 0.000000, bitrate: 150 kb/s
Stream #0.0: Video: theora, yuv420p, 320x240 [PAR 1:1 DAR 4:3], 30.00 tb(r)
Must supply at least one output file

В данном случае исходный файл — 30 кадров в секунду. Затем нужно решить, во сколько раз уменьшить число кадров (исходя из желаемой длительности ролика). Потом берём исходный файл (normal.ogg в примере) и преобразовываем его в YUV4MPEG-поток (первый вызов ffmpeg), после нужно дважды вызвать yuvfps, первый раз, чтобы изменить число кадров в потоке (yuvfps -s 5:2 -r 1:1 сокращает исходные 2.5 кадра до одного), второй раз, чтобы перезаписать заголовок потока, указав скорость воспроизведения (yuvfps -r 30:1 -c устанавливает скорость 30 кадров в секунду). В конце опять вызываем ffmpeg и кодируем в нужный формат (я сохранил в формате Ogg/Theora, чтобы можно было вставлять в веб-странички тэгом <video>). Всё вместе:
$ ffmpeg -i normal.ogg -sameq -f yuv4mpegpipe - | \
yuvfps -s 5:2 -r 1:1 | yuvfps -r 30:1 -c | \
ffmpeg -f yuv4mpegpipe -i - -sameq -y fast.ogg


Аналогично можно увеличить число кадров. Дополнительные кадры интерполируются:
$ ffmpeg -i normal.ogg -sameq -f yuv4mpegpipe - | \
yuvfps -s 1:3 -r 1:1 | yuvfps -r 30:1 -c | \
ffmpeg -f yuv4mpegpipe -i - -sameq -y slow.ogg

В этот раз видео замедляется в 3 раза: на каждую «треть» исходного кадра (-s 1:3) создаётся целый кадр (-r 1:1). Вообще, как легко заметить, в качестве частоты кадров для yuvfps можно указывать любые дроби в виде числитель:знаменатель.

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

Теперь можно сравнить все три видео. Для просмотра нужен современный браузер с поддержкой тэга <video/>.

Исходное видео:



Ускоренное в 2.5 раза:



И замедленное в 3 раза:



P.S. В качестве иллюстрации использовал ролик Breitenlee-VESTAS-V-52.

20090714

Вы не поверите — Ubuntu Cola!

Эта заметка не про линукс. Эта заметка про газированный напиток Ubuntu Cola. Вот он:



Купил вчера в автомате в итальянском университете за 1,80 €. На этикетке спереди значок Fairtrade. Сзади надпись:
Ubuntu. “Я есть, потому что есть мы”. Благодаря программе Fairtrade производители тростникового сахара в Малави и Замбии могут заключать более выгодные контракты и вкладывать средства в социальные, экономические и природоохранные проекты. К тому же, мы возвращаем 15% нашей прибыли в эти страны через программу Ubuntu Africa. Посетите: www.ubuntu-trading.com
Вот такая кола. Вот такая убунту. Кстати, на вкус оказалась довольно хороша.

20090712

Необыкновенно лёгкий парсинг в Python

Нашёл просто волшебную библиотечку для парсинга в Python (хм, правильно говорить синтаксического анализа), pyparsing. Ниже на простом примере я покажу, как её можно использовать для разбора пользовательских форматов данных.

Нашёл так: читая Real World Haskell, узнал про комбинаторную библиотеку для синтаксического анализа Parsec. Примеры в книжке впечатлили. В отличие от традиционного подхода, при этом нет разделения на лексический анализ (выделение «слов»-лексем) и синтаксический анализ (преобразование потока «слов» в упорядоченную структуру данных) — в комбинаторном парсинге эти два этапа объединяются. Берутся небольшие функции, распознающие элементы текста, и затем они комбинируются в соответветствии с синтаксисом текста. Таким образом, сама комбинация функций непосредственно отражает грамматику, и она же, естественно, является и программой для разбора текста. Как у всякой удачной идеи, у Parsec есть множество подражаний. Для Python комбинаторных парсеров нашлось целых два уже три уже четыре — Pysec, Pyparsing, LEPL (для Python 2.6/3.0) и funcparselib. Я буду говорить о pyparsing.

В следующей заметке — Ещё одна библиотека для комбинаторного парсинга — смотрите аналогичный пример для библиотечки funcparserlib.


Итак, перейдём к делу. Предположим нужно читать файлы состоящие из записей следующего вида:
Inspection
# 2 SHOULD Ref. Sys 1
X 28.7493
Y 78.9960
Z -1.0014

Всё необходимое импортируем из модуля pyparsing. При работе поглядываем в документацию к модулю. Для простоты примера импортируем всё:
from pyparsing import *

Теперь начинаем описывать грамматику. Например, определим числа как слова, состоящие из цифр, знака точки и дефиса (минуса)
number = Word(nums + ".-")

а значения переменных определим как пару заглавной латинской буквы и числа:
var = Regex("[A-Z]") + number

Обратим внимание, что плюс между двумя простыми парсерами (регулярное выражение и слово) создаёт новый парсер, который распознаёт уже последовательность выражений. По-умолчанию pyparsing игнорирует все лишние пробелы и переводы строк между элементами разбираемого текста (обычно именно это и нужно), поэтому указывать в грамматике наличие пробелов между элементами необязательно.

Уже на этом этапе мы можем попробовать наш парсер переменных. Запускаем интерпретатор и выполняем:
>>> var.parseString("X   42.0")
(['X', '42.0'], {})

— на выходе получили структуру данных в соответствии с нашей грамматикой (имя переменной и число за ним).

Допишем всё остальное. Для простоты будем считать комментарием всё после знака «#» до конца строки (комбинатор restOfLine):
comment = "#" + restOfLine

Теперь мы можем описать грамматику всей записи в целом.
record = Suppress("Inspection" + comment) + OneOrMore(var)

Запись опознаём по слову «Inspection» в начале (здесь строковой литерал Python автоматически конвертируется в Literal-парсер, проверяющий буквальное соответствие слову). Далее, обнаружив начало записи, состоящие из слова «Inspection» и следующий за ней комментарий, мы можем их просто пропустить (комбинатор Suppress), а вот то, что следует дальше — нам интересно. Мы ожидаем, что дальше могут идти значения для одной или нескольких переменных (применяем комбинатор OneOrMore).

Последний штрих. Нужно указать, что в файле таких записей может быть несколько. Для удобства работы с полученной структурой переменные каждой из записей группируем вместе (комбинатор Group):
datafile = OneOrMore(Group(record))

Всё! Синтаксический анализатор для нашего формата данных готов. Использовать можно, например, так:
import sys
print datafile.parseString(sys.stdin.read())


Проверяем:
$ python example.py << END
> Inspection
> # 2 SHOULD Ref. Sys 1
> X 28.7493
> Y 78.9960
> Z -1.0014
>
> Inspection
> # 3 SHOULD Ref. Sys 1
> X 54.0394
> Y 64.3977
> Z -0.9950
>
> END
[['X', '28.7493', 'Y', '78.9960', 'Z', '-1.0014'],
['X', '54.0394', 'Y', '64.3977', 'Z', '-0.9950']]

Получили вполне пригодную к использованию в программе структуру данных. Вся грамматика — на пять строк. В общем-то, поняв идею и поглядывая в справку, несложно описать и более сложную грамматику.

Например, чтобы разбирать также и строчку с «#» в моём примере, программку можно изменить так:
from pyparsing import *
number = Word(nums + ".-")
var = Regex("[A-Z]") + number
desc = Suppress("#") + Word(nums) + Word(alphas) \
+ Suppress("Ref. Sys") + Word(nums)
record = Suppress("Inspection") + desc + Group(OneOrMore(Group(var)))
datafile = OneOrMore(Group(record))

На выходе этот парсер даст:
[['2', 'SHOULD', '1', [['X', '28.7493'], ['Y', '78.9960'], ['Z', '-1.0014']]],
['3', 'SHOULD', '1', [['X', '54.0394'], ['Y', '64.3977'], ['Z', '-0.9950']]]]


P.S. Нормального тьюториала по pyparsing в сети я не нашёл, но автор библиотеки написал и продаёт на O’Reilly учебное электропособие за 10 долларов. Справочная же документация и разные примеры в интернете — вполне толковы.

См. также заметку про funcparserlib.

20090701

Сортировка фото по дате EXIF

На днях и я, и virens почти синхронно написали две заметки об EXIF: как исправить дату EXIF в фото и как добавлять пользовательские теги в raw-файлы. Раз уж тема начата, то поделюсь ещё одним приёмом.

Фотографии я храню, группируя каталоги по годам и по датам съёмки (указывая дату в формате ISO), то есть в архиве путь к альбому у меня примерно такой: photos/2009/20090628 - название альбома/. Очень удобно, потому что обычно один день — одна тема, и даже при алфавитной сортировке каталога альбомы упорядочены хронологически. И такая организация не зависит ни от операционной системы, ни от конкретной программы-каталогизатора.

Однако если на карточке фотографии разных дней — раскидывать их по альбомам вручную утомительно. Поэтому у меня есть ещё и скрипт-сортировщик для внесения фото в архив. Он смотрит на дату в EXIF, создаёт нужные каталоги и помещает в них фото:
#!/bin/sh

ARCHIVE=$HOME/photos

for f in "$@"; do
DT=$(exiftool -s -DateTimeOriginal "$f")
YEAR=$(echo $DT|awk '{print $3;}'|awk -F: '{print $1;}')
ISODAY=$(echo $DT|awk '{print $3;}'|sed 's/://g')
TARGET="$ARCHIVE/$YEAR/$ISODAY"
install -d "$TARGET" && \
install "$f" "$TARGET"
echo "$f -> $TARGET"
done


Запускаю из каталога с фотокарточки:
$ import-photos *

Так можно импортировать и JPEG-и, и RAW. И там, и там EXIF обычно есть.

Как ускорить Firefox на eeePC 901

На моём eeePC 901 Firefox часто подтормаживал, и это явно совпадало с работой диска. Объяснение нашлось здесь.

Дело в том, что в eeePC 901 два флэш-диска, маленький на 4 ГБ и большой 16 ГБ, и этот большой флэш-диск — медленный. Домашний же раздел /home у меня, естественно, на большом, и там же профиль Firefox-а. Браузер же по-умолчанию сохраняет в него текущую сессию каждые 10 секунд и туда же пытается писать кэш. Пишет он синхронно (то есть ждёт, пока не запишется), поэтому и замирает на секунду при каждом чихе.

Народная медицина в этом случае рекомендует пойти в about:config и там

1) создать ключ toolkit.storage.synchronous с целым значением 0 (запись на диск вести асинхронно, то есть не ждать, пока, например, состояние сессии действительно запишется)

2) создать или изменить ключ browser.cache.disk.parent_directory, его строковое значение установить в /dev/shm/firefox-username (сохранять кэш в памяти, а не писать на диск; кэш, конечно, будет утерян при перезагрузке, и памяти потребуется больше, но зато и работать будет быстрее)

Дополнительно я увеличил промежуток времени между записями состояния сессии (списка открытых вкладок):

3) в browser.sessionstore.interval поставил 60000 (60 секунд), вместо 10000 (10 секунд).

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

Советы, полагаю, применимы не только к eeePC, но и к другим нетбукам с SSD (флэш-дисками).

В предыдущей заметке можно прочитать, как мы ставили и настраивали Debian на eeePC.

20090629

Как исправить дату EXIF в фото

Иногда, отфотографировав день-два-три можно обнаружить, что всё это время в камере стояла неправильная дата. Я тут как раз ошибся: на один год (вперёд), 12 часов (a.m./p.m, эх) и один часовой пояс.

Дело поправимое. Поможет exiftool. Чтобы откатить дату на один год и 12+1 (13) часов, поступаем так:
$ exiftool "-DateTimeOriginal-=1:0:0 13:00:00" *.jpg

Смотрим, что получилось, и если что-то не так, то возвращаемся к оригиналам (exiftool их услужливо сохранит):
$ for f in *_original ; do mv $f ${f%_original} ; done

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

Чтобы изменить сразу три временны́х тега (DateTimeOriginal, CreateDate и ModifyDate), можно использовать опцию -AllDates. Например, чтобы сдвинуть время на один час вперёд для всех фоток в папке:
$ exiftool "-AllDates+=1:00" имя_папки

А чтобы сдвинуть на час назад — -AllDates-=1:00.

20090625

Metacity: сочетания клавиш, чтобы прижимать окна к краю

При всей любви и уважении к мозаичным оконным менеджерам, я пользуюсь традиционными перекрывающимися окошками. С мозаичными менеджерами на маленьком ноутбучном экране у меня обычно всё складывалось примерно так:

Первый экран — терминал во всю стену. Второй экран — браузер на 80% ширины, IM на 20% ширины. Остальные экраны — для остальных приложений (отдельный для Gimp, отдельный для полноэкранного плеера, и т.д.).

Итак, я на практике понял, что в основных рабочих приложениях удобно работать или в полноэкранном режиме, или поместив рядом два приложения. В двух этих случаях, кстати, мозаичность удобна. В остальных случаях, мозаичность иногда даже мешает. Однако для терминала во весь экран никакой мозаичности не нужно. Достаточно нажать F11 в Гноме (аналогичное сочетание клавиши где угодно). Остаётся случай «два окна рядом», одно справа, другое слева. Раз в день разложить так окна руками, конечно, можно.

Однако удобнее, когда есть клавиатурные сочетания. Во-первых, нужна максимизация по горизонтали и особенно по вертикали. Тогда окно сразу займёт всю ширину (или высоту) экрана, а для другого окна рядом останется место. Во-вторых, очень полезны сочетания, позволяющие подвинуть окно к краю экрана. С парой окон на экране и такими сочетаниями можно наслаждаться мозаичностью в любом оконном менеджере. В Openbox такие сочетания легко настраиваются, а вот в конфигураторе для Metacity (Gnome) — команд, чтобы прижать окно к краю, нет.

Так я думал и продолжал передвигать окна ручками. Одно влево, другое вправо. А сегодня узнал, что команды такие в Metacity есть. И сочетания клавиш к ним привязать можно в gconf-editor. Запускаем, идём в /apps/metacity/window_keybinding и там для команд move_to_side_x (где x — n, e, s, w) прописываем сочетания клавиш (строковое значение вида <Shift><Control>Up). Всё сразу работает.

сочетания клавиш для того, чтобы прижимать окна Metacity к краю экрана

С моим настройками я нажимаю Ctrl+Shift+F11, и окно вытягивается на всю высоту экрана. Нажимаю Ctrl+Shift+, и окно прижимается к правому краю. Другое окно вытягиваю и прижимаю к левому краю. На другом экране я запускаю терминал и нажимаю F11 (полный экран). Всё остальное, что собираюсь держать открытым дольше минуты (читалку PDF, плеер, и т.п.) запускаю на других экранах.

Примечание: при более внимательно рассмотрении оказалось, что прижимание окон к краю не только недокументированная, но и весьма глючная возможность в Metacity. Вот такой замечательный WM. В минималистичном Openbox, однако, всё ОК.

Кстати, полноэкранный терминал (без заголовков окон, без полосы прокрутки) — по-моему, ещё и идеальный полноэкранный редактор, то есть такая среда для работы с текстом, в которой ничего не отвлекает. То, что людям такая среда нужна — доказывает обилие специальных полноэкранных редакторов. А ничего специального не нужно: достаточно полноэкранного терминала и vi (или emacs). Вообще, полноэкранный режим чрезвычайно удобен, не только для терминала, но и для некоторых других приложений.

20090611

Debian Lenny на Samsung X22

Мой новый рабочий ноутбук — Samsung X22. Черновой отчёт об установке и настройке Debian здесь (по-английски).

Когда разрешу оставшиеся вопросы, перепишу по-русски и помещу здесь. Пока очень кратко.

WiFi нормально работает после установки firmware-iwlwifi.

Комбинации клавиш Fn+... работают не все. Из коробки работают «кнопка сна» и регулировка громкости. Оказалось, не страшно. Простейший патч к hal-info, починяющий кнопки, разработчикам отправлен.

Видео-драйвер radeon работает, но не летает. Версия в Lenny недостаточно свежая и XVideo не поддерживает. Буду разбираться. fglrx вроде работает.
Дополнение 2009-07-01: fglrx который в Lenny, работает без нареканий, а вот с Xorg из unstable мне завести fglrx не удалось. Зато в Xorg из unstable драйвер radeon — уже 6.12.2. А именно в 6.12 появилась поддержка ускорения EXA и XVideo для чипов серии R6xx. Чтобы заработало, нужно также либо ядро 2.6.30 или новее, либо пересобрать модули radeon.ko и drm.ko для старых ядер. Как это сделать — написано в X.org вики (это минутное дело). Да, если ядро Debian — нужно ещё доустановить пакет firmware-linux (то, что выкинули из дебиановского ядра). И действительно — после этого новый свободный драйвер radeon работает. Видео высокого разрешения играется, по-моему, даже лучше и ровнее, чем с fglrx. 3D, правда, пока в свободном драйвере нет, только в fglrx. В общем, сейчас есть выбор: жить с Xorg из stable и проприетарным fglrx, или с Xorg из unstable и со свободным radeon (видео лучше, нет 3D).


Звук играет и с драйвером из Lenny, но, чтобы заработал микрофон, нужна свежая ALSA и в /etc/modprobe.d/alsa-base пришлось добавить
options snd-hda-intel model=ultra
По нажатию на Fn+Esc машина и с самого начала засыпала хорошо, но после просыпания экран не включался. Установил, что работают:
s2ram -f -a 2
и
pm-suspend --quirk-s3-mode
Соответственно в /usr/share/hal/fdi/information/10freedesktop/20-video-quirk-pm-samsung.fdi переделал так:
     <!-- this does not work for my SX22S! -->
<match key="system.hardware.product" string_outof="R40/R41;CoronaR">
<merge key="power_management.quirk.vbestate_restore" type="bool">true</merge>
</match>
<!-- I use this one: -->
<match key="system.hardware.product" string="SX22S">
<merge key="power_management.quirk.s3_mode" type="bool">true</merge>
</match>
После этого и спим хорошо, и просыпаемся.

Вебкамера, кардридер (xD/SD/MS) работают отлично. Оптический привод, вроде, тоже. Bluetooth пока не пробовал, а вот ExpressCard и HDMI проверять просто не на чем.

20090526

Twtrize — сократитель речи

Как известно, письменность избыточна: мы можем угадывать написанные слова, даже если некоторые буквы неразборчивы, перепутаны местами или вообще отсутствуют. К счастью, в компьютерной письменности все буквы разборчивы, почерк у всех одинаково хорош. Именно поэтому появилась возможность очень сильно сокращать слова, убирая из них «лишние» буквы.

Люди иногда сознательно сокращают слова, набирая SMS или твиты — чтобы потратить меньше денег или укоротить сообщение.

Идея возникла, когда на одном из многочисленных «сократителей URL» я увидел надпись «Shrink text». И мне пришло в голову, что вот он возьмёт, и сократит сам текст: выдаст что-нибудь вроде «shrnk txt». Конечно, сервис всего лишь заменял в тексте URL, но я подумал, что можно было бы сокращать и сам текст.

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

Программа преобразует текст на русском языке, выкидывая из него некоторые буквы и символы. Прошу рассматривать это как забавную игрушку и программой не злоупотреблять.

Зависимости


Программа написана на Literate Haskell (это значит, что то, что, вы сейчас читаете, и есть программа!). Используются следующие модули:
> import System.IO.UTF8 as U
> import Data.Char (toLower)
> import Text.Regex.Posix ((=~))
> import Data.Char (isPunctuation)

TODO: Я использую старый способ работать с UTF-8 (utf8-string), надо переделать под новую библиотеку text.

Алгоритм


Данная программа «сжимает» русский текст так:
I. Из слов убираются (почти) все гласные и мягкие знаки,
> filterVowels = filter (`notElem` (aVowels ++ jVowels))

Неприкосновенны гласные, которые:
I.a. являютя частью приставки «не-»
> rmVowels = map wordFilter
> where
> wordFilter ('н':'е':cs) = "не" ++ wordFilter cs

I.b. стоят в трёх- и менее -буквенных словах
>    wordFilter w = if length w <= 3
> then w

I.c. стоят в начале или конце слова
>                    else
> let (prefix,inner,ending) = splitWord w
> in prefix ++ (ajaFilter inner) ++ ending

>    splitWord s  = let p = takeWhile dontRemove s
> r = drop (length p) s
> e = reverse $ takeWhile dontRemove $ reverse r
> m = take ((length r) - (length e)) r
> dontRemove c = c `elem` vowels || isPunctuation c
> in (p,m,e)

I.d. являются комбинациями со звуком «й»: «-ою-», «-ая—» и проч.
>    ajaFilter [] = []
> ajaFilter s = let (b,m,a) = s =~ diftPat :: (String,String,String)
> diftPat = "[" ++ vowels ++ "][" ++ jVowels ++ "]"
> in (sameConsFilter b) ++ m ++ (ajaFilter a)

I.e. стоят меж двух одинаковых согласных
>    sameConsFilter [] = []
> sameConsFilter s =
> let (b,m,a) = s =~ sameConsPat :: (String,String,String)
> sameConsPat = "(["++consonants++"])[" ++ vowels ++ "]\\1"
> in (filterVowels b) ++ m ++ (sameConsFilter a)

Программа использует такой список гласных:
> vowels = aVowels ++ jVowels

где есть и простые гласные (к ним же причислен и мягкий знак)
> aVowels = "аиоуыэь"

и дифтонгообразующие (не знаю правильного термина — в общем, дающие звук «й»),
к ним же причислена и буква «й»:
> jVowels = "яйёюе"

Для некоторых правил требуется также список русских согласных:
> consonants = "бвгджзклмнпрстфхцчшщ"

II. из предложений убираются знаки препинания, кроме точек, вопросительных и восклицательных знаков
> rmSomePunctuation = filter (not . null) . map rmTrailing
> where rmTrailing = reverse . rmHead . reverse
> rmHead [] = []
> rmHead s@(c:cs) = case c `elem` rmlist of
> True -> rmHead cs
> False -> s

Список подлежащих удалению знаков препинания:
>         rmlist = ",;-—:–"

III. из текста удаляются некоторые предлоги (в телеграфном стиле)
> rmPrepositions = filter (`notElem` preps) . words
> where preps = [ "в", "во", "на", "над", "к", "от", "из"
> , "по", "под", "через" ]

IV. для пущей стилизации текст пишется в нижнем регистре
> tolower = map toLower


Использование программы


Программу можно использовать как простой unix-фильтр: он читает текст из потока stdin и печает «сжатый» текст в стандартный вывод (stdout).
> main = U.interact $ (++ "\n") . twtrize

> twtrize = unwords . filter ( not . null ) .
> rmVowels . rmSomePunctuation . rmPrepositions . tolower

Пример:
    $ printf "Гласные, а также некоторые предлоги — как, например, «на», — из \
текста удаляются, но какие-то остаются.\n" | runhaskell twtrize.lhs

глсные а ткже нектрые прдлги как нпрмр «на» ткста удляются но какие-то
остаются.


Последняя версия: исходник здесь. Лицензия: BSD-3.

20090515

Ledger — бухучёт в командной строке

Решил, что надо наводить порядок в своей жизни и деньги считать. В общем, вести домашнюю бухгалтерию. Познания в бухучёте у меня очень скромные (хотя когда-то и прослушал вводный курс), помню только, что такое двойная запись, и дебет/кредит. Когда-то, правда, пользовался GNUcash.

Кроме GNUcash в линуксе есть ещё несколько программ для учёта личных финансов: KMyMoney и Grisbi. Программы красивые, удобные, наглядные. Однако меня впечалил и заинтересовал ledger. Это бухучёт в стиле unix. В общем, для фанатов.

Идея проста: записываем все расходы/доходы в текстовый файл (файл редактируем сами, программа его не трогает), а программа всегда поможет проверить баланс и составить отчёт о текущем состоянии или по периоду. Что может быть естественней, проще, надёжней? Полный простор в организации учёта.

Отмечу, что вначале я нашёл не сам ledger, а его клон hledger, написанный на Haskell. Есть ещё и вариант написанный на Python — beancount. Какую программу выбрать — дело вкуса. Формат файла, к счастью, у них (почти) одинаковый. «Старший» ledger уже есть в репозиториях Ubuntu и Debian, но мне пока больше понравился вариант на Haskell (исходники короче и понятнее), про него и буду рассказывать.

Хотя сложного в использовании ledger вроде ничего нет, трудно начать, потому что все примеры в сети на английском и используют английскую бухгалтерскую терминологию. Для меня, чтобы разобраться, было важно понять формат в котором вести записи. Итак, файл состоит из записей, формат каждой записи
ДАТА[=ФАКТИЧЕСКАЯ ДАТА] [*|!] [(КОД)] [ОПИСАНИЕ] [ ; КОММЕНТАРИЙ ]
отступ НАЗВАНИЕ СЧЕТА хотя бы два пробела СУММА [ ; КОММЕНТАРИЙ ]
отступ НАЗВАНИЕ ДРУГОГО СЧЕТА [ хотя бы два пробела СУММА ] [ ; КОММЕНТАРИЙ ]
[другие счета, если необходимо]
Каждая запись начинается с цифры, то есть даты. Даты пишем в формате ISO, ГГГГ-ММ-ДД. Для краткости можем указать в файле
Y2009
и все последующие записи без указания года будут относится к 2009-му году.

Описание проводки может быть любым. Номера квитанций, счетов и тому подобные коды можно вписывать в поле (КОД) (указывать перед описанием в скобках).

СУММЫ можно писать так, как удобно, «13», «$42», «17 EUR», «121 Kb», «9 L». Ledger понимает, что разные единицы измерения нужно считать отдельно. Единицы можно указывать любые, те, которые нужно учитывать. В том числе и неденежные (и программа умеет их правильно пересчитывать, если ей дать файл с историей цен).

В каждой записи обычно два или три счета, но указывать сумму для одного из них необязательно, понятно, что изменение будет равно сумме всех других, но с противоположенным знаком:
2009-05-14
расходы:обед 123 RUB
актив:наличные ; понятно, что здесь должно быть -123 RUB
Счета можно называть как угодно. В том числе и по-русски. Можно делать подсчета, используя двоеточие в названии счёта, например, «расходы:услуги:интернет». Видимо, необходимо иметь счета как минимум пяти категорий: актив, долги, доходы, расходы и какой-то счёт для уравнивания балансов. Я назвал его «собственные», в англоязычных примерах его обычно называют «equity».

Дело в том, что двойная запись подразумевает, что всегда выполняется закон сохранения денег. Всегда, когда к какому-то счёту мы их приписываем, точно такую же сумму мы должны с какого-то счёта списать. Поэтому уже в начале, чтобы указать сумму денег в кошельке и на счету в банке, нужно ввести некий счёт, с которого они «пришли».

Составим такой файл со счетами, для начала. Пусть у нас есть текущий счёт в банке, на котором лежит 1001 рубль (единицы не будем указывать для краткости), есть что-то в кошельке, скажем 150 рублей. Пишем файл:
Y2009

05-14 начальное состояние счета в банке
актив:банк 1001
собственные:начало

05-14 начальное содержимое кошелька
актив:наличные 150
собственные:начало
Сохраняем в файл, например ~/.ledger (там его по-умолчанию ищет hledger). Смотрим, что получилось:
~$ hledger
1151 актив
1001 банк
150 наличные
-1151 собственные:начало
Программа сама догадалась подсчитать баланс виртуального счёта «актив». Всё сходится: всё, что было списано со счёта «собственные:начало» оказалось в «активе». Если что-то не сходится, программа будет страшно ругаться.

Как записывать расход-приход — см пример записи выше. Допустим, мы что-то купили и что-то получили в подарок:
05-15 * подарок
актив:наличные 100
доходы:подарки
05-15 * яблоко
расходы:покупки 9
актив:наличные
Я добавил звёздочку перед описнием проводок. Как её интерпретировать — дело хозяйское. Я помечаю ей те операции, которые уже завершены (а восклицательным знаком — те, которые запланированы). Программа потом позволяет легко отбирать проводки со звёздочкой и без.

Предположим, например, что на завтра я запланировал ответный подарок, но ещё его не сделал:
05-16 ! ответный подарок
расходы:подарки 90
актив:наличные
Теперь, если посмотреть баланс с ключиком -C, увидим только то, что завершено:
$ hledger -C bal
91 актив:наличные
-100 доходы:подарки
9 расходы:покупки
Чтобы посмотреть не баланс, а список проводок, есть команда register:
~$ hledger reg
2009/05/14 начальное состояни.. актив:банк 1001 1001
собственные:начало -1001 0
2009/05/14 начальное содержим.. актив:наличные 150 150
собственные:начало -150 0
2009/05/15 подарок актив:наличные 100 100
доходы:подарки -100 0
2009/05/15 яблоко расходы:покупки 9 9
актив:наличные -9 0
2009/05/16 ! ответный подарок расходы:подарки 90 90
актив:наличные -90 0
Отмечу, что в «сишной» версии ledger колонки здесь разъедутся. А вот в hledger-0.5 уже всё в порядке (и для 0.4 я тоже сделал патчик).

В общем, баланс (balance) и проводки (register) — два главных отчёта. Можно ограничивать отчёты по диапазону дат или периодам. Или по названиям счетов. Так, чтобы выбрать все счета со словом «подарки» в названии, используем регулярное выражение:
$ hledger reg .*подарки.*
2009/05/15 подарок доходы:подарки -100 -100
2009/05/16 ! ответный подарок расходы:подарки 90 -10
Чтобы выбрать записи по описанию, поступаем так:
$ hledger reg desc:яблоко
2009/05/15 яблоко расходы:покупки 9 9
актив:наличные -9 0
В общем, идея понятна. Для отчётов по неделям и по месяцам есть ключики -W и -M.

Бывают ещё периодические записи, но только в ledger, в hledger их пока нет. Они не означают, что со счёта что-то автоматически списывается. Проводку нужно всё равно вносить вручную, но периодические записи позволяют рассчитывать прогноз бюджета. В ledger также можно делать виртуальные записи, ими можно автоматически учитывать разные комиссии или проценты.

Вот такая программка. Думаю, не одному мне понравится. Вижу большое достоинство программы в том, что формат данных очень простой и естественный. Посмотрю теперь, будет ли её хватать для моих нужд.

Вот пример ledger-файла.