Visionneuse XML

Arborescence pliable et source, côte à côte.

Entrée
ArborescenceCliquez sur un nœud pour le replier

La structure du document apparaît ici dès que vous collez quelque chose.

En attenteCollez un document pour le vérifier. La validation se fait à la frappe.

Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.

Collez un document ci-dessus et il apparaît deux fois : la source à gauche avec coloration syntaxique, numéros de ligne et repliage, et un arbre pliable à droite construit à partir de la même analyse. Tous les nœuds démarrent dépliés ; cliquez sur un élément pour le replier avec tout ce qu’il contient. Les deux volets viennent d’un seul balayage, donc l’arbre et la liste d’erreurs décrivent toujours le même document.

Une vue en arbre justifie sa place quand ce n’est pas vous qui avez écrit le document. Une réponse d’API d’un fournisseur dont la documentation est un PDF, une configuration de 4 000 lignes laissée par une équipe précédente, un export censé contenir un enregistrement et qui en contient peut-être deux cents. La question n’est en général pas où se trouve l’erreur de syntaxe, mais quelle forme a cette chose et où vit la valeur qu’il vous faut.

Il valide pendant que vous lisez, en signalant chaque problème de bonne formation avec une ligne, une colonne et une correction suggérée au lieu de s’arrêter au premier. Et rien n’est envoyé : l’analyseur, l’arbre et l’éditeur tournent tous dans cet onglet.

Ce que montre l’arbre

Chaque élément est une ligne : le nom de balise tel qu’il est écrit, préfixe compris, suivi de ses attributs sous forme de paires nom="valeur". Un élément sans enfant est marqué « vide », vous distinguez donc <status/> d’un nœud que vous avez replié. Le texte apparaît en feuille sous son élément, tronqué à 200 caractères, parce qu’un arbre sert à la structure et qu’un blob base64 de 40 Ko affiché en entier l’enterrerait. Les commentaires apparaissent comme une ligne de repère.

Deux choses n’apparaissent délibérément pas : les sections CDATA et les instructions de traitement. Pour celles-là, lisez le volet source ; il montre le document exactement tel que vous l’avez collé et fait toujours autorité. L’arbre est du vrai DOM, un nœud pliable par élément, ce qui rend le repliage instantané et le texte sélectionnable. C’est aussi ce que cela coûte : un document de plusieurs dizaines de milliers d’éléments met plus de temps à construire l’arbre qu’à analyser le XML.

Ce à quoi un arbre est bon, et ce à quoi il est mauvais

Un arbre répond aux questions de forme plus vite que le défilement ne le fera jamais. Repliez tout un niveau plus bas et les sections de premier niveau tiennent sur un écran. Repliez un Header SOAP et le Body remonte en haut du volet. Regardez les lignes répétées sous un conteneur et vous savez si la réponse contient un enregistrement ou plusieurs, ce qui fait la différence entre un consommateur qui marche en test et un qui perd des données en production. Il est mauvais sur trois points, et le dire est plus utile que de prétendre le contraire.

  • Éditer. L’arbre est régénéré à partir de votre saisie à chaque frappe : il n’y a rien où taper. Modifiez dans le volet source et regardez l’arbre se reconstruire.
  • Comparer. Deux arbres côte à côte sont une moins bonne façon de trouver une différence qu’un diff qui normalise d’abord la mise en forme des deux côtés.
  • Extraire. « Tous les prix de toutes les lignes » est une expression XPath, pas un exercice de défilement.

Imbrication profonde, et pourquoi les préfixes sont atténués

Le XML avec espaces de noms est difficile à lire pour une raison qui n’a rien à voir avec la complexité des espaces de noms. Dans soap:Body et ns1:PlaceOrder, le préfixe est un échafaudage : il dit à quel vocabulaire le nom appartient, et une fois que vous le savez, c’est du bruit sur chaque ligne suivante. À huit niveaux d’indentation, une bonne part des caractères à l’écran sont des préfixes et des deux-points.

L’éditeur affiche donc le préfixe et son deux-points à opacité réduite face au nom local, et soap:Body se lit comme Body avec un marqueur accroché. Rien n’est caché ni modifié ; le texte reste sélectionnable et se copie intact. L’exception est une déclaration : dans xmlns:soap, le préfixe est la charge utile plutôt que l’échafaudage, il garde donc toute son intensité. Un préfixe utilisé sans avoir jamais été lié est signalé comme une erreur, avec la déclaration à ajouter, ce qui est le résultat habituel d’un fragment copié depuis un document plus grand.

Trouver une valeur

Pour une chaîne littérale, placez le curseur dans le volet source et appuyez sur Ctrl-F, ou Cmd-F sur un Mac. Le panneau de recherche de l’éditeur s’ouvre en haut, avec surlignage des occurrences et champ de remplacement, et il cherche dans la source, qui contient le texte complet de chaque nœud, y compris les parties que l’arbre tronque.

