Fuzi (斧子)

Build Status CocoaPods Compatible License Carthage Compatible Platform Twitter

一个快速且轻量级的 Swift XML/HTML 解析器,让你的生活更轻松。 [文档]

Fuzi 基于 Mattt Thompson 的 Ono(斧) 的 Swift 移植版,使用了它的大部分底层实现,并根据标准的 Swift 约定进行了适度的类和接口重新设计,以及一些 bug 修复。

Fuzi(斧子) 意味着“axe(斧头)”,向 Ono(斧) 致敬,而 Ono 则受到了 Nokogiri (鋸) 的启发,Nokogiri 意味着 “saw(锯子)”。

简体中文 日本語

快速概览

let xml = "..."
// or
// let xmlData = <some NSData or Data>
do {
  let document = try XMLDocument(string: xml)
  // or
  // let document = try XMLDocument(data: xmlData)
  
  if let root = document.root {
    // Accessing all child nodes of root element
    for element in root.children {
      print("\(element.tag): \(element.attributes)")
    }
    
    // Getting child element by tag & accessing attributes
    if let length = root.firstChild(tag:"Length", inNamespace: "dc") {
      print(length["unit"])     // `unit` attribute
      print(length.attributes)  // all attributes
    }
  }
  
  // XPath & CSS queries
  for element in document.xpath("//element") {
    print("\(element.tag): \(element.attributes)")
  }
  
  if let firstLink = document.firstChild(css: "a, link") {
    print(firstLink["href"])
  }
} catch let error {
  print(error)
}

特性

继承自 Ono

Fuzi 中的改进

要求

对于 Swift 2.3,请使用 0.4.0 版本。

安装

有 4 种方法可以将 Fuzi 安装到你的项目中。

使用 CocoaPods

你可以使用 CocoaPods 来安装 Fuzi,只需将其添加到你的 Podfile

platform :ios, '8.0'
use_frameworks!

target 'MyApp' do
	pod 'Fuzi', '~> 1.0.0'
end

然后,运行以下命令

$ pod install

使用 Swift Package Manager

Swift Package Manager 现在内置于 Xcode 11 (目前是 beta 版)。 你可以很容易地添加 Fuzi 作为依赖,通过选择 File > Swift Packages > Add Package Dependency... 或者在项目文件的 Swift Packages 标签中点击 +。 简单地使用 https://github.com/cezheng/Fuzi 作为 repository, Xcode 应该会自动解析当前版本。

手动安装

  1. Fuzi 目录中的所有 *.swift 文件添加到你的项目中。
  2. 在你的 Xcode 项目 Build Settings
    1. 找到 Search Paths,将 $(SDKROOT)/usr/include/libxml2 添加到 Header Search Paths
    2. 找到 Linking,将 -lxml2 添加到 Other Linker Flags

使用 Carthage

在项目的根目录中创建一个 CartfileCartfile.private,并添加以下行

github "cezheng/Fuzi" ~> 1.0.0

运行以下命令

$ carthage update

然后在 Xcode 中进行以下操作

  1. 将 Carthage 构建的 Fuzi.framework 拖到你的目标的 General -> Embedded Binaries 中。
  2. Build Settings 中,找到 Search Paths,将 $(SDKROOT)/usr/include/libxml2 添加到 Header Search Paths

用法

XML

import Fuzi

let xml = "..."
do {
  // if encoding is omitted, it defaults to NSUTF8StringEncoding
  let document = try XMLDocument(string: html, encoding: String.Encoding.utf8)
  if let root = document.root {
    print(root.tag)
    
    // define a prefix for a namespace
    document.definePrefix("atom", defaultNamespace: "http://www.w3.org/2005/Atom")
    
    // get first child element with given tag in namespace(optional)
    print(root.firstChild(tag: "title", inNamespace: "atom"))

    // iterate through all children
    for element in root.children {
      print("\(index) \(element.tag): \(element.attributes)")
    }
  }
  // you can also use CSS selector against XMLDocument when you feels it makes sense
} catch let error as XMLError {
  switch error {
  case .noError: print("wth this should not appear")
  case .parserFailure, .invalidData: print(error)
  case .libXMLError(let code, let message):
    print("libxml error code: \(code), message: \(message)")
  }
}

HTML

HTMLDocumentXMLDocument 的子类。

import Fuzi

let html = "<html>...</html>"
do {
  // if encoding is omitted, it defaults to NSUTF8StringEncoding
  let doc = try HTMLDocument(string: html, encoding: String.Encoding.utf8)
  
  // CSS queries
  if let elementById = doc.firstChild(css: "#id") {
    print(elementById.stringValue)
  }
  for link in doc.css("a, link") {
      print(link.rawXML)
      print(link["href"])
  }
  
  // XPath queries
  if let firstAnchor = doc.firstChild(xpath: "//body/a") {
    print(firstAnchor["href"])
  }
  for script in doc.xpath("//head/script") {
    print(script["src"])
  }
  
  // Evaluate XPath functions
  if let result = doc.eval(xpath: "count(/*/a)") {
    print("anchor count : \(result.doubleValue)")
  }
  
  // Convenient HTML methods
  print(doc.title) // gets <title>'s innerHTML in <head>
  print(doc.head)  // gets <head> element
  print(doc.body)  // gets <body> element
  
} catch let error {
  print(error)
}

我不在乎错误处理

import Fuzi

let xml = "..."

// Don't show me the errors, just don't crash
if let doc1 = try? XMLDocument(string: xml) {
  //...
}

let html = "<html>...</html>"

// I'm sure this won't crash
let doc2 = try! HTMLDocument(string: html)
//...

我想访问文本节点

不仅是文本节点,你还可以指定你想访问的节点类型。

let document = ...
// Get all child nodes that are Element nodes, Text nodes, or Comment nodes
document.root?.childNodes(ofTypes: [.Element, .Text, .Comment])

从 Ono 迁移?

查看示例程序是了解差异的最快方法。 以下两个示例做完全相同的事情。

Ono 示例

Fuzi 示例

访问子节点

Ono

[doc firstChildWithTag:tag inNamespace:namespace];
[doc firstChildWithXPath:xpath];
[doc firstChildWithXPath:css];
for (ONOXMLElement *element in parent.children) {
  //...
}
[doc childrenWithTag:tag inNamespace:namespace];

Fuzi

doc.firstChild(tag: tag, inNamespace: namespace)
doc.firstChild(xpath: xpath)
doc.firstChild(css: css)
for element in parent.children {
  //...
}
doc.children(tag: tag, inNamespace:namespace)

遍历查询结果

Ono

符合 NSFastEnumeration

// simply iterating through the results
// mark `__unused` to unused params `idx` and `stop`
[doc enumerateElementsWithXPath:xpath usingBlock:^(ONOXMLElement *element, __unused NSUInteger idx, __unused BOOL *stop) {
  NSLog(@"%@", element);
}];

// stop the iteration at second element
[doc enumerateElementsWithXPath:XPath usingBlock:^(ONOXMLElement *element, NSUInteger idx, BOOL *stop) {
  *stop = (idx == 1);
}];

// getting element by index 
ONOXMLDocument *nthElement = [(NSEnumerator*)[doc CSS:css] allObjects][n];

// total element count
NSUInteger count = [(NSEnumerator*)[document XPath:xpath] allObjects].count;

Fuzi

符合 Swift 的 SequenceTypeIndexable

// simply iterating through the results
// no need to write the unused `idx` or `stop` params
for element in doc.xpath(xpath) {
  print(element)
}

// stop the iteration at second element
for (index, element) in doc.xpath(xpath).enumerate() {
  if idx == 1 {
    break
  }
}

// getting element by index 
if let nthElement = doc.css(css)[n] {
  //...
}

// total element count
let count = doc.xpath(xpath).count

评估 XPath 函数

Ono

ONOXPathFunctionResult *result = [doc functionResultByEvaluatingXPath:xpath];
result.boolValue;    //BOOL
result.numericValue; //double
result.stringValue;  //NSString

Fuzi

if let result = doc.eval(xpath: xpath) {
  result.boolValue   //Bool
  result.doubleValue //Double
  result.stringValue //String
}

许可

Fuzi 在 MIT 许可下发布。 有关详细信息,请参阅 LICENSE