28 янв. 2009 г.

Задачка для собеседования

Была у меня в своё время любимая задачка для собеседования. Есть веб-страничка принимающая данные от посетителей сайта:
import cgi

form = cgi.FieldStorage()
message = form.getfirst('message', '')
attr = form.getfirst('attr', '')

result = '''<?xml version='1.0'?>
<message attr='%s'>%s</message>''' % (attr, message)
Далее данные отправляются, например, стороннему сервису. Так вот периодически этот сервис нам возвращает ошибку "not well-formed". И предлагаю найти и исправить ошибки. При этом я сразу оговариваю, что задача не столько на знание XML, сколько на умение решать проблемы, возникающие в ходе разработки. Кроме того, я делаю акцент на том, что пользователь может ввести произвольные данные.
Большинство соискателей сходу называют одну ошибку (представление спец-символов) и относительно быстро находят вторую (связанная с кодировкой). А вот третья проблема всегда остаётся незамеченной и "обходит" все предложенные тесты. Кроме того, большинство современных средств построения XML (например, ElementTree) молча пропускают такие ошибки. Как вариант, я предлагаю выбрать первый пришедший в голову blog-движок (как правило, это byteflow на django) с трансляцией RSS или Atom и написать тест, показывающий, что любой комментатор может "сломать" (весьма условно, учитывая что большиство современных читалок умеют переваривать и битый XML) feed комментариев. В короткий срок и без большого количества наводящих подсказок с этой задачей смог справиться только Олег Бройтман. Типичное время решения с подсказками — более суток.

16 комментариев:

Анонимный комментирует...

Вот в таком виде гораздо понятней, чем на слух во время того сборища программистов :-). Я тогда все таки забросил думать о ней, отвлекся чем-то.

Сейчас мне кажется, что еще неизвестные entity смогут ее поломать (&anything;). Или это считается как вариант первого ответа?

Unknown комментирует...

Не представляю, откуда могут взяться неизвестные entity references. Разве что если мы наплодим их входе устранения первой проблемы.

Не стоит гадать и перечитывать описание стандарта на XML, хотя там и, безусловно, есть ответ. Все ошибки легко ловятся с помощью простого теста. В качестве арбитра очень хорошо подходит expat — парсер по умолчанию в Python, независимо от используемого интерфейса.

Анонимный комментирует...

Не представляю, откуда могут взяться неизвестные entity references

Человек придет в форму комментария и напишет слово "&anything;". Поскольку код не делает escape, это будет не well-formed xml.

Все ошибки легко ловятся с помощью простого теста.

Вопрос ведь не в том, как определить не well-formed xml, а в том, чтобы его придумать с помощью пользовательского ввода. В прошлом комментарии мой вопрос сводился к тому, считаем ли мы все ошибки от отсуствия escape() одним и тем же случаем или нет.

Анонимный комментирует...

Да, и еще не понял. Вы пишете, что ElementTree пропускает эту ошибку, но в то же время, что ее можно проверить expat'ом. Хотя ElementTree как раз expat и использует. Как это понимать?

Unknown комментирует...

Я хотел сказать, что после применения escape()/quoteattr() или использования средств вроде ElementTree для генерации XML, которые это делают сами, (то есть после исправления первой ошибки) неизвестных entity references быть не может.

ElementTree применяет expat только для парсинга, но не для генерации. Так что генерация с помощью ElementTree не гарантирует, что ты сможешь распарсить им же результат. Следует ли считать это багом в ElementTree — спорный вопрос. Я бы предпочёл, чтобы он гарантировал корректный XML на выходе независимо от используемых данных, давая исключение, когда это невозможно.

Анонимный комментирует...

Что-то мы о двух разных вещах говорим.

В предложенном коде нет генерации XML'а через ElementTree. Мне интересно понять, что там за такая третья возможность сделать из него не well-formed xml. Первая -- использовать во входных строках 5 символов, которые нуждаются в escape (угловые скобки, амперсанд, два вида кавычек). Вторая -- использовать байты, приводящие к невалидному utf-8 (например 0 или произвольные символы выше 127). Третью я представить не могу :-).

Второй вопрос -- гарантирует ли ElementTree well-formed XML на выходе -- мне тоже интересен, но не имеет отношения к первому. Его можно сломать например указанием имени элемента с необъявленным namespace'ным префиксом или с использованием тех самых пяти символов.

Unknown комментирует...

От ответа на первый вопрос я пока воздержусь. Как я же сказал, все три ошибки ловятся одним простым тестом, скармливающий произвольные данные этому коду.

По поводу ElementTree. Ничего не могу сказать про необъявленые пространства имён. Неизвестные элементы могут сделать XML not valid, но никак не well-formed. Кроме того, использование пользовательских данных это, скорее, экзотика. Я имел ввиду именно значения атрибутов и содержимое (текст) элементов — их ElementTree "квотит" автоматически, но этого недостаточно для того, чтобы гарантировать на выходе well-formed документ.