Pour une question de structure, utilisez XPath. //order[status="failed"]/@id répond en une étape à ce que le défilement met vingt étapes à donner, et le testeur XPath de ce site évalue XPath 1.0 sur votre document avec les préfixes qu’il y récolte. Un détail explique la plupart des « mon expression ne renvoie rien » : XPath 1.0 n’a pas d’espace de noms par défaut. Si la racine déclare xmlns="urn:example:orders", alors //order ne correspond à rien, car //order désigne un élément order sans espace de noms. Liez un préfixe et écrivez //o:order, ou repliez-vous sur //*[local-name()="order"].

Parcourir un document en code

Lire un document à la main est en général une étape vers l’écriture du code qui le lira tous les jours. Ces exemples parcourent un document et impriment sa structure, l’équivalent programmatique de l’arbre ci-dessus, et chacun analyse de façon sûre, car les valeurs par défaut de Java et de Python résolvent les entités externes.

// Print the shape of a document: element paths with a count, which is
// usually more useful than the tree itself when you are writing a consumer.
function outline(source) {
  const doc = new DOMParser().parseFromString(source, 'application/xml');
  const error = doc.querySelector('parsererror');
  if (error) throw new Error(error.textContent.trim());

  const counts = new Map();
  const walk = (el, path) => {
    const here = path + '/' + el.nodeName;
    counts.set(here, (counts.get(here) ?? 0) + 1);
    for (const child of el.children) walk(child, here);
  };
  walk(doc.documentElement, '');

  for (const [path, n] of [...counts].sort()) {
    console.log(n.toString().padStart(6), path);
  }
}

// nodeName keeps the prefix as written. For namespace-aware work use
// el.localName with el.namespaceURI, since the prefix is arbitrary and the
// URI is what identifies the vocabulary.
# iterparse reads the document as a stream and lets you drop each element
# once you are done with it, so memory stays flat on files far larger than a
# browser will open.
from defusedxml.ElementTree import iterparse
from collections import Counter

counts = Counter()
stack = []

for event, el in iterparse('document.xml', events=('start', 'end')):
    if event == 'start':
        stack.append(el.tag)
        counts['/'.join(stack)] += 1
    else:
        stack.pop()
        el.clear()          # release the subtree; this is the whole point

for path, n in sorted(counts.items()):
    print(f'{n:6d}  {path}')

# ElementTree reports tags as '{namespace-uri}local' rather than 'prefix:local'
# because the prefix is arbitrary. Split on '}' when you want the local name.
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;

public final class Outline {

    public static void main(String[] args) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);                 // off by default
        dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
        dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        dbf.setXIncludeAware(false);

        Document doc = dbf.newDocumentBuilder().parse(new java.io.File(args[0]));
        print(doc.getDocumentElement(), 0);
    }

    static void print(Node node, int depth) {
        if (node.getNodeType() != Node.ELEMENT_NODE) return;
        Element el = (Element) node;

        StringBuilder line = new StringBuilder("  ".repeat(depth));
        line.append(el.getNodeName());
        NamedNodeMap attrs = el.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            Node a = attrs.item(i);
            line.append(' ').append(a.getNodeName())
                .append("=\"").append(a.getNodeValue()).append('"');
        }
        System.out.println(line);

        NodeList kids = el.getChildNodes();
        for (int i = 0; i < kids.getLength(); i++) print(kids.item(i), depth + 1);
    }
}
using System.Xml;
using System.Xml.Linq;

// XDocument.Load uses a reader with DtdProcessing.Prohibit by default, so
// external entities are never fetched. Construct the reader yourself when
// you want that guarantee stated in the code rather than in the docs.
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
    MaxCharactersInDocument = 20L * 1024 * 1024,
};

using var reader = XmlReader.Create("document.xml", settings);
var doc = XDocument.Load(reader);

// One line per distinct element path, with how often it occurs.
var paths = doc.Descendants()
    .GroupBy(e => string.Join(
        "/",
        e.AncestorsAndSelf().Reverse().Select(a => a.Name.LocalName)))
    .OrderBy(g => g.Key);

foreach (var group in paths)
{
    Console.WriteLine(group.Count().ToString().PadLeft(6) + "  " + group.Key);
}
# xmllint has an interactive tree browser almost nobody knows about. It gives
# you ls, cd, cat, du and pwd over the document, with XPath as the path
# syntax: the terminal equivalent of the tree on this page.
xmllint --nonet --shell document.xml
#   / > du                      show the subtree shape
#   / > cd //order[1]           navigate by XPath
#   / > ls                      list children of the current node
#   / > cat status              print a node
#   / > exit

# Non-interactive: tag counts, a rough outline of an unfamiliar document.
xmllint --nonet --format document.xml |
  grep -o '<[a-zA-Z_][^ />]*' |
  sort | uniq -c | sort -rn | head -30

# One value, straight out:
xmllint --nonet --xpath 'string(//order[1]/status)' document.xml

Le plan par grep dans l’exemple shell est une astuce de comptage, pas une analyse : il comptera volontiers un nom de balise apparaissant dans un commentaire ou une section CDATA. Tous les autres exemples construisent d’abord un arbre, ce qui fait la différence entre une réponse approximative et une réponse juste.

Questions fréquentes

Mon XML est-il envoyé pour être affiché ?

Non. L’analyseur, l’arbre et l’éditeur sont du JavaScript qui tourne dans cet onglet. Pas de composant serveur, pas d’analytics ayant accès à l’éditeur, pas de script tiers. Ouvrez l’onglet Réseau et collez un document : la page charge ses propres ressources une fois, puis se tait.

Cette vérification compte ici parce que consulter, c’est précisément ce qu’on fait avec les données des autres. Le document que l’on colle dans une visionneuse est en général une réponse d’API en production ou un export réel, avec noms, numéros de compte ou jeton dans un en-tête. Plusieurs outils bien placés sur cette recherche l’envoient à un serveur, et l’un d’eux rend publics par défaut les documents enregistrés.

À quoi sert vraiment une vue en arbre ?

À répondre à des questions de structure : quels éléments se répètent, jusqu’où va l’imbrication, dans quelle branche vit une valeur, si un élément optionnel est présent dans cette réponse. Ce sont des choses difficiles à voir dans du texte indenté et immédiates dans un arbre.

Le flux qui se rentabilise : repliez tout un niveau sous la racine, lisez les noms de sections, dépliez celle qui vous intéresse, ignorez les 3 000 autres lignes. Si la question porte plutôt sur la syntaxe, c’est le volet source et la liste d’erreurs qu’il vous faut.

Puis-je modifier le XML dans l’arbre ?

Non. L’arbre est régénéré à partir de votre saisie au fil de la frappe : il n’y a rien de persistant à modifier. Faites les changements dans le volet source, à gauche.

C’est une limite délibérée. Un éditeur d’arbre doit décider, à chaque modification, quoi faire des espaces, du contenu mixte, du placement des commentaires et de l’ordre des attributs ; les réponses prudentes le rendent pénible, et les réponses commodes réécrivent en silence des parties du document que vous n’avez pas touchées.

Comment trouver une valeur précise ?

Pour une chaîne littérale, placez le curseur dans le volet source et appuyez sur Ctrl-F, ou Cmd-F sur un Mac. Le panneau de recherche s’ouvre en haut avec surlignage, et il cherche dans le texte complet plutôt que dans le texte tronqué qu’affiche l’arbre.

Pour tout ce qui est structurel, utilisez XPath : //order[status="failed"]/@id répond en une étape. Si une expression ne renvoie rien alors que l’élément est visiblement là, soupçonnez un espace de noms par défaut : XPath 1.0 n’en a pas, et //order ne correspondra à un élément de urn:example:orders qu’une fois un préfixe lié et //o:order écrit.

Pourquoi mon navigateur affiche-t-il « Cette page contient les erreurs suivantes » au lieu du fichier ?

C’est la visionneuse XML intégrée de Chrome ou de Firefox qui vous dit que le document n’est pas bien formé. Toutes deux s’arrêtent à la première erreur et n’affichent le document que jusque-là. Le message vient de libxml2 ou d’Expat et est formulé pour celui qui a écrit l’analyseur : « Opening and ending tag mismatch », « Document is empty ».

Collez le document ici et vous obtenez tous les problèmes d’un coup, avec ligne, colonne et suggestion, plus un arbre pour la partie qui a été analysée. Les causes fréquentes : une esperluette brute, un téléchargement tronqué, une marque d’ordre d’octets avant la déclaration XML, et une page d’erreur HTML servie avec un type de contenu XML.

Quelle taille de fichier peut-il ouvrir ?

Jusqu’à 20 Mo, et en pratique l’arbre devient lourd avant l’analyse. Balayer un document de 1 Mo prend environ 110 millisecondes ; construire un nœud pliable par élément prend plus de temps dès qu’on atteint des dizaines de milliers d’éléments.

Le plafond existe parce que rien n’est envoyé : le document et son arbre vivent tous deux dans la mémoire de cet onglet. Pour des fichiers plus gros, travaillez en local avec quelque chose qui traite en flux, comme xmllint --shell pour naviguer ou iterparse en Python pour extraire.

Outils associés

Pour aller plus loin