Но здесь есть еще одна удивительно тонкая (и, к счастью, редкая) особенность: нужно быть осторожными с символами & в строках URL, которые встраиваются в разметку HTML (например, в теги гиперссылок <A>). В адресах URL символ & используется как разделитель параметров в строке запроса (?a=1&b=2), а в HTML он является первым символом в экранированных последовательностях (<). Следовательно, есть вероятность конфликтов, если определение параметра запроса совпадет с экранированной последовательностью HTML. Например, параметр запроса с именем amp, который записывается как &=1 и имеет порядковый номер 2 или выше в адресе URL, может интерпретироваться некоторыми анализаторами HTML как экранированная последовательность HTML и преобразовываться в &=1.
Даже если части строки URL экранированы в соответствии с соглашениями для URL, при наличии нескольких параметров они разделяются символом &, а разделитель & также может оказаться преобразованным в & в соответствии с соглашениями для HTML. Чтобы понять причину, взгляните на следующий тег гиперссылки HTML:
<A HREF="file.py?name=a&job=b&=c§=d<=e">hello</a>
При отображении в большинстве проверенных мной броузеров, включая Internet Explorer в Windows 7, этот адрес URL-ссылки выглядит неправильно, как показано ниже (символ S в первом из них интерпретирован как маркер раздела, код которого выходит за пределы диапазона ASCII):
file.py?name=a&job=b&=cS=d<=e результат в IE
file.py?name=a&job=b&=c%A7=d%3C=e результат в Chrome (0x3C — это <)
Первые два параметра будут сохранены, как и можно было бы ожидать (name=a, job=b), потому что перед name нет символа &, а &job не распознается как допустимая экранированная последовательность HTML. Однако последовательности символов &, § и < интерпретируются как специальные символы, потому что совпадают с именами допустимых в HTML экранированных последовательностей, даже несмотря на отсутствие завершающего символа точки с запятой.
Чтобы убедиться в этом, откройте файл test-escapes.html, входящий в состав пакета с примерами, в своем броузере и выделите или выберите эту ссылку — имена параметров в строке запроса могут быть интерпретированы как экранированные последовательности HTML. Похоже, что этот текст правильно интерпретируется модулем синтаксического анализа разметки HTML, описанным выше (если только фрагменты строки запроса не завершаются точкой с запятой), — это может помочь при получении ответов вручную, с помощью модуля urllib.request, но не при отображении в броузерах:
>>> from html.parser import HTMLParser
>>> html = open(‘test-escapes.html’).read()
>>> HTMLParser().unescape(html)
‘<HTML>\n<A HREF="file.py?name=a&job=b&=c§=d<=e">hello</a>\n</HTML>’
Исключение конфликтов
Так что же делать? Чтобы все действовало так, как нужно, необходимо экранировать разделители &, если имена параметров могут конфликтовать с экранированными последовательностями HTML:
<A HREF="file.py?name=a&job=b&amp=c&sect=d&lt=e">hello</a>
Такую полностью экранированную ссылку броузеры Python выводят как надо (откройте файл test-escapes2.html, чтобы убедиться), и синтаксический анализатор HTML из стандартной библиотеки также корректно интерпретирует ее:
file.py?name=a&job=b&=c§=d<=e результат в IE и Chrome
>>> h = ‘<A HREF="file.py?name=a&job=b&amp=c&
sect=d&lt=e">hello</a>’
>>> HTMLParser().unescape(h)
‘<A HREF="file.py?name=a&job=b&=c§=d<=e">hello</a>’
Из-за этого конфликта между синтаксисом HTML и URL большинство серверных инструментов (включая инструменты разбора параметров запроса в модуле Python urlib.parse, используемые модулем cgi) позволяют также использовать символ точки с запятой вместо & в качестве разделителя параметров. Следующая ссылка, например, действует точно так же, как и полностью экранированный адрес URL, но для нее не требуется выполнять дополнительное экранирование HTML (по крайней мере для ;):
file.py?name=a;job=b;amp=c;sect=d;lt=e
Инструменты обратного преобразования экранированных последовательностей в модуле Python html.parser позволяют передавать символы точки с запятой без изменений, просто потому, что они не являются специальными символами HTML. Чтобы полностью протестировать все три эти ссылки одновременно, поместите их в файл HTML, откройте его в броузере, используя адрес URL http://localhost/badlink.html, и посмотрите, как выглядят эти ссылки. Файла HTML, представленного в примере 15.24, будет вполне достаточно.
Пример 15.24. PP4E\Internet\Web\badlink.html
<HTML><BODY>
<p><A HREF=
"cgi-bin/badlink.py?name=a&job=b&=c§=d<=e">unescaped</a>
<p><A HREF=
"cgi-bin/badlink.py?name=a&job=b&amp=c&sect=d&lt=e"> escaped</a>
<p><A HREF=
"cgi-bin/badlink.py?name=a;job=b;amp=c;sect=d;lt=e">alternative</a>
</BODY></HTML>
Щелчок на любой из этих ссылок будет вызывать запуск простого сценария CGI, представленного в примере 15.25. Этот сценарий выводит входные параметры, полученные им от клиента, в стандартный поток ошибок, чтобы избежать необходимости дополнительных преобразований (при использовании нашего локального веб-сервера из примера 15.1 вывод будет осуществляться в окно консоли сервера).
Пример 15.25. PP4E\Internet\Web\cgi-bin\badlink.py
import cgi, sys
form = cgi.FieldStorage() # выводит все входные параметры # в stderr; stodout=reply page for name in form.keys():
print(‘[%s:%s]’ % (name, form[name].value), end=’ ‘, file=sys.stderr)
Ниже приводится (отредактированный, чтобы уместился по ширине страницы) вывод, полученный в окне консоли нашего локального вебсервера для каждой из трех ссылок в странице HTML при использовании Internet Explorer. Второй и третий результаты содержат корректные наборы параметров благодаря экранированию в соответствии с соглашениями для HTML или URL, но случайные совпадения имен параметров с экранированными последовательностями HTML в первой неэкранированной ссылке вызывают серьезные проблемы — синтаксический анализатор HTML на стороне клиента интерпретирует их не так, как предполагалось (в Chrome получаются похожие результаты, но в первой ссылке отображается символ раздела, не входящий в набор ASCII, соответствующий другой экранированной последовательности):
mark-VAIO — [16/Jun/2010 10:43:24] b'[:c\xa7=d<=e] [job:b] [name:a] ‘ mark-VAIO — [16/Jun/2010 10:43:24] CGI script exited OK
mark-VAIO — [16/Jun/2010 10:43:27] b'[amp:c] [job:b] [lt:e] [name:a] [sect:d]’ mark-VAIO — [16/Jun/2010 10:43:27] CGI script exited OK
mark-VAIO — [16/Jun/2010 10:43:30] b'[amp:c] [job:b] [lt:e] [name:a] [sect:d]’ mark-VAIO — [16/Jun/2010 10:43:30] CGI script exited OK
Из всей этой истории должен быть сделан следующий вывод: если нет уверенности, что все имена параметров в строке запроса URL, встраиваемой в разметку HTML, кроме самого левого, отличны от экранированных последовательностей HTML, таких как amp, вы должны или использовать в качестве разделителя точку с запятой (если это поддерживается инструментами, с которыми вы имеете дело), или пропустить весь URL через функцию cgi.escape после того, как имена параметров и их значения будут экранированы с помощью urllib.parse.quote_plus:
>>> link = ‘file.py?name=a&job=b&=c§=d<=e’
# экранирование в соответствии с соглашениями для HTML
>>> import cgi
>>> cgi.escape(link)
‘file.py?name=a&job=b&amp=c&sect=d&lt=e’
# экранирование в соответствии с соглашениями для URL
>>> import urllib.parse
>>> elink = urllib.parse.quote_plus(link)
>>> elink
‘file.py%3Fname%3Da%26job%3Db%26amp%3Dc%26sect%3Dd%26lt%3De’
# экранирование URL также удовлетворяет соглашениям для HTML: тот же результат
>>> cgi.escape(elink)
‘file.py%3Fname%3Da%26job%3Db%26amp%3Dc%26sect%3Dd%26lt%3De’
При этом я должен добавить, что в некоторых примерах этой книги разделители & в адресах URL, встраиваемых в HTML, не были экранированы, поскольку известно, что имена параметров в этих URL не конфликтуют с экранированными последовательностями HTML. В действительности, эта проблема редко возникает на практике, поскольку имена параметров, ожидаемых программой, полностью подконтрольны программистам. Однако нельзя считать, что этой проблемы не существует, особенно если имена параметров могут извлекаться динамически из базы данных — при наличии сомнений экранируйте много и часто.
«Взгляните на жизнь с ее приятной стороны»
Какими бы корявыми (и заставляющими с криком просыпаться ночью!) не казались вам все эти правила форматирования HTML и URL, обратите внимание, что эти соглашения по экранированию HTML и URL диктуются самим Интернетом, а не Python. (Мы уже знаем, что в Python существует другой механизм экранирования специальных символов в строковых константах — с помощью символов обратного слэша.) Эти правила проистекают из того обстоятельства, что Сеть основана на идее пересылки по всему свету форматированных текстовых строк, а правила вырабатывались в ответ на то, что разные группы разработчиков использовали различные системы обозначений.
Можете, однако, утешиться тем, что часто не требуется заботиться о подобных таинственных вещах, а если требуется, то Python автоматизирует процесс с помощью библиотечных средств. Просто имейте в виду, что всякому сценарию, генерирующему разметку HTML или адреса URL динамически, возможно, требуется для устойчивости воспользоваться средствами Python преобразования в экранированные последовательности. В дальнейших примерах этой и следующей глав часто будут использоваться средства экранирования HTML и URL. Кроме того, фреймворки и инструменты для разработки веб-приложений, такие как Zope и другие, стремятся избавить нас от некоторых низкоуровневых сложностей, с которыми сталкиваются разработчики сценариев CGI. И, как обычно в программировании, ничто не может заменить интеллект — ценой доступа к поразительным технологиям вроде Интернета является их сложность.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, II том, 2011