SwiftSoup
是一个纯 Swift 库,跨平台(macOS、iOS、tvOS、watchOS 和 Linux!),用于处理真实世界的 HTML。它提供了一个非常方便的 API,可以使用 DOM、CSS 和类似 jQuery 的方法的最佳实践来提取和操作数据。SwiftSoup
实现了 WHATWG HTML5 规范,并将 HTML 解析为与现代浏览器相同的 DOM。
SwiftSoup
旨在处理在野外发现的各种 HTML;从原始和验证良好的,到无效的标签汤;SwiftSoup
将创建一个合理的解析树。Swift 5 >=2.0.0
Swift 4.2 1.7.4
SwiftSoup 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile 中
pod 'SwiftSoup'
SwiftSoup 也可通过 Carthage 获得。要安装它,只需将以下行添加到您的 Cartfile 中
github "scinfu/SwiftSoup"
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"]),
]
...
pod try SwiftSoup
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>...
中)html
包含一个 head
和一个 body
,并且只有适当的元素在 head 中)Document
扩展 Element
扩展 Node.TextNode
扩展 Node
。解析文档并找到一些元素后,您需要获取这些元素内部的数据。
Node.attr(_ String key)
方法Element.text()
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)
所有这些访问器方法都具有相应的 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
及其父类 Element
和 Node
中的适当方法来访问数据。
您有一个正文 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 以在您的网站上输出(例如,作为评论提交)。 您需要清理此 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 编辑器(如 CKEditor 或 TinyMCE)。 这些输出 HTML,并允许用户以可视方式工作。 但是,它们的验证是在客户端完成的:您需要应用服务器端验证来清理输入并确保 HTML 可以安全地放置在您的网站上。 否则,攻击者可以避免客户端 Javascript 验证,并将不安全的 HMTL 直接注入到您的网站中
SwiftSoup 白名单 sanitizer 的工作方式是解析输入的 HTML(在安全、沙盒化的环境中),然后迭代解析树,并且只允许已知的安全标签和属性(以及值)进入清理后的输出。
它不使用正则表达式,这不适合此任务。
SwiftSoup 提供了一系列 Whitelist
配置,以满足大多数要求; 如果需要,可以修改它们,但请小心。
cleaner 不仅可用于避免 XSS,还可用于限制用户可以提供的元素范围:您可能可以接受文本 a
、strong
元素,但不能接受结构 div
或 table
元素。
Document
而不是 String 返回,请参阅 Cleaner
参考Whitelist
参考nofollow
链接属性您有一个已解析的文档,您想要更新其上的属性值,然后再将其保存到磁盘或作为 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。
使用 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.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
的文本 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 > 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")
}
文本 setter 方法镜像了 [[HTML setter|设置元素的 HTML]] 方法
Element.text(_ text: String)
清除元素中任何现有的内部 HTML,并将其替换为提供的文本。Element.prepend(_ first: String)
和 Element.append(_ last: String)
分别将文本节点添加到元素内部 HTML 的开头或结尾。 文本应以未编码的形式提供:<
、>
等字符将被视为文字,而不是 HTML。您有一个 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")
}
Elements 提供了一系列类似 DOM 的方法来查找元素,并提取和操作其数据。 DOM getter 是上下文相关的:在父 Document 上调用它们会找到文档下的匹配元素; 在子元素上调用它们会找到该子元素下的元素。 这样,您就可以在您想要的数据中进行窗口化。
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()
获取数据内容(例如,脚本和样式标签)tag()
和 tagName()
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 调用来进行过滤。
Select 返回一个 Elements
列表 (作为 Elements
),它提供了一系列用于提取和操作结果的方法。
tagname
:按标签查找元素,例如 a
ns|tag
:按命名空间中的标签查找元素,例如 fb|name
查找 <fb:name>
元素#id
:按 ID 查找元素,例如 #logo
.class
:按类名查找元素,例如 .masthead
[attribute]
:具有属性的元素,例如 [href]
[^attr]
:具有属性名称前缀的元素,例如 [^data-]
查找具有 HTML5 数据集属性的元素[attr=value]
:具有属性值的元素,例如 [width=500]
(也可以引用,例如 [data-name='launch sequence']
)[attr^=value]
, [attr$=value]
, [attr*=value]
:属性以 value 开头、结尾或包含 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].highlight
child
:从祖先元素派生的子元素,例如 .body p
查找类为“body”的块下的任何位置的 p
元素parent > child
:直接从父元素派生的子元素,例如 div.content > p
查找 p 元素;body > *
查找 body 标签的直接子元素siblingA + siblingB
:查找紧跟在 sibling A 元素后面的 sibling B 元素,例如 div.head + div
siblingA ~ siblingX
:查找在 sibling A 元素之后的 sibling X 元素,例如 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)
:查找其自身的文本与指定的正则表达式匹配的元素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() {
[...]
}
}
try doc.head()?.append("<style>html {font-size: 2em}</style>")
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
guard let doc: Document = try? SwiftSoup.parse(html) else { return } // parse html
guard let txt = try? doc.text() else { return }
print(txt)
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"
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")
}
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 ¾ © »"
Nabil Chatbi, scinfu@gmail.com
SwiftSoup 是从 Java Jsoup 库移植到 Swift 的。
SwiftSoup 在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。