一个结果构建器,用于构建 HTML 解析器并将 HTML 元素转换为强类型结果,灵感来自 RegexBuilder。
注意:
CaptureTransform.swift
,TypeConstruction.swift
是从 apple/swift-experimental-string-processing 复制的。
dependencies: [
// ...
.package(name: "HTMLParserBuilder", url: "https://github.com/danny1113/html-parser-builder.git", from: "2.0.0"),
]
解析 HTML 可能很复杂,例如您想解析下面简单的 HTML
<h1 id="hello">hello, world</h1>
<div id="group">
<h1>INSIDE GROUP h1</h1>
<h2>INSIDE GROUP h2</h2>
</div>
现有的 HTML 解析库有以下缺点
let htmlString = "<html>...</html>"
let doc: any Document = HTMLDocument(string: htmlString)
let first = doc.querySelector("#hello")?.textContent
let group = doc.querySelector("#group")
let second = group?.querySelector("h1")?.textContent
let third = group?.querySelector("h2")?.textContent
if let first = first,
let second = second,
let third = third {
// ...
} else {
// ...
}
HTMLParserBuilder 具有一些非常棒的优点
您可以构建反映原始 HTML 结构的解析器
let capture = HTML {
TryCapture("#hello") { (element: any Element?) -> String? in
return element?.textContent
} // => HTML<String?>
Local("#group") {
Capture("h1", transform: \.textContent) // => HTML<String>
Capture("h2", transform: \.textContent) // => HTML<String>
} // => HTML<(String, String)>
} // => HTML<(String?, (String, String))>
let htmlString = "<html>...</html>"
let doc: any Document = HTMLDocument(string: htmlString)
let output = try doc.parse(capture)
// => (String?, (String, String))
// output: (Optional("hello, world"), ("INSIDE GROUP h1", "INSIDE GROUP h2"))
注意: 您现在可以在构建器中组合最多 10 个组件,但您可以将捕获内容分组在
Local
中作为一种解决方法。
HTMLParserBuilder 不依赖于任何 HTML 解析器,因此您可以选择您想使用的任何 HTML 解析器,只要它符合 Document
和 Element
协议即可。
例如,您可以使用 SwiftSoup 作为 HTML 解析器,符合 Document
和 Element
协议的示例可在 Tests/HTMLParserBuilderTests/SwiftSoup+HTMLParserBuilder.swift
中找到。
dependencies: [
// ...
.package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
.package(name: "HTMLParserBuilder", url: "https://github.com/danny1113/html-parser-builder.git", from: "2.0.0"),
],
targets: [
.target(name: "YourTarget", dependencies: ["SwiftSoup", "HTMLParserBuilder"]),
]
HTMLParserBuilder 提供了 2 个用于解析的函数
public func parse<Output>(_ html: HTML<Output>) throws -> Output
public func parse<Output>(_ html: HTML<Output>) async throws -> Output
注意: 您可以选择异步版本以获得更好的性能,因为它使用结构化并发来并行处理子任务。
您可以在 HTML
中构建您的解析器,它还可以转换为其他数据类型。
struct Group {
let h1: String
let h2: String
}
let capture = HTML {
Capture("#group h1", transform: \.textContent) // => HTML<String>
Capture("#group h2", transform: \.textContent) // => HTML<String>
} transform: { (output: (String, String)) -> Group in
return Group(
h1: output.0,
h2: output.1
)
} // => HTML<Group>
使用 Capture
与 querySelector
相同,您传入 CSS 选择器以查找 HTML 元素,并且您可以将其转换为您想要的任何其他类型
注意: 如果
Capture
找不到与选择器匹配的 HTML 元素,它将抛出一个错误导致整个解析失败,对于可失败的捕获,请参见TryCapture
。
您可以将此 API 与各种最适合您的声明一起使用
Capture("#hello", transform: \.textContent)
Capture("#hello") { $0.textContent }
Capture("#hello") { (e: any Element) -> String in
return e.textContent
}
TryCapture
与 Capture
有一点不同,它也调用 querySelector
来查找 HTML 元素,但它返回一个可选 HTML 元素。
对于此示例,它将产生 String?
的结果类型,当找不到 HTML 元素时,结果将为 nil
。
TryCapture("#hello") { (e: (any Element)?) -> String? in
return e?.innerHTML
}
使用 CaptureAll
与 querySelectorAll
相同,您传入 CSS 选择器以查找所有与选择器匹配的 HTML 元素,并且您可以将其转换为您想要的任何其他类型
您可以将此 API 与各种最适合您的声明一起使用
CaptureAll("h1") { $0.map(\.textContent) }
CaptureAll("h1") { (e: [any Element]) -> [String] in
return e.map(\.textContent)
}
您还可以捕获内部的其他元素并转换为其他类型
<div class="group">
<h1>Group 1</h1>
</div>
<div class="group">
<h1>Group 2</h1>
</div>
CaptureAll("div.group") { (elements: [any Element]) -> [String] in
return elements.compactMap { e in
return e.querySelector("h1")?.textContent
}
}
// => [String]
// output: ["Group 1", "Group 2"]
Local
将查找与选择器匹配的 HTML 元素,并且内部的所有捕获都将基于 Local
找到的元素查找其元素,这在您只想捕获本地组内的元素时很有用。
就像 HTML
一样,Local
也可以通过添加 transform
将捕获结果转换为其他数据类型
struct Group {
let h1: String
let h2: String
}
Local("#group") {
Capture("h1", transform: \.textContent) // => HTML<String>
Capture("h2", transform: \.textContent) // => HTML<String>
} transform: { (output: (String, String)) -> Group in
return Group(
h1: output.0,
h2: output.1
)
} // => Group
注意: 如果
Local
找不到与选择器匹配的 HTML 元素,它将抛出一个错误导致整个解析失败,您可以将TryCapture
用作替代方法。
此库还带有一个方便的属性包装器:LateInit
,它可以延迟初始化,直到您第一次访问它为止。
struct Container {
@LateInit var capture = HTML {
Capture("h1", transform: \.textContent)
}
}
// it needs to be `var` to perform late initialization
var container = Container()
let output = doc.parse(container.capture)
// ...
API | 用例 |
---|---|
Capture(捕获) | 当无法捕获元素时抛出错误 |
TryCapture(尝试捕获) | 当无法捕获元素时返回 nil |
CaptureAll(捕获全部) | 捕获所有与选择器匹配的元素 |
Local(本地) | 捕获本地范围内的元素 |
LateInit(延迟初始化) | 延迟初始化到第一次访问时 |
HTMLComponent
传递到另一个struct Group {
let h1: String
let h2: String
}
// |--------------------------------------------------------------|
let groupCapture = HTML { // |
Local("#group") { // |
Capture("h1", transform: \.textContent) // => HTML<String> // |
Capture("h2", transform: \.textContent) // => HTML<String> // |
} // => HTML<(String, String)> // |
// |
} transform: { output -> Group in // |
return Group( // |
h1: output.0, // |
h2: output.1 // |
) // |
} // => HTML<Group> // |
// |
let capture = HTML { // |
TryCapture("#hello") { (element: (any Element)?) -> String? in // |
return element?.textContent // |
} // => HTML<String?> // |
// |
groupCapture // => HTML<Group> -------------------------------------|
} // => HTML<(String?, Group)>
let htmlString = "<html>...</html>"
let doc: any Document = HTMLDocument(string: htmlString)
let output = try doc.parse(capture)
// => (String?, Group)