SwiftSoup

Platform OS X | iOS | tvOS | watchOS | Linux SPM compatible 🐧 linux: ready Carthage compatible Build Status Version License Twitter

SwiftSoup 是一个纯 Swift 库,跨平台(macOS、iOS、tvOS、watchOS 和 Linux!),用于处理真实世界的 HTML。它提供了一个非常方便的 API,可以使用 DOM、CSS 和类似 jQuery 的方法的最佳实践来提取和操作数据。SwiftSoup 实现了 WHATWG HTML5 规范,并将 HTML 解析为与现代浏览器相同的 DOM。

Swift

Swift 5 >=2.0.0

Swift 4.2 1.7.4

安装

Cocoapods

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

pod 'SwiftSoup'

Carthage

SwiftSoup 也可通过 Carthage 获得。要安装它,只需将以下行添加到您的 Cartfile 中

github "scinfu/SwiftSoup"

Swift Package Manager

SwiftSoup 也可通过 Swift Package Manager 获得。要安装它,只需将依赖项添加到您的 Package.Swift 文件中

...
dependencies: [
    .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"),
],
targets: [
    .target( name: "YourTarget", dependencies: ["SwiftSoup"]),
]
...

试用

试用简单的在线 CSS 选择器网站

SwiftSoup 测试站点

试用示例项目,打开终端并输入

pod try SwiftSoup

SwiftSoup SwiftSoup

解析 HTML 文档

