Co se děje
Atom feed, sitemap, XHTML, SOAP nebo ISDOC deklarují na kořeni výchozí jmenný prostor. Všechny elementy bez prefixu v něm pak leží, i když to v textu dokumentu není vidět:
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Novinky XML.cz</title>
<entry>
<title>JSON v PostgreSQL</title>
<link href="https://xml.cz/programovani/postgresql/"/>
</entry>
<entry>
<title>Billion laughs</title>
<link href="https://xml.cz/xxe/"/>
</entry>
</feed>
XPath ale nečte deklarace ze vstupního dokumentu. Podle XPath 1.0 má jméno bez prefixu ve výrazu
prázdný jmenný prostor. Krok entry tedy hledá element s lokálním jménem
entry a bez namespace. Takový v dokumentu není, a proto je výsledek prázdný. Nejde o chybu
parseru ani syntaxe dotazu, žádná hláška se neobjeví.
Chybně a správně
//entry/title //atom:entry/atom:title (prefix atom → http://www.w3.org/2005/Atom)
Prefix ve výrazu nemusí odpovídat prefixu v dokumentu, rozhoduje jen URI. Dokument žádný prefix
nemá, a přesto ho dotaz potřebuje. Atribut href zůstává bez prefixu
(atom:link/@href), protože výchozí jmenný prostor se na atributy nevztahuje.
Registrace prefixu v jednotlivých jazycích
Všechny ukázky pracují se souborem feed.xml výše a vypíšou „JSON v PostgreSQL“ a „Billion laughs“.
JavaScript v prohlížeči
const doc = new DOMParser().parseFromString(xml, "application/xml");
const resolver = prefix => ({ atom: "http://www.w3.org/2005/Atom" })[prefix] ?? null;
const r = doc.evaluate("//atom:entry/atom:title", doc, resolver,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < r.snapshotLength; i++) console.log(r.snapshotItem(i).textContent);
doc.evaluate("//atom:entry", doc, null, XPathResult.ANY_TYPE, null);
// NamespaceError: … '//atom:entry' contains unresolvable namespaces. C# / .NET
var doc = new XmlDocument();
doc.Load("feed.xml");
doc.SelectNodes("//entry/title")!.Count; // 0
var nsm = new XmlNamespaceManager(doc.NameTable);
nsm.AddNamespace("atom", "http://www.w3.org/2005/Atom");
foreach (XmlNode t in doc.SelectNodes("//atom:entry/atom:title", nsm)!)
Console.WriteLine(t.InnerText);
// LINQ to XML bez XPath: jméno = XNamespace + lokální jméno
XNamespace a = "http://www.w3.org/2005/Atom";
var hrefs = XDocument.Load("feed.xml")
.Descendants(a + "entry").Elements(a + "link").Attributes("href"); Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); // výchozí je false
Document doc = dbf.newDocumentBuilder().parse(new File("feed.xml"));
XPath xp = XPathFactory.newInstance().newXPath();
xp.setNamespaceContext(new NamespaceContext() {
public String getNamespaceURI(String prefix) {
return "atom".equals(prefix) ? "http://www.w3.org/2005/Atom" : XMLConstants.NULL_NS_URI;
}
public String getPrefix(String uri) { return null; }
public Iterator<String> getPrefixes(String uri) { return null; }
});
NodeList tituly = (NodeList) xp.evaluate("//atom:entry/atom:title", doc, XPathConstants.NODESET);
Výchozí DocumentBuilderFactory není namespace-aware. Pak //entry/title
dva elementy najde, ale prefixový dotaz fungovat nebude. Kód, který „funguje bez prefixu“, se
rozbije, jakmile někdo parser správně nastaví. Namespace-aware parser proto zapínej vždy.
Python
import xml.etree.ElementTree as ET # cizí vstup: defusedxml.ElementTree, stejné API
strom = ET.parse("feed.xml")
strom.findall(".//entry/title") # []
ns = {"atom": "http://www.w3.org/2005/Atom"}
for t in strom.findall(".//atom:entry/atom:title", ns):
print(t.text)
# Python 3.8+: klíč "" nastaví výchozí namespace pro jména bez prefixu
strom.findall(".//entry/title", {"": "http://www.w3.org/2005/Atom"})
# lxml (plné XPath 1.0): root.xpath("//atom:entry/atom:title", namespaces=ns) PHP
$doc = new DOMDocument();
$doc->load('feed.xml');
$xp = new DOMXPath($doc);
$xp->query('//entry/title')->length; // 0
$xp->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
foreach ($xp->query('//atom:entry/atom:title') as $t) echo $t->textContent, "\n";
$sx = simplexml_load_file('feed.xml');
$sx->registerXPathNamespace('a', 'http://www.w3.org/2005/Atom');
foreach ($sx->xpath('//a:entry/a:link/@href') as $href) echo $href, "\n"; PostgreSQL
SELECT xpath('//a:entry/a:title/text()', doc,
ARRAY[ARRAY['a', 'http://www.w3.org/2005/Atom']])
FROM feed; Více o XML v databázi popisuje kapitola JSON a XML v PostgreSQL.
Bez registrace prefixu
XPath 2.0 a novější mají zápisy, které prefix nepotřebují. Všechny tři vrátí oba titulky:
//Q{http://www.w3.org/2005/Atom}entry/Q{http://www.w3.org/2005/Atom}title //*[local-name()='entry']/*[local-name()='title'] *:entry a local-name() namespace ignorují. Vyberou i cizí element
stejného jména, třeba entry z rozšíření feedu. Hodí se pro diagnostiku a rychlý
dotaz, do produkčního kódu patří prefix nebo Q{URI}. Podmínka
[local-name()='entry' and namespace-uri()='http://www.w3.org/2005/Atom'] je
přesná i v XPath 1.0, jen dlouhá.
Procesory XPath 2.0+ mohou mít nastavený výchozí elementový namespace, se kterým
//entry najde elementy i bez prefixu. Je to nastavení procesoru, ne vlastnost
dokumentu. Tester na XML.cz ho v režimu XPath 3.1 přebírá z kořene dokumentu, takže tam
//entry/title výsledek vrátí. V režimu XPath 1.0 (prohlížeč) nevrátí nic.
Jak chybu poznat
- Podívej se na kořen dokumentu. Atribut
xmlns="…"bez dvojtečky znamená výchozí jmenný prostor. - Vyzkoušej
//*[local-name()='entry']. Pokud najde výsledky a//entryne, jde o tento problém. - Vypiš
namespace-uri()nalezeného elementu a porovnej ho znak po znaku s URI v kódu. Liší se i koncové lomítko,httpahttpsnebo velikost písmen. - Pokud prefixový dotaz hlásí nevyřešený prefix (
NamespaceError,XPST0081,Namespace prefix … is not defined), registrace v kódu chybí nebo se nepředala do volání.
| Prostředí | //entry/title bez prefixu | Řešení |
|---|---|---|
| Prohlížeč, document.evaluate (XPath 1.0) | nic | resolver prefixů (funkce) jako 3. argument |
| .NET, XmlDocument.SelectNodes | nic | XmlNamespaceManager; v LINQ to XML XNamespace |
| Java JAXP, setNamespaceAware(true) | nic | XPath.setNamespaceContext |
| Java JAXP, výchozí factory | najde (parser namespace ignoruje) | zapni setNamespaceAware(true), jinak se chování změní později |
| Python ElementTree | nic | slovník namespaces; klíč "" od Pythonu 3.8 |
| PHP DOMXPath / SimpleXML | nic | registerNamespace / registerXPathNamespace |
| PostgreSQL xpath() | nic | pole dvojic prefix–URI jako 3. argument |
| XPath 2.0+ (Saxon, BaseX…) | podle nastavení výchozího elementového namespace | prefix, *:entry nebo Q{URI}entry |
Neodstraňuj jmenný prostor z dokumentu
Častá rada „smaž xmlns a dotaz začne fungovat“ mění data. Elementy pak mají jiné
rozšířené jméno, dokument přestane odpovídat schématu a mohou splynout elementy z různých
slovníků. Kdy je to přesto nutné, popisuje návod odstranění
jmenného prostoru.