- PVSM.RU - https://www.pvsm.ru -

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

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

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

  1. Сделать максимально простой forvo-клиент, который должен удовлетворять требованиям указанным выше;
  2. Должен иметь простой интерфейс командной строки:
    $say hello world # произнести "hello world"
    $say -lng=ru # персистентно сменить язык произношения на русский (тут можно указывать любой язык en, ru, tt, etc...)

Выбор инструментария

Подходов к решению данной задачи конечно же много. Я посчитал, что оптимальным выбором будет использование связки bash + awk + mpg123 (или другой какой-нибудь плеер). Так что прежде ставим нужные пакеты, например для Debian-based систем:

$sudo apt-get install gawk 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-скрипты. В репозитории [2] можно найти реализацию функции getAwkProgram, которая дает возможность именовать и загружать по имени, интегрированные в shell-скрипт, awk программы. Решил эту функцию здесь не приводить, так как это, думаю, отвлекло бы от основной темы.
Теперь наш 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 [2].

Послесловие

На хабре не один раз публиковались полезные посты которые так или иначе касались темы иностранных языков. Сравнительно недавно пробегал пост из песочницы [3], в котором мне понравилась сама идея создания пользовательского словаря. А также пост [4], в котором, была предложена идея привязывания комбинации клавиш для перевода выделенных слов/фраз. Объединив идеи этих статей, можно порекомендовать такую схему:

  • пользовательский словарь пополнять и использовать при помощи Anki
  • по аналогии со вторым постом назначить forvo-клиенту комбинацию клавиш

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

Автор: unixod


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/11449

Ссылки в тексте:

[1] Forvo: http://www.forvo.com/

[2] репозитории: https://github.com/unixod/say/blob/master/say

[3] пост из песочницы: http://habrahabr.ru/sandbox/44842/

[4] пост: http://habrahabr.ru/post/137215/