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

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.

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.

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 (этот пост по-английски)

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 онлайн

20080903

Простая CMS для AppEngine

Я далёк от серьёзной веб-разработки. Однако довольно регулярно мне приходится делать или браться поддерживать/изменять разные небольшие сайты (до 10–20 страниц). Надо сказать, что готовые решения в этой весовой категории меня ни разу не удовлетворяли.

  • Полноценные CMS всегда оказывались слишком тяжёлые (слишком универсальные) и навязывали слишком сложную (ИМХО) модель и структуру сайта. Их код слишком большой, чтобы можно было что-то быстро под себя переделать. В общем, в программировании я не любитель «отсекать лишнее».
  • Комбинирование языков облёгчённой разметки (Markdown, reST), щепотки PHP и готовых систем шаблонов (вроде Smarty) решало задачу, но требовало решать её каждый раз заново («лепить»).
  • Собственный набор макросов m4 для генерации сайтов был не очень удачен (но «велосипед был вновь изобретён»).
  • Попытки использовать Google Pages или облегчённые вики в качестве движка оставили смешанное чувство (здесь нужно долго и нудно «замазывать»).

В общем, на каникулах развлёк себя знакомством с Google AppEngine (который предоставляет бесплатный и масштабируемых хостинг для веб-приложений), а заодно, в свете того, что нужно сделать очередной мини-сайт, решил сделать для AppEngine простую мини-CMS. Требования у меня к ней были такие:

  • Максимально простая модель сайта: «плоский» сайт из нескольких страниц с автоматически генерируемым «меню».
  • Человек далёкий от веб-разработки должен иметь возможность легко изменять содержимое сайта. Легко — это значит не только «не зная HTML», но и «без страха перед навороченной админкой», и «не зная английского».
  • Должна быть возможность использовать облегчённые языков разметки (Markdown, reST), которые мне кажутся более предпочтительными, чем WYSIWYG-редакторы.
  • Код должен быть достаточно коротким и простым, чтобы понять и изменить его было легко.
  • Доступность бесплатного безрекламного хостинга для динамического сайта:)
В результате пары дней приятных усилий получилось вот что. Дабы подчеркнуть простоту кода (в данный момент это всего 5 кБ), я эту мини-систему управления сайтом нарёк Hello Web. Кое-что ещё надо доработать, но, в целом, уже вполне функциональная вещь. Сам сайт HelloWeb на ней же и работает.

Надеюсь, кому-то кроме меня эта мини-CMS окажется полезна. Лицензия — преданная гуглоанафеме, но свободная и мне симпатичная GNU Affero GPL. Собственно, узнал об этой политике Гугла по отношению к Affero GPL как раз попытавшись завести проект на Google Code. «Ну не хотят, так не хотят,» — решил я, — репозиторий исходного кода был размещён на freeHg.org.

В общем, исправления и дополнения кода приветствуются.

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

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, но и скачивать альбомы друзей. Я доволен.

20070905

formula2png: как сделать картинку с формулой

Набирать математические формулы в LaTeX легко. Получается в результате документ PDF или PS. А вот что делать, если нужно показать одну-две формулы на веб-странице? Самый простой (и работающий везде) способ: поместить на сайт картинку, на которой и будет формула. Этот пост, рассказывает, как можно быстро делать такие картинки.

Дополнение: несколько ещё более лёгких способов поместить формулу в блоге или на форуме перечислено здесь.


Для этого, кроме LaTeX, Ghostscript и Netpbm потребуется скрипт textogif Джона Уокера. Вот прямая ссылка на textogif_1.1.tar.gz.

К этому скрипту я использую вот такую обвязку, чтобы не создавать файл TeX каждый раз вручную: formula2png. Обвязка создаёт файл LaTeX, в который и помещает нужную формулу, и скармливает этот файл textogif. Использую я её так:
$ formula2png imagename formula
Например:
$ formula2png schröd 'i\hbar\frac{\partial}{\partial t}\psi=-\frac{\hbar^2}{2m}\nabla^2\psi + V\psi'
После этого в текущем каталоге у меня появляется файл schröd.png с прозрачным фоном.