Анонимный комментирует...

Как я же сказал, все три ошибки ловятся одним простым тестом, скармливающий произвольные данные этому коду.

Если туда выдать произвольный поток байт, то сломается он именно на кодировке. По крайней мере, у меня так получается, если скормить туда ''.join([chr(random.randint(0, 255)) for i in range(10000)]) -- expat говорит "invalid token"

Unknown комментирует...

Правильный тест, но не факт, что правильно интерпретированы его результаты (по приведённым данным не видно, на какой token ругается парсер и с высокой веротностью он не имеет отношения к кодировке). Если исправить ошибку с кодировкой, то он продолжит так ругаться. Hint: метод decode() имеет второй аргумент, который позволяет решить проблему с кодировкой. Если это слишком сложно, то может просто попробовать добавить комментарий от uradom() скриптом на собственный блог? Уверен, движог блога успешно заквотит спец-символы и решит проблемы с кодировкой.

Похоже, пора опубликовать новую заметку на эту тему, подробно описывающую проблему.

Анонимный комментирует...

по приведённым данным не видно, на какой token ругается парсер и с высокой веротностью он не имеет отношения к кодировке

Там был некий '\xda', кажется. Так что это действительно была невалидная utf-8 последовательность, я проверил. Именно этим тест с random хреновый, потому что первое ошибочное вхождение ничего не доказывает.

Впрочем, я сейчас сделал decode/encode с 'replace' ошибкам и парсер упал на символе '\x00'. Честно говоря, я относил это тоже к понятию "кодировка", потому что считал, что utf-8 не может содержать нулевой символ. Оказывается, может.

P.S. Честно говоря, я бы никогда не задал такой вопрос на собеседовании...

Unknown комментирует...

А вот пример как некорректная обработка "плохих" символов влияет на правильность получаемого HTML. К сожалению, для HTML5 описание ошибок пока что оставляет желать лучшего.

Анонимный комментирует...

О, да, это замечательный пример :-). Он хорошо иллюстрирует в том числе и то, почему я никогда бы не задал этот вопрос на собеседовании. Попробую пояснить.

Сразу скажу, что я согласен, что это действительно ошибка у меня на сервере. Но я не буду ее чинить, и вот почему.

Во-первых, она очень незначительна с практической точки зрения. Она не огорчает ни один реальный user-agent, а огорчает только валидатор. При современном отношении к стандартам, валидатор -- это контрольный инструмент автора документа, который тот может использовать на свое усмотрение. Я совершенно честно на свое усмотрение не воспринимаю эту ошибку серьезной, потому что она а) не влияет на восприятие страницы и, что важнее, б) возникает только для тех людей, которые хотят ее найти. Она не пугает пользователей, которые пользуются интерфейсом в обычном режиме (никто не пишет руками нетекстовые символы в поисковую строку).

Возвращаясь к собеседованию... Спрашивая такой вопрос, я не знаю, как интерпретировать ответ. Он покажет только то, случалось ли человеку когда-то выяснять этот мелкий вопрос со стандартом в руках. Уверен, я могу навыдумывать с десяток похожих вопросов, и не найдется ни одного человека, который бы знал про них все. Это задачки, про которые прикольно поболтать в программистском кругу, но они ничего не говорят о том, умеет ли человек решать реальные задачи. Потому что в реальности задача никогда не будет стоять как: "посмотрите на код и найдите в нем все потенциальные ошибки". Человечество такую задачу в общем случае пока не решилоь. В реальности же задача начинается с ошибки. Здесь, если программист получит XML c нулевым символом, про который парсер скажет "not well-formed", решение задачи очевидно, даже если он никогда не читал спецификацию XML.

Впрочем, что-то это я уже увлекся.

Unknown комментирует...

Интересно, что я узнал об этом как раз из-за неправильной обработки нулевого символа в одном браузере. Только в задаче, заметь, ни слова о HTML и браузерах. А expat и в реальной жизни не прощает ошибок.

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

Анонимный комментирует...

Что же касается ценности задачки для собеседования, то тут ты, вероятно, прав. Тесты мало кто умеет писать, а кто умеет, всё равно не пишет.

Мой поинт был не в этом. А в том, что такой тест невозможно придумать, если уже заранее точно не знать, где именно ошибка. А если это известно, то ее надо исправить.

Потому что важно не качество продукта, а видимый результат.

Вообще-то, одно на другое влияет. Другое дело, что качество -- такой же параметр задачи, как и другие. Поэтому если человек тратит несообразные ресурсы на умозрительное увеличение качества -- он плохой инженер. Либо же у него все остальное уже так хорошо, что просто совсем нечего делать :-)

Анонимный комментирует...

"Мозголом", +1 товарищу Сагалаеву.

Unknown комментирует...

Согласен, и потому больше не даю никому эту задачку. Да вот только жизнь порой подкидывает задачки и посложнее...