beakerchi
beakerchi

Reputation: 117

Parse XML with Python resolving an external ENTITY reference

In my S1000D xml, it specifies a DOCTYPE with a reference to a public URL that contains references to a number of other files that contain all the valid character entities. I've used xml.etree.ElementTree and lxml to try to parse it and get a parse error with both indicating:

undefined entity −: line 82, column 652

Even though − is a valid entity according to the ENTITY Reference specfied.

The xml top is as follow:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dmodule [
<!ENTITY % ISOEntities PUBLIC 'ISO 8879-1986//ENTITIES ISO Character Entities 20030531//EN//XML' 'http://www.s1000d.org/S1000D_4-1/ent/ISOEntities'>
%ISOEntities;]>

If you go out and get http://www.s1000d.org/S1000D_4-1/ent/ISOEntities, it will include 20 other ent files with one called iso-tech.ent which contains the line:

<!ENTITY minus "&#x2212;"> <!-- MINUS SIGN -->

in line 82 of the xml file near column 652 is the following: ....Refer to 70&minus;41....

How can I run a python script to parse this file without get the undefined entity?

Sorry I don't want to specify parser.entity['minus'] = chr(2212) for example. I did that for a quick fix but there are many character entity references. I would like the parser to check Entity reference that is specified in the xml.

I'm surprised but I've gone around the sun and back and haven't found how to do this (or maybe I have but couldn't follow it). if I update my xml file and add <!ENTITY minus "&#x2212;"> It won't fail, so It's not the xml.

It fails on the parse. Here's code I use for ElementTree

 fl = os.path.join(pth, fn)
 try:
     root = ET.parse(fl)
 except ParseError as p:
     print("ParseError : ", p)

Here's the code I use for lxml

fl = os.path.join(pth, fn)
try:
    parser = etree.XMLParser(load_dtd=True, resolve_entities=True)
    root = etree.parse(fl, parser=parser)
except etree.XMLSyntaxError as pe:
    print("lxml XMLSyntaxError: ", pe)

I would like the parser to load the ENTITY reference so that it knows that − and all the other character entities specified in all the files are valid entity characters.

Thank you so much for your advice and help.

Upvotes: 4

Views: 5772

Answers (1)

Daniel Haley
Daniel Haley

Reputation: 52888

I'm going to answer for lxml. No reason to consider ElementTree if you can use lxml.

I think the piece you're missing is no_network=False in the XMLParser; it's True by default.

Example...

XML Input (test.xml)

<!DOCTYPE doc [
<!ENTITY % ISOEntities PUBLIC 'ISO 8879-1986//ENTITIES ISO Character Entities 20030531//EN//XML' 'http://www.s1000d.org/S1000D_4-1/ent/ISOEntities'>
%ISOEntities;]>
<doc>
    <test>Here's a test of minus: &minus;</test>
</doc>

Python

from lxml import etree

parser = etree.XMLParser(load_dtd=True,
                         no_network=False)

tree = etree.parse("test.xml", parser=parser)

etree.dump(tree.getroot())

Output

<doc>
    <test>Here's a test of minus: −</test>
</doc>

If you wanted the entity reference retained, add resolve_entities=False to the XMLParser.


Also, instead of going out to an external location to resolve the parameter entity, consider setting up an XML Catalog. This will let you resolve public and/or system identifiers to local versions.

Example using same XML input above...

XML Catalog ("catalog.xml" in the directory "catalog test" (space used in directory name for testing))

<!DOCTYPE catalog PUBLIC "-//OASIS//DTD XML Catalogs V1.1//EN" "http://www.oasis-open.org/committees/entity/release/1.1/catalog.dtd">
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
    <!-- The path in @uri is relative to this file (catalog.xml). -->
    <uri name="http://www.s1000d.org/S1000D_4-1/ent/ISOEntities" uri="./ents/ISOEntities_stackoverflow.ent"/>
</catalog>

Entity File ("ISOEntities_stackoverflow.ent" in the directory "catalog test/ents". Changed the value to "BAM!" for testing)

<!ENTITY minus "BAM!">

Python (Changed no_network to True for additional evidence that the local version of http://www.s1000d.org/S1000D_4-1/ent/ISOEntities is being used.)

import os
from urllib.request import pathname2url
from lxml import etree

# The XML_CATALOG_FILES environment variable is used by libxml2 (which is used by lxml).
# See http://xmlsoft.org/catalog.html.
try:
    xcf_env = os.environ['XML_CATALOG_FILES']
except KeyError:
    # Path to catalog must be a url.
    catalog_path = f"file:{pathname2url(os.path.join(os.getcwd(), 'catalog test/catalog.xml'))}"
    # Temporarily set the environment variable.
    os.environ['XML_CATALOG_FILES'] = catalog_path

parser = etree.XMLParser(load_dtd=True,
                         no_network=True)

tree = etree.parse("test.xml", parser=parser)

etree.dump(tree.getroot())

Output

<doc>
    <test>Here's a test of minus: BAM!</test>
</doc>

Upvotes: 7

Related Questions