Schrödinger equation: i\hbar\frac{\partial}{\partial t}\psi=-\frac{\hbar^2}{2m}\nabla^2\psi + V\psi

В данном случае формула набрана 12 пунктом, и изображение отрисовано из расчёта на разрешение 120 dpi. Эти параметры, а также уровень серого для фона, на котором предполагается размещать формулу, задаются в параметрах textogif (см. примеры внутри formula2png).

Другой способ размещения формул на веб-страницах — MathML, но он работает не со всеми браузерами и сложнее в использовании. Есть ещё конвертеры, вроде latex2html, но для одной–двух формул — они «тяжеловаты».

20070830

Веб-сервер в одну строчку: версия 2.1

Алексей Свешников опубликовал в своём блоге пример веб-сервера, занимающего одну строчку на bash. Очень меня этой безделушкой порадовал. Красиво!

Однако его «сервер» у меня не заработал :)

Если запрос слать телнетом, то вроде что-то шлёт в ответ, а если запрашивать браузером или wget — нет. Пишется: ««127.0.0.1» разорвал соединение».

У меня возникло подозрение, что это из-за того, что скрипт начинает отвечать, не дождавшись конца HTTP запроса (а должен читать заголовки запроса до пустой строки). Я это исправил и wget заработал.

Также, при выдаче каталога хорошо бы отдавать Content-Type, подумал я. Например, без верного Content-Type wget сохраняет пустой файл.

И честно говорить, что это HTTP/1.0. Или поставить HTTP/1.1 и добавить Connection: close. (HTTP/1.1 applications that do not support persistent connections MUST include the "close" connection option in every message. —RFC2616)

Потом ещё я увидел, что символы перевода строк где-то «защищаются» bash. А именно во время вывода echo. Приходят литералы «\n» вместо кодов CRLF. А положено, чтобы шли именно CRLF. И заголовок ответа также должен заканчиваться пустой строкой.

В общем, вот мой вариант (эта версия у меня работает и с браузером, и с wget):

true; while [ $? -eq 0 ];do nc -vlp 8080 -c'(r=read;e=echo;$r a b c;E=NOTYET;while [ ${#E} -gt 0 ];do $r E;E=`$e $E|tr -d "\r\n"`;done;f=`$e $b|sed 's/[^a-z0-9_.-]//gi'`;h="HTTP/1.0";o="$h 200 OK\r\n";c="Content";if [ -z $f ];then($e -e $o;ls|(while $r n;do if test -f ${n}; then $e "<a href=\"/$n\">$n</a><br>";fi;done););elif [ -f $f ];then $e -e "$o$c-Type: `file -ib $f`\n$c-Length: `stat -c%s $f`\n";cat $f;else $e -e "$h 404 Not Found\n\n404\n";fi)';done


Эта версия ещё и скрывает подкаталоги текущего каталога, показывая только файлы. Набирать одной строчкой и смотреть результаты на 127.0.0.1:8080.

20070827

TiddlyWiki: персональный вики-блокнот

TiddlyWiki — это очень необычное приложение, это полноценная вики, умещающаяся в одном HTML файле. Да, в одном файле! При этом, если его открыть в почти любом современном браузере*, то появиться настоящая вики с довольно удобным интерфейсом. Можно вносить в неё любые изменения и сохранять их в этот же файл.

*С TiddlyWiki умеют работать Firefox и другие основанные на Gecko, а также Opera**, IE, Safari**, Camino. А вот в Konqueror не работает редактирование.


Применений множество. Во-первых, это удобно использовать для разных записей, например в качестве личного органайзера, ведь при этом есть возможность связывать записи вики-ссылками. Во-вторых, если потом этот файл выложить на любой веб-сервер как статическую страницу, то она превращается в полноценный вики-сайт. С поиском, тегами, AJAX-подобным интерфейсом и прочими вкусностями. Впрочем, при доступе к странице по HTTP редактирование недоступно. Это всё таки персональная вики.

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

