简体   繁体   中英

Complex Conversion XML to Dictionary (Swift / iOS)

I'm racking my brain how to convert this parsed xml into arrays or dictionaries. the xml tags are not helpful because the labels are generic and there are ~10 headers. I might be able to do something based on the order of the labels. any ideas?

NSXMLParser Method Code:

class MyXMLParserDelegate: NSObject, NSXMLParserDelegate {

@objc func parserDidStartDocument(parser: NSXMLParser) {
    print("parserDidStartDocument")
}

@objc func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
    print("didStartElement       --> \(elementName)")
}

@objc func parser(parser: NSXMLParser, foundCharacters string: String) {
    print("foundCharacters       --> \(string)")
}

@objc func parser(parser: NSXMLParser, didEndElement elementName: String,
                  namespaceURI: String?, qualifiedName qName: String?) {
    print("didEndElement         --> \(elementName)")
}

@objc func parser(parser: NSXMLParser, didStartMappingPrefix prefix: String,
                  toURI namespaceURI: String) {
    print("didStartMappingPrefix --> Prefix: \(prefix) toURI: \(namespaceURI)")
}

@objc func parser(parser: NSXMLParser, didEndMappingPrefix prefix: String) {
    print("didEndMappingPrefix   --> Prefix: \(prefix)")
}

@objc func parserDidEndDocument(parser: NSXMLParser) {
    //reload table with array
    print("parserDidEndDocument")
}
}

Sample results of XML parsing using NSXMLParser methods:

<result>
 <header>
    <col>
      <label>Tree Name</label>
    </col>
    <col>
      <label>Num Levels</label>
    </col>
    <col>
      <label>Defaults Weight</label>
    </col>
    <col>
      <label>Name</label>
    </col>
    <col>
      <label>Abbrev</label>
    </col>
    <col>
      <label>Level</label>
    </col>
    <col>
      <label>Full Name</label>
    </col>
  </header>
  <body>
    <row>
      <col>Cost Center 1</col>
      <col>2</col>
      <col>5</col>
      <col>Miami Dolphins Front Office</col>
      <col/>
      <col>0</col>
      <col/>
    </row>
    <row>
      <col>Cost Center 1</col>
      <col>2</col>
      <col>5</col>
      <col>Accounts Receivable</col>
      <col>A/R</col>
      <col>1</col>
      <col>Accounts Receivable</col>
    </row>
    <row>
      <col>Cost Center 1</col>
      <col>2</col>
      <col>5</col>
      <col>06</col>
      <col>06</col>
      <col>1</col>
      <col>06</col>
    </row>
    <row>
      <col>Cost Center 2</col>
      <col>3</col>
      <col>5</col>
      <col>Cost Center 2</col>
      <col/>
      <col>0</col>
      <col/>
    </row>
    <row>
      <col>Cost Center 2</col>
      <col>3</col>
      <col>5</col>
      <col>test2</col>
      <col/>
      <col>1</col>
      <col>test2</col>
    </row>
    <row>
      <col>Cost Center 2</col>
      <col>3</col>
      <col>5</col>
      <col>test</col>
      <col/>
      <col>1</col>
      <col>test</col>
    </row>
    <row>
      <col>Cost Center 3</col>
      <col>3</col>
      <col>5</col>
      <col>Cost Center 3</col>
      <col/>
      <col>0</col>
      <col/>
    </row>
    <row>
      <col>Cost Center 3</col>
      <col>3</col>
      <col>5</col>
      <col>test</col>
      <col/>
      <col>1</col>
      <col>test</col>
    </row>
  </body>
  <footer/>
</result>

parserDidStartDocument

didStartElement --> result

foundCharacters -->

didStartElement --> header

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Tree Name

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Num Levels

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Defaults Weight

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Name

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Abbrev

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Level

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters -->

didStartElement --> label

foundCharacters --> Full Name

didEndElement --> label

foundCharacters -->

didEndElement --> col

foundCharacters -->

didEndElement --> header

foundCharacters -->

didStartElement --> body

foundCharacters -->

didStartElement --> row

foundCharacters -->

didStartElement --> col

foundCharacters --> Cost Center 1

didEndElement --> col

foundCharacters -->

didStartElement --> col

foundCharacters --> 2

didEndElement --> col

foundCharacters -->

...

Take a look at https://github.com/nicklockwood/XMLDictionary library.
Library has written in Obj-C but it is not a problem to use it in Swift .

A simple way to parse and generate XML on iOS and Mac OS. Converts an XML file to an NSDictionary which can then be easily traversed using the standard Cocoa keyPath mechanism. Can also output the contents of any dictionary as XML.

I needed something like this for testing generated XML but had to roll my own. I created a nested tree of elements, each containing information about the xml tag.

fileprivate class XmlToDictionaryParserDelegate: NSObject, XMLParserDelegate {

    private var currentElement: XmlElement?

    fileprivate init(_ element: XmlElement) {
        self.currentElement = element
    }

    public func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        self.currentElement = self.currentElement?.pop(elementName)
    }

    public func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        self.currentElement = self.currentElement?.push(elementName)
        self.currentElement?.attributeDict = attributeDict
    }

    func parser(_ parser: XMLParser, foundCharacters string: String) {
        self.currentElement?.text += string
    }
}

public class XmlElement {
    public private(set) var name = "unnamed"
    public private(set) var children = [String: XmlElement]()
    public private(set) var parent: XmlElement? = nil
    public fileprivate(set) var text = ""
    public fileprivate(set) var attributeDict: [String : String] = [:]

    private init(_ parent: XmlElement? = nil, name: String = "") {
        self.parent = parent
        self.name = name
    }

    public convenience init?(fromString: String) {
        guard let data = fromString.data(using: .utf8) else {
            return nil
        }
        self.init(fromData: data)
    }

    public init(fromData: Data) {
        let parser = XMLParser(data: fromData)
        let delegate = XmlToDictionaryParserDelegate(self)
        parser.delegate = delegate
        parser.parse()
    }

    fileprivate func push(_ elementName: String) -> XmlElement {
        let childElement = XmlElement(self, name: elementName)
        children[elementName] = childElement
        return childElement
    }

    fileprivate func pop(_ elementName: String) -> XmlElement? {
        assert(elementName == self.name)
        return self.parent
    }

    public subscript(name: String) -> XmlElement? {
        return self.children[name]
    }
}

To use create an element from a string (or data)

let xml = XmlElement(fromString: "<first>text<second bar="foo"/></first>")

Then use like this:

XCTAssert(xml["first"]?.text == "text")

XCTAssert(xml["first"]?["second"].attributeDict["bar"] == "foo")

This is too long, so I cannot recommend you to use it as is. Just to show you using NSXMLParser would sometimes be a mess like this.

import Foundation

let url = NSBundle.mainBundle().URLForResource("complex", withExtension: "xml")!

protocol ElementParserType: class {
    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [String : String],
        in parserController: MyXMLParserController)
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        in parserController: MyXMLParserController)
    func foundCharacters(string: String, in parserController: MyXMLParserController)

}
class HeaderCol {
    var label: String = ""
}
class Header {
    var cols: [HeaderCol] = []
}
class Row {
    var cols: [String] = []
}
class Body {
    var rows: [Row] = []
}
class Result {
    var header: Header?
    var body: Body?
}
class LabelParser: ElementParserType {
    var label: String?
    let parent: HeaderColParser

    init(parent: HeaderColParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        parserController.didFail("Invalid start element `\(elementName)` in label")
    }

    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        in parserController: MyXMLParserController)
    {
        guard elementName == "label" else {
            parserController.didFail("Invalid end element `\(elementName)` in label")
            return
        }
        parent.col.label = self.label ?? ""
        parserController.popParser()
    }
    func foundCharacters(string: String,
            in parserController: MyXMLParserController)
    {
        self.label = string
    }
}
class HeaderColParser: ElementParserType {
    let col = HeaderCol()
    let parent: HeaderParser

    init(parent: HeaderParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        switch elementName {
        case "label":
            let labelParser = LabelParser(parent: self)
            parserController.pushParser(labelParser)
        default:
            parserController.didFail("Invalid start element `\(elementName)` in col")
        }
    }
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "col" else {
            parserController.didFail("Invalid end element `\(elementName)` in col")
            return
        }
        parent.header.cols.append(self.col)
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        if string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty {
            parserController.didFail("Invalid characters '\(string)' in col")
        }
    }
}
class HeaderParser: ElementParserType {
    let header = Header()
    let parent: ResultParser

