A razão pela qual escrevemos isto: somos a Geonode e vendemos proxies a pessoas que recolhem dados, e grande parte desses dados chega no formato XML — mapas de sites, feeds RSS e Atom, respostas SOAP, catálogos de produtos. A verdade é que uma falha de análise quase nunca é um problema de rede. Se o seu analisador de XML estiver a apresentar erros, exiba os primeiros 200 caracteres do que recebeu antes de alterar qualquer código. Em nove em cada dez casos, trata-se de uma página de erro HTML, e o analisador está a indicar corretamente que não lhe foi enviado XML.
A armadilha do erro: não lança uma exceção
O comportamento que apanha toda a gente à primeira.
Alimente DOMParser com XML malformado e não será lançada nenhuma exceção. O MDN é explícito: «o objeto XMLDocument devolvido conterá um nó <parsererror> que descreve o erro de análise», e o erro «também pode ser reportado na consola JavaScript do navegador».
Por isso, deve verificar:
const doc = parser.parseFromString(xmlString, "application/xml");
const errorNode = doc.querySelector("parsererror");
if (errorNode) {
throw new Error(`XML parse failed: ${errorNode.textContent}`);
}
Sem essa verificação, um documento malformado produz um objeto Document contendo uma mensagem de erro no local onde os seus dados deveriam estar. A sua chamada subsequente a querySelectorAll não devolve nada, e o sintoma parece ser um problema de seletor, em vez de uma falha de análise.
Envolva-o uma vez:
function parseXml(text) {
const doc = new DOMParser().parseFromString(text, "application/xml");
const err = doc.querySelector("parsererror");
if (err) {
throw new Error(
`XML parse failed: ${err.textContent.trim()}. ` +
`First 200 chars: ${text.slice(0, 200)}`
);
}
return doc;
}
O text.slice(0, 200) é a parte que poupa tempo. Um erro de análise indica que a entrada era inválida; os primeiros 200 caracteres indicam que se tratava de uma página de erro HTML.
No Node: Escolher uma biblioteca
O Node não possui um analisador XML integrado, pelo que esta é uma decisão que depende das dependências. Os números de downloads semanais dão uma ideia aproximada da adoção — estes dados provêm do registo npm, consultados em setembro de 2026.
| Biblioteca | Downloads semanais | Estilo |
|---|---|---|
sax |
| ~88,9 milhões | Streaming, baseado em eventos |
| fast-xml-parser
| ~85,0 milhões | XML para objetos JavaScript simples |
| @xmldom/xmldom
| ~48,7 milhões | Implementação DOM para o Node |
| xml2js
| ~44,5 milhões | Conversão de XML em objetos, APIs de callback e promise |
| xpath
| ~12,2 milhões | Consultas XPath, funciona em conjunto com o xmldom |
**fast-xml-parser
** é a opção padrão mais prática para a maioria dos trabalhos. Converte XML em objetos JavaScript comuns, o que significa que se navega com notação de ponto em vez de métodos DOM:
import { XMLParser } from "fast-xml-parser";
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
const obj = parser.parse(xmlString);
console.log(obj.rss.channel.item[0].title);
A desvantagem a ter em conta: a conversão em objetos implica uma perda de informação de uma forma específica. Um elemento que aparece uma vez torna-se um objeto; o mesmo elemento que aparece duas vezes torna-se uma matriz. Assim, channel.item
é uma matriz para um feed com três itens e um objeto para um feed com um — e o código que assume uma matriz deixa de funcionar no caso de um único item. A maioria das bibliotecas oferece uma opção para produzir sempre matrizes para elementos nomeados, e vale a pena ativá-la para tudo o que for iterar, antes que isso lhe cause problemas.
**@xmldom/xmldom
** fornece-lhe um DOM real no Node, o que é importante se quiser que o mesmo código funcione em ambos os ambientes ou se precisar de XPath. Combine-o com o pacote xpath
:
import { DOMParser } from "@xmldom/xmldom";
import xpath from "xpath";
const doc = new DOMParser().parseFromString(xmlString, "text/xml");
const titles = xpath.select("//item/title/text()", doc);
**sax
** é um analisador de streaming que emite eventos à medida que lê. É a solução para documentos demasiado grandes para caberem na memória — um índice de mapa do site com vários gigabytes, uma exportação em massa de um catálogo — onde uma abordagem DOM simplesmente não se adequa.
**xml2js
** é um pacote há muito estabelecido e amplamente utilizado. A sua API está um pouco desatualizada, mas é perfeitamente funcional, e existe uma grande quantidade de código que a utiliza.
Espaços de nomes: o que faz com que os documentos reais deixem de funcionar
A razão mais comum pela qual um seletor que funcionava deixa subitamente de encontrar nada.
Muitos formatos XML reais declaram espaços de nomes:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://example.com/</loc></url>
</urlset>
O facto de xmlns
colocar todos os elementos num espaço de nomes predefinido. Num analisador que reconhece espaços de nomes, <loc>
não é simplesmente loc
— é loc
no espaço de nomes «sitemaps», e um simples getElementsByTagName("loc")
pode não encontrar nada.
Três formas de lidar com isto, por ordem crescente de correção.
Utilize os métodos sensíveis ao namespace:
const NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
const locs = doc.getElementsByTagNameNS(NS, "loc");
Utilize um namespace curinga quando não se importar com qual deles:
const locs = doc.getElementsByTagNameNS("*", "loc");
Configure a sua biblioteca para ignorar os namespaces. A maioria das bibliotecas de mapeamento de objetos tem uma opção para remover prefixos de namespace, o que produz chaves simples do tipo loc
. É prático e irá fundir silenciosamente dois elementos genuinamente diferentes que por acaso partilham um nome local — aceitável para um mapa do site, mas perigoso para um documento que misture vocabulários.
Note-se que querySelector
comporta-se de forma diferente de getElementsByTagNameNS
neste contexto: os seletores CSS têm a sua própria sintaxe de namespace, que é pouco prática e raramente utilizada; por isso, para XML com namespace, os métodos NS
ou o XPath são mais fiáveis.
XPath em JavaScript
Disponível nos navegadores através de document.evaluate e no Node através do pacote xpath com xmldom.
const result = doc.evaluate(
"//item/title/text()",
doc,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < result.snapshotLength; i++) {
console.log(result.snapshotItem(i).nodeValue);
}
A API é suficientemente complexa para que a maioria das pessoas a utilize uma vez e depois se esqueça dela. O que faz com que valha a pena o esforço é o facto de o XPath expressar coisas que os seletores CSS não conseguem — correspondência com base no conteúdo de texto, navegação para antepassados e lógica posicional relativa a elementos irmãos.
Duas restrições. Os navegadores implementam o XPath 1.0, pelo que não são suportados matches(), lower-case() nem ends-with(). E os namespaces requerem uma função de resolução — o terceiro argumento — que mapeia prefixos para URIs de namespace. Passar null só funciona para documentos sem namespaces, o que exclui a maioria dos feeds reais.
Para documentos com namespace num navegador:
const resolver = prefix => ({ sm: "http://www.sitemaps.org/schemas/sitemap/0.9" }[prefix] || null);
const result = doc.evaluate("//sm:loc/text()", doc, resolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
Note que é o utilizador que define o prefixo — sm, neste caso — independentemente do que o documento utilize, porque o XPath 1.0 não tem o conceito de namespace predefinido.
Conversão de XML para JSON e o que se perde
O que as pessoas mais procuram, na verdade, e é importante compreender que a conversão não é isenta de perdas.
O XML e o JSON têm modelos de dados diferentes. O XML tem atributos, elementos, nós de texto, comentários, instruções de processamento, espaços de nomes e ordenação. O JSON tem objetos, matrizes, cadeias de caracteres, números, valores booleanos e nulos. Há quatro aspetos que não sobrevivem à conversão sem perdas.
Atributos versus elementos filhos. <item id="1"><name>x</name></item> tem um atributo e um elemento filho, e o JSON não faz distinção entre eles. As bibliotecas lidam com isto colocando um prefixo nas chaves dos atributos — normalmente @ ou $ — que o utilizador configura e depois tem de se lembrar:
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
// { item: { "@id": "1", name: "x" } }
Os elementos repetidos transformam-se em matrizes de forma inconsistente. Já abordado acima, mas vale a pena repetir, pois é o erro mais comum nesta área: uma ocorrência resulta num objeto, duas resultam numa matriz. Defina a opção «sempre matriz» da biblioteca para todos os elementos que pretenda iterar.
O conteúdo misto não tem uma representação clara. <p>Hello <b>world</b>!</p> intercala texto e elementos. Quando convertido num objeto, os fragmentos de texto e as suas posições relativas ao elemento filho são difíceis de representar, e a maioria das bibliotecas ou concatena o texto ou omite partes dele. Se o seu XML contiver marcação de prosa, a conversão para objeto é a abordagem errada — mantenha o DOM.
A ordem não é garantida. As chaves dos objetos JSON não têm uma ordem definida no modelo de dados, pelo que um documento em que a sequência dos elementos tenha significado perde essa informação. As matrizes preservam a ordem; os elementos irmãos com nomes diferentes, não.
E tudo se torna uma cadeia de caracteres, a menos que se especifique o contrário. O XML não tem tipos, pelo que <price>42.50</price> é texto. A maioria das bibliotecas oferece coerção numérica, o que é conveniente e transforma facilmente um código de produto com zeros à esquerda num número e uma cadeia de caracteres de versão num número de tipo float. Para tudo o que seja um identificador em vez de uma quantidade, desative a coerção.
A orientação prática: a conversão de objetos é adequada para XML em formato de dados — feeds, catálogos, configurações, respostas de API — onde os elementos são registos e campos. Mantenha um DOM para XML em formato de documento, onde a marcação está incorporada no texto e a estrutura tem significado.
Segurança: XXE e Injeção
Dois riscos distintos, ambos reais.
Processamento de entidades externas em XML. O XML pode declarar entidades que fazem referência a recursos externos, incluindo ficheiros locais e URLs de rede. Um analisador que as resolva pode ser levado a ler ficheiros do servidor ou a efetuar pedidos em nome de um atacante. Trata-se de uma classe de vulnerabilidade clássica e ainda comum.
A medida de mitigação consiste em desativar o processamento de entidades externas e de DTD em qualquer analisador que utilize. O DOMParser do navegador não resolve entidades externas, pelo que, por predefinição, o navegador é seguro. As bibliotecas Node variam, pelo que vale a pena verificar em vez de partir de pressupostos — se estiver a analisar XML proveniente de uma fonte não fiável no Node, confirme o tratamento de entidades do seu analisador antes de lançar o produto.
Injeção ao reinserir no DOM. O MDN alerta que parseFromString «é um “recipiente de injeção” e um vetor potencial para ataques XSS se a entrada vier de um atacante». A nuance é importante: com text/html, «os elementos<script> são marcados como não executáveis e os manipuladores de eventos não são chamados» — mas os scripts «serão executados se o documento analisado for posteriormente injetado no DOM visível».
Portanto, a análise é segura; a inserção do resultado não é. As recomendações da MDN consistem em passar objetos TrustedHTML em vez de cadeias de caracteres, impor tipos confiáveis através da diretiva CSP require-trusted-types-for e sanitizar com uma biblioteca como o DOMPurify através de um TrustedTypePolicy.
A regra é simples: nunca insira marcação analisada e não confiável no DOM ativo sem a sanitizar e dê preferência a textContent em vez de innerHTML quando precisar apenas do texto.
Padrões Práticos
Análise de um feed RSS ou Atom:
const doc = parseXml(await res.text());
const items = [...doc.getElementsByTagNameNS("*", "item")].map(item => ({
title: item.getElementsByTagNameNS("*", "title")[0]?.textContent?.trim(),
link: item.getElementsByTagNameNS("*", "link")[0]?.textContent?.trim(),
date: item.getElementsByTagNameNS("*", "pubDate")[0]?.textContent?.trim(),
}));
O namespace com caracteres curinga lida tanto com RSS como com Atom sem necessidade de ramificações, e o encadeamento opcional lida com feeds em que faltam campos — o que acontece na maioria deles.
Análise de um mapa do site, incluindo ficheiros de índice:
const doc = parseXml(xml);
const isIndex = doc.documentElement.localName === "sitemapindex";
const locs = [...doc.getElementsByTagNameNS("*", "loc")].map(n => n.textContent.trim());
// if isIndex, these are sitemap URLs to fetch; otherwise they are page URLs
Verificar o atributo ``localName`
no elementodocument é a forma fiável de distinguir os dois, uma vez que ambos contêm elementos ``<loc>
` e apenas o elemento envolvente difere.
Tratamento de um documento de grande dimensão — utilize um analisador de streaming em vez de construir um DOM:
import sax from "sax";
const stream = sax.createStream(true, { trim: true });
let current = null;
stream.on("opentag", node => { if (node.name === "loc") current = ""; });
stream.on("text", t => { if (current !== null) current += t; });
stream.on("closetag", name => { if (name === "loc") { emit(current); current = null; } });
Memória constante, independentemente do tamanho do documento, à custa de escrever uma pequena máquina de estados.
Perguntas frequentes
Como analiso XML em JavaScript?
Num navegador, utilize o analisador integrado «DOMParser»: new DOMParser().parseFromString(xml, "application/xml") devolve um objeto «Document» que pode consultar com métodos DOM. No Node não existe um analisador integrado, pelo que é necessário instalar um — fast-xml-parser para conversão em objeto, @xmldom/xmldom para um DOM verdadeiro.
Por que é que o DOMParser não lança uma exceção em caso de XML inválido?
Por design. Em vez de uma exceção, o documento devolvido contém um nó <parsererror> que descreve a falha. É necessário verificar isso explicitamente com doc.querySelector("parsererror"); caso contrário, um documento malformado produzirá silenciosamente resultados de consulta vazios.
O Node.js tem um analisador de XML integrado?
Não. Ao contrário do JSON, o XML requer uma dependência. As opções mais utilizadas são fast-xml-parser e xml2js para a conversão em objetos, @xmldom/xmldom para uma implementação DOM e sax para o streaming de documentos muito grandes.
Porque é que o meu seletor XML não encontra nada?
Normalmente, devido aos namespaces. Um documento que declare xmlns coloca todos os elementos nesse namespace, e um simples getElementsByTagName pode não corresponder. Utilize getElementsByTagNameNS com o URI do namespace, ou "*" como curinga, ou configure a sua biblioteca para ignorar namespaces.
Como utilizo o XPath com XML em JavaScript?
Nos navegadores, document.evaluate com um tipo XPathResult — suficientemente detalhado para ser envolvido uma vez. No Node, o pacote xpath em conjunto com @xmldom/xmldom. Note-se que os navegadores implementam apenas o XPath 1.0, e os documentos com namespace necessitam de uma função de resolução que mapeie prefixos para URIs.
A análise de XML em JavaScript representa um risco de segurança?
Existem dois riscos. O processamento de Entidades Externas XML pode fazer com que um analisador leia ficheiros locais ou envie pedidos — o DOMParser do navegador não resolve entidades externas, mas as bibliotecas do Node variam e devem ser verificadas. Além disso, inserir marcação analisada não confiável no DOM ativo pode executar scripts, por isso, é necessário sanitizar o conteúdo antes da inserção.
Como analiso um ficheiro XML muito grande?
Utilize um analisador de streaming, como o sax, que emite eventos à medida que lê, em vez de construir um documento na memória. Isto permite lidar com ficheiros de qualquer tamanho com uma utilização constante de memória, tendo como contrapartida a necessidade de escrever uma pequena máquina de estados para acompanhar a posição em que se encontra.
Por que razão o meu XML analisado, por vezes, resulta num array e, outras vezes, num objeto?
Porque as bibliotecas de mapeamento de objetos produzem um array apenas quando um elemento se repete. Um feed com três itens dá-lhe um array; o mesmo feed com um único item dá-lhe um objeto. A maioria das bibliotecas tem uma opção para produzir sempre arrays para elementos nomeados — ative-a para tudo o que itera.
Conclusão
O ambiente é que determina a maior parte disto. Num navegador, tem-se o DOMParser e não há dependências; no Node, escolhe-se uma biblioteca, e essa escolha determina a forma como se escreve tudo o que se segue.
Dois comportamentos são responsáveis pela maior parte do tempo que as pessoas perdem. O DOMParser reporta falhas com um nó parsererror em vez de uma exceção, pelo que um documento malformado produz resultados vazios que parecem um problema de seletor — verifique esse nó e registe os primeiros 200 caracteres da entrada, porque normalmente trata-se de uma página de erro HTML. Além disso, os namespaces impedem silenciosamente a pesquisa de nomes de tags simples precisamente nos documentos que mais se deseja analisar: mapas de sites, feeds e respostas SOAP declaram-nos todos.
Para além disso, adapte a ferramenta ao tamanho. Mapeamento de objetos para documentos comuns, um DOM real quando precisar de XPath ou de código partilhado entre o navegador e o servidor, e um analisador de streaming quando o ficheiro for demasiado grande para ser carregado na memória. E se a fonte não for fiável, verifique o tratamento de entidades externas do seu analisador Node antes de lançar o produto — essa tem sido uma classe de vulnerabilidade há duas décadas e continua a sê-lo.