На мой взгляд, TiddlyWiki может быть удачной заменой таким приложениям как Tomboy или Zim. В прошлом я пользовался Tomboy, и TiddlyWiki удовлетворяет меня гораздо больше: она быстрее и отзывчивее, она кроссплатформенна, набор заметок и навигация по ним удобнее, и в добавок файл TiddlyWiki всегда можно выложить в веб :)

Ещё одно возможное применение: ведение дневника-блога. Этому помогает функция датированных записей и встроенная поддержка RSS.

Преимущества TiddlyWiki

Итак, я вижу следующие преимущества TiddlyWiki:
  • Переносимость. Ведь все данные и сама программа находятся в одном файле. Его можно поместить на флэшку и иметь доступ к своим записям где бы вы ни оказались. Это большое преимущество перед традиционными программами для ведения записей, которые требуют установки.
  • Кроссплатформенность. Для работы с ней достаточно браузера. И неважно, какая операционная система у вас стоит.
  • Быстрый запуск. Ведь веб-браузер и так почти всегда открыт :) Для приложения, предназначенного для коротких быстрых записей это существенно.
  • Удобный ввод данных. Богатый набор средств разметки. Ненавязчивый интерфейс.
  • Удобство навигации по записям: поиск, теги, возможность выбирать какие записи видны на экране.
  • Формат данных максимально приближен к plain-text. При желании всегда можно отредактировать в обычном текстовом редакторе. При этом, благодаря вики-разметке, записи остаются наглядными и читаемыми.
  • TiddlyWiki является свободным программным обеспечением с очень активным сообществом. Действительно, попробовав, наверное, нельзя в эту программку не влюбиться. Как результат: есть много модов, плагинов и тому подобных вещей.

С чего начать

Инструкция по «установке» следующая:
  1. Пойти на http://www.tiddlywiki.com/#DownloadSoftware и скачать пустой файл вики.
  2. (Необязательно) Пойти на http://glebsite.ru/tw/ и следуя приведённым там инструкциям установить модуль русификаци написанный Глебом Тржемецким (спасибо!)
  3. (Необязательно) По аналогии с установкой модуля русификации можно установить другие модули с других TW-сайтов, например, панельку инструментов wikibar c http://aiddlywiki.sourceforge.net/wikibar_demo_2.html
Для тех, кому лень проделывать все эти шаги самостоятельно, я выложил уже готовые русифицированные шаблоны TiddlyWiki. Они дополнены также небольшой справкой по вики-разметке. Мне её не хватало в умолчальном «пустом» шаблоне. Поэтому для близких и друзей я её написал.

Вот мои шаблоны:
empty-ru.html — базовая TW + русификация + ReminderMacros + справка по вики-разметке
empty-ru-wikibar.html — базовая TW + русификация + ReminderMacros + справка по вики-разметке + wikibar

Впрочем, если времени с момента написания этого поста (август 2007) до момента, когда вы его читаете, прошло немало, лучше пойти на www.tiddlywiki.com и взять последнюю версию оттуда. Кстати, там же есть и инструкция как обновлять код TW с сохранением данных.

Внимание! При сохранении важно использовать вариант «Сохранить ссылку как...» в контекстном меню при правом щелчке мыши по ссылке. Другие варианты могут не работать. Вариант wget URL тоже приемлим.

После этого сохранённый файл можно открывать в браузере и начинать редактировать. Первая запись, которая будет открыта называется GettingStarted. В ней можно задать своё имя и перейти к специальным записям SiteTitle и SiteSubtitle задающим название вашей вики. Ну а дальше на ваше усмотрение. Полезно также заглянуть в раздел «Установки» («Options») справа. После сеанса редактирования вики не забудьте нажать ссылку «Сохранить изменения» или ALT+S (впрочем, браузер вам напомнит, если что). При попытке сохранить файл на диск, браузер вероятно попросит разрешения на диск писать (обычно у него такого права нет, так что это нормально).

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

