MirrorXML 4.0.2

MirrorXML 4.0.2

Mike Spears 维护。



MirrorXML 4.0.2

  • 作者:
  • Mike Spears

MirrorXML

MirrorXML 是 libxml2 的 SAX (push) xml 和 html 解析器的包装器。它也是 libxml2 的可流式 XPath 模式匹配功能的包装器。

但是这两者并不能很好地描述 MirrorXML 中这两个功能如何一起工作来使事件驱动 xml 解析更简单。

让我们换一种说法:MirrorXML 是一个基于块的、事件驱动的解析 xml (和基本 html) 的 API。

MirrorXML 不会尝试将 XML 奇迹般转换为 Swift 模型对象,相反,它让您掌握控制权,同时帮助您创建更易于维护、显式和良好的代码。

它还提供了一套小巧的、可定制的 html 到 NSAttributedString API。

示例

要运行示例 iOS 项目,请先克隆仓库,然后从 Example 目录运行 pod install

要求

MirrorXML 使用 Objective-C 编写。它可以用于 Swift 和 Objective-C 项目。

MirrorXML 与 iOS 9.0+ 和 macOS 10.11+ 目标兼容。

安装

MirrorXML可以通过CocoaPods获取。要安装它,只需将以下行添加到您的Podfile中

pod 'MirrorXML'

我非常兴奋地开始解析XML数据!我该如何使用这个工具呢?

这里有一个基本示例。假设我们想获取RSS文档中所有条目的标题

var titles = [String]() // array to store the titles

// Create an MXMatch object with the XML path we want.
let titleMatch = try! MXMatch(path: "/rss/channel/item/title")
// Create a block that will be called at the end of every title element.
titleMatch.exitHandler = {(elm) in
    if let text = elm.text {
        titles.append(text)
    }    
}

// Give the MXMatch object to a parser instance and parse the data.
let xmlParser = MXParser(matches: [titleMatch])
xmlParser.parseDataChunk(xmlData)
xmlParser.dataFinished()

解析完成后,titles数组将包含通过MXMatch

MXMatch对象具有不同的回调属性,可以分配块。这些块在解析器读取xml数据时在适当的点被调用。

当块在xml元素的开始时被调用,它可以返回临时的MXMatch对象,这些对象用于解析当前元素内的数据。由于块是闭包,所以它们将保留对在相同上下文中创建的任何新对象的引用。

例如,如果我们想解码具有几个不同属性的对象,这些属性代表RSS条目,我们可以这样做

var items = [RSSItem]() // An array to store our RSS items

// Create an MXMatch object with the XML path we want.
let itemMatch = try! MXMatch(path: "/rss/channel/item")

// Create a block that will be called at the beginning of every item element.
itemMatch.entryHandler = {(elm) in
    //create a new instance of the RSSItem class and add it to the array
    var thisRSSItem = RSSItem()
    items.append(thisRSSItem)
    
    // Create a temporary MXMatch object with a path that matches title elements.
    // Note that the path is relative to the parent 'item' element.
    let titleMatch = try! MXMatch(path: "/title")
    titleMatch.exitHandler = { (elm) in        
        thisRSSItem.title = elm.text
    }
    // Similar idea for link elements.
    let linkMatch = try! MXMatch(path: "/link")
    titleMatch.exitHandler = { (elm) in
        thisRSSItem.link = elm.text
    }
    // Return the temporary MXMatch objects. They will only apply to the current 'item' element.
    return [titleMatch, linkMatch]
}

// Give the MXMatch object to a parser instance and parse the data.
let xmlParser = MXParser(matches: [itemMatch])
xmlParser.parseDataChunk(xmlData)
xmlParser.dataFinished()

不使用MirrorXML的典型方式编写事件驱动的(推)xml解析器是编写几个函数。一个函数会在每个新元素开始时被调用,另一个函数会在每个元素结束时被调用。

由于这些函数对每个元素、在每个xml文档结构级别都会被调用,它们不会自然地知道它们被调用的上下文。因此,你必须跟踪很多状态,比如isInsideChannelisInsideItemcurrentRSSItem。由于管理不同类型数据项的代码混合在一起,所以可能会变得很混乱。