do {
   let html = "<html><head><title>First parse</title></head>"
       + "<body><p>Parsed HTML into a doc.</p></body></html>"
   let doc: Document = try SwiftSoup.parse(html)
   return try doc.text()
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

文档的对象模型

从元素中提取属性、文本和 HTML

问题

解析文档并找到一些元素后,您需要获取这些元素内部的数据。

解决方案

do {
    let html: String = "<p>An <a href='http://example.com/'><b>example</b></a> link.</p>"
    let doc: Document = try SwiftSoup.parse(html)
    let link: Element = try doc.select("a").first()!
    
    let text: String = try doc.body()!.text() // "An example link."
    let linkHref: String = try link.attr("href") // "http://example.com/"
    let linkText: String = try link.text() // "example"
    
    let linkOuterH: String = try link.outerHtml() // "<a href="http://example.com/"><b>example</b></a>"
    let linkInnerH: String = try link.html() // "<b>example</b>"
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

描述

上述方法是元素数据访问方法的核心。 还有其他附加的方法

所有这些访问器方法都具有相应的 setter 方法来更改数据。

从字符串解析文档

问题

您在 Swift 字符串中有 HTML,并且您想解析该 HTML 以访问其内容,或确保其格式正确,或修改它。 该字符串可能来自用户输入、文件或网络。

解决方案

使用静态 SwiftSoup.parse(_ html: String) 方法,或 SwiftSoup.parse(_ html: String, _ baseUri: String)

do {
    let html = "<html><head><title>First parse</title></head>"
        + "<body><p>Parsed HTML into a doc.</p></body></html>"
    let doc: Document = try SwiftSoup.parse(html)
    return try doc.text()
} catch Exception.Error(let type, let message) {
    print("")
} catch {
    print("")
}

描述

parse(_ html: String, _ baseUri: String) 方法将输入的 HTML 解析为新的 Document。 base URI 参数用于将相对 URL 解析为绝对 URL,应设置为从中获取文档的 URL。 如果这不适用,或者如果您知道 HTML 具有基本元素,则可以使用 parse(_ html: String) 方法。

只要您传入一个非空字符串,就可以保证成功、合理的解析,并且 Document 包含(至少)一个 head 和一个 body 元素。

获得 Document 后,您可以使用 Document 及其父类 ElementNode 中的适当方法来访问数据。

解析正文片段

问题

您有一个正文 HTML 片段(例如,包含几个 p 标签的 div;而不是完整的 HTML 文档),您想要解析它。 也许它是由用户提交评论或在 CMS 中编辑页面正文提供的。

解决方案

使用 SwiftSoup.parseBodyFragment(_ html: String) 方法。

do {
    let html: String = "<div><p>Lorem ipsum.</p>"
    let doc: Document = try SwiftSoup.parseBodyFragment(html)
    let body: Element? = doc.body()
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

描述

parseBodyFragment 方法创建一个空的 shell 文档,并将解析后的 HTML 插入到 body 元素中。 如果您使用普通的 SwiftSoup(_ html: String) 方法,您通常会得到相同的结果,但将输入显式地视为正文片段可确保用户提供的任何 bozo HTML 都被解析到 body 元素中。

Document.body() 方法检索文档 body 元素的元素子项; 它等效于 doc.getElementsByTag("body")

保持安全

如果您要接受来自用户的 HTML 输入,您需要小心避免跨站脚本攻击。 请参阅基于 Whitelist 的 cleaner 的文档,并使用 clean(String bodyHtml, Whitelist whitelist) 清理输入。

清理不受信任的 HTML(以防止 XSS)

问题

您想允许不受信任的用户提供 HTML 以在您的网站上输出(例如,作为评论提交)。 您需要清理此 HTML 以避免 跨站脚本 (XSS) 攻击。

解决方案

将 SwiftSoup HTML Cleaner 与由 Whitelist 指定的配置一起使用。

do {
    let unsafe: String = "<p><a href='http://example.com/' onclick='stealCookies()'>Link</a></p>"
    let safe: String = try SwiftSoup.clean(unsafe, Whitelist.basic())!
    // now: <p><a href="http://example.com/" rel="nofollow">Link</a></p>
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

如果您提供一个包含 <head> 标签的完整 HTML 文档,则 clean(_: String, _: String, _: Whitelist) 方法将仅返回清理后的正文 HTML。 您可以通过为每个标签提供 Whitelist 来清理 <head><body>

do {
    let unsafe: String = """
    <html>
        <head>
            <title>Hey</title>
            <script>console.log('hi');</script>
        </head>
        <body>
            <p>Hello, world!</p>
        </body>
    </html>
    """

    var headWhitelist: Whitelist = {
        do {
            let customWhitelist = Whitelist.none()
            try customWhitelist
                .addTags("meta", "style", "title")
            return customWhitelist
        } catch {
            fatalError("Couldn't init head whitelist")
        }
    }()

    let unsafeDocument: Document = try SwiftSoup.parse(unsafe)
    let safe: String = try SwiftSoup.Cleaner(headWhitelist: headWhitelist, bodyWhitelist: .relaxed())
                            .clean(unsafeDocument)
                            .html()
    // now: <html><head><title>Hey</title></head><body><p>Hello, world!</p></body></html>
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

讨论

针对您网站的跨站脚本攻击真的会毁了您的一天,更不用说您的用户了。 许多网站通过不允许用户提交的内容中包含 HTML 来避免 XSS 攻击:它们仅强制执行纯文本,或使用替代的标记语法,如 wiki-text 或 Markdown。 对于用户而言,这些很少是最佳解决方案,因为它们降低了表达能力,并迫使用户学习新的语法。

更好的解决方案可能是使用富文本 WYSIWYG 编辑器(如 CKEditorTinyMCE)。 这些输出 HTML,并允许用户以可视方式工作。 但是,它们的验证是在客户端完成的:您需要应用服务器端验证来清理输入并确保 HTML 可以安全地放置在您的网站上。 否则,攻击者可以避免客户端 Javascript 验证,并将不安全的 HMTL 直接注入到您的网站中

SwiftSoup 白名单 sanitizer 的工作方式是解析输入的 HTML(在安全、沙盒化的环境中),然后迭代解析树,并且只允许已知的安全标签和属性(以及值)进入清理后的输出。

它不使用正则表达式,这不适合此任务。

SwiftSoup 提供了一系列 Whitelist 配置,以满足大多数要求; 如果需要,可以修改它们,但请小心。

cleaner 不仅可用于避免 XSS,还可用于限制用户可以提供的元素范围:您可能可以接受文本 astrong 元素,但不能接受结构 divtable 元素。

另请参阅

设置属性值

问题

您有一个已解析的文档,您想要更新其上的属性值,然后再将其保存到磁盘或作为 HTTP 响应发送。

解决方案

使用属性 setter 方法 Element.attr(_ key: String, _ value: String)Elements.attr(_ key: String, _ value: String)

如果您需要修改元素的 class 属性,请使用 Element.addClass(_ className: String)Element.removeClass(_ className: String) 方法。

Elements 集合具有批量属性和类方法。 例如,要将 rel="nofollow" 属性添加到 div 中的每个 a 元素

do {
    try doc.select("div.comments a").attr("rel", "nofollow")
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

描述

Element 中的其他方法一样,attr 方法返回当前 Element(或来自选择的集合的 Elements)。 这允许方便的方法链接

do {
    try doc.select("div.masthead").attr("title", "swiftsoup").addClass("round-box")
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

设置元素的 HTML

问题

您需要修改元素的 HTML。

解决方案

使用 Element 中的 HTML setter 方法

do {
    let doc: Document = try SwiftSoup.parse("<div>One</div><span>One</span>")
    let div: Element = try doc.select("div").first()! // <div>One</div>
    try div.html("<p>lorem ipsum</p>") // <div><p>lorem ipsum</p></div>
    try div.prepend("<p>First</p>")
    try div.append("<p>Last</p>")
    print(div)
    // now div is: <div><p>First</p><p>lorem ipsum</p><p>Last</p></div>
    
    let span: Element = try doc.select("span").first()! // <span>One</span>
    try span.wrap("<li><a href='http://example.com/'></a></li>")
    print(doc)
    // now: <html><head></head><body><div><p>First</p><p>lorem ipsum</p><p>Last</p></div><li><a href="http://example.com/"><span>One</span></a></li></body></html>
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

讨论

另请参阅

您还可以使用 Element.prependElement(_ tag: String)Element.appendElement(_ tag: String) 方法来创建新元素,并将它们作为子元素插入到文档流中。

设置元素的文本内容

问题

您需要修改 HTML 文档的文本内容。

解决方案

使用 Element 的文本 setter 方法

do {
    let doc: Document = try SwiftSoup.parse("<div></div>")
    let div: Element = try doc.select("div").first()! // <div></div>
    try div.text("five > four") // <div>five &gt; four</div>
    try div.prepend("First ")
    try div.append(" Last")
    // now: <div>First five &gt; four Last</div>
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

讨论

文本 setter 方法镜像了 [[HTML setter|设置元素的 HTML]] 方法

使用 DOM 方法导航文档

问题

您有一个 HTML 文档,您想要从中提取数据。 您通常了解 HTML 文档的结构。

解决方案

在将 HTML 解析为 Document 后,使用可用的类似 DOM 的方法。

do {
    let html: String = "<a id=1 href='?foo=bar&mid&lt=true'>One</a> <a id=2 href='?foo=bar&lt;qux&lg=1'>Two</a>"
    let els: Elements = try SwiftSoup.parse(html).select("a")
    for link: Element in els.array() {
        let linkHref: String = try link.attr("href")
        let linkText: String = try link.text()
    }
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

描述

Elements 提供了一系列类似 DOM 的方法来查找元素,并提取和操作其数据。 DOM getter 是上下文相关的:在父 Document 上调用它们会找到文档下的匹配元素; 在子元素上调用它们会找到该子元素下的元素。 这样,您就可以在您想要的数据中进行窗口化。

查找元素

元素数据

操作 HTML 和文本

使用选择器语法查找元素

问题

您想使用 CSS 或类似 jQuery 的选择器语法来查找或操作元素。

解决方案

使用 Element.select(_ selector: String)Elements.select(_ selector: String) 方法

do {
    let doc: Document = try SwiftSoup.parse("...")
    let links: Elements = try doc.select("a[href]") // a with href
    let pngs: Elements = try doc.select("img[src$=.png]")
    // img with src ending .png
    let masthead: Element? = try doc.select("div.masthead").first()
    // div with class=masthead
    let resultLinks: Elements? = try doc.select("h3.r > a") // direct a after h3
} catch Exception.Error(let type, let message) {
    print(message)
} catch {
    print("error")
}

描述

SwiftSoup 元素支持一种类似 CSS (或 jQuery) 的选择器语法,用于查找匹配的元素,这使得查询非常强大和健壮。

select 方法可在 DocumentElementElements 中使用。 它是上下文相关的,因此您可以通过从特定元素中选择或通过链接 select 调用来进行过滤。

Select 返回一个 Elements 列表 (作为 Elements),它提供了一系列用于提取和操作结果的方法。

选择器概述

选择器组合

伪选择器

示例

从字符串解析 HTML 文档

let html = "<html><head><title>First parse</title></head><body><p>Parsed HTML into a doc.</p></body></html>"
guard let doc: Document = try? SwiftSoup.parse(html) else { return }

获取所有文本节点

guard let elements = try? doc.getAllElements() else { return html }
for element in elements {
    for textNode in element.textNodes() {
        [...]
    }
}

使用 SwiftSoup 设置 CSS

try doc.head()?.append("<style>html {font-size: 2em}</style>")

获取 HTML 值

let html = "<div class=\"container-fluid\">"
    + "<div class=\"panel panel-default \">"
    + "<div class=\"panel-body\">"
    + "<form id=\"coupon_checkout\" action=\"http://uat.all.com.my/checkout/couponcode\" method=\"post\">"
    + "<input type=\"hidden\" name=\"transaction_id\" value=\"4245\">"
    + "<input type=\"hidden\" name=\"lang\" value=\"EN\">"
    + "<input type=\"hidden\" name=\"devicetype\" value=\"\">"
    + "<div class=\"input-group\">"
    + "<input type=\"text\" class=\"form-control\" id=\"coupon_code\" name=\"coupon\" placeholder=\"Coupon Code\">"
    + "<span class=\"input-group-btn\">"
    + "<button class=\"btn btn-primary\" type=\"submit\">Enter Code</button>"
    + "</span>"
    + "</div>"
    + "</form>"
    + "</div>"
    + "</div>"
guard let doc: Document = try? SwiftSoup.parse(html) else { return } // parse html
let elements = try doc.select("[name=transaction_id]") // query
let transaction_id = try elements.get(0) // select first element
let value = try transaction_id.val() // get value
print(value) // 4245

如何从字符串中删除所有 HTML

guard let doc: Document = try? SwiftSoup.parse(html) else { return } // parse html
guard let txt = try? doc.text() else { return }
print(txt)

如何获取和更新 XML 值

let xml = "<?xml version='1' encoding='UTF-8' something='else'?><val>One</val>"
guard let doc = try? SwiftSoup.parse(xml, "", Parser.xmlParser()) else { return }
guard let element = try? doc.getElementsByTag("val").first() else { return } // Find first element
try element.text("NewValue") // Edit Value
let valueString = try element.text() // "NewValue"

如何获取所有 <img src>

do {
    let doc: Document = try SwiftSoup.parse(html)
    let srcs: Elements = try doc.select("img[src]")
    let srcsStringArray: [String?] = srcs.array().map { try? $0.attr("src").description }
    // do something with srcsStringArray
} catch Exception.Error(_, let message) {
    print(message)
} catch {
    print("error")
}

获取 <a> 的所有 href

let html = "<a id=1 href='?foo=bar&mid&lt=true'>One</a> <a id=2 href='?foo=bar&lt;qux&lg=1'>Two</a>"
guard let els: Elements = try? SwiftSoup.parse(html).select("a") else { return }
for element: Element in els.array() {
    print(try? element.attr("href"))
}

输出

"?foo=bar&mid&lt=true"
"?foo=bar<qux&lg=1"

转义和取消转义

let text = "Hello &<> Å å π 新 there ¾ © »"

print(Entities.escape(text))
print(Entities.unescape(text))


print(Entities.escape(text, OutputSettings().encoder(String.Encoding.ascii).escapeMode(Entities.EscapeMode.base)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.ascii).escapeMode(Entities.EscapeMode.extended)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.ascii).escapeMode(Entities.EscapeMode.xhtml)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.utf8).escapeMode(Entities.EscapeMode.extended)))
print(Entities.escape(text, OutputSettings().charset(String.Encoding.utf8).escapeMode(Entities.EscapeMode.xhtml)))

输出

"Hello &amp;&lt;&gt; Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"


"Hello &amp;&lt;&gt; &Aring; &aring; &#x3c0; &#x65b0; there &frac34; &copy; &raquo;"
"Hello &amp;&lt;&gt; &angst; &aring; &pi; &#x65b0; there &frac34; &copy; &raquo;"
"Hello &amp;&lt;&gt; &#xc5; &#xe5; &#x3c0; &#x65b0; there &#xbe; &#xa9; &#xbb;"
"Hello &amp;&lt;&gt; Å å π 新 there ¾ © »"
"Hello &amp;&lt;&gt; Å å π 新 there ¾ © »"

作者

Nabil Chatbi, scinfu@gmail.com

注意

SwiftSoup 是从 Java Jsoup 库移植到 Swift 的。

许可

SwiftSoup 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。