Показаны сообщения с ярлыком скрипт. Показать все сообщения
Показаны сообщения с ярлыком скрипт. Показать все сообщения

20101126

unplot.py: извлекаем табличные данные из графиков

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

Скачать: unplot.py.

Исходники лежат на bitbucket. Исправляйте и улучшайте.

Перед запуском программы нужно вначале определить координаты (в пикселях) той части графика, которую надо обработать. Это можно сделать в Gimp или в Geeqie.

Если на графике несколько линий, то нужную из них нужно покрасить отдельным цветом, отличным от всех других. Это тоже можно сделать в Gimp. Нужно записать HTML-код её цвета.

В общем, исходный график должен выглядеть примерно так:

colourized plot

После этого можно запустить и сам скрипт:

./unplot.py "#00ff00" 0 151 0 475 5.0 824 0.09 85 /путь/к/plot.png > /путь/к/data.txt


Первый параметр — это код цвета линии. Вторые два числа это значение и координата X левого нижнего угла. Потом ещё два числа, значение и координата Y того же угла. Потом ещё четыре числа — соответственно для правого верхнего угла. И в конце имя файла с графиком. Результаты (текстовую таблицу с данными) перенаправляем в файл. Можно ещё задавать «чувствительность» к цвету (параметр -s).

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



Обновление 20101203: теперь скрипт умеет обрабатывать графики с линией толще 1 пикселя; также в репозитории появился и небольшой скрипт для тестирования unplot.py на вменяемость.


Flattr this


По-английски: Unplot.py: from plots to tabular data.

20100124

Три способа отрезать поля у PDF-документа

Речь пойдёт о том, как отрезать поля (и вообще изменить размер страниц) в PDF-документе. После того, как у меня появилась читалка на электронных чернилах, делать это приходится довольно часто.

В чём проблема: большинство PDF* свёрстаны под печатную страницу формата A4 (29,7×21 см) или Letter, с полями, колонтитулами, всё как положено. А типичный экран читалки — 12×9 см с разрешением 800×600 точек. Даже если показывать по половине странице, на страницу приходится всего 1200×800 точек (и 18×12 см площади экрана). Значит, даже при просмотре страниц «половинками» буквы будут примерно в 1,65 раза мельче, вдобавок и разрешение при этом будет тоже как минимум раза в полтора ниже. Короче говоря, значительная доля PDF, свёрстанных под печать, на нынешних электронных читалка нечитаема.

Впрочем, во многих случаях можно легко из обычного PDF сделать PDF, легко читаемый на экране читалки. Дело в том, что значительную часть площади страницы обычно занимают поля. Они нужны для бумажной версии, но без них вполне можно обойтись на электронной читалке. И если обрезать поля (а в некоторых случаях можно обрезать и колонтитулы), то часто содержательная часть страницы будет выглядеть вполне читаемой и на маленьком экране читалки.

На сегодняшний день я нашёл и попробовал три способа обрезать поля у PDF-файла.

1. Обрезка полей с помощью pdfcrop



Есть скрипт pdfcrop на перле (не путать с одноимённым скриптом на питоне), который умеет обрезать поля автоматически. В Debian он входит в состав пакета texlive-extra-utils.

Использовать так:
$ pdfcrop --clip --margin 5 исходный.pdf обрезанный.pdf


Советую всегда всё равно оставлять небольшое поле (--margin 5), иначе касающиеся края буквы могут не отображаться на экране читалки.

В общем, всё просто. Преимущества: простой автоматический способ, по полученному таким способом PDF сохраняется возможность поиска. Недостатки такого способа: pdfcrop очень медленно работает с большими документами (сотни страниц), нельзя автоматически отрезать колонтитулы и заметки на полях (в некоторых случаях проще обойтись без номеров страниц и названия главы сверху, зато получить более крупное изображение основного текста), конкретно моя читалка иногда аварийно перегружается на полученных таким способом PDF, на некоторых файлах pdfcrop неправильно определяет границы текста, на некоторых портит шрифты.

2. Растеризация и обрезка страниц в ImageMagick



Пару раз мне пришлось прибегнуть к написанию самодельного скрипта, заточенного под определённый исходный PDF. Общая схема такая:

Исходный PDF → растеризованные изображения страниц (использую pdftoppm) → разрезание страниц на части и обрезка полей (использую convert из ImageMagick) → сборка нового PDF или DjVu из обрезанных страниц.

Вот пример такого скрипта, которым пользуюсь (он не только позволяет разрезать страницы на несколько колонок, но также отрезать поля и пережать, отбросив пустые страницы) — pdf-trim-to-djvu:



Как пользоваться — должно быть ясно из его справки:

Usage: pdf-trim-to-djvu [options] document.pdf

Options:
-f the first page to process [default: 1]
-t the last page to process
-d resolution in DPI [default: 150]
-c|--columns multi-column mode [default: 1]
--mono bitonal compression (black and white only)
--gray DjVuPhoto compression (shades of gray images) [default]
--color DjVuPhoto compression (color images)
-h|--help print this message


Автоматическая обрезка полей довольно хорошо реализована в команде -trim ImageMagick, но можно задать параметры обрезки и вручную (приходилось). Например, чтобы принудительно обрезать по 3% с каждой стороны, в опции convert можно вставить -shave 3%x3% +repage.

Если хочется не DjVu, а именно PDF, то собрать из изображений PDF можно так (о создании PDF с помощью IM см. здесь):
convert -define pdf:use-trimbox=true `ls -v *.ppm` -density разрешение_в_dpi книжка.pdf
Если страниц много, такой способ будет очень медленным (и прожорливым), лучше конвертировать каждую отдельно (можно тем же convert, если качество устраивает, можно специально для этих целей предназначенным sam2p), а потом объединять страницы вместе. Для объединения PDF-страниц в PDF-документ я использую pdftk:
$ pdftk *.pdf cat output книжка.pdf


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

3. Изменение границ страницы в PDFedit



Наконец, есть ещё способ. Совмещающий и возможность указать вручную что именно следует отрезать, и сохраняющий PDF в почти исходном виде. Есть редактор для PDF-файлов — PDFedit. Однако хотя эта программа и с графическим интерфейсом, методы всё те же.

как обрезать поля страницы в pdfedit

Порядок действий:
  1. открываем копию PDF-файла в PDFedit и выбираем страницу, целиком заполненную текстом, чтобы было видно его границы;
  2. засекаем примерные численные координаты углов прямоугольника обрезки;
  3. в меню «Страница» выбираем «Изменить метрики страницы»; далее вводим новые параметры страницы цифрами, жмём «Изменить», чтобы проверить результат (такой вот GUI; что от чего отмеряется придётся познать на опыте), подобрав параметры страницы применяем обрезку ко всем с 1 по последнюю;
  4. сохраняем результат.

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


Flattr this


* Вот, кстати, не пойму. Современные научные статьи распространяются почти исключительно в электронном виде (бумажные отпечатки — сувениры для авторов). За каждую операцию копирования файла издатели стараются взымать по 30 долларов (думаю, не бедствуют), а вот набирают статьи зачастую таким скупым кеглем, словно свою бумагу жалко. Сравните публикации XIX века и нынешнего. Отчего?

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.

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 в терминале: сохранение отступов вставленного текста

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 обычно есть.

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.

20090330

Скрипты на Хаскеле (пробую писать)

Я, кажется, созрел, чтобы переходить от чтения книжек и статей про Хаскель к попыткам что-то на нём писать самому. Вначале какую-нибудь мелочь. Скрипты, в общем. Поскольку я уже как-то публиковал здесь bash-скрипт rss2lj (кросспост RSS в ЖЖ), то решил в качестве упражнения его переписать и улучшить. Думаю, получилось. В этой заметке расскажу о том, как писал. Ну и о впечатлениях. Скрипт выложен на BitBucket и на Hackage.

Содержание:
 Задача состоит из кучи рутинных операций. Я думаю, именно поэтому, будет полезно и мне на будущее, и другим начинающим и пробующим, увидеть, как они выполняются на Хаскеле. В частности, по ходу дела я разобрался как
  • обрабатывать аргументы коммандной строки,
  • читать и писать файлы,
  • использовать регулярные выражения,
  • отсылать HTTP-запросы,
  • выполнять ввод-вывод в уникоде (UTF-8),
  • получать системное время.
Писать буду как начинающий — начинающим. На словах получается довольно долго, но сам код получился гораздо короче, чем эта статья (около 200 строк, считая комментарии, необязательные декларации типов, пустые строки и декларации импорта внешних модулей).

Хотя Хаскель язык компилируемый и строго типизированный, использовать его для таких дел вполне можно. Код получается примерно такой же, если не более, краткий, как на Python, а компилируется даже на лету достаточно быстро. Есть и особенности. Во-первых, вместо беззаботного duck-typing здесь — строгая типизация. Поэтому писать надо аккуратнее (но и ошибок при исполнении меньше). Однако в Хаскеле эта строгая типизация сделана на основе системы типов Хиндли–Миллнера и, в отличие от C++, под ногами не путается. Во-вторых, чтобы использовать преимущества функционального подхода (например, отложенные вычисления, частичное применение функций) нужно отделять чисто функциональную часть программы от императивных фрагментов. В простейшем случае, это означает необходимость отделить операции ввода-вывода от вычислений (преобразования информации). Переводя на Хаскель: функции ввода-вывода будут иметь монадный тип IO a, остальные же будут чистыми (без IO в типе).

Предварительное описание задачи и подхода

В моём примере можно выделить следущие операции ввода-вывода:
  • получение URL из аргументов командной строки,
  • чтение содержимого RSS или Atom фида по заданному URL,
  • чтение (и потом запись) файла со списком уже обработанных записей,
  • чтение файла с настройками доступа к учётной записи ЖЖ,
  • получение системного времени,
  • коммуникация с ЖЖ по установленному протоколу.
И соответственно следующие преобразования данных:
  • извлечение идентификаторов всех записей в фиде,
  • отсев уже обработанных записей,
  • извлечение заголовков, ссылок и текста оставшихся записей,
  • форматирование записей по заданному шаблону,
  • разбор файла с настройками.
Для разбора произвольных фидов я велосипед изобретать не стал, а воспользовался библиотекой feed. А для всех коммуникаций по HTTP протоколу использовал библиотеку curl (мне понравился её интерфейс). Обе библиотечки нашёл на Hoogle, а установил с помощью cabal. Из остальных зависимостей: нужен модуль Codec.Binary.UTF8.String (в убунту и дебиан он помещён в пакет libghc6-utf8-string-dev), модуль Text.Regex.Posix (соответственно, пакет libghc6-regex-posix-dev). Потом я сейчас заметил, что использовал urlEncode из Network.HTTP (у меня в ~/.cabal), хотя можно было обойтись пакетным escapeURIString (из Network.URI). То есть одна зависимость могла бы быть попроще.

В отдельный модуль я выделил всё, что касается связи связи с ЖЖ и его протокола (файл LjPost.hs). Собственно всю логику скрипта я поместил в другом файле (Feed2Lj.hs). Вспомогательную утилитку для тестирования модуля LjPost я поместил в RunLjPost.hs. Для использования скрипта она не нужна, я её использовал при его написании.

Модуль отправки сообщений в ЖЖ (LjPost)

Использование библиотеки Curl

Как я уже сказал, для работы по HTTP протоколу я использовал библиотечку curl. Соответственно, помещаю в списке импортов
import Network.Curl
а основную функцию оформляю так, всё это достаточно «императивно»:
postToLj ljuser ljpass subj msg = withCurlDo $ do
curl <- initialize
...
Функция withCurlDo должна охватывать все вызовы к curl и отвечает за инициализацию и деинициализацию библиотеки; initialize собственно и позволяет к библиотеке потом обращаться. Собственно HTTP запрос делается так (запрашиваю аутентификационный токен ЖЖ):
  r <- do_curl_ curl ljFlatUrl getChallengeOpts :: IO CurlResponse
Т.е. используем do_curl_, чтобы получить данные HTTP-ответа; результат (HTTP-ответ) связываю (<-) с переменной r; аргументы do_curl_ были определены мной ранее, URL ЖЖ-API
ljFlatUrl = "www.livejournal.com/interface/flat"
и собственно параметры запроса:
getChallengeOpts = CurlPostFields ["mode=getchallenge"] : postFlags
postFlags = [CurlPost True]
Дальнейшие действия определяются логикой протокола ЖЖ.

Разбор ответа ЖЖ

Во flat-протоколе, ответ сервера выглядит так:
ключ_1
значение_1
ключ_2
значение_2
...
Нужно, во-первых, проверять значение ключа success, во-вторых извлекать значения других ключей, для начала ключа challenge.

Поскольку здесь никакого ввода-вывода уже нет, эту часть кода вполне можно написать «функционально». Самый простой и универсальный сделать это, мне кажется, разбить тело ответа (respBody) на строчки (lines), преобразовать их в ассоциативный список (list2alist) и поискать в нём нужный ключ (lookup), получив, может быть (монада Maybe), значение:
lookupLjKey :: String -> CurlResponse -> Maybe String
lookupLjKey k = ( lookup k . list2alist . lines . respBody )
При этом функция преобразования списка в ассоциативный список простая двухстрочная рекурсия:
list2alist :: [a] -> [(a,a)]
list2alist (k:v:rest) = (k,v) : list2alist rest
list2alist _ = []
Всё, мы написали всё необходимое, чтобы разбирать ответы сервера.

Вспомогательная функция, проверяем, успешен ли был запрос (тогда и только тогда, когда в ответе есть ключ success со значением OK):
isSuccess :: CurlResponse -> Bool
isSuccess = (=="OK") . fromMaybe "" . lookupLjKey "success"
Мы определили isSuccess композицией трёх функций. lookupLjKey возвращает монаду Maybe String. Функция fromMaybe достаёт из неё строковое значение. Функция сравнения (==) записана в префиксной форме и сравнивает значение со строкой «OK».

Прошу заметить, что вытащить из монады Maybe собственно значение всегда можно с помощью fromJust, но если там ничего нет (Nothing), то будет возбуждена ошибка. Здесь функция fromMaybe возвращает в такой ситуации значение по умолчанию (пустую строку), но в других местах скрипта я часто использую fromJust без проверок (т.е. при отсутствии значения скрипт будет прерываться). В программах посерьёзнее, я думаю, лучше всегда использовать функции maybe или fromMaybe, позволяющие использовать Maybe-значения, указав для них значения по-умолчанию.

Отправка сообщения в ЖЖ

Возвращаемся к функции postToLj и пишем, что если аутентификационный токе был успешно получен (isSuccess r), взять текущее время (timeopts <- currentTimeOpts, об этом ниже), подготовить запрос для публикациии сообщения (let opts = postOpts ...) и отправить. Результатом функции будет ответ на последний выполненный запрос:
  if (isSuccess r) 
then do
let challenge = fromJust $ lookupLjKey "challenge" r
timeopts <- currentTimeOpts
let opts = postOpts ljuser ljpass challenge subj msg timeopts
r <- do_curl_ curl ljFlatUrl opts :: IO CurlResponse
return r
else return r
Как всегда в Хаскеле, если сказал if — then, говори и else (с тем же типом).

Ещё одно «новичковое» замечание: в блоке do мы связываем переменные с монадным значением с помощью (<-) (это соответствует присваиванию в императивных языках), но определяем переменные чистыми выражениями с помощью (=). Вообще, (=) в Хаскеле почти всегда можно читать как «равно по определению». Как только я это понял — жить стало проще ;-)

Теперь подробности. Чтобы отправить сообщение, нужно сформировать POST-запрос согласно протоколу. В моём примере этим занимается функция
postOpts u p c subj msg topts =
CurlPostFields ("mode=postevent" : (authOpts u p c)
++ ["event=" ++ quoteOpt msg, "subject=" ++ quoteOpt subj,
"lineendings=unix", "ver=1"]
++ topts ) : postFlags
которая аналогичная getChallengeOpts, только список полей, которые нужно отослать, гораздо больше. И есть некоторые тонкости.

Во-первых, нужно защищать («квотировать») некоторые символы в отсылаемых значениях. Их немного, на помощь приходит определение функции с помощью шаблонов аргумента:
quoteOpt ('=':xs) = "%3d" ++ quoteOpt xs
quoteOpt ('&':xs) = "%26" ++ quoteOpt xs
quoteOpt (x:xs) = x : quoteOpt xs
quoteOpt [] = []
Одно дело сделано. Во-вторых, нужно по имени пользователя, паролю и аутентификационному токену подготовить все поля запроса, касающиеся аутентификации:
authOpts u p c = [ "user=" ++ quoteOpt u, "auth_method=challenge",
"auth_challenge=" ++ quoteOpt c,
"auth_response=" ++ quoteOpt (evalResponse c p) ]
Собственно ответ на токен рассчитывается в одну строчку:
evalResponse c p = smd5 ( c ++ (smd5 p) ) where smd5 = md5sum . fromString
Кроме этого нужно импортировать соответствующие функции преобразования уникодной строки в байт-строку UTF-8 и функцию вычисления MD5-суммы:
import Data.ByteString.UTF8 (fromString)
import Data.Digest.OpenSSL.MD5 (md5sum)
И в-третьих, нужно заполнить в запросе поля, касающиеся времени публикации (текущего времени). Импортируем:
import Data.Time
import System.Locale (defaultTimeLocale)
Берём текущее время:
currentTime = do
t <- getCurrentTime
tz <- getCurrentTimeZone
return $ utcToLocalTime tz t
Заметим, что функция эта связана с вводом-выводом и не является «чистой» (не возвращает одно и то же значение всякий раз). По этой причине я предпочёл не вызывать её из «чистой» postOpts, а передать уже готовый список опций, касающихся времени в postOpts из postToLj. Там, напомню, я писал:
timeopts <- currentTimeOpts
а currentTimeOpts определил так:
currentTimeOpts :: IO [String]
currentTimeOpts = do
t <- currentTime
let opts = [ "year=%Y", "mon=%m", "day=%d", "hour=%H", "min=%M" ]
return $ map (flip showTime t) opts
Т.е. взял текущее время и подставил его в каждый из списка форматов (ЖЖ хочет в таков виде). Вспомогательная функция преобразования времени в строку по формату выглядит так:
showTime = formatTime defaultTimeLocale
Эта функция двух (неуказанных) аргументов получена каррированием функции formatTime. В map я меняю местами её аргументы (flip), чтобы формат передавался последним, и «перчу» ещё раз текущим временем.

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

