2025, Dec 08 09:02
Как починить проверку XMLSchema в lxml на Linux: xml:specialAttrs и XML‑каталог
Почему в lxml на Linux с libxml2 падает проверка XMLSchema с ошибкой xml:specialAttrs, и как это исправить: настройка XML‑каталога и локальный импорт xml.xsd.
Как починить проверку XMLSchema в lxml на Linux, когда не удаётся разрешить xml:specialAttrs
Проверка XML по XSD иногда внезапно ломается на разных платформах. Типичная ошибка, которая проявилась на свежих Linux-стэках, выглядит так:
lxml.etree.XMLSchemaParseError: Element '{http://www.w3.org/2001/XMLSchema}attributeGroup', attribute 'ref': The QName value '{http://www.w3.org/XML/1998/namespace}specialAttrs' does not resolve to a(n) attribute group definition., line 15
Тот же код без проблем проходит валидацию на Windows, но падает на Linux с lxml 6.x в связке с новой libxml2. Ниже показан воспроизводимый сценарий, что именно изменилось и как снова сделать проверку надёжной.
Минимальная конфигурация, которая вызывает ошибку
XML с XInclude и небольшим XSLT проверяется по схеме, которая импортирует пространство имён XML. Критичны две части: импорт в XML Schema и ссылка на xml:specialAttrs.
main.xml
<?xml version="1.0" encoding="UTF-8"?>
<root xmlns:xi="http://www.w3.org/2001/XInclude">
<title>Main XML</title>
<elements>
<element name="main element" foo="main foo">This text is from main.xml</element>
<xi:include href="include.xml" parse="xml" xpointer="xpointer(/elements/element)"/>
</elements>
</root>
include.xml
<?xml version="1.0" encoding="UTF-8"?>
<elements>
<element name="element1" foo="foo1">Text 1: This content is included from another file.</element>
<element name="element2" foo="foo2">Text 2: This content is included from another file.</element>
<element name="element3" foo="foo3">Text 3: This content is included from another file.</element>
</elements>
transform.xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="element[@name='element2']">
<xsl:copy>
<xsl:apply-templates select="@*"/>
<xsl:attribute name="foo">spam</xsl:attribute>
<xsl:attribute name="name">message99</xsl:attribute>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
schema.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
<xs:element name="root">
<xs:complexType>
<xs:sequence>
<xs:element name="title" type="xs:string"/>
<xs:element name="elements">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="element" minOccurs="1" maxOccurs="unbounded">
<xs:complexType mixed="true">
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="foo" type="xs:string" use="required"/>
<xs:attributeGroup ref="xml:specialAttrs"/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Скрипт проверки на Python
#!/usr/bin/env python3
import os
import lxml
from lxml import etree
print("Using lxml version {0}".format(lxml.__version__), end="\n\n")
xml_doc = etree.parse("main.xml")
xml_doc.xinclude()
if os.path.isfile("transform.xslt"):
print("Applying transformation from transform.xslt")
style_tree = etree.parse("transform.xslt")
xslt_proc = etree.XSLT(style_tree)
transformed = xslt_proc(xml_doc)
xml_doc._setroot(transformed.getroot())
print(etree.tostring(xml_doc, pretty_print=True).decode())
xsd_obj = etree.XMLSchema(etree.parse("schema.xsd"))
if xsd_obj.validate(xml_doc):
print("XML is valid.")
else:
print("XML is invalid!")
for entry in xsd_obj.error_log:
print(entry.message)
На Linux с lxml 6.x и новой libxml2 запуск заканчивается указанной выше XMLSchemaParseError. На Windows этот же проект валидируется.
Что происходит на самом деле
В последних релизах libxml2 по соображениям безопасности жёстче применяют правило: внешние ресурсы должны разрешаться через XML‑каталоги. Когда схема импортирует схему пространства имён XML и пытается получить её из сети, резолвер может отказаться её загружать. В итоге компонент схемы, определяющий xml:specialAttrs, недоступен, и QName не удаётся разрешить — это и приводит к ошибке компиляции схемы для проверки. Разница в поведении между платформами объясняется версиями библиотек: на Linux новая libxml2 принуждает эту политику, тогда как на Windows с более старой libxml2 валидация проходит. Ту же проблему можно воспроизвести утилитой xmllint из свежей libxml2, так что это не особенность только lxml.
Правильное решение: используйте XML‑каталог
Настройте локальный XML‑каталог, который сопоставляет внешний URI схемы с локальным файлом, и укажите парсеру этот каталог. Рядом поместите локальную копию схемы пространства имён XML.
Один раз скачайте схему
wget "http://www.w3.org/2001/xml.xsd"
Создайте catalog.xml
<?xml version="1.0"?>
<!DOCTYPE catalog PUBLIC "-//OASIS//DTD XML Catalogs V1.0//EN"
"http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd">
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
<public publicId="http://www.w3.org/2001/xml.xsd"
uri="xml.xsd"/>
<system systemId="http://www.w3.org/2001/xml.xsd"
uri="xml.xsd"/>
<uri name="http://www.w3.org/2001/xml.xsd"
uri="xml.xsd"/>
</catalog>
Укажите lxml на каталог и выполните проверку
import os
import lxml
from lxml import etree
os.environ["XML_CATALOG_FILES"] = "catalog.xml"
print("Using lxml version {0}".format(lxml.__version__), end="\n\n")
xsd_tree = etree.parse("schema.xsd")
compiled_xsd = etree.XMLSchema(etree=xsd_tree)
src_tree = etree.parse("main.xml")
src_tree.xinclude()
if os.path.isfile("transform.xslt"):
print("Applying transformation from transform.xslt")
style_tree = etree.parse("transform.xslt")
xslt_runner = etree.XSLT(style_tree)
out_doc = xslt_runner(src_tree)
src_tree._setroot(out_doc.getroot())
print(etree.tostring(src_tree, pretty_print=True).decode())
if compiled_xsd.validate(src_tree):
print("XML is valid.")
else:
print("XML is invalid!")
for issue in compiled_xsd.error_log:
print(issue.message)
Такое сопоставление делает разрешение детерминированным и работает с современными версиями libxml2. Тот же каталог можно проверить через xmllint, например:
XML_CATALOG_FILES='catalog.xml' /home/lmc/Downloads/libxml2-v2.15.0/xmllint --noout --xinclude --schema schema.xsd main.xml
main.xml validates
Альтернатива: сослаться в схеме на локальную копию xml.xsd
Ещё один практичный вариант — положить рядом схему пространства имён XML и направить импорт на локальный файл. Скачайте один раз:
wget "http://www.w3.org/2001/xml.xsd"
После этого поменяйте импорт в schema.xsd на локальный путь:
<xs:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd"/>
С этой правкой xmllint и lxml успешно валидируют без доступа к сети.
Почему это важно
Проверка по схеме, зависящая от загрузки удалённых ресурсов, хрупка. Новая libxml2 ужесточила правила, и проекты, которые раньше работали, теперь падают, если схему пространства имён XML нельзя подтянуть по сети. Разрешение через каталог и локальные импорты убирают сетевую зависимость, дают одинаковое поведение в Linux, macOS и Windows и соответствуют подходу «безопасно по умолчанию».
Выводы
Если проверка падает с ошибкой о том, что xml:specialAttrs не удаётся разрешить, сделайте внешнее разрешение явным. Используйте XML‑каталог, который сопоставляет URI схемы пространства имён XML с локальным файлом, или импортируйте локальную копию напрямую из вашей XSD. Для проверки окружения выведите версии библиотек и протестируйте через xmllint; тот же сбой там означает, что это изменение на уровне libxml2, а не в коде приложения. Как только разрешение становится локальным и предсказуемым, XInclude, применение XSLT и проверка по схеме работают как ожидается.