MirrorXML简化了这一切,因为你的代码结构与XML文档的结构相匹配,并且只有对你感兴趣的元素才激活你的回调。正如上面所示,你需要在一个地方集中保留创建这些理论上的RSSItem对象所需的所有代码。而且由于我们使用块,它们隐式地保持了对其上下文的引用,因此我们不需要类似currentRSSitem的全局变量。

在入口和退出事件之间保持状态

您可能希望在元素的入口和退出处理程序之间传递状态。有两种方法可以做到这一点。

第一种方法是在元素的userInfo属性中使用。您可以在入口处理程序中将任意对象分配给此属性,它将在传递给退出处理程序的元素中可用。

第二种方法是在入口处理器本身中定义一个特殊的退出块。这是一个隐式保存入口处理器上下文的闭包。您可以使用MXMatch.onRootExit(:)创建这个特殊的块对象。当解析当前标签的退出时,它会调用。

例如(基于前面的示例),假设您决定只有在RSS项有有效的标题和链接时才将其添加到项目数组中。您可以编写以下代码:

let itemMatch = try! MXMatch(path: "/rss/channel/item")

// Create a block that will be called at the beginning of every item element.
itemMatch.entryHandler = {(elm) in
    // create a new instance of the RSSItem class
    // but don't add it to the storage array until later
    let thisRSSItem = RSSItem()

    let titleMatch = try! MXMatch(path: "/title")
    titleMatch.exitHandler = { (elm) in
        thisRSSItem.title = elm.text
    }
    let linkMatch = try! MXMatch(path: "/link")
    titleMatch.exitHandler = { (elm) in
        thisRSSItem.link = elm.text
    }

    // Only add the item if it is valid.
    // This block will run after titleMatch and linkMatch.
    // Note that in this circumstance we have a reference to the 'thisRSSItem' object we are building.
    let itemExit = MXMatch.onRootExit({ (elm) in
        guard thisRSSItem.title != nil && thisRSSItem.link != nil else {
            return
        }
        items.append(thisRSSItem)
    })
    // Return the temporary MXMatch objects. They will only apply to the current 'item' element.
    return [titleMatch, linkMatch, itemExit]
}

XPath样式的模式

前面两个示例使用简单的路径匹配xml文档中特定位置上的元素。还有一些更高级的技巧可以做

/root/item --> 匹配'item'元素,它们是'root'的子元素

/root/item/title --> 匹配'root'子元素的'item'子元素的'title'元素

/root/item|/root/otheritem --> 'OR操作符:匹配root子元素的item或其他item元素

/root/item/@attrName --> 匹配(root)子元素'item'具有名为'attrName'的属性。

/root/* --> 匹配根的每个子元素。

/root//item --> 匹配位于根以下各级的'item'元素。/root//* --> 匹配位于根以下各级的每个元素。//* --> 匹配文档中的每个元素。

/root/ns:item --> 将item元素指定为具有命名空间前缀。'ns'前缀在MXMatch对象传递给namespaces字典参数的完整命名空间URI中映射。

如果您熟悉XPath语法和语义,这些会看起来很熟悉,但请注意,XPath语法的更高级功能(如复杂的谓词或函数)不受支持。这是因为这些路径必须是“可流式处理的”,即我们在移动中评估这些路径。

这里有一个HTML示例。假设我们想要获取某些HTML数据中的所有链接:

var links = [String]()

// Match every 'a' element.
let linkElement = try! MXMatch(path:"//a")
linkElement.entryHandler = {
    // Please note that attributes are only available in 'entryHandler' blocks.
    if let url = $0.attributes["href"] {
        links.append(url)
    }
    return nil
}

let htmlParser = MXHTMLParser(matches: [linkElement])
htmlParser.parseDataChunk(htmlData)
htmlParser.dataFinished()

命名空间

MXMatch有一个更高级的初始化器可以处理命名空间

let nameSpacedMatch = try! MXMatch(path: "/rss/channel/item/georss:point", namespaces: ["georss":"http://www.georss.org/georss"])

namespaces参数接受一个前缀/URI对的字典。

在入口处理器块内部,可以通过MXElement的namespacedAttributes属性检索具有命名空间的属性。

错误处理

报告解析错误的libxml通过使用与元素“开始”和“结束”事件类似的回调块在MirrorXML API中传递。您将一个块分配给MXMatch的errorHandler属性,以便在遇到任何匹配MXMatch对象模式的元素时的错误时调用。

例如,为了创建一个在元素上遇到错误时被调用的错误处理程序

let errorMatch = try! MXMatch(path:"//*")
errorMatch.errorHandler = { (error, elm) in
    print("An error was encountered: \(error.localizedDescription), \(elm.elementName ?? "Unknown")")
}

// assuming itemMatch and otherItemMatch were previously declared
let xmlParser = MXParser(matches:[errorMatch, itemMatch, otherItemMatch])
// then parseDataChunk etc.

大文档解析

你可以多次调用 parseDataChunk: 以增量方式解析大文档。例如,你可以在大文档仍在下载时开始解析。

我在MirrorXML中xml解析期间努力保持内存使用量不变,但如果你在多次调用期间看到大量临时对象积累,可以在其中包装 parseDataChunk:

将HTML转换为NSAttributedString

MirrorXML还包括一个名为 MXHTMLToAttributedString 的类。你可以给它提供html片段或完整的html文档来转换。它是建立在 MXHTMLParser 之上的。

与NSAttributedString的html->string转换方法相比,它的优点是:

  1. 你可以在解析期间使用代理来自定义样式。

  2. 你可以在任何线程上使用它。

  3. 它似乎更快(如果它不对,不要叫我或任何东西)。

它仅处理基本的“Markdown样式”html标签、链接和图片。它不处理脚本或样式表或任何花哨的东西。

将对象分配给 MXHTMLToAttributedStringDelegate 代理属性来自定义结果的字体和段落属性。

默认情况下,将具有许多可自定义属性的 MXHTMLToAttributedStringDelegateDefault 实例分配给代理属性。

libxml的html解析器不是严格的,所以遇到的所有错误并不一定是致命的。你可以在转换字符串后检查转换器的 errors 属性以查看解析期间报告的任何错误。

它不需要输入是完整的带有“head”和“body”等内容的“html”结构化文档,所以你可以将简单的字符串(带有一些标签)解析为属性字符串,例如 <a>点击href="mailto:[email protected]这里</a>以 <b>联系支持</b>(注意:如果你想在类似UILabel的东西内激活链接,请确保启用UILabel的用户交互。)

如果遇到图像标签:会插入一个占位符,你可以稍后使用 +insertImage:withInfo:toString 替换它。

示例

let htmlString = "<a>Click href=\"mailto:[email protected]\"here</a> to <b>contact support.</b>"
let string = MXHTMLToAttributedString().convertHTMLString(htmlString)

这是一个有点实验性的示例,并不能保证生成的文本类似于真正浏览器的文本。最好用于你有一定控制权的数据集,而不是来自网络的任意数据。如果你需要更强大的东西,最好使用web视图。

线程安全

您可以在任何线程上使用MXParser、MXHTMLParser、MXMatch、MXPattern和MXHTMLToAttributedString,但不要从超过一个线程中访问相同的实例。

一个常见的场景是在后台线程中使用MXHTMLToAttributedString,然后将生成的AttributedString传回主线程以在文本视图或标签中显示。另一个常见场景是在后台线程中解析XML数据到您的模型对象中。

样式

MirrorXML可以使用多层的回调代码与高度层次化的代码一起工作,但也可以与非常简单的代码一起工作,只需很少的块——有点像标准NSXMLParserDelegate风格的交互。这取决于您!

进一步阅读

查看附带示例项目,了解一些使用MirrorXML的高级方法。我还建议查看附带单元测试,也许MXHTMLToAttributedString类的实现。

作者

Mike Spears, [email protected]

许可证

MirrorXML可在MIT许可证下使用。有关更多信息,请参阅LICENSE文件。