Парсинг XML в AOLserver

  Автор: © Irving Washington
Перевод: © Роман Шумихин.


 

AOLserver

AOLserver это open-source, многопоточный (multi-threaded), высокопроизводительный веб сервер. Хотя AOLserver менее известен, чем Apache, по некоторым характеристикам он его превосходит: богатое и хорошо продуманное расширение API, превосходный API для стыковки с базами данных, хорошо интегрированный интерпретатор Tcl. Чтобы узнать больше об AOLServer'е прочитайте мою предыдущую статью.

XML

Если вы собираетесь серьезно работать с языком XML, вам нужно изучить его, но где-нибудь в другом месте - явно не в этой статье. Самое лучшее резюме об XML, которое я видел: XML это (неэффективный) способ представления данных в древовидной форме в виде текстовых (ASCII) файлов. Представление данных в виде текста как раз подходит для таких целей, потому что текстовой формат прост. Древовидная структура подходит, потому что много всего можно представить в виде деревьев (например, незамкнутый список - это просто вырожденное дерево, а замкнутый список может быть описан с помощью некоторого множества деревьев). Неэффективность - это, конечно, нехорошо, но ей обычно жертвуют в пользу расширяемости и повсеместной применимости, которую имеет XML (много инструментов, много информации).

Поддержка XML в AOLserver

Обработка XML (парсинг и модификация XML-документов) в AOLserver возможна благодаря модулю ns_xml, который написан ArsDigita. Этот модуль - оболочка (wrapper) над версиями 2.x (>2.2.5) библиотеки libxml, она добавляет команду ns_xml во встроенный интерпретатор Tcl. Вы можете загрузить исходные тексты или получить этот модуль прямо из CVS repository так:
cvs -d:pserver:anonymous@cvs.aolserver.sourceforge.net:/cvsroot/aolserver login
cvs -z3 -d:pserver:anonymous@cvs.aolserver.sourceforge.net:/cvsroot/aolserver co nsxml
Нажмите Enter после ввода первой команды, так как CVS ждет ввода пароля (который в нашем случае пуст).

С декабря 2000-го года дистрибутивы Linux обычно поставляются с версией 1.x библиотеки libxml, так что есть вероятность, что вам самим придется установить версию 2.x (в будущем все изменится, так как все переходят на версии 2.x). Чтобы установить модуль nsxml зайдите в директорию nsxml, выборочно отредактируйте путь в Makefile, чтобы он указывал на директорию с исходниками AOLserver. Потом запустите make. Вы должны получить модуль nsxml.so, который нужно поместить в директорию bin AOLserver'а (та директория, в который лежит главный исполняемый файл nsd). Добавьте следующее к вашему файлу конфигурации nsd.tcl:

ns_section "ns/server/${servername}/modules"
ns_param   nsxml           ${bindir}/ns_xml.so
и перезапустите AOLserver. Чтобы проверить, что модуль загружается, посмотрите файл server.log, обычно я использую shell с такой командой:
tail -f $AOLSERVERDIR/log/server.log
Это так же хороший способ отладки скриптов Tcl, так как AOLserver будет сбрасывать детальную информацию об ошибках туда же, каждый раз, когда возникает ошибка в скрипте.

Краткая справка по XML

Вот небольшая справка по всем командам, доступным через ns_xml.

set doc_id [ns_xml parse ?-persist? $string]
Проанализировать документ XML в $string и возвратить id документа (указатель на дерево разбора в оперативной памяти). Если вы не установите флаг ?-persist?, память будет автоматически освобождаться по завершению работы скрипта. В противном случае вам придется освобождать ее, вызывая ns_xml doc free. Используйте флаг -persist, если вы хотите обеспечить совместный доступ к уже разобранным (parsed) документам XML между скриптами.
set doc_stats [ns_xml doc stats $doc_id]
Возвращает статистику документа.
ns_xml doc free $doc_id
Освобождает память, занимаемую документом. Следует использовать для документа только если флаг ?-persistent? был передан для ns_xml parse или ns_xml doc create
set node_id [ns_xml doc root $doc_id]
Возвращает id корневого узла документа (вы начинаете обход дерева документа отсюда.)
set children_list [ns_xml node children $node_id]
Возвращает список узлов-потомков данного узла.
set node_name [ns_xml node name $node_id]
Возвращает имя узла.
set node_type [ns_xml node type $node_id]
Возвращает тип узла. Возможные типы: element, attribute, text, cdata_section, entity_ref, entity, pi, comment, document, document_type, document_frag, notation, html_document
set content [ns_xml node getcontent $node_id]
Получает содержимое (текст) данного узла.
set attr [ns_xml node getattr $node_id $attr_name]
Возвращает значение атрибута данного узла.
set doc_id [ns_xml doc create ?-persist? $doc-version]
Создает новый документ в памяти. Если установлен флаг -persist, вы должны явно освобождать память, занятую документом, с помощью ns_xml doc free, в противном случае она освободится автоматически по завершению работы скрипта. $doc_version - это версия документа XML, если она не определена, то устанавливается по умолчанию в "1.0".
set xml_string [ns_xml doc render $doc_id]
Генерирует XML из представленного в памяти документа.
set node_id [ns_xml doc new_root $doc_id $node_name $node_content]
Создает корневой узел документа.
set node_id [ns_xml node new_sibling $node_id $name $content]
Создает нового брата данного узла.
set node_id [ns_xml node new_child $node_id $name $content]
Создает потомка данного узла.
ns_xml node setcontent $node_id $content
Устанавливает содержимое данного узла.
ns_xml node setattr $node_id $attr_name $value
Устанавливает значение атрибута данного узла.

Простой пример

Простой и поучительный пример - разбор древовидной структуры документа с последующей ее печатью. Что делает этот процесс:
  • использует ns_xml parse $xml_doc для парсинга документа XML в строку $xml_doc и получает id документа
  • использует ns_xml doc root $doc_id, чтобы получить идентификатор корневого узла
  • использует ns_xml node children $node_id для обхода дерева документа, и команды ns_xml node ... чтобы получить содержимое и атрибуты узла
Если вы установите флаг -persist для ns_xml parse , вам придется явно вызывать ns_xml doc free $doc_id чтобы освободить, память выделенную под данный документ, в противном случае она освободится автоматически после выполнения скрипта.

Код мог бы выглядеть вот так:

proc dump_node {node_id level} {
    set name [ns_xml node name $node_id]
    set type [ns_xml node type $node_id]
    set content [ns_xml node getcontent $node_id]
    ns_write "<li>"
    ns_write "node id=$node_id name=$name type=$type"
    if { [string compare $type "attribute"] != 0 } {
	ns_write " content=$content\n"
    }
}

proc dump_tree_rec {children} {
    ns_write "<ul>\n"
    foreach child_id $children {
	dump_node $child_id
	set new_children [ns_xml node children $child_id]
	if { [llength $new_children] > 0 } {
	    dump_tree_rec $new_children
	}
    }
}

proc dump_tree {node_id} {
    dump_tree_rec [list $node_id] 0
}

proc dump_doc {doc_id} {
    ns_write "doc id=$doc_id<br>\n"
    set root_id [ns_xml doc root $doc_id]
    dump_tree $root_id
}

set xml_doc "<test version="1.0">this is a
<blind>test</blind> of xml</test>"
set doc_id [ns_xml parse $xml_doc]
dump_doc $doc_id    
Команда ns_xml parse выдаст ошибку, если документ XML неправильный (например, плохо сформирован), поэтому в ходе выполнения программы мы можем выловить ошибку и выдать сообщение. Например:
if { [catch {set doc_id [ns_xml parse $xml_doc]} err] } {
    ns_write "There was an error parsing the following XML document: "
    ns_write [ns_quotehtml $xml_doc]
    ns_write "Error message is:"
    ns_write [ns_quotehtml $err]
    ns_write "\n"
    return
}
Такой код требует много времени для написания, но, однажды, он может сократить много времени на отладку (и такой день обязательно настанет).

Посмотрите, как код работает на практике [это сайт с запущеным AOLserver] и загрузите полные исходные тексты программы [они есть в Linux Gazette]. Эта программа немного сложнее, чем указанный выше кусок кода. Вы увидите структуру произвольного документа XML, набрав его в предоставленной текстовой области (text area). Скрипт также показывает, как разбирать данные формы и имеет более сильную систему обработки ошибок.

Пример из жизни

XML лучше, чем другие подобные форматы, потому что он является стандартом, он получил широкое распространение, и количество его пользователей растет очень быстро. Одно из возможных применений XML - это способ коммуникации между веб сайтами (веб сервисами). Самый простой сценарий: один веб сервер получает информацию в формате XML с другого веб сервера. Популярный пример такого рода взаимодействия - это собрание заголовков, например, если вы зайдете на freshmeat.net, вы увидите, что на сайте представлены текущие заголовки статей из linuxtoday.com. Мы сделаем то же самое.

В прошлом такое можно было осуществить только таким ужасным способом: скачивание всей странички HTML и попытка извлечь из нее относящуюся к делу информацию. Это трудно запрограммировать и недолговечно (изменение способа генерации данной страницы HTML скорее всего разрушит такой парсинг).

Сегодня сайт, который хочет предоставлять свои заголовки другим, может опубликовать данные в легком для парсинга формате XML, под какой-нибудь ссылкой. В нашем случае данные предоставлены по этому адресу http://www.linuxtoday.com/backend/linuxtoday.xml. Посмотрите на формат этого файла (используя недавно разработанный скрипт).

Вы увидите, как документ XML предоставляет заголовки сайта LinuxToday. Это множество статей, каждая из которых имеет заголовок, url, автора и т.д. Мы знаем, что после парсинга документа XML, мы бы хотели иметь легкий способ получения нужной нам информации. Давайте примем желаемое за действительное (другими словами, все наоборот) и будем писать код программы по методу, которого придерживается Структура и интерпретация компьютерных программ (Structure and interpretation of computer programs) (действительно хорошая компьютерная книга). Пусть мы сконвертировали внутреннее представление XML в объект. Чтобы построить таблицу на HTML, показывающую данные, нам нужны следующие процедуры:

  • получить количество статей: headlines_get_stories_count $headlines
  • получить n-ю статью: headlines_get_story $headline $story_no
  • получить URL данной статьи: story_get_url $story
  • получить заголовок данной статьи: story_get_title $story
Для простоты я использую только URL и заголовок, но добавление других атрибутов должно быть достаточно простым.

Имея эти процедуры, мы можем генерировать простую (но довольно ужасную) таблицу:

proc story_to_html_table_row { story } {
    set url [story_get_url $story]
    set title [story_get_title $story]
    return "- <a href=\"$url\"><font color=#000000>$title</font></a><br>\n"
}

# данные заголовки генерируют код HTML таблицы с этими данными
proc headlines_to_html_table { headlines } {
    set to_return "<table border=0 cellspacing=1 cellpadding=3>"
    append to_return "<tr><td><small>"

    set stories_count [headlines_get_stories_count $headlines]
    for {set i 0} {$i < $stories_count} {incr i} {
	set story [headlines_get_story $headlines $i]
	append to_return [story_to_html_table_row $story]
    }

    append to_return "</td></tr></table>\n"
    return $to_return
}
Tcl не предоставляет нам большой свободы выбора представления этого объекта; мы будем использовать списки.
proc headlines_get_stories_count { headlines } {
    return [llength $headlines]
}

proc headlines_get_story { headlines story_no } {
    return [lindex $headlines $story_no]
}

proc story_get_url { story } {
    return [lindex $story 0]
}

proc story_get_title { story } {
    return [lindex $story 1]
}
Заметьте, что если мы забудем о чистоте (совсем ненадолго), мы можем переписать следующую часть headlines_to_html_table:
set stories_count [headlines_get_stories_count $headlines]
for {set i 0} {$i < $stories_count} {incr i} {
    set story [headlines_get_story $headlines $i]
    append to_return [story_to_html_table_row $story]
}
более кратким способом:
foreach story $headlines {
    append to_return [story_to_html_table_row $story]
}
Теперь самая важная часть: конвертирование документа XML в представление, которое мы выбрали.
# устанавливает имя узла, на котрый указывает $node_id, равным $name
proc is_node_name_p { node_id name } {
    set node_name [ns_xml node name $node_id]
    if { [string_equal_p $name $node_name] } {
	return 1
    } else {
	return 0
    }
}

# устанавливает тип узла, на который указывает $node_id, равным $type
proc is_node_type_p { node_id type } {
    set node_type [ns_xml node type $node_id]
    if { [string_equal_p $type $node_type] } {
	return 1
    } else {
	return 0
    }
}

# это узел типа "аттрибут"?
proc is_attribute_node_p { node_id } {
    return [is_node_type_p $node_id "attribute"]
}

# выдать ошибку, если имя узла не $name
proc error_if_node_name_not {node_id name} {
    if { ![is_node_name_p $node_id $name] } {
	set node_name [ns_xml node name $node_id]
	error "node name should be $name and not $node_name"
    }
}

# выдать ошибку, если тип узла не $type
proc error_if_node_type_not {node_id type} {
    if { ![is_node_type_p $node_id $type] } {
	set node_type [ns_xml node type $node_id]
	error "node type should be $type and not $node_type"
    }
}

# данная URL и заголовок образуют объект story (статья) с атрибутами url, title
proc define_story { url title } {
    return [list $url $title]
}

# конвертировать узел с именем "story" в объект, который представляет статью
proc story_node_to_story {node_id} {
    set url ""
    set title ""
    # пройти всех потомков и получить содержимое узлов url и заголовка title
    set children [ns_xml node children $node_id]
    foreach node_id $children {
	# мы заинтересованы только в узлах с именами "url" или "title"
	if { [is_attribute_node_p $node_id]} {
	    if { [is_node_name_p $node_id "url"] || [is_node_name_p $node_id "title"]} {
		set node_children [ns_xml node children $node_id]
		# те должны иметь только одного потомка с именем
		# "text" и типом "cdata_section"
		if { [llength $node_children] != 1 } {
		    set name [ns_xml node name $node_id]
		    error "$name node should only have 1 child"
		}
		set one_node_id [lindex $node_children 0]
		error_if_node_type_not $one_node_id "cdata_section"
		error_if_node_name_not $one_node_id "text"
		set txt [ns_xml node getcontent $one_node_id]
		if { [is_node_name_p $node_id "url"] } {
		    set url $txt
		}
		if { [is_node_name_p $node_id "title"]} {
		    set title $txt
		}
	    }
	}
    }
    return [define_story $url $title]
}

# конвертируем документ XML в объект заголовков
proc xml_to_headlines { doc_id } {
    set headlines [list]
    set root_id [ns_xml doc root $doc_id]
    # корневой узел должен быть назван "linuxtoday" и быть типа "attribute"
    error_if_node_name_not $root_id "linuxtoday"
    error_if_node_type_not $root_id "attribute"
    set children [ns_xml node children $root_id]
    foreach node_id $children {
	# мы заинтересованы только в узлах типа атрибут, чь└ имя "story"
	if { [is_node_name_p $node_id "story"] && [is_attribute_node_p $node_id]} {
	    set story [story_node_to_story $node_id]
	    lappend headlines $story
	}
    }
    return $headlines
}
Код довольно прост. Мы используем знания о структуре файла XML. В этом случае мы знаем, что корневой узел называется linuxtoday и должен иметь потомка по имени story. Каждый узел story должен иметь потомков по имени url и title и т.д. В написании этой функции мне очень помог предыдущий скрипт, который печатает основную структуру дерева. Обратите внимание на использование команды error чтобы прервать скрипт, если XML не выглядит хорошо для нас.

