Saturday, January 21, 2012

perlfaq9 — работа в сети http://beshenov.ru/perlfaq/9.html

http://beshenov.ru/perlfaq/9.html

perlfaq9 — работа в сети

Этот раздел FAQ отвечает на вопросы по работе в сети Internet и отчасти — в WWW.
Какой должен быть ответ CGI-скрипта?
CGI-скрипт работает в командной строке, но не в браузере (ошибка №500)
Как получить более хорошие сообщения об ошибках в CGI-программе?
Как удалить HTML-разметку из строки?
Как извлечь URL?
Как скачать файл с пользовательского компьютера? Как открыть файл на другом компьютере?
Как сделать раскрывающийся список HTML?
Как скачать HTML-файл?
Как автоматизировать отправку HTML-форм?
Как декодировать или создать эти % в URL?
Как перенаправить на другую страницу?
Как защитить веб-страницы паролем?
Как отредактировать в Perl файлы .htpasswd и .htgroup?
Как убедиться в том, что пользователи не введут в форму значения, которые заставят CGI-скрипт сделать что-то нехорошее?
Как разобрать заголовки почтового сообщения?
Как декодировать CGI-форму?
Как проверить на корректность адрес электронной почты?
Как раскодировать строчку из MIME/BASE64?
Как определить почтовый адрес пользователя?
Как отправить почту?
Как использовать MIME, чтобы добавить в сообщение вложение?
Как прочитать почту?
Как узнать имя узла, домена, IP-адрес?
Как скачать сообщение из группы новостей или список активных групп?
Как скачать/закачать файл по FTP?
Как в Perl делать удаленный вызов процедур (RPC)?

Какой должен быть ответ CGI-скрипта?

(Отвечает Алан Флейвелл)
CGI определяет интерфейс между программой (CGI-скриптом) и веб-сервером (HTTPD). Это относится не только к Perl, и по CGI есть отдельные FAQ, руководства, а также группа новостейcomp.infosystems.www.authoring.cgi.
Спецификация CGI изложена в информационном RFC 3875.
Другая документация по теме перечислена в «CGI Programming MetaFAQ».
Эти FAQ по Perl очень избирательно покрывают некоторые вопросы CGI. Однако программирующим на Perl настоятельно рекомендуется использовать модуль CGI.pm, чтобы позаботиться о деталях.
Заголовки ответа CGI (определенные в спецификации CGI) намеренно схожи с заголовками ответа HTTP (определенными в спецификации HTTP, RFC 2616), но это может сбить с толку.
Спецификация CGI определяет два типа скриптов: с анализируемыми заголовками («Parsed Header»), и с неанализируемыми заголовками («Non Parsed Header», NPH). Посмотрите в документации своего сервера, что он поддерживает. Скрипты с анализируемыми заголовками во многих отношениях проще. Спецификация CGI допускает любое обычное представление перевода строки в ответе CGI (создание правильного ответа HTTP на основе CGI — уже дело сервера). Поэтому запись "\n" в текстовом режиме технически корректна и рекомендована. Скрипты с неанализируемыми заголовками более сложны: они должны отдавать полный и правильный набор HTTP-заголовков; спецификация требует завершать записи с CR и LF, то есть, ASCII-символами \015\012, записанными в бинарном режиме.
CGI.pm дает превосходную платформонезависимость, в том числе поддержку систем с EBCDIC. CGI.pmвыбирает правильное представление перевода строки ($CGI::CRLF) и соответствующим образом устанавливает binmode.

CGI-скрипт работает в командной строке, но не в браузере (ошибка №500)

Проблема может иметь различные источники. Посмотрите руководств «Troubleshooting Perl CGI scripts». Если вы его прочитали, но для проблемы не нашлось простого решения, то любезный совет можно получить вcomp.infosystems.www.authoring.cgi (если дело в протоколе HTTP или CGI). Сообщения, которые выдаются за вопросы по Perl, но на деле касаются CGI, обычно не очень хорошо принимаются в comp.lang.perl.misc.
Полезные FAQ, документы по теме и руководства по устранению проблем перечислены в «CGI Meta FAQ».

Как получить более хорошие сообщения об ошибках в CGI-программе?

