Как стать автором
Обновить

Клиент для сервиса Forvo.com подручными средствами

Время на прочтение6 мин
Количество просмотров5.4K
Думаю, ни для кого не секрет, что иностранные слова легче запомнить когда знаешь как они произносятся. Благо, для этого есть отличный online-сервис Forvo — база произношений слов. Этот сервис предлагает веб-интерфейс (а также api с некоторыми ограничениями, о котором чуть позже), для доступа к базе и прослушивания слов. Но каждый раз открывать браузер для прослушивания — не очень удобно. Поэтому я начал искать простенький forvo-клиент. Требования у меня были следующими: простота использования, никаких GUI, легкая переносимость, отсутствие требования хранения каких-либо настроек. Но вот незадача — все попытки найти подобный, простенький клиент под Linux не увенчались успехом, что меня сильно удивило. Ведь реализация такого клиента, является, по сути не слишком уж сложной задачей. Таким образом, я понял что придется написать утилиту самому.


Постановка задачи


  1. Сделать максимально простой forvo-клиент, который должен удовлетворять требованиям указанным выше;
  2. Должен иметь простой интерфейс командной строки:
    $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-клиенту комбинацию клавиш
Теперь при встрече незнакомого слова можно одним нажатием клавиши узнать его перевод и как оно произносится. После чего обязательно его добавить в свой персональный словарь с транскрипцией. Я пользуюсь похожей схемой, только перевод слов смотрю плагином для браузера.
Теги:
Хабы:
+5
Комментарии12

Публикации