Промежуточное представление данных может выглядеть излишним, т.к. оно требует от нас больше кода и некоторого быстродействия, но есть очень хорошие причины иметь его. Мы могли бы написать процедуру xml_to_html_table, которая бы создавала таблицу HTML прямо из документа XML, но такой код был бы более сложен, более насыщен багами, труден для последующей модификации. Разделение, которое мы сделали, представляет абстракцию, которая уменьшает сложность, что всегда хорошо. Это так же да└т нам большую гибкость: мы можем легко представить ещ└ одну процедуру headlines_to_html_table, которая выда└т нам немного другую таблицу.

Посмотрите, как это работает на практике [сайт на базе AOLserver] и загрузите исходные тексты [они есть в Linux Gazette]. Программа выдает нечто вроде этого:

linuxtoday
- Kernel Cousin Debian Hurd #73 By Paul Emsley And Zack Brown
- Zope 2.2.5 b1 released
- O#39;Reilly Network: Insecurities in a Nutshell: SAMBA, pine, ircd, and More
- ZDNet: Linux Laptop SuperGuide
- ComputerWorld: Think tank warns that Microsoft hack could pose national security risk
а

Единственное, чего не хватает в этом коде - кэширование. Так как программа будет забирать файл с севера другого сервера каждый раз, когда ее вызывают. Это не есть хорошо :-) Будет довольно легко внести кое-какие изменения, чтобы кэшировать файл XML (или его представление в памяти) и загружать новую версию только если, скажем, прошел 1 час с того момента, когда он был скачан последний раз.

В заключение об XML, как о языке обмена данными

Разве обмен данными между серверами новая идея? Нет. Вы смогли бы сделать все, что описано здесь с первым поколением веб серверов. Вероятно, вы бы использовали другие технологии (Си-программа запущенная на веб сервере или скрипт CGI вместо внедренного языка скриптов; несколько программных затычек, реанимирующих систему, или двоичный формат вместо XML), но идея была бы такой же: один веб сервер, действуя как клиент, получает данные с другого сервера, используя протокол HTTP, и делает с ними что-нибудь полезное. Другой веб сервер действует как сервер, предоставляя данные остальным. Это просто еще одно представление парадигмы клиент-сервер. Здесь нет ничего нового. Это просто знак, что веб-программирование развивается. После пяти с лишним лет мы, наконец-то, решили большинство проблем с представлением статических страниц html или генерированием динамических веб-страничек из данных, хранящихся на сервере (например, в базе данных). Наступили времена предоставления служб и данных другим веб сайтам. Современное применение этой технологии пока ограничено обменом заголовками и подобными пустяками, но возможности этой технологии больше: от простых вещей, таких как предоставление биржевых котировок или определений из словаря для исполнения сложных (например, финансовых) транзакций, следуя установленному заранее протоколу.

Заключение о парсинге XML в AOLserver

Кроме парсинга вы можете создавать и манипулировать документами XML в памяти, и преобразовывать их в представление XML ASCII. Это не описано в данной статье, но ясно, что вы сможете это сделать, просто глядя на API.

модуль ns_xml предоставляет основы процессинга XML. Хотя вы сможете сделать с ним немного, возможно, вам захочется сделать нечто большее. Вот то, чего очевидно не хватает:

  • SAX API (он уже присутствует в libxml, поэтому потребуется только расширение ns_xml)
  • поддержка XSLT (поддержка XSLT, хотя запланирована, еще не присутствует в libxml)
Альтернативный подход к модулю ns_xml был бы таким:
  • использование PyWx, интерпретатора Python встроенного в AOLserver, и стандартного PyXML модуля Python
  • написание еще одного оберточного модуля для какой-нибудь другой библиотеки парсинга XML
  • использование чистого парсера Tcl

Ссылки

Если у вас есть замечания или предложения, напишите мне.

 


Copyright © 2001, Irwing Washington.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 63 of Linux Gazette, Mid-February (EXTRA) 2001

Вернуться на главную страницу