Используйте модуль CGI::Carp. Он заменяет warn и die, а также функции carpcroak и confess обычного модуля Carp более информативными и безопасными. Модуль также заносит сообщения в обычный журнал ошибок сервера.
use CGI::Carp; warn "Это жалоба"; die "А это уже серьезная ошибка";
Следующее обращение к CGI::Carp также перенаправляет ошибки в заданный файл, и помещено в блок BEGIN для перехвата предупреждений этапа компиляции:
BEGIN {     use CGI::Carp qw(carpout);     open(LOG, ">>/var/local/cgi-logs/mycgi-log")         or die "Не могу дописать в mycgi-log: $!\n";     carpout(*LOG); }
Можете даже организовать вывод критических ошибок в браузере клиента, что удобно для отладки, но может сбить с толку конечного пользователя.
use CGI::Carp qw(fatalsToBrowser); die "Плохая ошибка";
Даже если ошибка случается до вывода HTTP-заголовка, модуль пытается перехватить ее, чтобы избежать вывода пугающих ошибок сервера №500. Простые предупреждения также будут заноситься в обычный журнал ошибок сервера (или куда-либо еще в соответствии с carpout) с указанием имени приложения и времени.

Как удалить HTML-разметку из строки?

Самый правильный, хотя и не самый быстрый способ — использовать HTML::Parser из CPAN. Другой почти правильный подход — HTML::FormatText, который не только удаляет HTML-разметку, но и пытается немного отформатировать полученный простой текст.
Многие пытаются обойтись простыми регулярными выражениями вроде s/<.*?>//g, но это часто не работает, потому что теги могут разрываться переводами строки и содержать экранированные угловые скобки; также могут встречаться HTML-комментарии. Кроме того, при этом упускается преобразование сущностей вроде &lt;.
Вот один простой подход, работающий для большинства файлов:
#!/usr/bin/perl -p0777 s/<(?:[^>'"]*|(['"]).*?\1)*>//gs
Если нужно более полное решение, смотрите трехэтапную обработку в программе striphtml.
Вот некоторые хитрые ситуации, о которых нужно помнить при выборе решения:
<IMG SRC = "foo.gif" ALT = "A > B">  <IMG SRC = "foo.gif"  ALT = "A > B">  <!-- <Комментарий> -->  <script>if (a<b && a>c)</script>  <# Просто данные #>  <![INCLUDE CDATA [ >>>>>>>>>>>> ]]>
В HTML-комментариях с другими тегами эти решения также споткнутся на таком тексте:
<!-- Эта часть закомментирована.     <B>Меня не видно!</B> -->

Как извлечь URL?

Все возможные URL из HTML можно извлечь при помощи HTML::SimpleLinkExtor — поддерживаются aimg,objectframe и многие другие теги, которые могут содержать URL. Если нужно что-то более хитрое, то можете наследовать свой класс от HTML::LinkExtor или HTML::Parser. Можете даже использоватьHTML::SimpleLinkExtor в качестве примера для кода, заточенного под ваши нужды.
Для извлечения URL из произвольного текстового документа можно использовать URI::Find.
Упрощенные решения на регулярных выражениях могут сэкономить много времени на обработке, если известно, что ввод простой. Это решение Тома Кристиансена работает в сто раз быстрее программ, использующих модули, но извлекает только ссылки из элементов A с первым и единственным атрибутом HREF:
#!/usr/bin/perl -n00 # qxurl - tchrist@perl.com print "$2\n" while m{     < \s*       A \s+ HREF \s* = \s* (["']) (.*?) \1     \s* > }gsix;

Как скачать файл с пользовательского компьютера? Как открыть файл на другом компьютере?

Здесь «скачивание с компьютера» сводится к закачиванию, реализованному в HTML-формах. Вы позволяете пользователю указать файл для отправки на сервер. Для вас это выглядит как скачивание, а для пользователя это закачивание. Как ни называйте, это делается при помощи кодирования вmultipart/form-data. Модуль CGI.pm (распространяется с Perl как часть стандартной бибиотеки) поддерживает это в методе start_multipart_form() (не путать с startform()).
За примерами и подробностями обратитесь к разделу документации CGI.pm по закачке файлов.
(Отвечает Брайан Д. Фой)
В модуле CGI.pm (прилагается к Perl) есть функции для создания компонентов форм HTML.
use CGI qw/:standard/; print header,     start_html('Любимые животные'),      start_form,         'Какое ваше любимое животное? ',     popup_menu(         -name   => 'animal',         -values => [ qw( Лама Альпака Верблюд Баран ) ]         ),     submit,         end_form,     end_html;
Дополнительные примеры см. в документации CGI.pm.

