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

    Думаю, ни для кого не секрет, что иностранные слова легче запомнить когда знаешь как они произносятся. Благо, для этого есть отличный 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-клиенту комбинацию клавиш
    Теперь при встрече незнакомого слова можно одним нажатием клавиши узнать его перевод и как оно произносится. После чего обязательно его добавить в свой персональный словарь с транскрипцией. Я пользуюсь похожей схемой, только перевод слов смотрю плагином для браузера.
    Поделиться публикацией

    Комментарии 12

      +1
      Вариант 3 — Использовать here-файлы (те, которые вводятся через <<).

      Вы его просто забыли или он по каким-то конкретным причинам не подошёл?
        0
        Да, действительно это тоже вариант, такой синтаксис я встречал однажды, но если честно ни разу не применял) Этот вариант гораздо лучше первого, но всеже считаю второй читабельнее, но это уже вопрос вкуса)

        PRG=$(cat <<END
        BEGIN{
        print «from awk program»
        }
        END
        )
        +1
        Универсальный способ — использовать многострочный one-liner:

        awk 'awk-code' files
        


        > Ответ напрашивается сам собой — используем awk
        Можно, но реализация сложная. Лучше использовать sed:

        sed -e '0,/^#!.*awk/d' $0
        


        Если все-таки хочется awk, вот пример чуть попроще:

        awk '/^#!.*awk/ { body = 1 } body' $0
        
          0
          В качестве маркера — привычный всем шебанг, что тоже радует глаз
            0
            Да согласен, лоадер выходит еще проще реализовать)
            Интересно возможно ли седом реализовать аналог getAwkProgram (та что в репозитории)?
              0
              Так ведь я и привел вариант sed. А так его можно использовать напрямую:

              sed -e '0,/^#!.*awk/d' $0 | awk -f - inputfile
              


              Аналогично и пример с awk
              0
              Ди и несовсем понял про универсальный способ мультилайн однострочников, можно поподробнее?
                0
                Вы его используете в своей функции getAwkProgram

                Вот тот же пример, но многострочный и расписанный подробно:
                awk '
                    BEGIN {
                        body = 0;
                    }
                
                    /^#!.*awk/ {
                        body = 1;
                    }
                
                    body {
                        print;
                    }
                ' inputfile
                
                  0
                  Теперь понял что Вы имели ввиду) Но это то что я описал в первом варианте, к сожалению этот метод для программ имеющих разнородные кавычки, по описанным в статье причинам, становится непригодным.
              0
              Спасибо, давно искал что-нибудь подобное. Здорово то, что на forvo есть достаточно редкие языки, произношение которых просто не найти.

              Насчет анки и других подобных программ весьма интересно. Правда лучше бы, чтобы это был плагин к анки. Ну или, что еще лучше, возможность скачать слова, так как не всегда есть интернет (например если учить на телефоне).
                0
                Рад что статья оказалось полезной, насчет возможности закачки, ее можно добавить таким образом:

                найти в скрипте эту строку
                mpg123 -q $URL
                
                и заменить (или дополнить) ее этой строкой
                curl -o "$WORD.mp3" $URL
                

                Таким образом будет скачиваться mp3 файл с названием того слова которое давалось в поиске.
                Кстати, вроде Anki позволяет в словари добавлять аудиозаписи, так что таким образом можно пополнять и произношения. Насчет плагинов для Anki — надо посмотреть, возможно ли их написание.
                  0
                  Спасибо за разъяснение :).
                  Анки действительно отлично работает с аудио. В последнее время только так и учу слова, то есть одновременно со звуком. Иначе произношение не могу усвоить.
                  Плагины для анки есть, например даже вытягивающий произношение с google translator. Он довольно удобен, но качество синтезированной речи хромает. И, разумеется, что самое главное, в google translate представлены только популярные языки. А это большой минус, если нужен редкий язык.
                  По-видимому раньше был плагин работающий с forvo, но сейчас точно не работает. Думаю что ваш вариант намного лучше плагина.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое