How to parse invalid (bad / not well-formed) XML?
Asked Answered
C

4

33

Currently, I'm working on a feature that involves parsing XML that we receive from another product. I decided to run some tests against some actual customer data, and it looks like the other product is allowing input from users that should be considered invalid. Anyways, I still have to try and figure out a way to parse it. We're using javax.xml.parsers.DocumentBuilder and I'm getting an error on input that looks like the following.

<xml>
  ...
  <description>Example:Description:<THIS-IS-PART-OF-DESCRIPTION></description>
  ...
</xml>

As you can tell, the description has what appears to be an invalid tag inside of it (<THIS-IS-PART-OF-DESCRIPTION>). Now, this description tag is known to be a leaf tag and shouldn't have any nested tags inside of it. Regardless, this is still an issue and yields an exception on DocumentBuilder.parse(...)

I know this is invalid XML, but it's predictably invalid. Any ideas on a way to parse such input?

Carbamidine answered 26/6, 2017 at 17:22 Comment(4)
Invalid XML really isn't XML, though. Parsers exist which expect XML to be valid, and it's not a leap to expect that, either; it's not like DOM which can be entirely invalid.Mustard
From a design standpoint, it should be the provider's responsibility to correct malformed XML, and not the consumer's responsibility to handle malformed XML.Babel
The XML can't be tested for validity because it is not well formed. "Valid" means that the document conforms to a schema or DTD, but if a document isn't even well-formed XML then the question of validity cannot even be asked. The proper thing for your code to do is to reject the bad input. Silently ignoring such egregious errors is a recipe for worse bugs.Argosy
you can use shell script or interpreted language like perl to patch up the errors to make it valid.Electrocardiogram
R
45

That "XML" is worse than invalid – it's not well-formed; see Well Formed vs Valid XML.

An informal assessment of the predictability of the transgressions does not help. That textual data is not XML. No conformant XML tools or libraries can help you process it.

Options, most desirable first:

  1. Have the provider fix the problem on their end. Demand well-formed XML. (Technically the phrase well-formed XML is redundant but may be useful for emphasis.)

  2. Use a tolerant markup parser to cleanup the problem ahead of parsing as XML:

  3. Process the data as text manually using a text editor or programmatically using character/string functions. Doing this programmatically can range from tricky to impossible as what appears to be predictable often is not -- rule breaking is rarely bound by rules.

    • For invalid character errors, use regex to remove/replace invalid characters:

      • PHP: preg_replace('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $s);
      • Ruby: string.tr("^\u{0009}\u{000a}\u{000d}\u{0020}-\u{D7FF}\u{E000‌​}-\u{FFFD}", ' ')
      • JavaScript: inputStr.replace(/[^\x09\x0A\x0D\x20-\xFF\x85\xA0-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm, '')
    • For ampersands, use regex to replace matches with &amp;: credit: blhsin, demo

      &(?!(?:#\d+|#x[0-9a-f]+|\w+);)
      

Note that the above regular expressions won't take comments or CDATA sections into account.

Rosebud answered 26/6, 2017 at 17:44 Comment(3)
Yeah, I was thinking these might be the only options, but I wanted to see if there were any other solutions first. Thanks for the input.Carbamidine
Adressing the regular expression for ampersands: The following regex will also treat comments and CDATA sections correctly: /&(?!(?:#\d+|#x[0-9a-fA-F]+|\w+);)(?!(?:(?!<!\[CDATA\[|\]\]>).)*\]\]>)(?!(?:(?!<!--|-->).)*-->)/gToxemia
xmllint --recover --html --dropdtd --xmlout --format 2>/dev/null file (in a recent version) does what xmlstarlet format -R -H -D 2>/dev/null file does. Like xsltproc and xmlstarlet, xmllint relies on -- and is often distributed with -- libxml2. Note: xmllint --help in version 20913 lists more options than man xmllint.Demoniac
T
4

A standard XML parser will NEVER accept invalid XML, by design.

Your only option is to pre-process the input to remove the "predictably invalid" content, or wrap it in CDATA, prior to parsing it.

Trellis answered 26/6, 2017 at 17:37 Comment(1)
"A standard XML parser will NEVER accept invalid XML", nor will it accept supposed XML that isn't well formed.Argosy
C
4

The accepted answer is good advice, and contains very useful links.

I'd like to add that this, and many other cases of not-wellformed and/or DTD-invalid XML can be repaired using SGML, the ISO-standardized superset of HTML and XML. In your case, what works is to declare the bogus THIS-IS-PART-OF-DESCRIPTION element as SGML empty element and then use eg. the osx program (part of the OpenSP/OpenJade SGML package) to convert it to XML. For example, if you supply the following to osx

<!DOCTYPE xml [
  <!ELEMENT xml - - ANY>
  <!ELEMENT description - - ANY>
  <!ELEMENT THIS-IS-PART-OF-DESCRIPTION -  - EMPTY>
]>
<xml>
  <description>blah blah
    <THIS-IS-PART-OF-DESCRIPTION>
  </description>
</xml>

it will output well-formed XML for further processing with the XML tools of your choice.

Note, however, that your example snippet has another problem in that element names starting with the letters xml or XML or Xml etc. are reserved in XML, and won't be accepted by conforming XML parsers.

Calculation answered 31/5, 2018 at 9:24 Comment(0)
H
1

IMO these cases should be solved by using JSoup.

Below is a not-really answer for this specific case, but found this on the web (thanks to inuyasha82 on Coderwall). This code bit did inspire me for another similar problem while dealing with malformed XMLs, so I share it here.

Please do not edit what is below, as it is as it on the original website.

The XML format, requires to be valid a unique root element declared in the document.
So for example a valid xml is:

<root>
     <element>...</element>
     <element>...</element>
</root>

But if you have a document like:

<element>...</element>
<element>...</element>
<element>...</element>
<element>...</element>

This will be considered a malformed XML, so many xml parsers just throw an Exception complaining about no root element. Etc.

In this example there is a solution on how to solve that problem and succesfully parse the malformed xml above.

Basically what we will do is to add programmatically a root element.

So first of all you have to open the resource that contains your "malformed" xml (i. e. a file):

File file = new File(pathtofile);

Then open a FileInputStream:

FileInputStream fis = new FileInputStream(file);

If we try to parse this stream with any XML library at that point we will raise the malformed document Exception.

Now we create a list of InputStream objects with three lements:

  1. A ByteIputStream element that contains the string: <root>
  2. Our FileInputStream
  3. A ByteInputStream with the string: </root>

So the code is:

List<InputStream> streams = 
    Arrays.asList(
        new ByteArrayInputStream("<root>".getBytes()),
    fis,
    new ByteArrayInputStream("</root>".getBytes()));

Now using a SequenceInputStream, we create a container for the List created above:

InputStream cntr = 
new SequenceInputStream(Collections.enumeration(str));

Now we can use any XML Parser library, on the cntr, and it will be parsed without any problem. (Checked with Stax library);

Helio answered 29/5, 2018 at 8:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.