Как скачать HTML-файл?

Один из подходов, если в системе установлен консольный браузер lynx:
$html_code = `lynx -source $url`; $text_data = `lynx -dump $url`;
Модули CPAN libwww-perl (LWP) дают более мощное решение. Они не требуют lynx, но тоже могут работать через прокси:
# Простейшая версия use LWP::Simple; $content = get($URL);  # либо вывести документ по указанному URL use LWP::Simple; getprint "http://www.linpro.no/lwp/";  # либо вывести ASCII-версию документа по указанному URL # (требуется пакет HTML-Tree из CPAN) use LWP::Simple; use HTML::Parser; use HTML::FormatText; my ($html, $ascii); $html = get("http://www.perl.com/"); defined $html     or die "Не могу загрузить документ с http://www.perl.com/"; $ascii = HTML::FormatText->new->format(parse_html($html)); print $ascii;

Как автоматизировать отправку HTML-форм?

Если вы делаете что-то сложное — например, работаете с многими страницами и формами, то можете использовать WWW::Mechanize. Подробности смотрите в документации.
Если вы отправляете значения при помощи метода GET, то создайте URL и кодируйте данные формы при помощи метода query_form:
use LWP::Simple; use URI::URL;  my $url = url('http://www.perl.com/cgi-bin/cpan_mod'); $url->query_form(module => 'DB_File', readme => 1); $content = get($url);
Если вы используете метод POST, то создайте собственный пользовательский агент ии кодируйте контент соответствующим образом.
use HTTP::Request::Common qw(POST); use LWP::UserAgent;  $ua = LWP::UserAgent->new(); my $req = POST 'http://www.perl.com/cgi-bin/cpan_mod',                [ module => 'DB_File', readme => 1 ]; $content = $ua->request($req)->as_string;

Как декодировать или создать эти % в URL?

(Отвечает Брайан Д. Фой)
Эти % представляют специальные символы в URI, как описано во втором разделе RFC 2396. Такая кодировка заменяет специальные символы шестнадцатеричными значениями из таблицы. Например, вместо «:» используется «%3A».
Если в CGI-скриптах используется CGI.pm, то о декодировании URI не нужно заботиться. Обрабатывать URL вручную не требуется, ни на входе, ни на выходе.
Если нужно самостоятельно кодировать строку, то никогда не кодируйте сформированный URI. Нужно обработать компоненты отдельно, а затем объединить. Для кодирования строки можно использовать модульURI::Escape; функция uri_escape возвращает экранированную строку:
my $original = "Colon : Hash # Percent %";  my $escaped = uri_escape( $original )  print "$string\n"; # 'Colon%20%3A%20Hash%20%23%20Percent%20%25%20'
Для декодирования строки используйте функцию uri_unescape:
my $unescaped = uri_unescape( $escaped );  print $unescaped; # вернуться к исходной строке
Если хотите сделать это сами, то просто замените специальные символы их кодами. Одно из решений — глобальная подстановка
# кодировать $string =~ s/([^^A-Za-z0-9\-_.!~*'()])/ sprintf "%%%0x", ord $1 /eg;  # декодировать $string =~ s/%([A-Fa-f\d]{2})/chr hex $1/eg;

Как перенаправить на другую страницу?

Укажите полный целевой URL (даже если это на том же сервере). Это один из двух возможных ответов CGI с заголовком Location, определенный спецификацией для скриптов с анализируемыми заголовками. Другой возможный ответ — абсолютный URLpath — разрешается на стороне сервера без перенаправления по HTTP. Ни в первом, ни во втором случае, спецификации CGI не позволяют использовать относительные URL.
Настоятельно рекомендуется использовать CGI.pm. В следующем примере используется полный URL. Перенаправление производится веб-браузером.
use CGI qw/:standard/;  my $url = 'http://www.cpan.org/'; print redirect($url);
В примере ниже используется абсолютный URLpath. Перенаправление производится локальным веб-сервером.
my $url = '/CPAN/index.html'; print redirect($url);
Напрямую это можно записать следующим образом (последний "\n" для ясности указан отдельно):
print "Location: $url\n";   # заголовок ответа CGI print "\n";                 # конец заголовков
$url может быть полным URL или абсолютным URLpath.