Чтение файла конфигурации

Где-то логин и пароль хранить надо, и самое простое, что приходит в голову, поместить его в файле настроек, написанном в виде
username=мойлогин
password=мойпароль
В коде скрипта указываю путь по-умолчанию к этому файлу:
ljPassFile = "~/.ljpass"
Читаем этот файл и делаем из него знакомый и удобный ассоциативный список:
readPassFile f = do
ljpass <- readFile f
return $ map (\(f,s) -> (f,tail s)) $ map (break (== '=')) $ lines ljpass
Поскольку файл заведомо небольшой, можно использовать простую в обращении readFile. Далее как обычно: режем на строки (lines), каждую строку разбиваем по первому знаку «равно» (map (break (== '='))), правим получившийся ассоциативный список список, откидывая знаки «равно» (λ-функция во втором map). Результат заворачиваем в IO-монаду (return) как того требует тип функции.

Почти готово. Для пущего удобства сделаем себе раскрытие тильды в пути к файлу:
expandhome ('~':'/':p) = do h <- getHomeDirectory ; return (h ++ "/" ++ p)
expandhome p = return p
и собственно функцию, которая, будет нам давать значение любого ключа из файла конфигурации:
readLjSetting key = do
passfile <- expandhome ljPassFile
s <- readPassFile passfile
return (lookup key s)
В этот раз нам надо добавить ещё две декларации импорта:
import IO
import System.Directory (getHomeDirectory)
Последний штрих: в объявлении модуля перечисляем экспортируемые вовне функции, а вспомогательные замалчиваем:
module LjPost (readLjSetting, postToLj, isSuccess, lookupLjKey, putLjKey) where
Наш модуль готов к использованию. Он позволяет нам задавать настройки доступа в файле конфигурации, понимает ЖЖ-протокол, поддерживает challenge-response аутентификацию и позволяет публиковать в ЖЖ сообщения. Меньше 100 строк кода, если не считать комментарии.

Обработка RSS/Atom фида (Feed2Lj)

Переходим к заключительной части рассказа. Скрипт Feed2Lj.hs берёт URL фида из командной строки, настройки ЖЖ из файла с настройками (для него там добавляем третью настройку, имя файла со списком уже обработанных записей), скачивает фид и отсеивает уже обработанные, необработанные преобразует в plain-text, форматирует по образцу и отсылает в ЖЖ, обновляя список обработанных записей. Теперь подробно.

Получение аргументов командной строки

Получить список аргументов просто, его даёт функция getArgs из System.Environment. У нас аргумент один, адрес фида, поэтому может сразу связать нужную переменную (url) с первым элементом списка, проигнорировав остальные:
  url:_ <- getArgs
Такое связывание по шаблону мне кажется очень элегантным приёмом.

Скачивание фида

На помощь опять приходит библиотечка curl. И опять связывание по шаблону, чтобы взять только интересующую нас часть результата:
  (_,rawfeed) <- curlGetString url []

Используем модуль LjPost для чтения настроек

В общем-то вся работа уже сделана, осталось только использовать функцию readLjSetting. У неё тип [Char] -> IO (Maybe [Char]), т.е. по строке она возвращает IO-монаду, внутри которой, может быть строка (значения настройки найдено и считано), а может и не быть (настройка не найдена). Поскольку у нас тут сразу две монады (IO и Maybe), одна в другой, то, чтобы вытащить просто (Just) значение, я поступаю так:
ljuser <- return fromJust `ap` readLjSetting "username"
т.е. функцию fromJust применяю внутри монады IO (ap из Control.Monad). Аналогично с остальными значениями из файла настроек. Кажется немного громоздно с непривычки, но не так уж сложно потом. Уверен, можно написать короче.

Чтение списка обработанных записей

Мой старый bash-скрипт писал ID записей в файл, одно на строчку, поэтому новый скрипт использует тот же формат (и тот же файл). Читаем файл и преобразуем в список строк:
sent_ids <- (return . lines) =<< readFile sentfile
Здесь, чтобы не вводить временную переменную, я явно указал функцию связывания вычислений (=<<). return требуется типом (=<<). Результат эквивалентен записи
tmp <- readFile sentFile
let sent_ids = lines tmp

Отсеиваем обработанные записи

Для начала разберём содержимое фида и подготовим список всех записей. Благодаря библиотечке feed это легко:
  let feed = fromJust $ parseFeedString rawfeed
let items = feedItems feed
Ну а отсеять уже обработанные можно с помощью filter:
  let newitems = reverse $ filter (isNotSent sent_ids) items
Функция-предикат получилась за счёт каррирования isNotSent:
isNotSent sent i = ((snd . fromJust . getItemId) i) `notElem` sent
Буквально: взять просто ID элемента (возможна ошибка), проверить, что не входит в список sent. Сразу подготовим список ID подлежащих обработке записей:
let new_ids = map ( snd . fromJust . getItemId) newitems

Отправляем запись в ЖЖ

Тупо используем уже написанный модуль LjPost. Если даны имя пользователя, пароль, шаблон записи для отправки и собственно запись:
postItem u p t i = do
let message = renderItem t i
let subj = fromJust $ getItemTitle i
r <- postToLj u p subj message
if isSuccess r
then putLjKey "url" r
else putLjKey "errmsg" r
Стоп-стоп-стоп! Какой ещё такой шаблон записи (t) и что делает renderItem? Объясняю: отослать запись нам надо в HTML-е, и хорошо бы можно было менять формат записи, не переделывая весь код. В общем, renderItem — это маленькая template engine, t — её шаблон. Я её опишу в следующих разделах статьи.

Вызываем из main для каждой записи из списка необработанных:
  let t = encodeString "<p>%text%</p><p>( <a href=\"%link%\" title=\"%title%\">дальше</a> )</p>"
mapM_ (postItem ljuser ljpass t) newitems
Здесь мы формируем список IO-действий и их последовательно исполняем (mapM_). То есть последовательно отсылаем все записи из нашего списка. Обратим ещё внимание на encodeString из Codec.Binary.UTF8.String, которая кодирует строку в UTF-8.

