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 и проверка по схеме работают как ожидается.