Skip to main content

xml-cmpr

Compare des documents XML produits par deux chaînes de production différentes et produit un rapport des différences, fichier par fichier.

L'outil parcourt la source A, cherche dans la source B le fichier de même chemin relatif (les fichiers sans correspondance sont ignorés), compare les deux documents sur leur forme canonique, et écrit un rapport Markdown et/ou JSON dans le répertoire de sortie.

Chaque source est soit un répertoire, soit une archive ZIP — les deux peuvent se mélanger.

Installation

Le paquet est publié sur PyPI sous le nom xml-cmpr, qui est aussi celui de la commande installée :

uv tool install xml-cmpr      # ou : pipx install xml-cmpr
xml-cmpr --help

Python 3.13 ou 3.14 est requis (le socle de saxonche, qui apporte le moteur XSLT).

Le paquet lui-même est en Python pur, mais saxonche est publié sans source.

L'installation n'aboutit donc que sur les plateformes pour lesquelles il existe la librairie a été publiée :

Système Architectures
Linux (glibc 2.24 ou plus, « manylinux ») x86_64, aarch64
macOS (11 ou plus) x86_64, arm64
Windows x86_64

Utilisation

uv run xml-cmpr SOURCE_A SOURCE_B REPERTOIRE_SORTIE

où SOURCE_A et SOURCE_B sont chacun un répertoire ou une archive ZIP :

uv run xml-cmpr chaine_a/ chaine_b/ rapport/
uv run xml-cmpr chaine_a.zip chaine_b.zip rapport/
uv run xml-cmpr chaine_a.zip chaine_b/ rapport/

Produit REPERTOIRE_SORTIE/rapport.md et REPERTOIRE_SORTIE/rapport.json (--format choisit les formats écrits) :

# Rapport de comparaison XML

- **Source A** : `tests/fixtures/a`
- **Source B** : `tests/fixtures/b`
- **Date** : 2026-08-18 11:45
- Fichiers analysés : **5** · identiques : **1** · différents : **3** · en erreur : **1**
- Ignorés (absents de la source B) : **1**

## Fichiers différents

### `factures/facture_001.xml` — 1 différence

| Chemin | Type | A | B |
|---|---|---|---|
| `/Facture/Lignes/Ligne[2]` | élément ajouté en B | — | `<Ligne id="2">deux` |

Options

Option Effet
--pattern GLOB motif des fichiers de A (défaut : *.xml)
--no-recursive ne pas descendre dans les sous-répertoires
--keep-whitespace ne pas normaliser les espaces des nœuds texte
--exclude FICHIER fichier contenant des expressions XPath à exclure de la comparaison
--max-diffs-per-file N différences détaillées par fichier (défaut : 50, 0 = illimité)
--no-size-limit extraire une archive ZIP quels que soient sa taille et son nombre d'entrées. Par défaut les limites sont fixées  à 1 Go ou de 100 000 fichiers.
--format FORMATS formats du rapport : md, json (défaut : md,json)
--fail-on-diff code de sortie 1 si au moins une différence
--fail-on-error code de sortie 3 si au moins un fichier est illisible
-v, --verbose progression sur stderr

Codes de sortie : 0 succès · 1 différences détectées (avec --fail-on-diff) · 2 erreur d'usage (source introuvable ou illisible, sortie inutilisable, XPath d'exclusion invalide, format inconnu) · 3 fichiers en erreur (avec --fail-on-error).

Un fichier XML malformé n'interrompt pas la campagne : il est reporté comme fichier en erreur, et sans --fail-on-error la comparaison se termine, avec un code retour 0.

Le rapport JSON

rapport.json contient le même bilan que le Markdown, sous une forme directement consommable par un autre traitement :

