SwiftSoup 2.7.3

SwiftSoup 2.7.3

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最后发布2024 年 7 月
SPM支持 SPM

Nabil Chatbi 维护。



SwiftSoup 2.7.3

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。

  • 从 URL、文件或字符串中抓取和解析 HTML
  • 使用 DOM 遍历或 CSS 选择器查找和提取数据
  • 操作 HTML 元素、属性和文本
  • 通过安全白名单清除用户提交的内容,以防止 XSS 攻击
  • 输出整洁的 HTML SwiftSoup 设计来处理野外遇到的各类 HTML;从纯净和验证的,到无效的标签汤;SwiftSoup 将创建一个合理的解析树。

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"]),
]
...

Try

尝试简单的在线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")
}
  • 未关闭的标签(例如 <p>Lorem <p>Ipsum 解析为 <p>Lorem</p> <p>Ipsum</p>
  • 隐含的标签(例如一个裸露的 <td>Table data</td> 包裹在 <table><tr><td>... 中)
  • 可靠地创建文档结构(含 headbodyhtml,头部中只包含合适的元素)

文档的对象模型

  • 文档由元素和文本节点组成
  • 继承链是:Document 扩展 Element 扩展 Node.TextNode 扩展 Node
  • 一个元素包含子节点的列表,并且有一个父元素。它们还提供了一个仅包含子元素的筛选列表。

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

问题

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

解决方案

  • 要获取属性值,请使用Node.attr(_ String key)方法
  • 对于元素上的文本(及其合并后的子元素),请使用Element.text()
  • 对于HTML,请使用Element.html()或适当的Node.outerHtml()
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")
}

描述

上述方法是获取元素数据的核心。还有其他方法

  • Element.id()
  • Element.tagName()
  • Element.className()Element.hasClass(_ String className)

这些获取器方法都有相应的设置器方法,用于更改数据。

从字符串中解析一个文档

问题

你在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。基础URI参数用于将相对URL解析为绝对URL,并应设置为由文档获取的URL。如果不适用,或者您知道HTML有一个基础元素,则可以使用 parse(_ html: String) 方法。

只要传入非空字符串,就有保证解析成功,并得到一个包含(至少)headbody 元素的Document。

一旦你有了 Document,你可以使用Document以及其超类ElementNode中的相应方法来访问数据。

解析HTML片段

问题

你有HTML片段(例如包含几个div标签的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方法创建了一个空的文档外壳,并将在body元素中插入解析后的HTML。如果你使用正常的SwiftSoup(_ html: String)方法,通常你会得到相同的结果,但要明确将输入视为body片段,确保用户提供的任何非标准HTML都能被解析到body元素中。

Document.body()方法检索文档body元素的子元素;它与doc.getElementsByTag("body")等价。

保持安全

如果您打算从用户那里接受 HTML 输入,您需要小心以避免跨站脚本攻击。请参阅基于 白名单 的清理器的文档,并使用 clean(String bodyHtml, Whitelist whitelist) 清理输入。

清除未经信任的 HTML(以防止 XSS 攻击)

问题

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

解决方案

使用由 白名单 指定配置的 SwiftSoup HTML 清理器

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

讨论

针对您网站的跨站脚本攻击可能会让您的一天变得非常糟糕,更不用说您的用户了。许多网站通过不允许用户提交的内容中有HTML来避免XSS攻击:他们只强制使用纯文本,或者使用类似的wiki-text或Markdown这样的替代标记语法。这些对于用户来说往往不是最佳解决方案,因为它们降低了表达性,并迫使用户学习新的语法。

更好的解决方案可能是使用富文本WYSIWYG编辑器(如CKEditorTinyMCE)。这些编辑器会输出HTML,并允许用户直观地进行操作。然而,它们的验证是在客户端进行的:您需要应用服务器端验证来清理输入并确保HTML可以放置到您的网站上。否则,攻击者可以绕过客户端JavaScript验证并将不安全的HTML直接注入到您的网站上。

SwiftSoup白名单净化器通过解析输入HTML(在一个安全、沙盒环境中)工作,然后迭代解析树,只允许已知安全的标签和属性(以及值)通过到清理后的输出中。

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

SwiftSoup提供了一系列适合大多数需求的Whitelist配置;如果需要可以修改它们,但请注意。

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

另请参阅

  • 请参阅XSS作弊单和筛选规避指南,了解正则表达式过滤器为何不起作用,以及为什么基于安全的白名单解析器的净化器是正确的方法。
  • 如果您希望返回一个Document而不是一个字符串,请参阅Cleaner参考。
  • 为了获取不同的示例选项并创建自定义白名单,请参阅Whitelist参考。
  • nofollow链接属性

设置属性值

问题

您有一个需要更新属性值以将其保存到磁盘或发送为一个 HTTP 响应的解析文档。

解决方案

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

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

例如,要将 rel="nofollow" 属性添加到 div 内部每个 a 元素上的 Elements 集合,请使用以下批量属性和类方法。

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设置方法

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.html(_ html: String)清除元素中现有的任何内部HTML,将其替换为解析后的HTML。
  • Element.prepend(_ first: String)Element.append(_ last: String)分别用于向元素的内部HTML的开始或末尾添加HTML。
  • Element.wrap(_ around: String)将HTML包裹在元素的内部HTML外部。

另请参阅

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

设置元素文本内容

问题

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

解决方案

使用 Element 的文本设置方法

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

讨论

文本设置方法与 [[HTML设置器|设置元素的HTML]] 方法相呼应

  • Element.text(_ text: String) 会在元素中清除任何现有的内部HTML,并将其替换为提供的文本。
  • Element.prepend(_ first: String)Element.append(_ last: String) 分别在元素的内部HTML的开始或末尾添加文本节点,提供的文本应为未编码的:字符如 <> 等,将被视为字面量,而不是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")
}

