Думаю, ни для кого не секрет, что иностранные слова легче запомнить когда знаешь как они произносятся. Благо, для этого есть отличный online-сервис Forvo — база произношений слов. Этот сервис предлагает веб-интерфейс (а также api с некоторыми ограничениями, о котором чуть позже), для доступа к базе и прослушивания слов. Но каждый раз открывать браузер для прослушивания — не очень удобно. Поэтому я начал искать простенький forvo-клиент. Требования у меня были следующими: простота использования, никаких GUI, легкая переносимость, отсутствие требования хранения каких-либо настроек. Но вот незадача — все попытки найти подобный, простенький клиент под Linux не увенчались успехом, что меня сильно удивило. Ведь реализация такого клиента, является, по сути не слишком уж сложной задачей. Таким образом, я понял что придется написать утилиту самому.
Подходов к решению данной задачи конечно же много. Я посчитал, что оптимальным выбором будет использование связки bash + awk + curl + mpg123 (или другой какой-нибудь плеер). Так что прежде ставим нужные пакеты, например для Debian-based систем:
Забегая вперед — forvo-api я не использовал, причины объясню в конце статьи.
При изучении страницы поиска forvo — можно заметить что форма сабмитится таким POST запросом:
таким образом, данный запрос мы можем реализовать путем вызова:
Ответ на данный запрос, нам приходит в виде html, в теле которого содержатся ссылки (нам нужна самая первая) в которых содержится url аудиопотока произношения искомого слова. Таким образом надо реализовать парсер извлекающий url аудио-потока. Реализация на awk:
Получив url аудиопотока, воспроизводим его при помощи микро-плеера mpg123. Тут может возникнуть резонный вопрос: почему именно mpg123, а не другой плеер? Хм… при выборе плеера я искал максимально минималистический плеер, способный воспроизводить потоковое аудио.
Таким образом главный скрипт будет выглядеть так:
Но тут встает первая проблема: у нас в результате получилось два файла (say и parser.awk), что для такой маленькой утилиты не очень хорошо. Хотелось бы чтобы эта утилита была представлена в одном файле. Отсюда встает вопрос: как объединить две разнородные программы написанные на shell (bash) и на awk?
Этот подход хорош для однострочных awk-программ. Если же программа чуть больше однострочника, могут возникнуть сложности с экранировкой кавычек (как одинарных так и двойных). Экранировка в свою очередь приводит к «замусориванию» самой программы, и усложнению ее поддержки и расширения. Так что этот подход в данном случае не подходит.
такой подход позволяет, без каких-либо-то изменений, включать код awk (да и не только) программ в shell-скрипты. В репозитории можно найти реализацию функции getAwkProgram, которая дает возможность именовать и загружать по имени, интегрированные в shell-скрипт, awk программы. Решил эту функцию здесь не приводить, так как это, думаю, отвлекло бы от основной темы.
Этот метод основан на heredoc-синтаксисе. Хотя, такой подход более естественен (с точки зрения bash) и несомненно лучше варианта №1 (inline-программы), но все же считаю его менее читабельным по сравнению с вариантом №2.
Таким образом, используя второй подход, теперь наш forvo-клиент легко умещается в одном файле:
Приведем плюсы и минусы описанного здесь подхода, и подхода при котором используются forvo-api
Стоит заметь еще одну мелочь — почему-то, у меня, mpg123 отказался воспринимать ссылку полученную через forvo-api запрос.
Так как целью статьи было показать возможный метод решения подобной задачи, то я решил привести здесь базовую реализацию клиента (без возможности персистентного переключения языка произношени��). Более полная версия клиента доступна на github.com.
На хабре не один раз публиковались полезные посты которые так или иначе касались темы иностранных языков. Сравнительно недавно пробегал пост из песочницы, в котором мне понравилась сама идея создания пользовательского словаря. А также пост, в котором, была предложена идея привязывания комбинации клавиш для перевода выделенных слов/фраз. Объединив идеи этих статей, можно порекомендовать такую схему:
Постановка задачи
- Сделать максимально простой 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-клиенту комбинацию клавиш
