Постановка задачи
- Сделать максимально простой forvo-клиент, который должен удовлетворять требованиям указанным выше;
- Должен иметь простой интерфейс командной строки:
$say hello world # произнести "hello world"
$say -lng=ru # персистентно сменить язык произношения на русский (тут можно указывать любой язык en, ru, tt, etc...)
Выбор инструментария
Подходов к решению данной задачи конечно же много. Я посчитал, что оптимальным выбором будет использование связки bash + awk + curl + mpg123 (или другой какой-нибудь плеер). Так что прежде ставим нужные пакеты, например для Debian-based систем:
$sudo apt-get install gawk curl mpg123Решение
Забегая вперед — forvo-api я не использовал, причины объясню в конце статьи.
При изучении страницы поиска forvo — можно заметить что форма сабмитится таким POST запросом:
params:
id_lang=$LANGUAGE_ID, где LANGUAGE_ID - идентификатор языка произношения
word_search=$WORD, где WORD - искомое слово
post-url:
http://www.forvo.com/search/ - адрес отправки запроса
таким образом, данный запрос мы можем реализовать путем вызова:
#!/bin/bash LANGUAGE_ID=39 #id английского языка (для других языков можно посмотреть в ниспадающем списке id_lang) WORD="hello world" curl -d "id_lang=$LANGUAGE_ID&word_search=$WORD" -L 'http://www.forvo.com/search/'
Ответ на данный запрос, нам приходит в виде html, в теле которого содержатся ссылки (нам нужна самая первая) в которых содержится url аудиопотока произношения искомого слова. Таким образом надо реализовать парсер извлекающий url аудио-потока. Реализация на awk:
# # parser.awk # /var (_SERVER_HOST|_AUDIO_HTTP_HOST)/{ if(match($0, /var[ \t]+(_SERVER_HOST|_AUDIO_HTTP_HOST)[ \t]*=[ \t]*'?([^']+)'?/, arr)){ if(arr[1] == "_SERVER_HOST"){ srv_host = arr[2]; } else if(arr[1] == "_AUDIO_HTTP_HOST") { audio_http_host = arr[2]; } } } /<a href.+onclick="Play\(/{ if(match($0, /onclick="Play\([^,]+,'([^,]+)'.+\)/, arr)){ mp3Path = arr[1]; if (srv_host == audio_http_host){ mp3Path = ("http://" srv_host "/player-mp3Handler.php?path=" mp3Path); } else { mp3Path = ("http://" audio_http_host "/mp3/" base64_decode(mp3Path)); } } exit; } function base64_decode(val){ command = ("echo '" val "' | base64 -d"); command | getline ret; close(command); return ret; } END{ if(mp3Path) print mp3Path; }
Получив url аудиопотока, воспроизводим его при помощи микро-плеера mpg123. Тут может возникнуть резонный вопрос: почему именно mpg123, а не другой плеер? Хм… при выборе плеера я искал максимально минималистический плеер, способный воспроизводить потоковое аудио.
Таким образом главный скрипт будет выглядеть так:
# # say # LANGUAGE_ID=39 WORD=$@ if [[ -n $WORD ]]; then URL=$(curl -d "id_lang=$LANGUAGE_ID&word_search=$WORD" -L 'http://www.forvo.com/search/' 2> /dev/null | awk -f ${0%/*}/parser.awk) if [[ -n $URL ]]; then mpg123 -q $URL else echo not found fi fi
Но тут встает первая проблема: у нас в результате получилось два файла (say и parser.awk), что для такой маленькой утилиты не очень хорошо. Хотелось бы чтобы эта утилита была представлена в одном файле. Отсюда встает вопрос: как объединить две разнородные программы написанные на shell (bash) и на awk?
Вариант 1
Использовать стандартную возможность awk — заключать программу в кавычки и передавать ее в виде параметра командной строки:# # examlpe1.sh # echo "from shell script" AWK_PRG="BEGIN{ print \"from awk program\" }" awk "$AWK_PRG"
Этот подход хорош для однострочных awk-программ. Если же программа чуть больше однострочника, могут возникнуть сложности с экранировкой кавычек (как одинарных так и двойных). Экранировка в свою очередь приводит к «замусориванию» самой программы, и усложнению ее поддержки и расширения. Так что этот подход в данном случае не подходит.
Вариант 2
Трюкачество. Для начала поразмыслим. Возьмем в учет что shell-скрипты — интерпретируемы, т.е. скрипт выполняется покомандно (или построчно). Таким образом возникает мысль: а что если поместить awk программу в самый конец shell-скрипта, а перед ней поставить командуexit, чтобы интерпретатор bash, после исполнения всего shell-скрипта, не начинал считывать awk программу. Так, объединить shell скрипт c awk программой нам удалось. Но как же теперь, эту, находящуюся в хвосте файла, awk программу прочесть и исполнить? Ответ напрашивается сам собой — используем awk) Т.е. нам надо просто пометить каким нибудь маркером (например комментарий) конец shell-скрипта и начало awk-программы и дать этот файл на обработку другой awk программе которая будет считывать все что после маркера:# # examlpe2.sh # echo "this is shell script" AWK_PRG=$(awk '(/^### AWK PROGRAMM MARKER ###$/ || body){body=1; print $0}' $0) awk "$AWK_PRG" exit ### AWK PROGRAMM MARKER ### BEGIN{ print "from awk program" }
такой подход позволяет, без каких-либо-то изменений, включать код awk (да и не только) программ в shell-скрипты. В репозитории можно найти реализацию функции getAwkProgram, которая дает возможность именовать и загружать по имени, интегрированные в shell-скрипт, awk программы. Решил эту функцию здесь не приводить, так как это, думаю, отвлекло бы от основной темы.
Вариант 3
Спасибо xaizek за напоминание еще одного метода интеграции awk программ в shell-скрипты:# # examlpe1.sh # echo "from shell script" AWK_PRG=$(cat << 'EOL' BEGIN{ print "from awk program" } EOL ) awk "$AWK_PRG"
Этот метод основан на heredoc-синтаксисе. Хотя, такой подход более естественен (с точки зрения bash) и несомненно лучше варианта №1 (inline-программы), но все же считаю его менее читабельным по сравнению с вариантом №2.
Таким образом, используя второй подход, теперь наш forvo-клиент легко умещается в одном файле:
#!/bin/bash LANGUAGE_ID=39 #english # Trick for mixing AWK and Shell programs in the same file PARSER_PRG=$(awk '(/^### AWK PROGRAMM MARKER ###$/ || body){body=1; print $0}' $0) WORD=$@ if [[ -n $WORD ]]; then URL=$(curl -d "id_lang=$LANGUAGE_ID&word_search=$WORD" -L 'http://www.forvo.com/search/' 2> /dev/null | awk "$PARSER_PRG") if [[ -n $URL ]]; then mpg123 -q $URL else echo not found fi fi exit ### AWK PROGRAMM MARKER ### # parser /var (_SERVER_HOST|_AUDIO_HTTP_HOST)/{ if(match($0, /var[ \t]+(_SERVER_HOST|_AUDIO_HTTP_HOST)[ \t]*=[ \t]*'?([^']+)'?/, arr)){ if(arr[1] == "_SERVER_HOST"){ srv_host = arr[2]; } else if(arr[1] == "_AUDIO_HTTP_HOST") { audio_http_host = arr[2]; } } } /<a href.+onclick="Play\(/{ if(match($0, /onclick="Play\([^,]+,'([^,]+)'.+\)/, arr)){ mp3Path = arr[1]; if (srv_host == audio_http_host){ mp3Path = ("http://" srv_host "/player-mp3Handler.php?path=" mp3Path); } else { mp3Path = ("http://" audio_http_host "/mp3/" base64_decode(mp3Path)); } } exit; } function base64_decode(val){ command = ("echo '" val "' | base64 -d"); command | getline ret; close(command); return ret; } END{ if(mp3Path) print mp3Path; }
Выводы
Приведем плюсы и минусы описанного здесь подхода, и подхода при котором используются forvo-api
- Текущий подход:
+ нет надобности иметь учетную запись на forvo.com
+ нет надобности хранить и переносить forvo-api ключи
- работоспособность клиента зависит от дизайна сайта (т.е. если на fovro проведут глобальные изменения, то придется фиксить парсер) - forvo-API подход:
+ простота реализации клиента
+ теоретически меньше входящий трафик для каждого запроса
- необходимость иметь учетную запись forvo.com (для получения forvo-api ключа)
- необходимость носить с собой forvo-api ключь
Стоит заметь еще одну мелочь — почему-то, у меня, mpg123 отказался воспринимать ссылку полученную через forvo-api запрос.
Заключение
Так как целью статьи было показать возможный метод решения подобной задачи, то я решил привести здесь базовую реализацию клиента (без возможности персистентного переключения языка произношения). Более полная версия клиента доступна на github.com.
Послесловие
На хабре не один раз публиковались полезные посты которые так или иначе касались темы иностранных языков. Сравнительно недавно пробегал пост из песочницы, в котором мне понравилась сама идея создания пользовательского словаря. А также пост, в котором, была предложена идея привязывания комбинации клавиш для перевода выделенных слов/фраз. Объединив идеи этих статей, можно порекомендовать такую схему:
- пользовательский словарь пополнять и использовать при помощи Anki
- по аналогии со вторым постом назначить forvo-клиенту комбинацию клавиш