Как защитить веб-страницы паролем?

Для включения аутентификации нужно настроить веб-свервер. Настройки могут быть разными, в этом Apache отличается от iPlanet, а iPlanet отличается от IIS. Подробности смотрите в документации к вашему серверу.

Как отредактировать в Perl файлы .htpasswd и .htgroup?

Подходящиий объектно-ориентированный интерфейс для работы с этими файлами, не зависимо от их расположения, предоставляют модули HTTPD::UserAdmin и HTTPD::GroupAdmin. Базы данных могут быть в текстовом формате, DBM, Berkeley DB и любом другом виде, совместимом с DBI. HTTPD::UserAdminподдерживает файлы, используемые в схемах авторизации «Basic» и «Digest». Вот пример:
use HTTPD::UserAdmin (); HTTPD::UserAdmin     ->new(DB => "/foo/.htpasswd")     ->add($username => $password);

Как убедиться в том, что пользователи не введут в форму значения, которые заставят CGI-скрипт сделать что-то нехорошее?

См. ссылки по вопросам безопасности в «CGI Meta FAQ».

Как разобрать заголовки почтового сообщения?

Быстрое и неаккуратное решение есть в документации для split в perlfunc:
$/ = ''; $header = <MSG>; $header =~ s/\n\s+/ /g;     # объединить продолжающиеся строки %head = ( UNIX_FROM_LINE, split /^([-\w]+):\s*/m, $header );
Это решение не сработает верно, если, например, вы попытаетесь обработать все строки с Received. Более полный подход используется в модуле CPAN Mail::Header (часть пакета MailTools).

Как декодировать CGI-форму?

(Отвечает Брайан Д. Фой)
Используйте модуль CGI.pm, прилагающийся к Perl. Он быстрый, простой и берет на себя часть работы, чтобы удостовериться, что всё работает верно. Модуль поддерживает запросы GETPOST и HEAD, данныеmultipart/form, поля со множественными значениями, комбинации запросов и HTTP-сообщений и прочие вещи, о которых вам не хотелось бы заботиться.
Проще некуда: модуль CGI автоматически разбирает ввод и делает каждое значение доступным через функцию param().
use CGI qw(:standard);  my $total = param( 'price' ) + param( 'shipping' );  my @items = param( 'item' ); # несколько значений для одного поля
CGI.pm поддерживает и объектно-ориентированный подход.
use CGI;  my $cgi = CGI->new();  my $total = $cgi->param( 'price' ) + $cgi->param( 'shipping' );  my @items = $cgi->param( 'item' );
Кроме того, можете посмотреть облегченную версию CGI::Minimal. Другие модули CGI::* из CPAN также могут подойти лучше.
Многие пытаются написать собственный декодер (или скопировать из другой программы) и натыкаются на одну из многих тонкостей. Гораздо проще использовать CGI.pm.

Как проверить на корректность адрес электронной почты?

(Часть ответа от Аарона Шермана)
Это не так просто, как кажется. Есть два момента:
  1. Как проверить, что адрес правильно введен?
  2. Как проверить, что адрес указывает на существующего получателя?