{
  "summary": {
    "source_a": "chaine_a",
    "source_b": "chaine_b",
    "generated_at": "2026-08-18T11:45:12+02:00",
    "analyzed": 5,
    "identical": 1,
    "different": 3,
    "errors": 1,
    "skipped": 1
  },
  "files": [
    {
      "path": "factures/facture_001.xml",
      "status": "different",
      "differences": [
        {
          "path": "/Facture/Lignes/Ligne[2]",
          "kind": "element_added",
          "label": "élément ajouté en B",
          "a": null,
          "b": "<Ligne id=\"2\">deux"
        }
      ]
    },
    { "path": "identique.xml", "status": "identique" },
    { "path": "malforme.xml", "status": "erreur", "error": "SXXP0003 …" }
  ],
  "skipped": ["absent_de_b.xml"]
}
  • generated_at date la campagne, en ISO 8601 à la seconde, heure locale avec son décalage ; c'est la même date que celle du rapport Markdown.
  • files liste tous les fichiers analysés, un par chemin relatif ; status vaut identique, different ou erreur.
  • differences n'apparaît que pour un fichier different ; error que pour un fichier erreur ; truncated (nombre de différences non détaillées) que si --max-diffs-per-file a coupé la liste.
  • kind est la nature de la différence, stable et utilisable en code — element_added, element_removed, element_renamed, attribute_added, attribute_removed, attribute_changed, text_changed, text_added, text_removed — et label en est le libellé français affiché au rapport Markdown.
  • a et b sont les valeurs de part et d'autre, null quand la valeur n'existe pas du côté correspondant. Pour element_renamed, ce sont les deux noms : le nom local quand l'élément n'a pas d'espace de noms (Date), {uri}local sinon ({urn:facture}Date).
  • skipped liste les fichiers de A sans homologue dans B.

Sources en archive ZIP