    init(parent: ResultParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        switch elementName {
        case "col":
            let headerColParser = HeaderColParser(parent: self)
            parserController.pushParser(headerColParser)
        default:
            parserController.didFail("Invalid start element `\(elementName)` in header")
        }
    }
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "header" else {
            parserController.didFail("Invalid end element `\(elementName)` in header")
            return
        }
        parent.result.header = self.header
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        if string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty {
            parserController.didFail("Invalid characters '\(string)' in header")
        }
    }
}
class RowColParser: ElementParserType {
    var col: String?
    let parent: RowParser

    init(parent: RowParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        parserController.didFail("Invalid start element `\(elementName)` in col")
    }

    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "col" else {
            parserController.didFail("Invalid end element `\(elementName)` in col")
            return
        }
        parent.row.cols.append(self.col ?? "")
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        self.col = string
    }
}
class RowParser: ElementParserType {
    let row = Row()
    let parent: BodyParser

    init(parent: BodyParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        switch elementName {
        case "col":
            let rowColParser = RowColParser(parent: self)
            parserController.pushParser(rowColParser)
        default:
            parserController.didFail("Invalid start element `\(elementName)` in row")
        }
    }
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "row" else {
            parserController.didFail("Invalid end element `\(elementName)` in row")
            return
        }
        parent.body.rows.append(self.row)
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        if string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty {
            parserController.didFail("Invalid characters '\(string)' in row")
        }
    }
}
class BodyParser: ElementParserType {
    let body = Body()
    let parent: ResultParser

    init(parent: ResultParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        switch elementName {
        case "row":
            let rowParser = RowParser(parent: self)
            parserController.pushParser(rowParser)
        default:
            parserController.didFail("Invalid start element `\(elementName)` in body")
        }
    }
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "body" else {
            parserController.didFail("Invalid end element `\(elementName)` in body")
            return
        }
        parent.result.body = self.body
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        if string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty {
            parserController.didFail("Invalid characters '\(string)' in footer")
        }
    }
}
class FooterParser: ElementParserType {
    let parent: ResultParser

    init(parent: ResultParser) {
        self.parent = parent
    }

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      attributes attributeDict: [String : String],
                                 in parserController: MyXMLParserController)
    {
        switch elementName {
        default:
            parserController.didFail("Invalid start element `\(elementName)` in footer")
        }
    }
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "footer" else {
            parserController.didFail("Invalid end element `\(elementName)` in footer")
            return
        }
        //Do nothing
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        if string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty {
            parserController.didFail("Invalid characters '\(string)' in footer")
        }
    }
}
class ResultParser: ElementParserType {
    let result = Result()

    func startElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
        attributes attributeDict: [String : String],
                   in parserController: MyXMLParserController)
    {
        switch elementName {
        case "header":
            let headerParser = HeaderParser(parent: self)
            parserController.pushParser(headerParser)
        case "body":
            let headerParser = BodyParser(parent: self)
            parserController.pushParser(headerParser)
        case "footer":
            let headerParser = FooterParser(parent: self)
            parserController.pushParser(headerParser)
        default:
            parserController.didFail("Invalid start element `\(elementName)` in result")
        }
    }
    func endElement(
        elementName: String,
        namespaceURI: String?,
        qualifiedName qName: String?,
                      in parserController: MyXMLParserController)
    {
        guard elementName == "result" else {
            parserController.didFail("Invalid end element `\(elementName)` in result")
            return
        }
        parserController.result = self.result
        parserController.popParser()
    }
    func foundCharacters(string: String,
                         in parserController: MyXMLParserController)
    {
        if string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet()).isEmpty {
            parserController.didFail("Invalid characters '\(string)' in footer")
        }
    }
}
class MyXMLParserController: NSObject, NSXMLParserDelegate {
    let xmlParser: NSXMLParser
    var result: AnyObject?

    var parserStack: [ElementParserType] = []
    var currentParser: ElementParserType? {
        return parserStack.last
    }
    init?(url: NSURL) {
        if let xmlParser = NSXMLParser(contentsOfURL: url) {
            self.xmlParser = xmlParser
            super.init()
            self.xmlParser.delegate = self //The delegate is not retained.
        } else {
            return nil
        }
    }
    func pushParser(parser: ElementParserType) {
        parserStack.append(parser)
    }
    func popParser() {
        parserStack.removeLast()
    }
    func parse() {
        self.xmlParser.parse()
    }

