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:按标签查找元素,例如 ans|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#logoel.class:具有类的元素,例如 div.mastheadel[attr]:具有属性的元素,例如 a[href]a[href].highlightchild:从祖先元素派生的子元素,例如 .body p 查找类为“body”的块下的任何位置的 p 元素parent > child:直接从父元素派生的子元素,例如 div.content > p 查找 p 元素;body > * 查找 body 标签的直接子元素siblingA + siblingB:查找紧跟在 sibling A 元素后面的 sibling B 元素,例如 div.head + divsiblingA ~ siblingX:查找在 sibling A 元素之后的 sibling X 元素,例如 h1 ~ pel, 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 文件。