Une source ZIP est extraite dans un répertoire temporaire, supprimé à la fin de la campagne ; la comparaison, les options et le rapport sont ensuite exactement les mêmes que pour un répertoire (le rapport nomme l'archive, pas le répertoire temporaire).

Une archive dont le contenu décompressé dépasse 1 Gio, ou qui compte plus de 100 000 entrées (fichiers et répertoires), est refusée (code 2), avant toute extraction : les bornes valent pour chaque archive séparément, et ne concernent pas une source répertoire, qui n'est pas copiée. --no-size-limit lève les deux limites.

Utiliser la comparaison depuis Python

Le pipeline se décompose en trois étapes indépendantes : analyse, formatage, écriture. Le formatage retourne une chaîne qui n'est pas directement écrite sur le disque, elle peut donc être utilisée dans une réponse HTTP ou vers un autre traitement.

from pathlib import Path

from xml_cmpr import ReportFormat, render, run_comparison, write_report

summary = run_comparison(Path("chaine_a"), Path("chaine_b"))  # analyse -> RunSummary
corps = render(summary, ReportFormat.JSON)  # formatage -> str
# ... ou render(summary, "md") pour le Markdown

write_report(summary, Path("rapport"), "json")  # écriture -> Path (facultatif)
Étape Fonction Rend
analyse run_comparison(source_a, source_b, **options) RunSummary
formatage render(summary, "md"|"json") str
summary.as_dict() dict (JSON sans passer par la chaîne)
écriture write_report(summary, dossier, format) Path du fichier écrit
write_reports(summary, dossier, formats) list[Path]

run_comparison accepte les mêmes options que le CLI (pattern, recursive, keep_whitespace, exclude_xpaths, max_diffs_per_file), et label_a / label_b pour choisir les chemins affichés au rapport. Pour une source ZIP, as_directory(source) est le context manager qui extrait l'archive. Il applique la même bornes que le CLI, MAX_EXTRACTED_SIZE (1 Gio) et MAX_ENTRIES (100 000) ; size_limit= (en octets) et entry_limit= les changent une à une, et seul un None explicite lève la borne concernée :

from pathlib import Path

from xml_cmpr import as_directory, run_comparison

with as_directory(Path("chaine_a.zip")) as dir_a:
    summary = run_comparison(dir_a, Path("chaine_b"), label_a=Path("chaine_a.zip"))

Les exceptions

Les exceptions de xml_cmpr sont dérivées d'Exception :

Exception Levée par Cause
ExcludeError run_comparison, Canonicalizer un XPath d'exclusion est invalide
ArchiveError as_directory source introuvable, archive illisible, chemin d'entrée suspect
ArchiveSizeError as_directory sous-classe d'ArchiveError : archive au-delà de size_limit ou d'entry_limit (attributs size, limit, et measure : "bytes" ou "entries")
CanonicalError Canonicalizer.load un document est illisible ou malformé

Exclure des éléments ou attributs de la comparaison

--exclude FICHIER prend un fichier texte listant un XPath par ligne. Une ligne dont le premier caractère non blanc est # est un commentaire ; ailleurs dans la ligne, le # fait partie du XPath — il n'y a pas de commentaire de fin de ligne, justement pour que les URI de namespace terminées par # restent écrivables.

# ignorer l'horodatage de génération et l'identifiant technique
//DateGeneration
//Facture/@idTechnique
//Q{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description

Chaque élément ou attribut sélectionné par l'un de ces XPath, dans A comme dans B, est retiré de la forme canonique avant le calcul de l'empreinte : il ne compte ni pour le test d'identité, ni dans le diff. Un élément trouvé dans 1 XPath exclut tout son sous-arbre.

Le XPath est évalué par Saxon directement sur le document source, sans les préfixes de namespace déclarés dans le document. Pour un nom sans espace de noms, écrire //DateGeneration ; pour un nom avec espace de noms, utiliser la notation Q{uri}local, par ex. //Q{urn:facture}DateGeneration.

Ce que « identique » veut dire

La comparaison porte sur une forme canonique produite par Saxon (normalize.xsl). Ne sont pas des différences :

  • l'indentation et les retours à la ligne (texte normalisé) ;
  • l'ordre des attributs (triés par nom) ;
  • les préfixes de namespace (les noms sont comparés par URI, en notation de Clark) ;
  • les commentaires et instructions de traitement, où qu'ils soient ;
  • la déclaration XML, l'encodage, les sections CDATA.

Commentaires et instructions de traitement

Ils ne sont jamais comparés, et aucune option ne les réintègre. Ni ceux placés dans un élément, ni ceux du prologue — la bannière de génération qu'une chaîne écrit avant l'élément racine, la PI <?xml-stylesheet?> qui désigne une feuille de style.

Le contenu mixte

Quand un élément contient à la fois du texte et des éléments, le texte n'est pas agrégé : chaque nœud texte garde sa place parmi les enfants, et se désigne au rapport par le segment #text (numéroté quand il y en a plusieurs).

A : <p>Voir la <ref>note</ref> ci-dessous.</p>
B : <p>Voir la ci-dessous.<ref>note</ref></p>
Chemin Type A B
/p/#text[1] texte modifié Voir la Voir la ci-dessous.
/p/#text[2] texte absent de B ci-dessous. —

Les deux documents sont considérés comme différents.

Licence

Distribué sous licence MIT — © 2026 Frédéric Laurent.

Metadata

Release files for xml-cmpr 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xml-cmpr 0.2.1
File Size Uploaded
xml_cmpr-0.2.1.tar.gz 40.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xml-cmpr 0.2.1
File Interpreter ABI Platform
xml_cmpr-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.0 kB

Release files / xml_cmpr-0.2.1.tar.gz

Download URL xml_cmpr-0.2.1.tar.gz
Size 40.8 kB
Tags Source
SHA-256 checksum
How to use checksums
9d66546e5607bda267b4e92c62000fdb165edc65ec380cd5bedb8667423f985a
BLAKE2b-256 checksum
How to use checksums
5c9eb979f879ee77d693098cd7a84e85750e34fdaf5d9773616a863f91e2ee21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / xml_cmpr-0.2.1-py3-none-any.whl

Download URL xml_cmpr-0.2.1-py3-none-any.whl
Size 28.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d53f80a001d16e2e6b80f7c3f54e38287cfbd184c038a62aab74f3d0f26c178e
BLAKE2b-256 checksum
How to use checksums
780a53c11bb03b8aa4ec11cf4dd6fee682f533c579c912eb65be2418891393a9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page