描述

元素提供了一系列类似DOM的方法来查找元素,提取和操纵其数据。DOM获取器是上下文相关的:在父文档上调用时,它们在文档中查找匹配的元素;在子元素上调用时,它们在该子元素下查找元素。这样,你就可以聚焦于所需的数据。

查找元素

  • getElementById(_ id: String)
  • getElementsByTag(_ tag:String)
  • getElementsByClass(_ className: String)
  • getElementsByAttribute(_ key: String) (和相关方法)
  • 元素兄弟关系: siblingElements()firstElementSibling()lastElementSibling()nextElementSibling()previousElementSibling()
  • 图形: parent()children()child(_ index: Int)

元素数据

  • attr(_ key: Strin) 用于获取,以及 attr(_ key: String, _ value: String) 用于设置属性
  • attributes() 用于获取所有属性
  • id()className()classNames()
  • text() 用于获取和 text(_ value: String) 用于设置文本内容
  • html() 用于获取和 html(_ value: String) 用于设置内部 HTML 内容
  • outerHtml() 用于获取外部 HTML 值
  • data() 用于获取数据内容(例如 script 和 style 标签)
  • tag()tagName()

操作 HTML 和文本

  • append(_ html: String)prepend(html: String)
  • appendText(text: String)prependText(text: String)
  • appendElement(tagName: String)prependElement(tagName: String)
  • html(_ value: String)

使用选择器语法查找元素

问题

您想使用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返回一个Elements列表(作为Elements),它提供了一系列方法来提取和操作结果。

选择器概述

  • tagname:根据标记查找元素,例如a
  • ns|tag:在特定命名空间中根据标记查找元素,例如fb|name查找<fb:name>元素
  • #id:根据ID查找元素,例如#logo
  • .class:根据类名查找元素,例如.masthead
  • [attribute]:具有属性的元素,例如[href]
  • [^attr]:具有属性名前缀的元素,例如[^data-]查找具有HTML5 dataset属性的元素
  • [attr=value]:具有属性值的元素,例如[width=500](也可引用,如[data-name='启动序列']
  • [attr^=value][attr$=value][attr*=value]:具有以、以…结尾或包含特定值的属性的元素,例如[href*=/path/]
  • [attr~=regex]:具有与正则表达式匹配的属性值的元素;例如img[src~=(?i)\.(png|jpe?g)]
  • *:所有元素,例如*

选择器组合

  • el#id:具有ID的元素,例如div#logo
  • el.class:具有类的元素,例如div.masthead
  • el[attr]:具有属性的元素,例如a[href]
  • 任何组合,例如a[href].高亮
  • 祖先:从祖先衍生下来的子元素,例如.body p查找位于类名为"body"的块中的任何

    元素

  • parent > child:从父元素直接衍生下来的子元素,例如div.content > p查找p元素;以及body > *查找body标签的直接子元素
  • siblingA + siblingB:查找紧随siblingA后面的siblingB元素,例如div.head + div
  • siblingA ~ siblingX:查找由siblingA precedes的siblingX元素,例如h1 ~ p
  • elelel:分组多个选择器,找到匹配任何选择器的唯一元素;例如div.mastheaddiv.logo

伪选择器

  • :lt(n): 查找兄弟元素的索引(即相对于其父元素在DOM树中的位置)小于n的元素;例如:td:lt(3)
  • :gt(n): 查找兄弟元素的索引大于n的元素;例如:div p:gt(2)
  • :eq(n): 查找兄弟元素的索引等于n的元素;例如:form input:eq(1)
  • :has(selector): 查找包含符合选择器的元素的元素;例如:div:has(p)
  • :not(selector): 查找不符合选择器的元素;例如:div:not(.logo)
  • :contains(text): 查找包含给定文本的元素。搜索不区分大小写;例如:p:contains(swiftsoup)
  • :containsOwn(text): 查找直接包含给定文本的元素
  • :matches(regex): 查找文本与指定的正则表达式匹配的元素;例如:div:matches((?i)login)
  • :matchesOwn(regex): 查找自身文本与指定的正则表达式匹配的元素
  • 注意,上述索引伪选择器是从0开始的,也就是说,第一个元素索引为0,第二个为1,以此类推

示例

从字符串解析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")
}

获取所有 href<a>

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 ¾ © »"

作者

纳比勒·沙特比,[email protected]

注释

SwiftSoup是从JavaJsoup库移植到Swift的。

许可证

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