![]() |
Вот и закончилась наша небольшая история. Остались смешанные чувства — и радость и печаль, жизнь возвращается в прежнее русло. Но конец нашей истории — это начало четырёх других историй, радостных, интересных и наполненных любовью. Иначе и быть не может! ;) |
11 ноября 2014
Щенки
27 сентября 2014
Преобразование XML в CSV
Недавно я нашёл интересный файл в исходниках пакета iso-codes. Вернее, даже несколько файлов. В файле iso_639.xml (просмотреть) есть двух- и трёхбуквенные коды языков и их полное наименование. А в файле iso_3166.xml (просмотреть) — двух- и трёхбуквенные коды стран и территорий и их полное наименование. Вот кусочек одного файла:
<iso_639_entry iso_639_2B_code="afr" iso_639_2T_code="afr" iso_639_1_code="af" name="Afrikaans" /> <iso_639_entry iso_639_2B_code="ain" iso_639_2T_code="ain" name="Ainu" />
Я пишу в основном на shell для busybox. И я не работал с форматом XML. Гораздо проще было бы обрабатывать простой текстовый файл, в котором то же самое было бы записано вот так:
|af|afr|afr|Afrikaans| | |ain|ain|Ainu|
Тогда при помощи grep можно было бы выбрать из файла строку с нужным языком, дальше при помощи cut в этой строке выделить наименование языка. А уже потом, при необходимости, можно это наименование перевести на другой язык, используя переводы из этого же пакета iso-codes. Вот так:
LANGUAGE_CODE="af" LANGUAGE=$(grep -F "|$LANGUAGE_CODE|" /path/to/iso_639.tab | cut -d'|' -f5) echo "$LANGUAGE_CODE : $LANGUAGE : $(gettext -d iso_639 $LANGUAGE)"
Дело осталось за малым — нужно сделать эти tab-файлы. Можно, конечно, использовать текстовый редактор и при помощи поиска с заменой, а потом еще и при помощи напильника выполнить задачу. Но это отнимает много времени, и всё придётся повторять при выходе новой версии пакета iso-codes. Нужно это как-то автоматизировать.
Первым делом в голову почему-то пришло название — xmllint. Я повозился с ним, погуглил, но так и не смог добиться цели. Максимум пользы от xmllint было в том, что если ему на вход подать xml-файл, то он его «облагородит»:
xmllint /path/to/iso_639.xml
<iso_639_entry iso_639_2B_code="afr" iso_639_2T_code="afr" iso_639_1_code="af" name="Afrikaans" /> <iso_639_entry iso_639_2B_code="ain" iso_639_2T_code="ain" name="Ainu" />
Так, уже лучше. Это уже почти то, что нужно — одно определение теперь занимает одну строку. Что ж, вот несложный скрипт в качестве «напильника»:
#!/bin/sh TEMP=$(mktemp) xmllint $1 | grep '<iso_639_entry' > $TEMP while read line; do iso_639_2B_code="$(echo $line | sed 's|.* iso_639_2B_code=\"\([^"]*\)\".*|\1|')" iso_639_2T_code="$(echo $line | sed 's|.* iso_639_2T_code=\"\([^"]*\)\".*|\1|')" iso_639_1_code="$(echo $line | grep iso_639_1_code | sed 's|.* iso_639_1_code=\"\([^"]*\)\".*|\1|')" name="$(echo $line | sed 's|.* name=\"\([^"]*\)\".*|\1|')" common_name="$(echo $line | grep common_name | sed 's|.* common_name=\"\([^"]*\)\".*|\1|')" [ "x$iso_639_1_code" == "x" ] && iso_639_1_code=" " echo "|$iso_639_1_code|$iso_639_2B_code|$iso_639_2T_code|$name|$common_name" done < $TEMP
Скрипт работает, выполняет своё дело. Но какой же он медленный!
real 0m 49.83s user 0m 44.09s sys 0m 12.40s
Да к тому же и процессор нагружает около 100%.
Писать на shell легко, но программы получаются далеко не эффективными и это особенно заметно на таких вот циклах. Я буду продолжать писать на shell, но этот кусочек я хотел бы переделать.
Я погуглил ещё на тему преобразования и трансформации XML и в конце-концов вышел на хороший пример, который пришлось только немного переделать под мой файл.
На сцену выходит утилита xsltproc. Она умеет преобразовывать файл XML в соответствии с правилами, заложенными в указанном файле XSL. Некоторая аналогия с информацией, находящейся в HTML и правилами в CSS. Да, как-то много лет назад я возился с XML и XSL но, конечно же, подробности забылись и тогда я не сделал каких-то особенных успехов. Получится ли сейчас? Да, к счастью, задача оказалась простой. Вот файл iso_639.xsl:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text" /> <xsl:template match="/"> <xsl:for-each select="//iso_639_entries/*"> <xsl:text>|</xsl:text> <xsl:if test="not(@iso_639_1_code)"> <xsl:text> </xsl:text> </xsl:if> <xsl:value-of select="@iso_639_1_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@iso_639_2B_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@iso_639_2T_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@name" /> <xsl:text>|</xsl:text> <xsl:value-of select="@common_name" /> <xsl:text> </xsl:text> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Здесь для каждой записи выводятся через разделители «|» параметры iso_639_1_code, iso_639_2B_code, iso_639_2T_code, name, common_name. Причём iso_639_1_code и common_name могут отсутствовать. Я «для красоты» сделал так, если отсутствует iso_639_1_code (двухбуквенный код), то выводить два пробела. Так табличка выглядит ровной при просмотре глазами. Запускается преобразование командой:
xsltproc iso_639.xsl iso_639.xml > iso_639.tab
Вот его время работы:
real 0m 0.03s user 0m 0.03s sys 0m 0.00s
Чёрт! Впечатляет!
Почти так же был написан и файл iso_3166.xsl. Отличие только в том, что теперь файл XML состоит из двух частей. В первой части — действующие коды, а во второй — исторические, как например, СССР. Для исторических кодов применяется немного другой формат, например указано, когда этот СССР развалился и код перестал действовать.
Вот, собственно, и сам файл iso_3166.xsl:
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text" /> <xsl:template match="/"> <xsl:for-each select="//iso_3166_entries/iso_3166_entry"> <xsl:text>|</xsl:text> <xsl:value-of select="@alpha_2_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@alpha_3_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@numeric_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@name" /> <xsl:text>|</xsl:text> <xsl:value-of select="@official_name" /> <xsl:text>|</xsl:text> <xsl:value-of select="@common_name" /> <xsl:text>|</xsl:text> <xsl:text> </xsl:text> </xsl:for-each> <xsl:text> </xsl:text> <xsl:for-each select="//iso_3166_entries/iso_3166_3_entry"> <xsl:text>|</xsl:text> <xsl:value-of select="@alpha_4_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@alpha_3_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@numeric_code" /> <xsl:text>|</xsl:text> <xsl:value-of select="@date_withdrawn" /> <xsl:text>|</xsl:text> <xsl:value-of select="@names" /> <xsl:text>|</xsl:text> <xsl:value-of select="@comment" /> <xsl:text>|</xsl:text> <xsl:text> </xsl:text> </xsl:for-each> </xsl:template> </xsl:stylesheet>
Ну вот и всё. Если мне когда-нибудь понадобится преобразовать XML в CSV, я буду знать что делать.