    func parserDidStartDocument(parser: NSXMLParser) {
        print(#function)
    }

    func parserDidEndDocument(parser: NSXMLParser) {
        print(#function)
    }

    func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
        print(#function, elementName)
        if let parser = currentParser {
            parser.startElement(elementName,
                                namespaceURI: namespaceURI,
                                qualifiedName: qName,
                                attributes: attributeDict,
                                in: self)
        } else {
            guard elementName == "result" else {
                print("Root element needs to be `result`")
                parser.abortParsing()
                return
            }
            pushParser(ResultParser())
        }
    }

    func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if let parser = currentParser {
            parser.endElement(elementName,
                                namespaceURI: namespaceURI,
                                qualifiedName: qName,
                                in: self)
        } else {
            didFail("Invalid end element \(elementName) at top level")
        }
    }

    func parser(parser: NSXMLParser, foundCharacters string: String) {
        if let parser = currentParser {
            parser.foundCharacters(string,
                              in: self)
        } else {
            didFail("Invalid characters '\(string)' at top level")
        }
    }

    func didFail(error: String) {
        xmlParser.abortParsing()
    }
}

let parserController = MyXMLParserController(url: url)!
parserController.parse()
let result = parserController.result as! Result
if let header = result.header, body = result.body {
    var rowArray: [[String: String]] = []
    for row in body.rows {
        assert(header.cols.count == row.cols.count)
        var rowDict: [String: String] = [:]
        for i in 0..<header.cols.count {
            rowDict[header.cols[i].label] = row.cols[i]
        }
        rowArray.append(rowDict)
    }
    print(rowArray as NSArray)
} else {
    print("header or body is missing")
}

Figured it out, albeit likely has more complexity than necessary, will optimize later:

class MyXMLParserDelegate: NSObject, NSXMLParserDelegate {

var counter = 1
var insideBody = false
var insideElement = false
var treeName = [String]()
var numLevels = [String]()
var defaultsWeight = [String]()
var name = [String]()
var abbrev = [String]()
var level = [String]()
var fullName = [String]()


@objc func parserDidStartDocument(parser: NSXMLParser) {
    print("parserDidStartDocument")
}

@objc func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
    print("didStartElement       --> \(elementName)")
    if elementName == "body" {
        insideBody = true
    }
    if insideBody == true && elementName == "col" {
        insideElement = true
    }
    if elementName == "row" {
        counter = 1
    }
    print("inside element -->\(insideElement)")
}

@objc func parser(parser: NSXMLParser, foundCharacters string: String) {
    print("foundCharacters       --> \(string)")
    if insideElement == true && insideBody == true {
        switch counter {
        case 1: treeName.append(string)
        case 2: numLevels.append(string)
        case 3: defaultsWeight.append(string)
        case 4: name.append(string)
        case 5: abbrev.append(string)
        case 6: level.append(string)
        case 7: fullName.append(string)
        default: print("nothing to append")
        }
        insideElement = false
        print("inside element -->\(insideElement)")
    }
}

@objc func parser(parser: NSXMLParser, didEndElement elementName: String,
                  namespaceURI: String?, qualifiedName qName: String?) {
    print("didEndElement         --> \(elementName)")
    if elementName == "body" {
        insideBody = false
    }
    if insideBody == true && elementName == "col" {
        if insideElement == true {
            switch counter {
            case 1: treeName.append("--*n/a*--")
            case 2: numLevels.append("--*n/a*--")
            case 3: defaultsWeight.append("--*n/a*--")
            case 4: name.append("--*n/a*--")
            case 5: abbrev.append("--*n/a*--")
            case 6: level.append("--*n/a*--")
            case 7: fullName.append("--*n/a*--")
            default: print("nothing to append")
            }
            insideElement = false
        }
        counter += 1
    }

print("inside element \(insideElement)")
print("counter \(counter)")
}

@objc func parserDidEndDocument(parser: NSXMLParser) {
    //reload table with array
    print("parserDidEndDocument")
    print(treeName)
    print(numLevels)
    print(defaultsWeight)
    print(name)
    print(abbrev)
    print(level)
    print(fullName)
    struct ccArrays {
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM