Avatar billede chrisrj Forsker
14. august 2023 - 12:56 Der er 32 kommentarer

xml parsing driller - subtree-tag "forsvinder"!?

Hejsa

Jeg sidder og parser en kompliceret xml fil, og støder på det underlige problem, at den nægter at læse subtree-tag'et (her, case "Company":).

Alle de almindelige tags læses fint.

Hvordan kommer man uden om det problem?

Min kode (køres i .net 6):
public Header ParseHeader(XmlReader reader)
        {
            Header header = new Header();

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    string elementName = reader.Name; // Get the fully qualified name, including namespace

                    switch (reader.LocalName)
                    {
                        case "AuditFileVersion":
                            header.AuditFileVersion = reader.ReadElementContentAsString();
                            break;
                        case "AuditFileCountry":
                            header.AuditFileCountry = reader.ReadElementContentAsString();
                            break;
                        case "AuditFileRegion":
                            header.AuditFileRegion = reader.ReadElementContentAsString();
                            break;
                        case "AuditFileDateCreated":
                            header.AuditFileDateCreated = reader.ReadElementContentAsString();
                            break;
                        case "SoftwareCompanyName":
                            header.SoftwareCompanyName = reader.ReadElementContentAsString();
                            break;
                        case "SoftwareID":
                            header.SoftwareID = reader.ReadElementContentAsString();
                            break;
                        case "SoftwareVersion":
                            header.SoftwareVersion = reader.ReadElementContentAsString();
                            break;
                        case "Company":
                            header.Company = ParseCompany(reader.ReadSubtree());
                            break;
                        case "DefaultCurrencyCode":
                            header.DefaultCurrencyCode = reader.ReadElementContentAsString();
                            break;
                        case "SelectionCriteria":
                            header.SelectionCriteria = ParseSelectionCriteria(reader.ReadSubtree());
                            break;
                        case "TaxAccountingBasis":
                            header.TaxAccountingBasis = reader.ReadElementContentAsString();
                            break;
                        case "TaxEntity":
                            header.TaxEntity = reader.ReadElementContentAsString();
                            break;
                        case "UserID":
                            header.UserID = reader.ReadElementContentAsString();
                            break;
                    }
                }
            }

            return header;
        }


XML fil (starten af den):
<n1:Header>
<n1:AuditFileVersion>1.0</n1:AuditFileVersion>
<n1:AuditFileCountry>DK</n1:AuditFileCountry>
<n1:AuditFileRegion>DK-81</n1:AuditFileRegion>
<n1:AuditFileDateCreated>2022-10-03</n1:AuditFileDateCreated>
<n1:SoftwareCompanyName>MitRegnskabsSystem ApS</n1:SoftwareCompanyName>
<n1:SoftwareID>MitRegnskabsSystem</n1:SoftwareID>
<n1:SoftwareVersion>2.22.2</n1:SoftwareVersion>
<n1:Company>
<n1:RegistrationNumber>12345678</n1:RegistrationNumber>
<n1:Name>Selskabet ApS</n1:Name>
...
</n1:Company>
...
</n1:Header>
Avatar billede arne_v Ekspert
14. august 2023 - 14:52 #1
Inden vi bruger for meget tid på at finde ud af hvad der sker så er spørgsmål: hvor stor er den XML fil?

Hvis den er mindre end 100 MB vil jeg foreslå at droppe XmlReader og skifte til XmlDocument.
Avatar billede chrisrj Forsker
14. august 2023 - 14:54 #2
Dette er en lille test fil. Men de rigtige vil være i GB størrelsen.
Avatar billede arne_v Ekspert
14. august 2023 - 15:06 #3
OK. XmlReader.

Som tommelfingerregel bruger et XmlDocument 3-5 gange så meget memory som disk filen. Så en 5 GB fil kræver 15-25 GB RAM og det er lidt heftigt.

Jeg prøver om jeg kan få eksemplet parset.
Avatar billede chrisrj Forsker
14. august 2023 - 15:08 #4
Ja, det var også min konklusion. :)

Awesome, tak! :)
Avatar billede arne_v Ekspert
14. august 2023 - 15:27 #5
<n1:Header xmlns:n1="http://eksperten.dk/spm1042338">
    <n1:AuditFileVersion>1.0</n1:AuditFileVersion>
    <n1:AuditFileCountry>DK</n1:AuditFileCountry>
    <n1:AuditFileRegion>DK-81</n1:AuditFileRegion>
    <n1:AuditFileDateCreated>2022-10-03</n1:AuditFileDateCreated>
    <n1:SoftwareCompanyName>MitRegnskabsSystem ApS</n1:SoftwareCompanyName>
    <n1:SoftwareID>MitRegnskabsSystem</n1:SoftwareID>
    <n1:SoftwareVersion>2.22.2</n1:SoftwareVersion>
    <n1:Company>
        <n1:RegistrationNumber>12345678</n1:RegistrationNumber>
        <n1:Name>Selskabet ApS</n1:Name>
    </n1:Company>
</n1:Header>


using System;
using System.IO;
using System.Xml;

namespace BigXml
{
    public class Program
    {
        public static void ParseCompany(XmlReader xr)
        {
            while(xr.Read())
            {
                if(xr.NodeType == XmlNodeType.Element)
                {
                    if(xr.LocalName == "Company")
                    {
                        // nothing
                    }
                    else
                    {
                        Console.WriteLine("ParseCompany : {0} = {1}", xr.Name, xr.ReadElementContentAsString());
                    }
                }
            }
        }
        public static void ParseHeader(XmlReader xr)
        {
            while(xr.Read())
            {
                if(xr.NodeType == XmlNodeType.Element)
                {
                    if(xr.LocalName == "Header")
                    {
                        // nothing
                    }
                    else if(xr.LocalName == "Company")
                    {
                        ParseCompany(xr.ReadSubtree());
                    }
                    else
                    {
                        Console.WriteLine("ParseHeader : {0} = {1}", xr.Name, xr.ReadElementContentAsString());
                    }
                }
            }
        }
        public static void Main(string[] args)
        {
            using(StreamReader sr = new StreamReader(@"\Work\big.xml"))
            {
                XmlReader xr = new XmlTextReader(sr);
                ParseHeader(xr);
            }
            Console.ReadKey();
        }
    }
}


ParseHeader : n1:AuditFileVersion = 1.0
ParseHeader : n1:AuditFileCountry = DK
ParseHeader : n1:AuditFileRegion = DK-81
ParseHeader : n1:AuditFileDateCreated = 2022-10-03
ParseHeader : n1:SoftwareCompanyName = MitRegnskabsSystem ApS
ParseHeader : n1:SoftwareID = MitRegnskabsSystem
ParseHeader : n1:SoftwareVersion = 2.22.2
ParseCompany : n1:RegistrationNumber = 12345678
ParseCompany : n1:Name = Selskabet ApS


Det eneste jeg skulle gøre var at skippe element med elementer - det med // nothing markerede.
Avatar billede chrisrj Forsker
14. august 2023 - 15:36 #6
Øhm, ikke forstået?

Siger du, at det er et enten/eller issue??

Altså enten kan jeg hente data, eller også kan jeg detektere subtrees?

Det lyder uhensigtsmæssigt, i og med der er MANGE subtrees, heraf en del med samme navne. :-/
Avatar billede arne_v Ekspert
14. august 2023 - 15:51 #7
Slet ikke.

Du kan sagtens bruge både .ReadElementContentAsString() og .ReadSubtree() - det gør min kode.

Men jeg kunne konstatere at det var meget vigtigt ikke at komme til at kalde .ReadElementContentAsString() på noget som indeholder sub elementer (et tree).

Det var det eneste jeg måtte sørge for.

Og derfor:

        public static void ParseCompany(XmlReader xr)
        {
            while(xr.Read())
            {
                if(xr.NodeType == XmlNodeType.Element)
                {
                    if(xr.LocalName == "Company")
                    {
                        // nothing
                    }
                    else
...
            }
        }
        public static void ParseHeader(XmlReader xr)
        {
            while(xr.Read())
            {
                if(xr.NodeType == XmlNodeType.Element)
                {
                    if(xr.LocalName == "Header")
                    {
                        // nothing
                    }
                    else
...
                }
            }
        }
Avatar billede chrisrj Forsker
14. august 2023 - 16:08 #8
Ah, nu forstår jeg! :D

Det giver mening.

Det gør mit resultat så bare ikke. :P

Min kode vil stadig ikke gå ind i company funktionen. :-/

Gør det en forskel at ParseHeader ikke er rod parser funktionen?


Her er den opdaterede funktion:
public Header ParseHeader(XmlReader reader)
        {
            Header header = new Header();

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.LocalName == "Header")
                    {
                        // nothing
                    }
                    else if (reader.LocalName == "Company")
                    {
                        ParseCompany(reader.ReadSubtree());
                    }
                    else if (reader.LocalName == "SelectionCriteria")
                    {
                        ParseSelectionCriteria(reader.ReadSubtree());
                    }
                    else
                    {
                        switch (reader.LocalName)
                        {
                            case "AuditFileVersion":
                                header.AuditFileVersion = reader.ReadElementContentAsString();
                                break;
                            case "AuditFileCountry":
                                header.AuditFileCountry = reader.ReadElementContentAsString();
                                break;
                            case "AuditFileRegion":
                                header.AuditFileRegion = reader.ReadElementContentAsString();
                                break;
                            case "AuditFileDateCreated":
                                header.AuditFileDateCreated = reader.ReadElementContentAsString();
                                break;
                            case "SoftwareCompanyName":
                                header.SoftwareCompanyName = reader.ReadElementContentAsString();
                                break;
                            case "SoftwareID":
                                header.SoftwareID = reader.ReadElementContentAsString();
                                break;
                            case "SoftwareVersion":
                                header.SoftwareVersion = reader.ReadElementContentAsString();
                                break;
                            case "DefaultCurrencyCode":
                                header.DefaultCurrencyCode = reader.ReadElementContentAsString();
                                break;
                            case "TaxAccountingBasis":
                                header.TaxAccountingBasis = reader.ReadElementContentAsString();
                                break;
                            case "TaxEntity":
                                header.TaxEntity = reader.ReadElementContentAsString();
                                break;
                            case "UserID":
                                header.UserID = reader.ReadElementContentAsString();
                                break;
                        }
                       
                        //Console.WriteLine("ParseHeader : {0} = {1}", reader.Name, reader.ReadElementContentAsString());
                    }
                }
            }

            return header;
        }
Avatar billede arne_v Ekspert
14. august 2023 - 16:22 #9
Jeg tror at det er tid for at indsætte de sædvanelige 2 dusin debug statements.