Миграция записей Tomboy

До TiddlyWiki я пользовался (пытался пользоваться) Tomboy. Милое приложение, только несколько медленное. И не всегда было легко добиться, чтобы создавалась нужная ссылка. И как-то совсем перестало работать, потому что во время одного из distupgrade-ов сломалось Mono, точнее что-то, от чего оно где-то глубоко внутри зависит... В общем, как и с некоторыми другими Mono-приложениями*, Tomboy у меня был, а теперь его нет. Однако после него остались записи.

*В числе других Mono-приложений, от которых я отказался: F-Spot (хотя я однажды и дождался, когда он проиндексирует мой фотоархив, работал медленно, пользоваться тегами было неудобно, а функции редактирования в нём меня после Picasa только расстраивали) и Beagle (поиск это конечно здорово, но не очень-то быстро, и самое главное, что ищет далеко не всё, а это делает его несколько бесполезным). Так что из всех Mono-приложений пользуюсь только autopano-sift.

Для конвертации записей Tomboy в TiddlyWiki я написал маленький скрипт tomboy2tiddly, описание и инструкции к нему здесь. Скрипт сохраняет гиперссылки в записях Tomboy и преобразует их в вики-ссылки, но все выделения шрифтом игнорируются. Для работы скрипта нужен xmlstarlet и python.

Скорость работы

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

Я провёл следующий эксперимент: взял свою вики с несколькими десятками записей, открыл в vim, и вставил 1001 короткую запись со случайным названием и содержимым (формат записи довольно простой). После этого полученный файл, который увеличился примерно на 100 кБ, открыл в браузере. Результат такой: время открытия файла увеличилось, файл открывался около 2 секунд, но скорость работы с вики осталась субъективно такой же (мои записи среди этой кучи мусора открывались так же быстро и изменение-сохранение записей никак не пострадало). Надо отметить, что генерировались эти 1001 запись куда дольше ;)

Мне кажется это неплохой результат. Во всяком случае, для личного вики-блокнота этого вполне достаточно. И после загрузки, в отличие от того же Tomboy, время отклика зато гораздо меньше.

Ссылки по теме

В заключение хочу дать ссылки на сайты с разными расширениями для TiddlyWiki и её модами (большинство из них сделаны, кстати, на самой TW):
  • Список разных модификаций и вариантов есть как обычно на Wikipedia
  • mptw.tiddlyspot.com MonkeyPirateTiddlyWiki, популярная модификация с изменённой системой тегов
  • www.tiddlytools.com большой архив расширений, тем и прочего добра
  • tw.lewcid.org ещё один репозиторий с расширениями для TW
  • tiddlywiki.abego-software.de и ещё один архив...
  • tiddlythemes.com архив тем для TiddlyWiki
Есть также возможность пользоваться TiddlyWiki онлайн, для этого есть как минимум два сервиса. В этом случае вики находится на сервере, но её всегда можно скачать на локальный диск, и потом синхронизировать обратно:
Стоит также отдельно отметить сайт сообщества разработчиков www.tiddlywiki.org и очень активную гугл-группу, посвящённую обсуждению TiddlyWiki.

Приятных записей!

Обновления: упомянул о поддержке RSS и поставил ссылку на гугл-группу.

Обновление 7.9.2007
**В комментариях отметили, что в Opera 9 не работает сохранение. Я установил Оперу, попробовал — действительно. Впрочем, в Google-группе TiddlyWiki объясняется в чём дело (Javascript в Opera в принципе не может писать на локальный жёсткий диск) и предложено решение проблемы. Сохранять TiddlyWiki в Opera (и Safari) можно с помощью Java-апплета TiddlySaver, помещённого туда же, где находится файл TiddlyWiki. Конечно, в этом случае требуется, чтобы в Opera была включена Java, а в ~/.java.policy было добавлено разрешение на запись файлов
grant codeBase "file:${user.home}/text/wiki/*" {
permission java.io.FilePermission "${user.home}${/}text${/}wiki${/}*", "read,write";
};