Без отправки письма по адресу и проверки, что на другом конце вам ответят, на второй вопрос нельзя дать точный ответ, но модули Email::Valid и RFC::RFC822::Address позволяют ответить на первый и частично на второй, насколько это возможно в реальном времени.
Если хотите только проверить корректность адреса простым регулярным выражением, то можете столкнуться с пробемами, потому что существуют работающие адреса, не соответствующие RFC 2822(последнему стандарту на заголовки электронной почты), ровно как и соответствующие RFC адреса, по которым нельзя доставить почту. Следующий шаблон проверит адреса без комментариев, сворачивающихся пробельных символов и прочих устаревших или необязательных элементов. Этот шаблон соответствует только самому адресу:
my $atom       = qr{[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+}; my $dot_atom   = qr{$atom(?:\.$atom)*}; my $quoted     = qr{"(?:\\[^\r\n]|[^\\"])*"}; my $local      = qr{(?:$dot_atom|$quoted)}; my $domain_lit = qr{\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\]}; my $domain     = qr{(?:$dot_atom|$domain_lit)}; my $addr_spec  = qr{$local\@$domain};
Просто проверьте адрес на сответствие /^${addr_spec}$/, чтобы посмотреть, следует ли он RFC 2822. Впрочем, нельзя знать, что такой корректный адрес действительно используется определенным человеком, поэтому тут нужно быть аккуратнее.
Наш совет для проверки адресов почты — попросите пользователей вводить их дважды, как при смене пароля. Это исключает опечатки. Если оба ввода совпадают, отправьте на этот адрес личное сообщение. Если на сообщение будет получен ответ, и пользователь выполнит инструкции в нем, то можно быть увереным, что адрес настоящий.
Схожее решенее, менее подверженное подлогам — закрепить за пользователями личные идентификационные номера (PIN), лучше случайные, и хранить вместе с адресами для последующей обработки. В отправляемых сообщениях пользователей нужно просить включить в ответ PIN. Если сообщение возвращается, либо скрипт отвечает, что получатель в отпуске, то PIN всё равно будет в ответе. Поэтому лучше просить пользователей отвечать с несколько измененным PIN, например, записанным в обратном порядке, с добавлением или вычитанием единицы из каждой цифры, и так далее.

Как раскодировать строчку из MIME/BASE64?

Пакет MIME-Base64 из CPAN работает с MIME/BASE64 и MIME/QP. Декодирование BASE64 производится проще некуда:
use MIME::Base64; $decoded = decode_base64($encoded);
Пакет MIME-Tools из CPAN поддерживает выборку и декодирование вложений и самого текста сообщения.
Если строка для декодирования небольшая (меньше 84 байт), то после незначительного преобразования можно использовать прямое решение с форматом u функции unpack():
tr#A-Za-z0-9+/##cd;                   # удалить символы не из base64 tr#A-Za-z0-9+/# -_#;                  # кодировать в UUE $len = pack("c", 32 + 0.75*length);   # вычислить байт длины print unpack("u", $len . $_);         # декодировать UUE и вывести

Как определить почтовый адрес пользователя?

В системах, которые поддерживают getpwuid, переменную $< и модуль Sys::Hostname (из стандартной поставки Perl), можно попробовать нечто вроде
use Sys::Hostname; $address = sprintf('%s@%s', scalar getpwuid($<), hostname);
Правила компании относительно почтовых адресов могут быть такими, что код выше будет давать адреса, не принимаемые почтовой системой, поэтому адреса лучше спросить у самих пользователей. Кроме того, не все системы, на которых работает Perl, выдают подобную информацию, как это делает Unix.
Модуль Mail::Util из CPAN (часть пакета MailTools) предоставляет функцию mailaddress(), которая пытается определить почтовый адрес пользователя. Используется более умный подход, чем в коде выше, с привлечением информации, полученной при установке модуля, но и это может не дать правильных результатов. Опять же, лучше спросить у пользователя.

Как отправить почту?

Используйте напрямую программу sendmail:
open(SENDMAIL, "|/usr/lib/sendmail -oi -t -odq")                     or die "Не могу сделать fork для sendmail: $!\n"; print SENDMAIL <<"EOF"; From: Адрес автора письма <me\@host> To: Конечное назначение <you\@otherhost> Subject: Соответствующий заголовок  Тело письма идет после пустой строки и продолжается нужное число строк. EOF close(SENDMAIL)     or warn "sendmail завершился неудачно";
Опция -oi указывает sendmail не интерпретировать строку, содержащую одну точку, как конец сообщения. С опцией -t получатели определяются по заголовкам; -odq помещает сообщение в очередь. С последей опцией сообщение будет передано не сразу; уберите ее, если хотите отправить его незамедлительно.
Другие, менее удобные решения — прямой вызов mail (иногда называется mailx) или просто сокровенный разговор с удаленным SMTP-демоном на 25-м порту (возможно, с sendmail).
Или можно использовать модуль Mail::Mailer из CPAN:
use Mail::Mailer;  $mailer = Mail::Mailer->new(); $mailer->open({ From    => $from_address,                 To      => $to_address,                 Subject => $subject,               })     or die "Не могу открыть: $!\n"; print $mailer $body; $mailer->close();
Модуль Mail::Internet использует Net::SMTP — менее Unix-ориентированный, чем Mail::Mailer, но и менее надежный. Избегайте чистых SMTP-команд. Есть множество причин использовать агент передачи почты вроде sendmail, в том числе очереди сообщений, записи MX, безопасность.