:-)

                else if (reader.LocalName == "Company")
                    {
Console.WriteLine("before ParseCompany);
                        ParseCompany(reader.ReadSubtree());
Console.WriteLine("after ParseCompany);
                    }

og

      public static void ParseCompany(XmlReader reader)
        {
            while(reader.Read())
            {
Console.WriteLine("{0} {1}", reader.NodeType, reader.Name

for at se hvad pokker der sker.

(man kunne naturligvis også bruge debugger, men jeg er gammeldags)
Avatar billede chrisrj Forsker
14. august 2023 - 16:30 #10
Hehe, vi bruger db logger funktion. :P Det er sorteret fra, så chefen ikke bliver hys. XD

Rest asured, der ER logning. Både i ParseHeader og ParseCompany - og det er kun ParseHeader der kommer ud.
Avatar billede chrisrj Forsker
14. august 2023 - 16:47 #11
Interessant.

Nu prøvede jeg at bruge dine funktioner, og fik denne fejl fra "rodparser" funktionen:
ReadElementContentAs() methods cannot be called on an element that has child elements.
Den kommer "sjovt nok" lige efter "n1:RegistrationNumber "

Er vi montros ude i et async issue? Vi streamer filen fra en server...
Avatar billede arne_v Ekspert
14. august 2023 - 16:52 #12
Ja. Men hvad sker der?

Bliver ParseCompany kaldt?

Hvis ja:
* hvad sker der i den? Læser den noget?

Hvis nej:
* bliver noget efter company processet?

Der må jo være en eller anden forklaring.

Er der en catch som sluger exceptions uden at logge?

Er XML valid?

Matcher stavning af "Company" i XML og C#?
Avatar billede chrisrj Forsker
14. august 2023 - 17:05 #13
Ok.

ParseCompany bliver IKKE kaldt.

Ja, når jeg bruger min kode fortsætter i header funktionen.

Nej, når jeg bruger din kode, stopper den ved Company


Nej, alle exptions jeg laver, logges.

Er XML valid? Jeg må indrømme, at jeg ikke har tjekket den, men siden den kommer fra SKAT, så...

Matcher stavning af "Company" i XML og C#? Yup.
Avatar billede chrisrj Forsker
14. august 2023 - 17:08 #14
...nu har jeg så lige tjekket filen på xmlvalidation.com - ingen fejl.
Avatar billede arne_v Ekspert
14. august 2023 - 17:08 #15
re #11)

Så ParseCompany læser n1:RegistrationNumber men ikke n1:Namen for:

    <n1:Company>
        <n1:RegistrationNumber>12345678</n1:RegistrationNumber>
        <n1:Name>Selskabet ApS</n1:Name>
    </n1:Company>

?

Mystisk.

Streaming burde ikke gøre en forskel. XML parser bør vente indtil input er klart.

Men et eller andet mystisk foregår der.
Avatar billede arne_v Ekspert
14. august 2023 - 17:12 #16
Men måske skulle du bruge en "sladre stream".

En LogStreamReader der:
* arver fra passende interface/klasse
* har et member der er den rigtige StreamReader
* har metoder som læser fra den rigtige StreamReader og logger det læste

Så kan man se hvad der parses på.
Avatar billede chrisrj Forsker
14. august 2023 - 17:12 #17
Nej, ParseHeader læser n1:RegistrationNumber. ParseCompany  gør intet.
Avatar billede arne_v Ekspert
14. august 2023 - 17:14 #18
Se det er interessant!!

n1:RegistrationNumber er inden i n1:Company ikke?

så den if der skal kalde ParseCompany bliver ikke aktiveret og derfor er det ParseHeader som processer n1:RegistrationNumber og ikke ParseHeader.
Avatar billede chrisrj Forsker
14. august 2023 - 17:16 #19
Netop, nu er du med! :D
Avatar billede arne_v Ekspert
14. august 2023 - 17:20 #20
Er "Company" stavet ens i kode og XML? Med samme kapitalisering?
Avatar billede chrisrj Forsker
14. august 2023 - 17:24 #21
Yup.
Avatar billede chrisrj Forsker
14. august 2023 - 17:30 #22
Problemet er det samme med alle subtrees
Avatar billede chrisrj Forsker
14. august 2023 - 17:37 #23
Nu skete der noget!!

Jeg fjernede
XmlReaderSettings settings = new XmlReaderSettings
                    {
                        IgnoreWhitespace = true
                    };

fra stream kaldet
Avatar billede chrisrj Forsker
14. august 2023 - 17:38 #24
Nu ser loggen således ud:
XMLParsing.ParseXML: Getting stream
XMLParsing.ParseXML: Reading stream
Start XMLParsing.ParseHeader
XMLParsing.ParseHeader: n1:AuditFileVersion = 1.0
XMLParsing.ParseHeader: n1:AuditFileCountry = DK
XMLParsing.ParseHeader: n1:AuditFileRegion = DK-81
XMLParsing.ParseHeader: n1:AuditFileDateCreated = 2022-10-03
XMLParsing.ParseHeader: n1:SoftwareCompanyName = MitRegnskabsSystem ApS
XMLParsing.ParseHeader: n1:SoftwareID = MitRegnskabsSystem
XMLParsing.ParseHeader: n1:SoftwareVersion = 2.22.2
XMLParsing.ParseHeader: before ParseCompany
Start XMLParsing.ParseCompany
XMLParsing.ParseCompany: n1:RegistrationNumber = 12345678
XMLParsing.ParseCompany: n1:Name = Selskabet ApS
XMLParsing.ParseXML: XML error: 'Element' is an invalid XmlNodeType.
Avatar billede chrisrj Forsker
14. august 2023 - 17:42 #25
Kører jeg "hybrid versionen" hvor jeg har brugt din kode i mine funktioner, ser loggen således ud:
XMLParsing.ParseXML: Getting stream
XMLParsing.ParseXML: Reading stream
Start XMLParsing.ParseHeader
XMLParsing.ParseHeader: reader.LocalName == Header
XMLParsing.ParseHeader: reader.LocalName == AuditFileVersion
XMLParsing.ParseHeader: reader.LocalName == AuditFileCountry
XMLParsing.ParseHeader: reader.LocalName == AuditFileRegion
XMLParsing.ParseHeader: reader.LocalName == AuditFileDateCreated
XMLParsing.ParseHeader: reader.LocalName == SoftwareCompanyName
XMLParsing.ParseHeader: reader.LocalName == SoftwareID
XMLParsing.ParseHeader: reader.LocalName == SoftwareVersion
XMLParsing.ParseHeader: reader.LocalName == Company
Start XMLParsing.ParseCompany
XMLParsing.ParseXML: Unknown error: The ReadElementContentAsString method is not supported on node type Whitespace. Line 14, position 15.
Avatar billede arne_v Ekspert
14. august 2023 - 18:08 #26
OK.

Det sidste skylde vel at med:

IgnoreWhitespace = true

er:

<a>
    <b>xxx</b>
</a>

a element med nested b element med nested text xxx

med:

IgnoreWhitespace = false

er det a element med:
* text element med "\n    "
* b element med ...
* text element "\n"
Avatar billede arne_v Ekspert
14. august 2023 - 18:11 #27
IgnoreWhitespace = true

virker som det rigtige, men spørgsmålet er hvorfor den if ikke trigges når den option er sat.

Jeg forstår det ikke.
Avatar billede arne_v Ekspert
14. august 2023 - 18:45 #28
Med IgnoreWhitespace = true hvad indeholder reader.Name og reader.LocalName ved Company element?

Evt. reader.Name.Replace(' ', '_') og reader.LocalName.Replace(' ', '_') for at få mellemrum synlige.
Avatar billede chrisrj Forsker
15. august 2023 - 13:14 #29
Ok, nu har jeg eksperimenteret lidt, og nu kan jeg godt løbe funktionerne i gennem, yay. :)

Jeg blev dog nødt til at fjerne de der settings med IgnoreWhitespace = true helt og holdent.


Data bliver også gemt fint, så jeg vil egentligt erklære problemet løst. :)


Sådan ser den færdige funktion ud:
public Header ParseHeader(XmlReader reader)
        {
            Header header = new Header();

            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    string elmLocalName = reader.LocalName;

                    if (elmLocalName == "Header")
                    {
                       
                    }
                    else if (elmLocalName == "Company")
                    {
                        header.Company = ParseCompany(reader.ReadSubtree());
                    }
                    else if (elmLocalName == "SelectionCriteria")
                    {
                        header.SelectionCriteria = ParseSelectionCriteria(reader.ReadSubtree());
                    }
                    else
                    {
                        switch (elmLocalName)
                        {
                            case "AuditFileVersion":
                                header.AuditFileVersion = reader.ReadElementContentAsString();
                                break;
                            case "AuditFileCountry":
                                header.AuditFileCountry = reader.ReadElementContentAsString();
                                break;
                            case "AuditFileRegion":
                                header.AuditFileRegion = reader.ReadElementContentAsString();
                                break;
                            case "AuditFileDateCreated":
                                header.AuditFileDateCreated = reader.ReadElementContentAsString();
                                break;
                            case "SoftwareCompanyName":
                                header.SoftwareCompanyName = reader.ReadElementContentAsString();
                                break;
                            case "SoftwareID":
                                header.SoftwareID = reader.ReadElementContentAsString();
                                break;
                            case "SoftwareVersion":
                                header.SoftwareVersion = reader.ReadElementContentAsString();
                                break;
                            case "DefaultCurrencyCode":
                                header.DefaultCurrencyCode = reader.ReadElementContentAsString();
                                break;
                            case "TaxAccountingBasis":
                                header.TaxAccountingBasis = reader.ReadElementContentAsString();
                                break;
                            case "TaxEntity":
                                header.TaxEntity = reader.ReadElementContentAsString();
                                break;
                            case "UserID":
                                header.UserID = reader.ReadElementContentAsString();
                                break;
                        }
                    }
                }
            }

            return header;
        }
Avatar billede arne_v Ekspert
16. august 2023 - 01:36 #30
Godt at det virker.

Men jeg undrer mig stadig over hvad problemet var med IgnoreWhitespace = true.
Avatar billede chrisrj Forsker
16. august 2023 - 09:13 #31
Jaw jaw. :)

Jamen jeg er helt enig. :D Jeg prøvede dit replace forslag og med/uden IgnoreWhitespace = true. Den eneste forskel jeg så, var om den så Company eller ej. Ingen underscores blev sat nogen steder... Wierd.
Avatar billede arne_v Ekspert
16. august 2023 - 17:19 #32
crazy weird
Avatar billede Ny bruger Nybegynder

Din løsning...

Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.

Loading billede Opret Preview

Log ind eller opret profil

Hov!

For at kunne deltage på Computerworld Eksperten skal du være logget ind.

Det er heldigvis nemt at oprette en bruger: Det tager to minutter og du kan vælge at bruge enten e-mail, Facebook eller Google som login.

Du kan også logge ind via nedenstående tjenester