21

As I have been looking through the latest StackExchange data dump, it seems like a non-compliant XML serializer was used. There are numerous escape sequences that are simply invalid XML such as &#x1E or even &#x00. Here you can see the escape sequence in question near the beginning of the selected line:

Sample of x1E

Here is that same PostHistory record (from 3dprinting) in the April 2024 dump where these characters are not present: Sample of the same record in April 2024 dump

These issues break most XML parsers.

Further, if we ignore the invalid escape sequences, there is even an escape sequence for a character that is not valid UTF-8: Sample of xD835

This character has no representation in the Unicode code page. This breaks any attempts to serialize this using APIs such as C#'s Encoding.UTF8.GetBytes.

6
  • I'm a little confused about #x1E; being invalid. I recognize it's a non-printing character, and when I try to use it it in a browser's XML parser, I do indeed get an error "xmlParseCharRef: invalid xmlChar value 30". But the XML specification for Character References only says it has to refer to a valid Char, where Char is defined as "any Unicode character, excluding the surrogate blocks", and although document authors are "encouraged to avoid" characters such as #x1E;, it's not clear to me where it's defined as an error.
    – Jeremy
    Commented Aug 14 at 18:39
  • 6
    Oh, I see: XML 1.0 forbids them but XML 1.1 allows them and almost everything else except for the null character and surrogates, see this Stack Overflow answer. Not allowing them seems silly to me, but those feelings are irrelevant: we definitely don't want the data dumps to have characters that most real-world XML parsers can't handle!
    – Jeremy
    Commented Aug 14 at 18:46
  • 7
    @Jeremy And for context the xml files do claim themselves to be 1.0 in the header: <?xml version="1.0" encoding="utf-8"?>
    – Maxwell175
    Commented Aug 14 at 20:01
  • Maybe this late change is now causing issues if the comment block isn't added in a way that preserves XML peculiarities. It did see a few iteration before it was "correct"
    – rene Mod
    Commented Aug 14 at 21:07
  • 6
    @rene that was my first guess but I don’t think it fits: these illegal characters were stripped in the previous version of dump, so it isn’t just a matter of them being reencoded incorrectly. Additionally, I’ve noticed a few random changes in the data (like Answers now having a ViewCount of 0 instead of the field being omitted and GUIDs being capitalized differently) that indicate that something early in the pipeline had changed since the last release.
    – Jeremy
    Commented Aug 14 at 21:14
  • 1

3 Answers 3

6

That is very unfortunate. Until a better solution appears I've crafted a CleaningStreamReader (C#) that replaces any illegal character sequence with the space sequence so that at least the XmlReader can process the whole file.

The implementation really needs some StateMachine for cases where the characters are split across two reads. But that is for vNext and when we agree this is something that can work. Alternatively the FileStream itself can be cleaned but then you have similar problems but at the UTF8 Encoding for up to 4 bytes.

public class CleaningStreamReader:StreamReader
{
    public CleaningStreamReader(Stream fs, Encoding enc):base(fs,enc) {
    }
    
    public override int Read (char[] buffer, int index, int count)
    {
        var charcount = base.Read(buffer, index,count);
     
        for( var i = index; i < index+charcount; i++) {
            // this really needs a state machine because we might not have all chars we need in this current buffer
            // check if &#x01; till &#x1F; exists, replace 1F with 20
            if ((i + 5 < index + charcount) && buffer[i] == '&' && buffer[i+1] == '#' && buffer[i+2] == 'x' && buffer[i+5] == ';')
            {
               if ((buffer[i+3] == '0' || buffer[i+3] == '1' ) && (buffer[i+4] >= '0' && buffer[i+4] <= 'F')) {
                  buffer[i+3] = '2'; buffer[i+4] = '0';
               }
            }
        }
        return charcount;
    }
}

And here is how you use that class:

var filename = @"\some\path\3dprinting.stackexchange.com\posthistory.xml";
 
 var nodes = new Dictionary<XmlNodeType, int>();
 
 using(var fs = File.OpenRead(filename)) 
 using(var sr = new CleaningStreamReader(fs, Encoding.UTF8))
 using(var xr = XmlReader.Create(sr)) {
     while(xr.Read()) {
         if (nodes.Keys.Contains(xr.NodeType)) {
            nodes[xr.NodeType] =  nodes[xr.NodeType] +1 ;
         } else {
            nodes.Add(xr.NodeType, 1); 
         }
      
         if (xr.NodeType == XmlNodeType.Element) {
         
             bool ha = xr.MoveToFirstAttribute();
             while(ha) {
                 if (nodes.Keys.Contains(xr.NodeType)) {
                     nodes[xr.NodeType] =  nodes[xr.NodeType] +1 ;
                 } else {
                     nodes.Add(xr.NodeType, 1); 
                 }
                 ha = xr.MoveToNextAttribute();
             }
         }
     }
 }
 nodes.Dump();

This is what statistics of NodeTypes will look like when you processed the 3d printing PostHistory.xml successfully:

Element 52802 Attribute 435280

1
2

I was working on the dump recently and I faced the similar issue, I found the default XML python parser fails to parse, but there is a working solution with lxml below with using recover=True:

from lxml import etree

def skip_exceptions(it):
    while True:
      try:
          yield next(it)
      except StopIteration:
          break
      except Exception as e:
          print('Skipping iteration because of exception {}'.format(e))    

if __name__ == "__main__":
    xml_file = "<your_xml_filepath>.xml"
    f = open(xml_file, "rb")
    xmlit = iter(etree.iterparse(f, events=('start', 'end'), recover=True))
    for (event, elem) in xmlit:
          do what you want...
2

This change has been made. Please see this post for more details on other fixes and changes related to data dumps.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .