Swiftpack.co - Package - cezheng/Fuzi

Fuzi (斧子)

Build Status CocoaPods Compatible License Carthage Compatible Platform Twitter

A fast & lightweight XML/HTML parser in Swift that makes your life easier. [Documentation]

Fuzi is based on a Swift port of Mattt Thompson's Ono(斧), using most of its low level implementaions with moderate class & interface redesign following standard Swift conventions, along with several bug fixes.

Fuzi(斧子) means "axe", in homage to Ono(斧), which in turn is inspired by Nokogiri (鋸), which means "saw".

简体中文 日本語

A Quick Look

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)
}

Features

Inherited from Ono

  • Extremely performant document parsing and traversal, powered by libxml2
  • Support for both XPath and CSS queries
  • Automatic conversion of date and number values
  • Correct, common-sense handling of XML namespaces for elements and attributes
  • Ability to load HTML and XML documents from either String or NSData or [CChar]
  • Comprehensive test suite
  • Full documentation

Improved in Fuzi

  • Simple, modern API following standard Swift conventions, no more return types like AnyObject! that cause unnecessary type casts
  • Customizable date and number formatters
  • Some bugs fixes
  • More convenience methods for HTML Documents
  • Access XML nodes of all types (Including text, comment, etc.)
  • Support for more CSS selectors (yet to come)

Requirements

  • iOS 8.0+ / Mac OS X 10.9+
  • Xcode 8.0+

Use version 0.4.0 for Swift 2.3.

Installation

There are 4 ways you can install Fuzi to your project.

Using CocoaPods

You can use CocoaPods to install Fuzi by adding it to your to your Podfile:

platform :ios, '8.0'
use_frameworks!

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

Then, run the following command:

$ pod install

Using Swift Package Manager

The Swift Package Manager is now built-in with Xcode 11 (currently in beta). You can easily add Fuzi as a dependency by choosing File > Swift Packages > Add Package Dependency... or in the Swift Packages tab of your project file and clicking on +. Simply use https://github.com/cezheng/Fuzi as repository and Xcode should automatically resolve the current version.

Manually

  1. Add all *.swift files in Fuzi directory into your project.
  2. In your Xcode project Build Settings:
    1. Find Search Paths, add $(SDKROOT)/usr/include/libxml2 to Header Search Paths.
    2. Find Linking, add -lxml2 to Other Linker Flags.

Using Carthage

Create a Cartfile or Cartfile.private in the root directory of your project, and add the following line:

github "cezheng/Fuzi" ~> 1.0.0

Run the following command:

$ carthage update

Then do the followings in Xcode:

  1. Drag the Fuzi.framework built by Carthage into your target's General -> Embedded Binaries.
  2. In Build Settings, find Search Paths, add $(SDKROOT)/usr/include/libxml2 to Header Search Paths.

Usage

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

HTMLDocument is a subclass of XMLDocument.

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)
}

I don't care about error handling

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)
//...

I want to access Text Nodes

Not only text nodes, you can specify what types of nodes you would like to access.

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

Migrating From Ono?

Looking at example programs is the swiftest way to know the difference. The following 2 examples do exactly the same thing.

Ono Example

Fuzi Example

Accessing children

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)

Iterate through query results

Ono

Conforms to 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

Conforms to Swift's SequenceType and Indexable.

// 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

Evaluating XPath Functions

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
}

License

Fuzi is released under the MIT license. See LICENSE for details.

Github

link
Stars: 854

Dependencies

Used By

Total: 1

Releases

3.1.2 - 2020-03-27 18:30:41

  • SPM breakage fix for Xcode 11.4 beta and after. Thanks @thebluepotato for #107 .

3.1.1 - 2019-06-26 03:32:17

  • Swift Package Manager support for Xcode 11. Thanks @thebluepotato for #101 !

3.1.0 - 2019-05-25 04:16:30

  • Fuzi now supports defining custom prefix for non-default namespace for XPath queries! (#99) Thanks @mmenu-mantano!

Swift 5 - 2019-04-01 03:13:17

  • Swift 5 support.

Real final version for Swift 4.2. - 2019-04-01 02:44:49

  • Fixed a bug that parses an HTML document with xmlReadMemory. This worked previously because the compiler used to treat convenience initializers in a polymorphic manner, which does not match the spec.

Final version for Swift 4.* - 2019-04-01 00:42:35

  • Just leaving a final Swift 4.2 compatible version before switching to Swift 5.

2.1.0 - 2018-04-30 15:34:52

  • Added a throw version of xpath method to Queryable protocol so that there is now a way to see the error when you pass in an invalid XPath query string instead of just getting a silent empty result. The non-throw xpath method is still there so you don't need to change your code. Thanks @Parabak !
  • Fixed all warnings in Xcode 9.3.

2.0.2 - 2018-04-03 17:28:36

Removed module map for Xcode 9.3

2.0.1 - 2017-10-19 14:00:14

  • Disabled coverage to prevent potential AppStore rejection if using Carthage ( Thanks @mhmiles )
  • Fixed firstChild crash bug when ns is specified for certain documents ( #70. Thanks @banjun )

Swift 4 - 2017-09-14 16:48:25

  • Swift 4 is here!(credits to @jdivittorio3 and @ashleymills)
  • Support using StaticString as tag names and performance optimization(#51, credits to @banjun)

1.0.1 - 2016-10-26 06:33:35

  • Fixed a bug that causes Fuzi to crash when creating XMLDocument with invalid data.
  • Restructured directory structure (intending to support SPM, but no luck yet)
  • Use a single Xcode scheme for iOS/macOS/watchOS/tvOS.

1.0.0 (Swift 3) - 2016-09-17 09:23:58

  • Swift 3
  • Minor adjustments to CSS conversion logic

0.4.0 (Final release for Swift 2.3) - 2016-09-17 07:56:32

This is the final release for Swift 2.3. The master branch will be in Swift 3.

Changes

  • Updated with Swift 2.3
  • Fixed an issue that might dump html node as self-closed even if the original node in document is not
  • Fixed a bug in NSRange conversion that did not use utf16 indexes
  • Some cleanup

0.3.1 - 2016-03-29 13:39:30

  • Remove as many Swift 2.2 warnings as possible while keeping build compatibility with Swift 2.1
  • Refactored frameworks module map

0.3.0 - 2015-11-20 12:39:54

Changes

  • Added XMLNode type to represent other types of nodes that are not element
  • Import from libxml2 XMLNodeType enum for determining node types
  • Support fetching specified types of Nodes from children, README updated with an example (https://github.com/cezheng/Fuzi/issues/3)
  • Minor optimizations on HTML convenience methods

0.2.0 - 2015-10-22 05:24:43

  • tvOS support
  • added some convenience methods for HTMLDocument
  • minor optimizations

0.1.1 - 2015-09-26 01:53:59

  • Minor improvements such as printable documents & avoiding unnecessary iteration on finding first child
  • Minor fixes relating to encoding & strong reference cycle

0.1.0 - 2015-09-16 09:00:51

  • Initial release!
  • Interface redesign based on Ono's lower level implementation
  • All necessary Ono methods ported
  • All Ono tests imported
  • CSS to XPath conversion bug fixed
  • XPath eval memory leak bug fixed
  • libxml2 error bug fixed