Форматирование по шаблону (маленькая template engine)

Напишем нашу маленькую функцию форматирования по шаблону. Пусть, допустим, все параметры шаблона будут представлены как «%параметр%», а спецсимвол «%» будет представлен в шаблоне как «%%». Параметры будет передавать ассоциативным списком, а шаблон — строчкой. На выходе — строчка с подставленными в шаблон параметрами:
renderTemplate _ [] = []
renderTemplate alist s =
let (b,t,a) = s =~ "%[a-z0-9]*%" :: (String,String,String)
tagval t
| t == "%%" = Just "%"
| otherwise = let inner = take (length t - 2) $ drop 1 t
in lookup inner alist
val = tagval t
in if isJust val
then b ++ (fromJust val) ++ renderTemplate alist a
else b ++ t ++ renderTemplate alist a
Функция форматирования сообщения по шаблону готова. В ней мы последовательно «раскусываем» шаблон с помощью регулярных выражений на «текст-до», «тег» и «текст-после». Подставляем на место «тега» (t) значение соответствующего параметра, если есть, или буквальный «%», если тэг пустой. Продолжаем, пока не кончится шаблон.

О регулярных выражениях. Включаем импортом
import Text.Regex.Posix ((=~))
После этого можем в любой строчке искать регулярное выражение:
строка =~ выражение :: возвращаемый тип
Регулярные выражения ведут себя по-разному в зависимости от возвращаемого типа. Мне пока что пригождаются больше всего два из них: Bool для проверки соотвествия строки выражению и тройной кортеж (String,String,String), разрезающий строчку на три части.

Функция форматирования по шаблону готова. Она просто работает со строками (шаблонами) и ассоциативными списками (словарями). А где же обещанная renderItem?

Форматируем запись по шаблону

Итак, renderItem должна получать шаблон и запись из фида, а возвращать строчку. Всё, что делает эта функция — просто достаёт нужные параметры записи, помещает их в ассоциативный список и вызывает функцию форматирования по шаблону renderTemplate. В виде кода это выглядит гораздо понятнее:
renderItem :: String -> Item -> String
renderItem t i =
let title = ( fromJust . getItemTitle ) i
link = ( fromJust . getItemLink ) i
summary = ( takeSentences 5 . eatTags . fromJust . getItemSummary) i
tags = zip [ "title","link","text" ]
[ title, urlEncode link,summary ]
in renderTemplate tags t
Нетривиальна здесь только функция подготовки текста сообщения (summary).

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

Функция eatTags использует тот же приём рекурсивного раскусывания строки с помощью регулярных выражений, что и renderTemplate:
eatTags [] = []
eatTags s =
let (b,t,a) = s =~ "</?[^>]*/?>" :: (String,String,String)
in b ++ eatTags a
Все HTML и XHTML теги должны быть этой функцией вырезаны.

Упражнение: изменить функцию так, чтобы тег <img/> выразался не бесследно, а заменялся содержимым его аттрибута alt.

Теперь осталось лишь взять первые n предложений. Возьмём вначале одно:
takeSentence s = 
let ends = ".?!;"
(first,rest) = break (`elem` ends) s
in if not (null rest)
then (first ++ [head rest],tail rest)
else (first,[])
Тут я обошёлся без регулярных выражений, просто задав список разделителей (ends) и раскусывая строку по символу из их числа (break (`elem` ends)). Напоследок присоединяю разделитель, если он есть, к «откушенному» предложению (break прикрепляет его к «остатку»).

Осталось лишь взять первые n штук:
takeSentences n s
| n > 0 = let (s',r) = takeSentence s
in s' ++ takeSentences (n-1) r
| otherwise = ""
Теперь любая запись может быть представлена так, как мы захотим.

Обновляем список обработанных записей

Записи получены, отобраны, отформатированы, отправлены. Осталось только обновить список обработанных. Вначале сохраним предыдущую версию файла (переименованием), а потом запишем на его место новый список:
  renameFile sentfile (sentfile ++ "~")
writeFile sentfile $ unlines (sent_ids ++ new_ids)
Здесь использована функция renameFile из System.Directory.

Заключение

Вот вроде и всё. Можно вызывать получившийся скрипт:
$ runhaskell Feed2Lj.hs URL-вашего-фида
Пробовал пока только с GHC, но, думаю, и с Hugs должно работать. Я, кстати, осознал, что у интерпретатора Hugs есть важное преимущество перед GHC: установка GHC тянет около 100 МБ, а Hugs — всего порядка 10 МБ. Так что как разберусь с Hugs, буду стараться проверять свои скрипты и на нём.

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

Кроме, понятно, гугла, большой подмогой является Hoogle. Сообщения GHC довольно подробные и понятные (разбирать ошибки C++-компиляторов про шаблоны гораздо труднее). Радует, что уже сейчас коллекция библиотек весьма богата (кажется, сопоставима с набором библиотек Python в то время, когда я с ним впервые познакомился). С уникодом, опять же, никаких проблем.

Есть и всякие «но»: но в коде других людей мне ещё далеко не всё понятно, но пихать ввод-вывод в любую точку кода в Хаскеле неудобно и не нужно (сделано намеренно, для отладки служит trace из Debug.Trace), но представить порядок ленивых вычислений не всегда легко, но документированы библиотеки в Hackage весьма лаконично (строго, по делу, но не так доходчиво и очевидно для новичков, как, например в Python), но cabal до сих пор нет ни в Debian, ни в Ubuntu.

Но всё равно, мне понравилось. Буду рад замечаниям и вопросам. Уверен, что-то можно было написать лучше (короче, понятнее и выразительнее). Что-то, наверное, забыл объяснить.

20090224

epi2fox: конвертация закладок Epiphany в Firefox 3

Я давно уже использовал Эпифанию (Epiphany) в качестве основного браузера, в основном потому, что её система закладок с тагами мне кажется наиболее удобной. Однако поскольку в FF3 закладки тоже можно метить тагами, то я решил опять попробовать Firefox в качестве основного браузера. Оставалось только перенести свои закладки из Epiphany в Firefox.

Поскольку ничего готового не нашлось, то написал скрипт epi2fox.py. Предполагая, что профиль Firefox практически пуст, использовать так:
$ epi2fox.py ~/.mozilla/firefox/вашпрофиль/places.sqlite
Скрипт не идеален, в частности, есть проблема дублирующихся закладок. Epiphany такие закладки разрешает (несколько разных закладок на один и тот же URL), а FF — нет. Сейчас дубли просто игнорируются, хотя, наверное, правильнее было бы их объединять.

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

Ссылки:
epi2fox, репозиторий скрипта
схема базы данных закладок в FF3 (PDF)
epi2fox: import Epiphany bookmarks into Firefox 3 (это объявление по-английски)

Для облегчения поиска закладок по тагам, может ещё пригодится расширение TagSifter. С ним гораздо лучше, чем без него.

PS. Да, конечно, перед использованием лучше сделать резервную копию places.sqlite, если там что-то уже есть.

20090128

Скрипт rss2xmpp, кросспост чего угодно в Jabber

В общем, скрипт лежит на BitBucket: rss2xmpp.py.

Использование:
$ rss2xmpp.py адрес-фида джаббер-ID-адресата
Фид можно почти любой: и RSS, и Atom. Записи пересылаются отдельными сообщениями в прямом хронологическом порядке, история отосланных сохраняется. При первом запуске скрипт создаст файл~/.rss2xmpp, куда нужно внести настройки учётной записи Jabber для отправки сообщений. По умолчанию в этот файл вписываются настройки для GoogleTalk.

Для работы скрипты нужны: FeedParser, html2text, и xmpppy, и, конечно, Python. Забавное наблюдение: все зависимости оказались пакетированы во FreeBSD, все, кроме python-html2text в Debian unstable, а в убунте только python-feedparser.

Ещё одно постороннее замечание. BitBucket, как хостинг для Mercurial, мне понравился: сразу дают доступ по SSH (т.е. можно положить ключи и пользоваться без пароля), есть простой Issue-tracker и вики (отключаемы), а вдобавок разрешают и один бесплатный частный (закрытый) репозиторий. Зато freeHg полностью некоммерческий и минималистичный.

rss2xmpp, a script to crosspost any feed to Jabber (этот пост по-английски)

20081126

Массовое резервное копирование репозиториев Mercurial

Лёгкость создания репозиториев в системе контроля версий Mercurial (hg) приводит к тому, что начинаешь использовать контроль версий для всего подряд, и количество маленьких репозиториев быстро растёт. При этом они могут лежать где угодно. Соответственно, иногда целесообразно все созданные репозитори ① найти и ② сохранить на будущее. Нахожу по наличию каталога .hg в корне каждого репозитория. Сохранять можно клонированием или простым копированием.

Соответственно, мой велосипед для резервного копированя всех имеющихся в рабочем каталоге репозиториев имеет два колеса: find, чтобы найти ①, и небольшой скрипт backup-hg-repo, чтобы собственно сделать бэкап ②:

По сути скрипт делает две вещи: создаёт (предположительно уникальное) имя для резервной копии репозитория, исходя из его положения на диске, и дальше либо делает клон существующего, удаляет копию, если она уже есть, и клонирует туда заново. Можно же просто копировать. Благо, hg это позволяет (если, конечно, в репозиторий, никто не пишет).
#!/bin/sh
# usage: $0 path-to-hg-repository backup-directory

REPO="$1"
BACKUPDIR="$2"

if [ "x$BACKUPDIR" = "x" ] ; then
echo 'specify backup dir'
exit 1
fi
if [ ! -d "$BACKUPDIR" ] ; then
echo "$BACKUPDIR does not exist"
exit 2
fi

BACKUP="$BACKUPDIR/$(dirname "$REPO" | sed 's/^\.\///;s/\/$//;s/[\/_ ]/-/g')/"
REPO=$(dirname "$REPO")

# delete backup repo if it exists
hg status "$BACKUP" && rm -rf "$BACKUP"
hg clone "$REPO" "$BACKUP"
# or copy instead of cloning (if nobody is pushing to $REPO,
# see http://www.selenic.com/mercurial/wiki/index.cgi/BackUp)
# cp -r "$REPO" "$BACKUP"

Использовать так:
$ find . -name '.hg' -type d -exec backup-hg-repo '{}' /путь/по/которому/сохранять/бэкапы \;
Альтернативные подходы: переходить в целевой каталог и делать из него pull (думаю, это будет быстрее, но не так железобетонно-автоматически, как полный клон), или делать hg bundle -a "$BACKUPDIR/$(dirname ${REPO}).hg" (один сжатый файл истории будет занимать меньше места, но этот способ не сохраняет дополнительные файлы репозитория, вроде hgrc и не совместим с некоторыми расширениями).

20081106

Удаление потайных ID3 тегов вроде www.mp3-ogg.ru

Столкнулся с тем, что в mp3 бывают скрытые «неудаляемые» теги. В частности, в интернет-обращении находятся файлы, в которых в поле жанр указано «www.mp3-ogg.ru». И сколько бы я ни прописывал правильные теги в EasyTAG, или даже пытался удалять все (ID3v1 и ID3v2) теги из такого файла (в EasyTAG или с помощью id3v2 -D file.mp3), при добавлении файла в библиотеку Rhythmbox поганое «www.mp3-ogg.ru» всё равно вылазит.

Похоже, это связано с так называемыми APETAGS, будь они неладны. Только старый добрый mpg321 показывает нормальные ID3v2/ID3v1 теги.

Как заметил наблюдательный народ, потайные теги расположены в конце файла. Убедиться в этом можно, например, так:
$ strings test.mp3 | tail -14
APETAGEX
TITLE
ARTIST
ALBUM
YEAR
2007,
COMMENT
GENRE
www.mp3-ogg.ru
COPYRIGHT
Make Love Not War
TRACKNUMBER
1APETAGEX
2007
и предложил простенький скрипт, чтобы такую нежелательную концовку удалять. Моя версия скрипта здесь. Отличие от оригинала в том, что я решил обрезать по слову "APETAG" вместо "TAG", мне кажется так менее вероятна ошибка обрезки, и добавил опции командной строки. Применение:
$ tagwipe.py test.mp3
Конечно, лучше сделать резервную копию перед применением, но вроде бы обработанные файлы не испортились, а слишком умные проигрыватели теперь показывают правильные теги.

20081017

Обновление скрипта gpxplot

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

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


Исходники скрипта
Онлайн-версия

20080918

Визуализация профилей высоты и скорости GPS-треков

Помимо нанесения GPS-трека на карту, одним из наиболее интересных способов его представления является, на мой взгляд, построение графика изменения высоты или скорости движения как функции пройденного расстояния или прошедшего времени. Такие графики я называю «профилями» трека.

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

Так что предлагаю свой скрипт gpxplot, который строит такие графики по данному GPX-файлу. Прямая ссылка на скрипт: gpxplot.py. Собственно, тут всего две важных особенности:

1) GPX-файл может содержать несколько несвязных треков, каждый из которых может состоять из нескольких «сегментов», поэтому хотелось сохранить возможность показывать их на графике именно как отдельные участки трека.

2) Информации о пройденном расстоянии в GPX-файле нет. Скрипт рассчитывает его в предположении сферической формы земли. Думаю, для большинства приложений этого достаточно.

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

Скрипт может также построить график самостоятельно (с помощью gnuplot). Для этого используется опция -g:
$ ./gpxplot.py -g -x time -y elevation track.gpx
Если модуль python-gnuplot установлен, то картинка появится на экране. Если такого модуля нет, то будет напечатан скрипт для gnuplot. В этом случае запустить gnuplot можно так:
$ ./gpxplot.py -g -x time -y elevation track.gpx | gnuplot -persist -
Как уже, наверное, понятно, -x time указывает откладывать по оси абсцисс время (-x distance — пройденный путь), а -y elevation — откладывать по оси ординат высоту (-y velocity — скорость).

Чтобы сохранить результат в файл, указываем опцию -o имяфайла:
$ ./gpxplot.py -g -x time -y elevation -o track.png track.gpx
Поддерживаются форматы: SVG, EPS, PNG, JPG.

Указать местную временную зону можно опцией -t имя_зоны:
$ ./gpxplot.py -g -t 'Europe/Moscow' track.gpx
В этом случае необходимо присутствие в системе модуля pytz.

Получается примерно так:

пример графика в формате SVG

Может быть, в будущем добавлю всякую раскраску и украшательство графиков. Вероятно, добавлю другие механизмы отрисовки графиков (matplotlib, например). Было бы интересно сделать отрисовку средствами Google Charts (тогда уже и до веб-версии недалеко).

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

Дополнение 2008-10-17: Скрипт заметно обновился, появилась поддержка Google Charts, а также теперь доступна онлайн-версия.

Ссылки:
скрипт для визуализации профилей высоты GPS-треков
Visualizing altitude and velocity profiles of GPS tracks
GPXplot онлайн

20080830

Как сохранить видео-поток ASF в файл

Как сохранять видеоролики со всяких там ютубов все уже давно знают (см. clive). На некоторых же сайтах (к примеру, на vesti.ru) видео лежит в виде ASF-потоков. Если не плеваться на некошерность таких видео-потоков, то их вполне можно смотреть после установки плагина totem-mozilla, затем только нужно один раз, с помощью контекстного меню плагина, открыть видео непосредственно в видеопроигрывателе и согласиться на установку нужных кодеков. После этого все такие ролики будут показываться прямо в браузере.

Теперь о том, как сохранять эти ролики в файл. Опять же, используя контекстное меню плагина, копируем адрес потока, а затем перекодируем поток уже в нормальный видео-файл с помощью mencoder. В общем, вот мой мини-скрипт vstream2avi:

#!/bin/sh

usage="usage: `basename $0` video_stream_URL video_file"

if  [ $# -ne 2 ]; then
        echo $usage
        exit -1
fi

STREAM=$1
VFILE=$2
BITRATE=4000

mencoder "$STREAM" \
        -oac mp3lame -lameopts vbr=3 \
        -ovc xvid -xvidencopts chroma_opt:vhq=3:pass=2:bitrate=${BITRATE}:autoaspect \
        -o "$VFILE"


И пример использования:
$ vstream2avi.sh mms://video.rfn.ru/rtr-vesti/147104.asf Putin_interview_CNN_20080827.avi

20080728

less: автоматический просмотр архивов *.gz и других типов файлов

Популярную листалку для командной строки less легко научить просматривать не только текстовые файлы, но и многие другие. Например, ей можно смотреть сжатые файлы или файлы PDF не задумываясь о вызове архиватора или нужного конвертера. Примерно так:
$ less /usr/share/doc/less/changelog.gz
Собственно, во многих дистрибутивах так уже настроено по умолчанию. В Debian эта возможность по умолчанию выключена. Чтобы она заработала нужно в ~/.bashrc (если пользуетесь bash) добавить eval $(lesspipe) или eval $(lessfile). Это установит переменные окружения LESSOPEN и LESSCLOSE, говорящие less пропускать файлы через указанный фильтр. Фильтр /usr/bin/lesspipe смотрит на расширение файла и запускает нужный конвертрер, если необходимо. Разница между lesspipe и lessfile в том, что первый конвертирует файлы на лету, сразу отдавая результат в less, а lessfile вначале полностью конвертирует файл, и только потом вызывает less.

Понятно, что очень просто можно настроить пользовательские фильтры. Добавлять их нужно в пользовательский скрипт ~/.lessfilter. Например, чтобы просматривать с помощью less файлы OpenOffice, я добавляю фильтр, вызывающий antiodt, а чтобы просматривать файлы MSWord, фильтр, вызывающий antiword. Файл ~/.lessfilter выглядит при этом так:
#!/bin/sh
(
case `echo "$1" | tr '[:upper:]' '[:lower:]'` in
    *.odt)
        if [ -x "`which antiodt`" ]; then antiodt "$1" ;
        else cat "$1"; fi ;; # No antiodt available
    *.doc)
        if [ -x "`which antiword`" ]; then antiword "$1" ;
        else cat "$1"; fi ;; # No antiword available
esac
) 2>/dev/null
P.S. Код раскрасил с помощью плагина toblog к vim, создающего цветной HTML согласно текущей цветовой схеме редактора.

20080715

pdfgrep: поиск слов по PDF файлам

Дано: куча PDF файлов (разные статьи).
Найти: те из них, в которых упоминаются определённые ключевые слова.

Я привык grepать по текстовым файлам в поисках нужного слова или сочетания слов. Вот только grep шаблон файл.pdf ничего не находит. «Не беда,» — решил я и написал обвязку для grep, которая конвертирует PDF в простой текст с помощью pdftotext и ищёт уже в нём, а имя файла показывает исходное.

Получился у меня вот такой скрипт pdfgrep. Запускать можно, например, так:
$ pdfgrep --color=always -i '(navier[- ]){0,1}stokes' *.pdf
Конечно, стоило мне собраться написать об этом в блог и поискать в интернете — как оказалось, что я не первый, кто такой скрипт написал. Вот практически идентичные скрипты: pdfgrep-1 и pdfgrep-2… В общем, идея понятна.

20080609

antiodt: просмотр документов OpenOffice в виде простого текста

Для тех, кто не любит запускать офисные приложения для того лишь, чтобы просто прочесть файл в формате ворда, есть antiword, есть wvunrtf, и pdftotext для RTF и PDF соответственно). Вот только для открытого и всеми любимого ODT (OpenDocument, формат OpenOffice) — такой радости нет. o3read их не берёт.

В общем, запускать мне OpenOffice лениво, в AbiWord тоже не так уж шустр, так что написал себе скрипт-однострочник (antiodt):
#!/bin/sh
unzip -p "$1" content.xml | \
xmlstarlet sel -N text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" \
  -T -t -m '//text:p' -v . -n | less
Поскольку документ ODT это zip-архив, а внутри весь текст в виде XML лежит в content.xml, то отсавалось только выбрать содержимое текстовых элементов (забив на всё форматирование, кроме разбиения на абзацы). При этом для разбора XML используется утилита xmlstarlet. Все имеющиеся в наличие у меня ODT документы теперь в мгновение ока можно прочитать так:
$ antiodt документ.odt
Идею почерпнул здесь. Кто велосипед уже изобретал — не жалуйтесь.

Дополнение 2009-09-23: Если необходимо перевести документ из ODT в plain text с частичным сохранением форматирования, то можно ещё использовать скрипт odt2txt.py. Скрипт посложней, он преобразует ODT в текст с разметкой Markdown.

P.S. View OpenOffice files as plain text (эта заметка по-английски)

20080218

Кросспост RSS в ЖЖ (bash-скрипт)

Обновление: Более совершенный скрипт для кросспоста был написан мной на Haskell. Подробности здесь.


Захотелось мне наладить автоматическую публикацию ссылок на мои записи в ЖЖ…

Сразу же нашёл сервис rss2lj. Он, однако, в настоящий момент не работает. После этого стал искать, как сделать то же самое своими силами.

Нашёл, что есть два механизма автоматической публикации в ЖЖ. Первый — с использованием XMLRPC. Как это делается на PHP желющие могут увидеть в исходниках плагина к вордпресс lj_crosspost. Второй способ — использовать возможность emailpost в ЖЖ. В этом случае письма, отправленные на адрес вида пользователь@post.livejournal.com будут публиковаться как посты.

Я выбрал второй вариант, потому что письма можно слать из любого скрипта одной командой, а возиться с авторизацией через XMLRPC не хотелось. Правда, функция emailpost в ЖЖ не бесплатна, платить за неё надо либо чистым золотом, либо рекламой. Остановился на рекламе, включив в ЖЖ «улучшенный рекламой эккаунт» :)

После этого оставалось написать скрипт, который бы брал RSS и делал из него много писем. В действительности, такой скрипт уже есть. Можете взглянуть на rss2email, лежащий в репозиториях Debian. Впрочем, он скорее предназначен для чтения RSS через email, а я хотел лишь вытаскивать из RSS ссылки.

Вытащить нужные поля из RSS можно и xmlstarlet-ом:
curl -g http://feeds.feedburner.com/sovety?alt=rss -s | \
xmlstarlet sel -t -m "/rss/channel/item" \
-v "guid" -n -v "pubDate" -n \
-v "title" -n -v "link" -n
Оставалось только атоматически формировать из этих полей текст письма (awk-ом), отсылать всё это в ЖЖ (mutt-ом) и отмечать на память уже отосланные записи (чтобы не слать повторно). Получился вот такой скрипт.

Теперь анонсы моих постов можно прочитать и в ЖЖ: jetxee.livejournal.com.

Что не работает: ЖЖ приходит в ступор, получая письма, в Subject которых UTF-8. По идее Subject должен становиться заголовком поста, но выходит ерунда. ЖЖ должен особым образом обрабатывать в письме строки вида lj-tags: blabla, давая записям тэги, но у меня это тоже почему-то не срабатывает.

20071113

Как скачать альбом Picasaweb целиком (без Picasa)

Написал маленький скрипт download-picasa-album, позволяющий из коммандной строки за раз скачать все изображения из альбома Picasaweb в полном разрешении.

Использовать так:
download-picasa-album 'ссылка-на-RSS-фид-альбома'


И все фотографии албома скачиваются и помещаются в текущую директорию. Русские буквы в именах файлах обрабатываются нормально (в UTF-8 локали), а вот пробелы, думаю, вызовут проблемы в том месте, где awk вызывает wget. Надо бы поправить...

Да, можно вместо ссылки давать имя файла с уже скачанным фидом.

Скачать скрипт


Идею заимствовал у Дмитрия Астапова, но воспользовался xmlstarlet вместо Xtract и оформил всё это дело в виде скрипта. Кусок awk-кода, декодирующий URL взял с сайта Huidae Cho. Скрипту также требуется iconv.

Дополнение: Похоже, мой скрипт удачно скачивает только открытые альбомы; надо будет посмотреть, что там ломается с частными альбомами. В общем, если мой скрипт у вас не работает, можете попробовать ещё скрипт Loïc Serf-а. Код его скрипта можно найти также в комментариях ниже.

P.S. Вот, теперь из коммандной строки, не устанавливая громоздкую Picasa, можно не только выкладывать фотографии на Picasaweb, но и скачивать альбомы друзей. Я доволен.