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选择器网站
尝试示例项目,打开终端并输入
pod try 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>...
中) - 可靠地创建文档结构(含
head
和body
的html
,头部中只包含合适的元素)
文档的对象模型
- 文档由元素和文本节点组成
- 继承链是:
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)
方法。
只要传入非空字符串,就有保证解析成功,并得到一个包含(至少)head
和 body
元素的Document。
一旦你有了 Document
,你可以使用Document
以及其超类Element
和Node
中的相应方法来访问数据。
解析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编辑器(如CKEditor或TinyMCE)。这些编辑器会输出HTML,并允许用户直观地进行操作。然而,它们的验证是在客户端进行的:您需要应用服务器端验证来清理输入并确保HTML可以放置到您的网站上。否则,攻击者可以绕过客户端JavaScript验证并将不安全的HTML直接注入到您的网站上。
SwiftSoup白名单净化器通过解析输入HTML(在一个安全、沙盒环境中)工作,然后迭代解析树,只允许已知安全的标签和属性(以及值)通过到清理后的输出中。
它不使用正则表达式,这是不适用于此任务的。
SwiftSoup提供了一系列适合大多数需求的Whitelist
配置;如果需要可以修改它们,但请注意。
净化器不仅有助于避免XSS,还可以限制用户可以提供的元素范围:您可能可以接受文本的a
、strong
等元素,但不能接受结构性的div
或table
元素。
另请参阅
- 请参阅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 > four</div>
try div.prepend("First ")
try div.append(" Last")
// now: <div>First five > 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<=true'>One</a> <a id=2 href='?foo=bar<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
方法在 Document
、Element
或 Elements
中可用。它是上下文相关的,因此您可以从特定元素中进行选择,或者通过连接选择调用。
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
el
、el
、el
:分组多个选择器,找到匹配任何选择器的唯一元素;例如div.masthead
、div.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<=true'>One</a> <a id=2 href='?foo=bar<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<=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 &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
"Hello &<> Å å π 新 there ¾ © »"
作者
纳比勒·沙特比,[email protected]
注释
SwiftSoup是从JavaJsoup库移植到Swift的。
许可证
SwiftSoup在MIT许可证下可用。有关更多信息,请参阅LICENSE文件。