Как использовать MIME, чтобы добавить в сообщение вложение?

Ответ на вопрос взят из документации к MIME::Lite. Создайте сообщение типа «multipart» (то есть, с вложениями).
use MIME::Lite;  ### Создать новое сообщение типа «multipart»: $msg = MIME::Lite->new(              From    =>'me@myhost.com',              To      =>'you@yourhost.com',              Cc      =>'some@other.com, some@more.com',              Subject =>'Сообщение в двух частях...',              Type    =>'multipart/mixed'              );  ### Добавить части (каждый "attach" вызывается с теми же аргументами, что и "new"): $msg->attach(Type     =>'TEXT',              Data     =>"Вот GIF, который вы просили"              ); $msg->attach(Type     =>'image/gif',              Path     =>'aaa000123.gif',              Filename =>'logo.gif'              );  $text = $msg->as_string;
MIME::Lite также включает метод для отправки
$msg->send;
По умолчанию используется sendmail, но можно настроить использование SMTP через Net::SMTP.

Как прочитать почту?

Хотя можно использовать модуль Mail::Folder из CPAN (часть пакета MailFolder) или Mail::Internet (часть пакета MailTools), часто это перебор. Вот программа для сортировки почты.
#!/usr/bin/perl  my(@msgs, @sub); my $msgno = -1; $/ = '';                    # считывание абзацами while (<>) {     if (/^From /m) {         /^Subject:\s*(?:Re:\s*)*(.*)/mi;         $sub[++$msgno] = lc($1) || '';     }     $msgs[$msgno] .= $_; } for my $i (sort { $sub[$a] cmp $sub[$b] || $a <=> $b } (0 .. $#msgs)) {     print $msgs[$i]; }
Или более коротко
#!/usr/bin/perl -n00 # bysub2 - сортировка по теме сообщения в духе awk BEGIN { $msgno = -1 } $sub[++$msgno] = (/^Subject:\s*(?:Re:\s*)*(.*)/mi)[0] if /^From/m; $msg[$msgno] .= $_; END { print @msg[ sort { $sub[$a] cmp $sub[$b] || $a <=> $b } (0 .. $#msg) ] }

Как узнать имя узла, домена, IP-адрес?

(Отвечает Брайан Д. Фой)
Модуль Net::Domain, входящий в стандартную поставку, начиная с perl5.7.3, может определить полностью уточнённое доменное имя (FQDN), имя узла или домена.
use Net::Domain qw(hostname hostfqdn hostdomain);  my $host = hostfqdn();
Мудуль Sys::Hostname, входящий в стандартную поставку, начиная с perl5.6, также может получить имя узла.
use Sys::Hostname;  $host = hostname();
Для получения IP-адреса можно использовать встроенную функцию gethostbyname, преобразующую имя в число. Для преобразования этого числа в привычную последовательность разделенных точкой октетов (a.b.c.d) используйте функцию inet_ntoa из модуля Socket, который тоже прилагается к perl.
use Socket;  my $address = inet_ntoa(     scalar gethostbyname( $host || 'localhost' )     );

Как скачать сообщение из группы новостей или список активных групп?

Используйте Net::NNTP или News::NNTPClient из CPAN. С ними задачи вроде получения списка групп новостей решаются очень просто:
perl -MNews::NNTPClient   -e 'print News::NNTPClient->new->list("newsgroups")'

Как скачать/закачать файл по FTP?

С LWP::Simple (есть в CPAN) можно скачать файл, но не закачать. Net::FTP (тоже есть в CPAN) более сложный, но поддерживает и закачивание, и скачивание.

Как в Perl делать удаленный вызов процедур (RPC)?

(Отвечает Брайан Д. Фой)
Используйте один из модулей CPAN для RPC: http://search.cpan.org/search?query=RPC&mode=all.

No comments:

Post a Comment