Одна из областей, где Unix блистает особенно ярко, — это Интернет. Неважно, собираетесь ли вы запустить быстрый сервер на своем компьютере или просто с толком побродить по Сети, сценарии командной оболочки всегда придут на помощь.
Инструменты для работы с Интернетом допускают возможность управления из сценариев, даже если вы никогда не думали о таком их применении. Например, программой FTP, которая постоянно оказывается в ловушке отладочного режима, можно управлять интересными способами, как описывается в сценарии № 53 ниже. Сценарии командной оболочки часто позволяют улучшить производительность и вывод большинства утилит командной строки, выполняющих те или иные операции с Интернетом.
Первое издание этой книги уверяло читателей, что лучший инструмент для сценариев, работающих с Интернетом, — команда lynx; теперь мы рекомендуем использовать curl. Оба инструмента поддерживают исключительно текстовый интерфейс для доступа в Интернет, но если lynx предлагает механизм, напоминающий браузер, то curl специально проектировался для использования в сценариях и выводит исходный код HTML любых страниц, которые вы решите исследовать.
Например, ниже показано, как с помощью curl получить первые семь строк из главной страницы сайта Dave on Film:
$ curl -s http://www.daveonfilm.com/ | head -7
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<link rel="profile" href="http://gmpg.org/xfn/11" />
<link rel="pingback" href="http://www.daveonfilm.com/xmlrpc.php" />
<title>Dave On Film: Smart Movie Reviews from Dave Taylor</title>
Тот же результат можно получить с помощью lynx, если утилита curl недоступна, но, если у вас имеются обе утилиты, мы рекомендуем использовать curl. Именно с ней мы будем работать в данной главе.
ВНИМАНИЕ
Одно из ограничений приведенных в этой главе сценариев, извлекающих информацию из веб-сайтов, состоит в том, что, если веб-сайт, от которого зависит сценарий, изменит верстку или API после выхода книги, сценарий может перестать работать. Но, имея навык чтения разметки HTML или JSON (даже если вы не понимаете их в полном объеме), вы сумеете все исправить. Проблема трассировки других сайтов является основной причиной создания расширяемого языка разметки (Extensible Markup Language, XML): он позволяет разработчикам сайтов возвращать содержимое страниц отдельно от правил его размещения.
Когда-то одним из самых востребованных применений Интернета была передача файлов, а одним из самых простых решений этой задачи стал протокол передачи файлов (File Transfer Protocol, FTP). На базовом уровне все взаимодействия в Интернете сводятся к передаче файлов. Например, веб-браузер запрашивает передачу HTML-документа и сопутствующих изображений, чат-сервер постоянно передает строки дискуссии взад-вперед, почтовые программы пересылают электронные письма из одного конца мира в другой.
Оригинальная программа FTP все еще остается в строю, и, несмотря на довольно убогий интерфейс, она обладает достаточно мощными средствами и возможностями, чтобы иметь ее на вооружении. Существует богатое разнообразие программ с поддержкой FTP, из которых особенно примечательны FileZilla (http://filezilla-project.org/) и NcFTP (http://www.ncftp.org/), плюс масса замечательных графических интерфейсов, делающих работу с FTP более удобной. Однако FTP с успехом можно использовать для загрузки и выгрузки файлов, написав сценарии-обертки на языке командной оболочки.
Например, FTP часто используется для загрузки файлов из Интернета. Именно эту возможность реализует сценарий в листинге 7.1. Нередко файлы находятся на анонимных FTP-серверах, имеющих адреса URL следующего вида: ftp://<некоторый_сервер>/<путь>/<имя_файла>/.
Листинг 7.1. Сценарий ftpget
#!/bin/bash
# ftpget -- получая URL в стиле ftp, разворачивает его и пытается получить
# файл, используя прием доступа к анонимному ftp.
anonpass="$LOGNAME@$(hostname)"
if [ $# -ne 1 ] ; then
echo "Usage: $0 ftp://..." >&2
exit 1
fi
# Типичный URL: ftp://ftp.ncftp.com/unixstuff/q2getty.tar.gz
if [ "$(echo $1 | cut -c1-6)" != "ftp://" ] ; then
echo "$0: Malformed url. I need it to start with ftp://" >&2
exit 1
fi
server="$(echo $1 | cut -d/ -f3)"
filename="$(echo $1 | cut -d/ -f4-)"
basefile="$(basename $filename)"
echo ${0}: Downloading $basefile from server $server
ftp -np << EOF
open $server
user ftp $anonpass
get "$filename" "$basefile"
quit
EOF
if [ $? -eq 0 ] ; then
ls -l $basefile
fi
exit 0
Основу сценария составляет последовательность команд, передаваемых программе FTP, которая начинается в строке . Эта последовательность иллюстрирует основы пакетной работы: последовательность инструкций передается отдельной программе так, что принимающая программа (в данном случае FTP) думает, будто инструкции вводятся пользователем. Эта последовательность предписывает открыть соединение с сервером, вводит имя анонимного пользователя (FTP) и пароль по умолчанию, указанный в разделе с настройками сценария (обычно адрес электронной почты), затем дает команду загрузить файл с FTP-сервера и завершает программу после загрузки.
Сценарий очень прост в использовании: достаточно указать полный адрес URL файла на FTP-сервере, и файл будет загружен в текущий каталог, как показано в листинге 7.2.
Листинг 7.2. Запуск сценария ftpget
$ ftpget ftp://ftp.ncftp.com/unixstuff/q2getty.tar.gz
ftpget: Downloading q2getty.tar.gz from server ftp.ncftp.com
-rw-r--r-- 1 taylor staff 4817 Aug 14 1998 q2getty.tar.gz
Некоторые версии FTP более многословны, чем другие. Кроме того, нередки случаи несоответствия реализаций протокола на стороне клиента и на стороне сервера. В подобных ситуациях такие «многословные» программы FTP иногда выводят пугающие сообщения об ошибках, к примеру Unimplemented command («Нереализованная команда»). Вы можете без опаски игнорировать их. Например, в листинге 7.3 показан вывод того же сценария, запущенного в OS X.
Листинг 7.3. Запуск сценария ftpget в OS X
$ ftpget ftp://ftp.ncftp.com/ncftp/ncftp-3.1.5-src.tar.bz2
../Scripts.new/053-ftpget.sh: Downloading q2getty.tar.gz from server ftp.
ncftp.com
Connected to ncftp.com.
220 ncftpd.com NcFTPd Server (licensed copy) ready.
331 Guest login ok, send your complete e-mail address as password.
230-You are user #2 of 16 simultaneous users allowed.
230-
230 Logged in anonymously.
Remote system type is UNIX.
Using binary mode to transfer files.
local: q2getty.tar.gz remote: unixstuff/q2getty.tar.gz
227 Entering Passive Mode (209,197,102,38,194,11)
150 Data connection accepted from 97.124.161.251:57849; transfer starting for
q2getty.tar.gz (4817 bytes).
100% |*******************************************************| 4817
67.41 KiB/s 00:00 ETA
226 Transfer completed.
4817 bytes received in 00:00 (63.28 KiB/s)
221 Goodbye.
-rw-r--r-- 1 taylor staff 4817 Aug 14 1998 q2getty.tar.gz
Если ваша версия FTP чересчур многословна и вы пользуетесь OS X, программу FTP можно сделать более сдержанной, добавив в ее вызов флаг -V (то есть заменить команду ftp -n командой ftp -nV).
В этот сценарий можно добавить автоматическое разархивирование загружаемых файлов (пример разархивирования вы найдете в сценарии № 33, глава 4), имеющих определенные расширения. Многие сжатые файлы, такие как .tar.gz и .tar.bz2, разархивируются с помощью системной команды tar.
В этот сценарий можно также добавить функцию выгрузки указанного файла на FTP-сервер. Если сервер поддерживает анонимные соединения (в наши дни таких серверов осталось очень немного из-за взломщиков-дилетантов и других злоумышленников, но это уже другая история), вам достаточно будет определить каталог назначения в командной строке или в самом сценарии и заменить команду get на put в последовательности команд, как показано ниже:
ftp -np << EOF
open $server
user ftp $anonpass
cd $destdir
put "$filename"
quit
EOF
Для доступа к защищенной паролем учетной записи на сервере FTP можно добавить в сценарий запрос пароля в интерактивном режиме, отключив эхо-вывод перед инструкцией read, и включить его снова после ввода:
/bin/echo -n "Password for ${user}: "
stty -echo
read password
stty echo
echo ""
Однако самый грамотный способ организовать ввод пароля — позволить программе FTP самой предложить ввести его, что в нашем сценарии произойдет автоматически: если для доступа к указанной учетной записи потребуется пароль, программа FTP сама предложит сделать это.
Простейшее применение lynx заключается в извлечении списка адресов URL, находящихся в данной веб-странице, что может пригодиться при поиске ссылок в Интернете. Выше мы говорили, что в этом издании книги предпочли уйти от lynx в сторону curl, но, как оказывается, lynx в сто раз удобнее для решения этой задачи (см. листинг 7.4), чем curl, потому что автоматически анализирует разметку HTML, тогда как curl вынуждает вас делать это вручную.
В вашей системе нет программы lynx? Большинство современных систем Unix снабжается диспетчерами пакетов, такими как yum в Red Hat, apt в Debian и brew в OS X (впрочем, brew не устанавливается по умолчанию), с помощью которых можно установить lynx. Если вы решите скомпилировать lynx самостоятельно или пожелаете загрузить скомпилированные двоичные файлы, вы найдете все необходимое по адресу: http://lynx.browser.org/.
Листинг 7.4. Сценарий getlinks
#!/bin/bash
# getlinks -- получая URL, возвращает все относительные и абсолютные ссылки.
# Принимает три параметра: -d генерирует первичные домены в каждой ссылке,
# -i выводит список только внутренних ссылок на сайт (то есть на другие
# страницы на том же сайте), и -x выводит список только внешних ссылок
# (в противоположность -i).
if [ $# -eq 0 ] ; then
echo "Usage: $0 [-d|-i|-x] url" >&2
echo "-d=domains only, -i=internal refs only, -x=external only" >&2
exit 1
fi
if [ $# -gt 1 ] ; then
case "$1" in
-d) lastcmd="cut -d/ -f3|sort|uniq"
shift
;;
-r) basedomain="http://$(echo $2 | cut -d/ -f3)/"
lastcmd="grep \"^$basedomain\"|sed \"s|$basedomain||g\"|sort|uniq"
shift
;;
-a) basedomain="http://$(echo $2 | cut -d/ -f3)/"
lastcmd="grep -v \"^$basedomain\"|sort|uniq"
shift
;;
*) echo "$0: unknown option specified: $1" >&2
exit 1
esac
else
lastcmd="sort|uniq"
fi
lynx -dump "$1"|\
sed -n '/^References$/,$p'|\
grep -E '[[:digit:]]+\.'|\
awk '{print $2}'|\
cut -d\? -f1|\
eval $lastcmd
exit 0
Отображая страницу, lynx отображает ее текст, стремясь сохранить форматирование как можно ближе к оригиналу, а также список всех гипертекстовых ссылок, найденных на этой странице. Данный сценарий извлекает только ссылки с использованием команды sed для вывода всего, что следует за строкой «References» (Ссылки) в тексте веб-страницы . Затем сценарий обрабатывает полученный список, как определено флагами, заданными пользователями.
Этот сценарий демонстрирует один интересный прием: настройку переменной lastcmd (, , , ) для фильтрации списка ссылок в соответствии с флагами, заданными пользователем. После настройки переменной lastcmd применяется удивительно удобная команда eval , чтобы заставить командную оболочку интерпретировать содержимое переменной как команду, а не как значение.
По умолчанию сценарий выводит список всех ссылок, найденных на указанной веб-странице, и не только тех, которые начинаются с префикса http:. Сценарию может быть передано три необязательных флага, влияющих на результат: флаг -d требует выводить только доменные имена в совпавших адресах URL, флаг -r требует оставить в списке только относительные ссылки (то есть указывающие на другие страницы на том же сервере, откуда получена текущая страница), и флаг -a требует вывести только абсолютные ссылки (то есть указывающие на другие серверы).
Простой запуск сценария возвращает список всех ссылок, найденных на указанной странице, как показано в листинге 7.5.
Листинг 7.5. Запуск сценария getlinks
$ getlinks http://www.daveonfilm.com/ | head -10
http://instagram.com/d1taylor
http://pinterest.com/d1taylor/
http://plus.google.com/110193533410016731852
https://plus.google.com/u/0/110193533410016731852
https://twitter.com/DaveTaylor
http://www.amazon.com/Doctor-Who-Shada-Adventures-Douglas/
http://www.daveonfilm.com/
http://www.daveonfilm.com/about-me/
http://www.daveonfilm.com/author/d1taylor/
http://www.daveonfilm.com/category/film-movie-reviews/
Еще одно из возможных применений сценария — получение списка доменных имен, на которые ссылается указанный сайт. На этот раз воспользуемся стандартным инструментом Unix — командой wc, чтобы подсчитать общее количество найденных ссылок:
$ getlinks http://www.amazon.com/ | wc -l
219
На домашней странице сайта Amazon найдено 219 ссылок. Внушительное количество! А сколько разных доменных имен представлено в этих ссылках? Давайте отфильтруем список, запустив сценарий с флагом -d:
$ getlinks -d http://www.amazon.com/ | head -10
amazonlocal.com
aws.amazon.com
fresh.amazon.com
kdp.amazon.com
services.amazon.com
www.6pm.com
www.abebooks.com
www.acx.com
www.afterschool.com
www.alexa.com
Сайт Amazon не стремится уводить посетителей за свои пределы, но есть ряд партнерских сайтов, ссылки на которые все же присутствуют на главной странице. Конечно, не все придерживаются такой политики.
А что, если ссылки на странице Amazon разделить на абсолютные и относительные?
$ getlinks -a http://www.amazon.com/ | wc -l
51
$ getlinks -r http://www.amazon.com/ | wc -l
222
Вполне ожидаемо, что количество относительных ссылок на странице Amazon, ссылающихся на внутренние страницы, в четыре раза превышает количество абсолютных ссылок, уводящих на другие веб-сайты. Всякий коммерческий сайт должен стремиться удержать пользователей на своих страницах!
Как видите, сценарий getlinks может быть очень полезным аналитическим инструментом. Далее в книге вы найдете один из вариантов его дальнейшего усовершенствования: сценарий № 69 в главе 9 помогает быстро проверить действительность всех гипертекстовых ссылок.
Сайт GitHub создавался как серьезное подспорье для индустрии открытого программного обеспечения и открытого сотрудничества людей по всему миру. Многие системные администраторы и разработчики посещают GitHub, чтобы получить исходный код какого-нибудь открытого проекта или оставить отчет о проблеме. Так как по сути GitHub — это социальная платформа для разработчиков, возможность быстро получить основную информацию о том или ином пользователе была бы весьма кстати. Сценарий в листинге 7.6 выводит некоторые сведения о заданном пользователе GitHub и позволяет познакомиться с очень мощным GitHub API.
Листинг 7.6. Сценарий githubuser
#!/bin/bash
# githubuser -- Получая имя пользователя GitHub, выводит информацию о нем.
if [ $# -ne 1 ]; then
echo "Usage: $0 <username>"
exit 1
fi
# Флаг -s подавляет вывод дополнительной информации,
# которую обычно выводит curl.
curl -s "https://api.github.com/users/$1" | \
awk -F'"' '
/\"name\":/ {
print $4" is the name of the GitHub user."
}
/\"followers\":/{
split($3, a, " ")
sub(/,/, "", a[2])
print "They have "a[2]" followers."
}
/\"following\":/{
split($3, a, " ")
sub(/,/, "", a[2])
print "They are following "a[2]" other users."
}
/\"created_at\":/{
print "Their account was created on "$4"."
}
'
exit 0
Следует признать, что это сценарий скорее на языке awk, чем на языке bash, но иногда для анализа данных приходится привлекать дополнительные возможности awk (GitHub API возвращает данные в формате JSON). С помощью curl сценарий запрашивает у сайта GitHub информацию о пользователе , заданном в аргументе, и передает данные в формате JSON команде awk. В сценарии awk определяется разделитель полей — символ двойной кавычки, чтобы упростить анализ JSON-данных. Затем выполняется сопоставление данных с несколькими регулярными выражениями в сценарии awk и выводятся результаты в удобочитаемом виде.
Сценарий принимает единственный аргумент: имя пользователя GitHub. Если указанное имя пользователя не будет найдено, сценарий ничего не выведет.
Если сценарию передается существующее имя пользователя, он должен вывести сводную информацию об этом пользователе GitHub, как показано в листинге 7.7.
Листинг 7.7. Запуск сценария githubuser
$ githubuser brandonprry
Brandon Perry is the name of the GitHub user.
They have 67 followers.
They are following 0 other users.
Their account was created on 2010-11-16T02:06:41Z.
Этот сценарий имеет большой потенциал благодаря объему информации, возвращаемому GitHub API. Он выводит только четыре значения из возвращаемых JSON-данных. Создание «резюме» на основе информации, которую API возвращает подобно многим веб-службам, лишь одна из возможностей.
Для демонстрации еще одного приема извлечения информации из Интернета, на этот раз с помощью curl, создадим простой инструмент поиска почтовых индексов. Передайте сценарию в листинге 7.8 почтовый индекс, и вы узнаете город и штат (в США), которому он принадлежит. Достаточно просто.
Самой очевидной была бы идея использовать официальный веб-сайт почтовой службы США (US Postal Service), но мы задействуем другой сайт, http://city-data.com/, в котором для каждого почтового индекса отводится своя веб-страница, что упрощает извлечение информации.
Листинг 7.8. Сценарий zipcode
#!/bin/bash
# zipcode -- получая почтовый индекс, определяет город и штат в США.
# Использует сайт city-data.com, в котором для каждого почтового
# индекса отводится своя веб-страница.
baseURL="http://www.city-data.com/zips"
/bin/echo -n "ZIP code $1 is in "
curl -s -dump "$baseURL/$1.html" | \
grep -i '<title>' | \
cut -d\( -f2 | cut -d\) -f1
exit 0
Адреса URL страниц с информацией о почтовых индексах на сайте http://city-data.com/ имеют единообразную организацию: сам почтовый индекс является заключительной частью URL:
http://www.city-data.com/zips/80304.html
Такое единообразие позволяет легко сконструировать адрес URL, соответствующий заданному почтовому индексу. Возвращаемая страница содержит название города в заголовке, которое легко отличить по открывающей и закрывающей круглым скобкам, как показано ниже:
<title>80304 Zip Code (Boulder, Colorado) Profile - homes, apartments,
schools, population, income, averages, housing, demographics, location,
statistics, residents and real estate info</title>
Строка длинная, но легко поддается анализу!
Чтобы воспользоваться сценарием, достаточно просто передать ему почтовый индекс в аргументе командной строки. Если указан действительный индекс, сценарий выведет название города и штата, как показано в листинге 7.9.
Листинг 7.9. Запуск сценария zipcode
$ zipcode 10010
ZIP code 10010 is in New York, New York
$ zipcode 30001
ZIP code 30001 is in <title>Page not found – City-Data.com</title>
$ zipcode 50111
ZIP code 50111 is in Grimes, Iowa
Так как 30001 не является действительным почтовым индексом, сценарий сгенерировал сообщение об ошибке Page not found («Страница не найдена»). Оно выглядит немного неопрятно, но мы можем улучшить его.
Наиболее очевидным усовершенствованием могло бы стать выполнение каких-то действий в ответ на ошибки вместо вывода невнятной последовательности <title>Page not found – City-Data.com</title>. Еще более интересный вариант — добавить флаг -a, который сообщал бы сценарию о необходимости вывода дополнительной информации о регионе, тем более что http://city-data .com/ предлагает довольно много информации, помимо названий городов, включая площадь, сведения о населении и цены на недвижимость.
Сценарий поиска по телефонному коду города является разновидностью предыдущего. Как оказывается, реализовать такой сценарий действительно очень просто, благодаря существованию простых для анализа веб-страниц с кодами городов. Например, страница по адресу http://www.bennetyee.org/ucsd-pages/area.html легко поддается анализу, не только потому, что она хранит информацию в табличной форме, но и потому, что автор использовал атрибуты HTML для идентификации элементов. Например, строка с информацией о коде 207 выглядит так:
<tr><td align=center><a name="207">207</a></td><td align=center>ME</td><td
align=center>-5</td><td> Maine</td></tr>
Мы использовали этот сайт в сценарии (листинг 7.10) поиска по телефонному коду города.
Листинг 7.10. Сценарий areacode
#!/bin/bash
# areacode -- получая трехзначный телефонный код, действующий в США,
# определяет город и штат по данным в простой табличной форме, на
# веб-сайте Беннета Йи (Bennet Yee).
source="http://www.bennetyee.org/ucsd-pages/area.html"
if [ -z "$1" ] ; then
echo "usage: areacode <three-digit US telephone area code>"
exit 1
fi
# wc -c вернет количество символов + символ перевода строки,
# то есть для 3 цифр = 4 символа
if [ "$(echo $1 | wc -c)" -ne 4 ] ; then
echo "areacode: wrong length: only works with three-digit US area codes"
exit 1
fi
# Все символы -- цифры?
if [ ! -z "$(echo $1 | sed 's/[[:digit:]]//g')" ] ; then
echo "areacode: not-digits: area codes can only be made up of digits"
exit 1
fi
# Теперь можно выполнить поиск по телефонному коду...
result="$(curl -s -dump $source | grep "name=\"$1" | \
sed 's/<[^>]*>//g;s/^ //g' | \
cut -f2- -d\ | cut -f1 -d\( )"
echo "Area code $1 =$result"
exit 0
Основная часть этого сценария выполняет проверку ввода, чтобы убедиться, что телефонный код, указанный пользователем, действителен. Наиболее важна тут команда curl — она извлекает данные из сети и передает их по конвейеру команде sed для анализа и команде cut для выделения информации, которую требуется вывести.
Этот сценарий принимает единственный аргумент — телефонный код города для поиска. Примеры использования сценария демонстрируются листинге 7.11.
Листинг 7.11. Тестирование сценария areacode
$ areacode 817
Area code 817 = N Cent. Texas: Fort Worth area
$ areacode 512
Area code 512 = S Texas: Austin
$ areacode 903
Area code 903 = NE Texas: Tyler
Самое простое усовершенствование, которое можно предложить, — реализовать обратный поиск, когда по названию города и штата сценарий находит и выводит все телефонные коды, соответствующие заданному городу.
Если вы проводите весь день в кабинете или в серверном зале, уткнувшись носом в терминал, вам наверняка иногда очень хочется выйти на улицу, прогуляться, особенно в хорошую погоду. Weather Underground (http://www.wunderground.com/) — отличный веб-сайт, который предлагает прикладной интерфейс (API) с бесплатным доступом для разработчиков. Вам нужно только зарегистрировать API-ключ. Имея API-ключ, можно написать короткий сценарий командной оболочки (показан в листинге 7.12), сообщающий, насколько хороша (или плоха) погода. Знание погоды поможет нам решить, стоит ли выходить на короткую прогулку.
Листинг 7.12. Сценарий weather
#!/bin/bash
# weather -- использует Wunderground API для получения информации
# о погоде по почтовому индексу (США).
if [ $# -ne 1 ]; then
echo "Usage: $0 <zipcode>"
exit 1
fi
apikey="b03fdsaf3b2e7cd23" # Это недействительный API-ключ -- вы
# должны получить свой.
weather=`curl -s \
"https://api.wunderground.com/api/$apikey/conditions/q/$1.xml"`
state=`xmllint --xpath \
//response/current_observation/display_location/full/text\(\) \
<(echo $weather)`
zip=`xmllint --xpath \
//response/current_observation/display_location/zip/text\(\) \
<(echo $weather)`
current=`xmllint --xpath \
//response/current_observation/temp_f/text\(\) \
<(echo $weather)`
condition=`xmllint --xpath \
//response/current_observation/weather/text\(\) \
<(echo $weather)`
echo $state" ("$zip") : Current temp "$current"F and "$condition" outside."
exit 0
Сценарий вызывает команду curl, чтобы отправить запрос к Wunderground API и сохранить HTTP-ответ в переменной weather . Затем он использует утилиту xmllint (ее легко установить с помощью диспетчера пакетов, такого как apt, yum или brew) для выполнения XPath-запроса к полученным данным , причем в конце каждого вызова xmllint применяется интересный синтаксис <(echo $weather), поддерживаемый языком bash. Эта конструкция принимает вывод команды внутри скобок и передает его указанной программе в виде дескриптора файла, то есть программа думает, что читает настоящий файл. После выборки необходимой информации из полученных данных в формате XML она выводится в виде удобочитаемого сообщения с краткими сведениями о погоде.
Запуская сценарий, достаточно передать ему почтовый индекс, как показано в листинге 7.13. Очень просто!
Листинг 7.13. Тестирование сценария weather
$ weather 78727
Austin, TX (78727) : Current temp 59.0F and Clear outside.
$ weather 80304
Boulder, CO (80304) : Current temp 59.2F and Clear outside.
$ weather 10010
New York, NY (10010) : Current temp 68.7F and Clear outside.
Откроем небольшой секрет. В действительности этот сценарий принимает не только почтовые индексы. Службе Wunderground API можно также передать название региона, например CA/San_Francisco (попробуйте передать эту строку сценарию weather!). Однако такой формат не очень удобен: он требует использовать символы подчеркивания вместо пробелов и символ слеша (/) в середине. В качестве одного из усовершенствований можно было бы добавить в сценарий запрос на ввод аббревиатуры штата и названия города и автоматически заменять пробелы символами подчеркивания, если сценарий запущен без аргумента. Как обычно, можно также добавить дополнительную проверку ошибок. Например, что получится, если передать сценарию четырехзначный или недействительный почтовый индекс?
Сценарий в листинге 7.14 демонстрирует более сложный пример доступа к Интернету с помощью lynx для поиска в базе данных Internet Movie Database (http://www.imdb.com/) сведений о кинофильмах по указанному шаблону. База данных IMDb назначает уникальный числовой код каждому фильму, каждому телевизионному сериалу и даже каждой отдельной серии; если пользователь укажет такой код, данный сценарий вернет краткое описание фильма. В противном случае он вернет список фильмов, частично или полностью соответствующих указанному названию.
В зависимости от типа запроса (числовой код или название) сценарий обращается по разным адресам URL и сохраняет результаты в кэше, чтобы многократно обойти содержимое страницы для извлечения разных фрагментов информации. Для этого используется много — очень много! — вызовов команд sed и grep, в чем вы можете убедиться лично.
Листинг 7.14. Сценарий moviedata
#!/bin/bash
# moviedata -- получая название фильма или сериала, возвращает список
# совпадений. Если пользователь укажет числовой код IMDb, вернет
# краткое описание фильма. Использует базу данных Internet Movie Database.
titleurl="http://www.imdb.com/title/tt"
imdburl="http://www.imdb.com/find?s=tt&exact=true&ref_=fn_tt_ex&q="
tempout="/tmp/moviedata.$$"
summarize_film()
{
# Форматирует описания фильма.
grep "<title>" $tempout | sed 's/<[^>]*>//g;s/(more)//'
grep --color=never -A2 '<h5>Plot:' $tempout | tail -1 | \
cut -d\< -f1 | fmt | sed 's/^/ /'
exit 0
}
trap "rm -f $tempout" 0 1 15
if [ $# -eq 0 ] ; then
echo "Usage: $0 {movie title | movie ID}" >&2
exit 1
fi
#########
# Выяснить тип запроса: по названию или по коду IMDb
nodigits="$(echo $1 | sed 's/[[:digit:]]*//g')"
if [ $# -eq 1 -a -z "$nodigits" ] ; then
lynx -source "$titleurl$1/combined" > $tempout
summarize_film
exit 0
fi
##########
# Это не код IMDb, поэтому нужно выполнить поиск...
fixedname="$(echo $@ | tr ' ' '+')" # для формирования URL
url="$imdburl$fixedname"
lynx -source $imdburl$fixedname > $tempout
# Нет результатов?
fail="$(grep --color=never '<h1 class="findHeader">No ' $tempout)"
# Если найдено несколько похожих названий...
if [ ! -z "$fail" ] ; then
echo "Failed: no results found for $1"
exit 1
elif [ ! -z "$(grep '<h1 class="findHeader">Displaying' $tempout)" ] ; then
grep --color=never '/title/tt' $tempout | \
sed 's/</\
</g' | \
grep -vE '(.png|.jpg|>[ ]*$)' | \
grep -A 1 "a href=" | \
grep -v '^--$' | \
sed 's/<a href="\/title\/tt//g;s/<\/a> //' | \
awk '(NR % 2 == 1) { title=$0 } (NR % 2 == 0) { print title " " $0 }' | \
sed 's/\/.*>/: /' | \
sort
fi
exit 0
Этот сценарий конструирует разные адреса URL, в зависимости от содержимого аргумента. Если пользователь указал числовой код, сценарий конструирует соответствующий URL, загружает с помощью lynx сведения о фильме, сохраняет их в файле $tempout и затем вызывает функцию summarize_film() . Ничего сложного.
Но если пользователь указал название, тогда сценарий конструирует URL с запросом поиска к базе данных IMDb и сохраняет полученную страницу во временном файле. Если базе данных IMDb не удалось найти совпадений, она возвращает в HTML-странице тег <h1> с атрибутом class="findHeader" и текстом No results («Нет результатов»). Именно эту ситуацию проверяет команда в строке . Далее следует простая проверка: если содержимое $fail имеет ненулевую длину, сценарий сообщает об отсутствии результатов.
Однако если $fail ничего не содержит, это означает, что поиск по заданному шаблону удался и в файле хранятся некоторые результаты. Далее в результатах выполняется поиск шаблона /title/tt, но здесь есть одна сложность: разобрать результаты, возвращаемые базой данных IMDb, очень непросто, потому что для каждой заданной ссылки в результатах имеется несколько совпадений. Остальная последовательность замысловатых команд sed|grep|sed пытается идентифицировать и удалить повторяющиеся совпадения и оставить только то, что имеет значение.
Кроме того, когда IMDb находит совпадение, такое как "Lawrence of Arabia (1962)", она возвращает название и год в двух разных элементах HTML, в двух разных строках. М-да. Однако год нам определенно необходим, чтобы различать фильмы с одинаковыми названиями. Этим занимается команда awk в строке , ,используя весьма хитроумный способ.
Для тех, кто не знаком с awk, отметим, что в общем случае awk-сценарий имеет следующую организацию: (условие) { действие }. Эта строка сохраняет нечетные строки в $title, и затем, когда очередь доходит до четной строки (с годом и данными о соответствии), она выводит предыдущую и текущую строки в одну строку.
Хотя этот сценарий невелик, он обладает большой гибкостью в отношении формата входных данных, как видно из листинга 7.15. Вы можете указать название фильма в кавычках или как набор отдельных слов, а можете ввести восьмизначный числовой код IMDb, чтобы выбрать конкретный фильм.
Листинг 7.15. Запуск сценария moviedata
$ moviedata lawrence of arabia
0056172: Lawrence of Arabia (1962)
0245226: Lawrence of Arabia (1935)
0390742: Mighty Moments from World History (1985) (TV Series)
1471868: Mystery Files (2010) (TV Series)
1471868: Mystery Files (2010) (TV Series)
1478071: Lawrence of Arabia (1985) (TV Episode)
1942509: Lawrence of Arabia (TV Episode)
1952822: Lawrence of Arabia (2011) (TV Episode)
$ moviedata 0056172
Lawrence of Arabia (1962)
A flamboyant and controversial British military figure and his
conflicted loyalties during his World War I service in the Middle East.
Одним из очевидных усовершенствований этого сценария могло бы стать удаление числовых кодов IMDb из вывода. Не составит труда скрыть коды (потому что, как показывает практика, они трудно запоминаются и пользователи допускают в них опечатки) и реализовать в сценарии вывод простого меню с уникальными индексами, которые могут применяться для выбора конкретного фильма.
В ситуации, когда для шаблона, заданного пользователем, обнаруживается только одно совпадение (попробуйте выполнить команду moviedata monsoon wedding), сценарий мог бы распознавать это, извлекать из полученных данных числовой код фильма и повторно вызывать самого себя, чтобы получить более подробную информацию. Вот такой круговорот получается!
Основная проблема этого и большинства других сценариев, извлекающих информацию из сторонних веб-сайтов, в том, что, если IMDb изменит верстку своей страницы, сценарий станет неработоспособным и вам придется исправлять его. Это скрытая ошибка, ждущая своего часа, но с такими сайтами, как IMDb, которые не меняются годами, вероятно, не особенно опасная.
В первом издании этой книги задача пересчета денежных сумм из одной валюты в другую оказалась довольно сложной, и для ее решения потребовалось написать два сценария: один извлекал сведения о курсах валют из финансового веб-сайта и сохранял их в особом формате, а другой использовал эти данные для фактического пересчета, например, из долларов США в евро. В минувшие годы, однако, Всемирная паутина продолжала развиваться, и сейчас мы не видим причин перелопачивать горы информации, когда имеются такие сайты, как Google, предлагающие простые и дружественные для использования из сценариев калькуляторы.
Представленный в листинге 7.16 сценарий пересчета валют по курсу просто использует валютный калькулятор, доступный по адресу: http://www.google.com/finance/converter.
Листинг 7.16. Сценарий convertcurrency
#!/bin/bash
# convertcurrency -- принимая сумму и базовую валюту, пересчитывает эту
# сумму в другой валюте. Для обозначения валют используются идентификаторы
# ISO. Для фактических вычислений использует валютный калькулятор Google:
# http://www.google.com/finance/converter
if [ $# -eq 0 ]; then
echo "Usage: $(basename $0) amount currency to currency"
echo "Most common currencies are CAD, CNY, EUR, USD, INR, JPY, and MXN"
echo "Use \"$(basename $0) list\" for a list of supported currencies."
fi
if [ $(uname) = "Darwin" ]; then
LANG=C # Для решения проблемы в OS X с ошибочными последовательностями
# байтов и lynx
fi
url="https://www.google.com/finance/converter"
tempfile="/tmp/converter.$$"
lynx=$(which lynx)
# Так как эти данные используются многократно, извлечем их,
# а потом займемся всем остальным.
currencies=$($lynx -source "$url" | grep "option value=" | \
cut -d\" -f2- | sed 's/">/ /' | cut -d\( -f1 | sort | uniq)
########### Выполнить все запросы, не связанные с пересчетом.
if [ $# -ne 4 ] ; then
if [ "$1" = "list" ] ; then
# Вывести список всех символов валют, известных калькулятору.
echo "List of supported currencies:"
echo "$currencies"
fi
exit 0
fi
########### Теперь выполним пересчет.
if [ $3 != "to" ] ; then
echo "Usage: $(basename $0) value currency TO currency"
echo "(use \"$(basename $0) list\" to get a list of all currency values)"
exit 0
fi
amount=$1
basecurrency="$(echo $2 | tr '[:lower:]' '[:upper:]')"
targetcurrency="$(echo $4 | tr '[:lower:]' '[:upper:]')"
# Наконец, фактический вызов калькулятора!
$lynx -source "$url?a=$amount&from=$basecurrency&to=$targetcurrency" | \
grep 'id=currency_converter_result' | sed 's/<[^>]*>//g'
exit 0
Валютный калькулятор Google принимает три параметра непосредственно в URL: сумму, исходную валюту и конечную валюту. Как выглядит такой URL, можно видеть в следующем примере, запрашивающем пересчет 100 долларов США в мексиканские песо:
https://www.google.com/finance/converter?a=100&from=USD&to=MXN
Сценарий ожидает, что пользователь определит все три поля в аргументах, и затем передает их сайту Google в URL.
Сценарий также выводит несколько сообщений с информацией о порядке использования, что намного упрощает работу с ним. Чтобы увидеть эти сообщения, перейдем к разделу с демонстрационными примерами.
Сценарий спроектирован так, что им очень легко пользоваться, как можно заметить в листинге 7.17, однако знание валют хотя бы нескольких стран лишним не будет.
Листинг 7.17. Запуск сценария convertcurrency
$ convertcurrency
Usage: convert amount currency to currency
Most common currencies are CAD, CNY, EUR, USD, INR, JPY, and MXN
Use "convertcurrency list" for a list of supported currencies.
$ convertcurrency list | head -10
List of supported currencies:
AED United Arab Emirates Dirham
AFN Afghan Afghani
ALL Albanian Lek
AMD Armenian Dram
ANG Netherlands Antillean Guilder
AOA Angolan Kwanza
ARS Argentine Peso
AUD Australian Dollar
AWG Aruban Florin
$ convertcurrency 75 eur to usd
75 EUR = 84.5132 USD
Несмотря на строгость и простоту веб-калькулятора, в вывод результатов все же можно добавить немного порядка. Например, вывод результатов пересчета в листинге 7.17 лишен смысла, поскольку сумма в долларах США в нем выражена числом с четырьмя знаками после запятой, даже при том, что для отображения количества центов достаточно двух знаков. Правильнее было бы вывести 84,51 или округлить до 84,52. Эту ошибку в сценарии желательно исправить.
И еще, пока вы не отвлеклись, хорошо бы добавить в сценарий проверку сокращенных обозначений валют. Пригодилось бы и преобразование кодов валют в полные названия, например, чтобы можно было выяснить, что AWG — это арубанские флорины или что BTC — это Bitcoin (Биткоин).
Система Биткоин (Bitcoin) вихрем ворвалась в наш мир, и даже появились компании, полностью основанные на цепочках блоков (blockchain, базовой технологии, на которой основана эта криптовалюта). Для тех, кому приходится работать с данной системой, получение полезной информации о конкретном адресе Биткоин нередко становится главной проблемой. Однако мы легко можем автоматизировать сбор данных с использованием короткого сценария на языке командной оболочки, представленного в листинге 7.18.
Листинг 7.18. Сценарий getbtcaddr
#!/bin/bash
# getbtcaddr -- получая адрес Биткоин, возвращает полезную информацию.
if [ $# -ne 1 ]; then
echo "Usage: $0 <address>"
exit 1
fi
base_url="https://blockchain.info/q/"
balance=$(curl -s $base_url"addressbalance/"$1)
recv=$(curl -s $base_url"getreceivedbyaddress/"$1)
sent=$(curl -s $base_url"getsentbyaddress/"$1)
first_made=$(curl -s $base_url"addressfirstseen/"$1)
echo "Details for address $1"
echo -e "\tFirst seen: "$(date -d @$first_made)
echo -e "\tCurrent balance: "$balance
echo -e "\tSatoshis sent: "$sent
echo -e "\tSatoshis recv: "$recv
Сценарий несколько раз вызывает команду curl, чтобы извлечь ценные сведения из заданного адреса Биткоин. Соответствующая служба, доступная по адресу: http://blockchain.info/, дает простую возможность получить полную информацию об адресе Биткоин и цепочке блоков. Фактически, нам даже не потребовалось анализировать информацию, получаемую от службы, потому что она возвращает простые одиночные значения. Получив баланс для заданного адреса, сведения о количестве полученных и потраченных монет и о том, когда осуществлялись платежи, сценарий выводит эту информацию на экран.
Сценарий принимает единственный аргумент — адрес Биткоин, информацию о котором требуется получить. Следует отметить, что, если передать сценарию строку, не являющуюся действительным адресом Биткоин, он выведет нули в строках, сообщающих о балансе и полученных и потраченных суммах, а в качестве даты создания будет указан 1969 год. Любые ненулевые суммы указываются в сатоши (satoshi) — минимальных единицах обозначения сумм в Биткоин (как, например, пенни, но с намного большим количеством знаков после запятой).
Пользоваться сценарием getbtcaddr очень просто, как показано в листинге 7.19, так как он принимает единственный аргумент, адрес Биткоин, информацию о котором требуется получить.
Листинг 7.19. Запуск сценария getbtcaddr
$ getbtcaddr 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
Details for address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
First seen: Sat Jan 3 12:15:05 CST 2009
Current balance: 6554034549
Satoshis sent: 0
Satoshis recv: 6554034549
$ getbtcaddr 1EzwoHtiXB4iFwedPr49iywjZn2nnekhoj
Details for address 1EzwoHtiXB4iFwedPr49iywjZn2nnekhoj
First seen: Sun Mar 11 11:11:41 CDT 2012
Current balance: 2000000
Satoshis sent: 716369585974
Satoshis recv: 716371585974
Сценарий по умолчанию выводит очень большие числа, которые трудно прочитать. Чтобы отобразить данные в единицах, более простых для восприятия (например, в целых Биткоинах), можно использовать сценарий scriptbc (сценарий № 9 в главе 1). Поддержка аргумента точности позволила бы выводить данные в удобочитаемом формате.
Иногда, просматривая существующие решения, мы с воодушевлением говорим себе: «Оказывается, это совсем несложно». Слежение за изменениями на веб-сайтах — удивительно простой способ собирать такие воодушевляющие образцы. Сценарий в листинге 7.20, changetrack, автоматизирует эту задачу. Данный сценарий имеет одну интересную особенность: обнаружив изменения на сайте, он не просто выводит уведомление в командной строке, а посылает пользователю новую веб-страницу по электронной почте.
Листинг 7.20. Сценарий changetrack
#!/bin/bash
# changetrack -- проверяет страницу по указанному URL и, если она
# изменилась с момента последнего посещения, посылает новую страницу
# по указанному адресу электронной почты.
sendmail=$(which sendmail)
sitearchive="/tmp/changetrack"
tmpchanges="$sitearchive/changes.$$" # Временный файл
fromaddr="[email protected]"
dirperm=755 # чтение+запись+выполнение для владельца каталога
fileperm=644 # чтение+запись для владельца, только чтение для других
trap "$(which rm) -f $tmpchanges" 0 1 15 # Удалить временный файл при выходе.
if [ $# -ne 2 ] ; then
echo "Usage: $(basename $0) url email" >&2
echo " tip: to have changes displayed on screen, use email addr '-'" >&2
exit 1
fi
if [ ! -d $sitearchive ] ; then
if ! mkdir $sitearchive ; then
echo "$(basename $0) failed: couldn't create $sitearchive." >&2
exit 1
fi
chmod $dirperm $sitearchive
fi
if [ "$(echo $1 | cut -c1-5)" != "http:" ] ; then
echo "Please use fully qualified URLs (e.g. start with 'http://')" >&2
exit 1
fi
fname="$(echo $1 | sed 's/http:\/\///g' | tr '/?&' '...')"
baseurl="$(echo $1 | cut -d/ -f1-3)/"
# Загрузить копию веб-страницы и поместить в файл архива. Обратите
# внимание, что изменения определяются по чистому содержимому
# (используется флаг -dump, а не -source), поэтому можно не заниматься
# парсингом разметки HTML....
lynx -dump "$1" | uniq > $sitearchive/${fname}.new
if [ -f "$sitearchive/$fname" ] ; then
# Этот сайт просматривался прежде, так что сравним старую и новую
# копии с помощью diff.
diff $sitearchive/$fname $sitearchive/${fname}.new > $tmpchanges
if [ -s $tmpchanges ] ; then
echo "Status: Site $1 has changed since our last check."
else
echo "Status: No changes for site $1 since last check."
rm -f $sitearchive/${fname}.new # Ничего нового...
exit 0 # Изменений нет, выйти.
fi
else
echo "Status: first visit to $1. Copy archived for future analysis."
mv $sitearchive/${fname}.new $sitearchive/$fname
chmod $fileperm $sitearchive/$fname
exit 0
fi
# Сюда сценарий попадает, когда обнаружены изменения и нужно послать
# пользователю содержимое файла .new и заменить им старую копию
# для следующего вызова сценария.
if [ "$2" != "-" ] ; then
( echo "Content-type: text/html"
echo "From: $fromaddr (Web Site Change Tracker)"
echo "Subject: Web Site $1 Has Changed"
echo "To: $2"
echo ""
lynx -s -dump $1 | \
sed -e "s|src=\"|SRC=\"$baseurl|gi" \
-e "s|href=\"|HREF=\"$baseurl|gi" \
-e "s|$baseurl\/http:|http:|g"
) | $sendmail -t
else
# Вывод различий на экран не кажется хорошим решением.
# Сможете предложить что-то получше?
diff $sitearchive/$fname $sitearchive/${fname}.new
fi
# Обновить сохраненную копию веб-сайта.
mv $sitearchive/${fname}.new $sitearchive/$fname
chmod 755 $sitearchive/$fname
exit 0
Получив URL и адрес электронной почты, этот сценарий извлекает содержимое веб-страницы и сравнивает его с содержимым сайта, сохраненным при предыдущей проверке. Если сайт изменился, новая страница отправляется по электронной почте указанному адресату после небольших изменений, цель которых — обеспечить работоспособность ссылок на изображения и в атрибутах href. Остановимся подробнее на этих изменениях, начиная со строки .
Команда lynx извлекает исходный код веб-страницы , после чего команда sed вносит в него три разных изменения. Во-первых, все фрагменты SRC=" замещаются фрагментами SRC="baseurl/ , чтобы заменить все относительные пути вида SRC="logo.gif" абсолютными путями, включающими доменное имя, и тем самым обеспечить их работоспособность. Для сайта с доменным именем http://www.intuitive.com/ упомянутая выше ссылка примет вид SRC="http://www.intuitive.com/logo.gif". Аналогично изменяются атрибуты href . Затем, чтобы гарантировать целостность всех ссылок, измененных на предыдущих этапах, выполняется третье изменение, в рамках которого из исходного кода HTML удаляются строки baseurl, если они были добавлены по ошибке . Например, ссылка HREF="http://www.intuitive.com/http://www.somewhereelse.com/link" явно недействительная, и ее следует исправить.
Обратите также внимание, что адрес получателя указан в команде echo (echo "To: $2"), а не передается команде sendmail как аргумент. Это простая предохранительная мера: передавая адрес команде sendmail во входном потоке (которая знает, что должна извлечь адрес получателя из потока благодаря флагу -t), мы избавляем себя от необходимости беспокоиться о пользователях, любящих поиграть с такими адресами, как "joe;cat /etc/passwd|mail larry". Этот прием демонстрирует безопасный способ вызова sendmail из сценариев командной оболочки.
Данный сценарий требует два параметра: URL сайта (для правильной работы сценария должны использоваться полные адреса URL, начинающиеся с http://) и адрес электронной почты (или список адресов, разделенных запятыми), куда следует послать измененную веб-страницу. Или, если хотите, вместо адреса электронной почты можно просто использовать - (дефис), чтобы только вывести на экран результаты сравнения командой diff.
Когда сценарий загружает веб-страницу в первый раз, он автоматически посылает ее по указанному адресу, как показано в листинге 7.21.
Листинг 7.21. Первый запуск сценария changetrack
$ changetrack http://www.intuitive.com/ [email protected]
Status: first visit to http://www.intuitive.com/. Copy archived for future
analysis.
Все последующие проверки сайта http://www.intuitive.com/ будут заканчиваться отправкой копии по электронной почте, только если страница изменится после предыдущего вызова сценария. Это может быть результатом простого исправления единственной опечатки или сложного переоформления всей страницы. С помощью сценария можно следить за изменениями на любых веб-сайтах, но лучше всего, пожалуй, он будет работать с теми, которые обновляются нечасто: если выбрать целью главную страницу BBC News, проверка потребует значительного объема процессорного времени, потому что этот сайт постоянно обновляется.
Если после предыдущего вызова сценария сайт не изменился, при повторном запуске сценарий ничего не выведет и ничего не пошлет указанному адресату:
$ changetrack http://www.intuitive.com/ [email protected]
$
Очевидный недостаток текущей версии сценария — он поддерживает только ссылки с префиксом http://. То есть он будет отвергать любые веб-страницы, обслуживаемые по протоколу HTTPS. Чтобы добавить поддержку обоих протоколов, необходимо применить несколько не самых простых регулярных выражений, но в целом это возможно!
Другое усовершенствование, которое сделает сценарий более полезным: добавить аргумент, определяющий степень изменений, чтобы пользователи могли указать, что, если изменилась только одна строка, сценарий не должен считать сайт обновившимся. Подсчет изменившихся строк реализуется передачей вывода diff команде wc -l. (Имейте в виду, что для каждой измененной строки diff обычно выводит три строки.)
Этот сценарий можно сделать еще более практичным, если запускать его из ежедневного или еженедельного задания cron. У нас есть подобные сценарии, они запускаются каждую ночь и посылают нам обновившиеся веб-страницы с разных сайтов, за которыми мы установили наблюдение.
Особенно интересно было бы приспособить этот сценарий для работы с файлом данных, содержащим адреса URL и электронной почты, и избавиться от необходимости постоянно вводить входные параметры. Добавьте такую модифицированную версию сценария в задание cron, напишите веб-интерфейс к утилите (подобной сценариям в главе 8) и вы создадите функцию, за использование которой компании берут с пользователей плату. Серьезно.
Биткоин = 100 000 000 сатоши. — Примеч. пер.