Swift 的 Web 抓取库。这是 cweatureapps/SwiftScraper 的一个分支,旨在将该库作为 Swift 包提供。
此框架提供了一种简单的方式,以声明式地定义 Swift 中的一系列步骤,这些步骤代表如何抓取网站,从而使应用程序能够读取此网页数据。
在本教程中,我们将通过执行 Google 搜索来介绍此框架的基本用法。
将此依赖项添加到您的 Package.swift
文件中
.package(url: "https://github.com/Nef10/SwiftScraper.git", .exact("X.Y.Z")),
注意:根据语义版本控制,所有 < 1.0.0 的版本更改都可能是破坏性的,因此现在请使用 .exact
按照惯例,所有步骤都将使用在单个 JavaScript 文件中定义的单个模块中公开的函数。
对于本练习,创建一个名为 GoogleSearch.js
的新文件。
首先创建空白的 JavaScript 模块结构,确保模块名称与文件名相同
var GoogleSearch = (function() {
return {
};
})()
创建一个新的视图控制器。
导入框架
import SwiftScraper
在视图控制器中,我们将创建一个步骤并运行它
var stepRunner: StepRunner!
override func viewDidLoad() {
super.viewDidLoad()
let step1 = OpenPageStep(path: "https://www.google.com")
stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1])
stepRunner.insertWebViewIntoView(parent: view)
stepRunner.run()
}
当您运行此代码时,您将看到一个 Web 视图打开 Google 首页。
Web 视图通常需要具有可见的框架大小,因为网站通常使用响应式断点,甚至有时会根据页面的尺寸更改 HTML 结构。
insertWebViewIntoView
方法帮助您轻松地将 Web 视图插入到您拥有的任何NSView
/UIView
中,同时自动将其约束为相同的大小。是否设置父视图的尺寸取决于您,您甚至可以将其隐藏起来,让用户看不到。
我们可以添加一个断言,在页面加载时运行一些 JavaScript 代码,以确保加载的页面是预期的页面。我们可以通过引用模块公开的 JavaScript 函数来做到这一点。
在 GoogleSearch.js
文件中,添加以下函数,该函数将仅检查页面标题是否正确。
var GoogleSearch = (function() {
function assertGoogleTitle() {
return document.title == "Google";
}
return {
assertGoogleTitle: assertGoogleTitle
};
})()
在您创建步骤的视图控制器中,包含断言函数的名称
let step1 = OpenPageStep(path: "https://www.google.com", assertionName: "assertGoogleTitle")
断言函数在页面加载时立即运行。有时,您要断言的内容可能在页面加载时尚未准备好,因为网站可能会在加载后异步修改页面。在这种情况下,请查看高级用法,以添加一个步骤,该步骤等待页面加载完成。
您可以通过向 stepRunner 添加 stateObserver
来观察执行进度。
stepRunner.stateObservers.append { newValue in
print("-----", newValue, "-----")
switch newValue {
case .inProgress(let index):
print("About to run step at index", index)
case .failure(let error):
print("Failed: \(error.localizedDescription)")
case .success:
print("Finished successfully")
default:
break
}
}
stepRunner.run()
现在,让我们运行一些自定义 JavaScript 来提交 Google 搜索。这是 PageChangeStep
,它运行一些 JavaScript,这将导致加载新页面。当页面加载时,它将继续执行下一步。
首先,在 GoogleSearch.js
文件中,添加以下 2 个函数来执行搜索,并在模块中公开它们
var GoogleSearch = (function() {
// ...
function performSearch(searchText) {
document.querySelector('input[type="text"], input[type="Search"]').value = searchText;
document.forms[0].submit();
}
function assertSearchResultTitle() {
return document.title == "SwiftScraper iOS - Google Search";
}
return {
assertGoogleTitle: assertGoogleTitle,
performSearch: performSearch,
assertSearchResultTitle: assertSearchResultTitle
};
})()
在视图控制器中,添加步骤 2,即 PageChangeStep
,引用您刚刚实现的 JavaScript 函数
let step2 = PageChangeStep(functionName: "performSearch", params: "SwiftScraper iOS", assertionName: "assertSearchResultTitle")
请注意初始化器中的 params
参数,它允许您将数据传递给 JavaScript 函数。
在创建 StepRunner
时,请确保将其包含在步骤数组中
stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1, step2])
我们来到了最后一步 - 我们可以运行一个脚本来抓取页面的内容。添加以下 JavaScript 函数,该函数将获取搜索结果,并返回包含每个链接的文本和 href 的 JSON 对象数组。
var GoogleSearch = (function() {
// ...
function getSearchResults() {
var headings = document.querySelectorAll('h3.r');
return Array.prototype.slice.call(headings).map(function (h3) {
return { 'text': h3.innerText, 'href': h3.childNodes[0].href };
});
}
return {
assertGoogleTitle: assertGoogleTitle,
performSearch: performSearch,
assertSearchResultTitle: assertSearchResultTitle,
getSearchResults: getSearchResults
};
})()
在 Swift 代码中,添加第 3 步,即 ScriptStep
,这是一个运行 JavaScript 函数并返回该函数返回的响应的步骤。
let step3 = ScriptStep(functionName: "getSearchResults") { response, _ in
if let responseArray = response as? [JSON] {
responseArray.forEach { json in
if let text = json["text"], let href = json["href"] {
print(text, "(", href, ")")
}
}
}
return .proceed
}
在创建 StepRunner
时,请确保将其包含在步骤数组中
stepRunner = StepRunner(moduleName: "GoogleSearch", steps: [step1, step2, step3])
运行它。您应该看到步骤成功完成,并将搜索结果打印到控制台。
恭喜!您已完成本库基本用法的教程! 🎉
可以运行一些不会立即返回的 JavaScript,并等待它在一段时间后异步回调 Swift 代码。例如,您可能需要在网页上执行某些操作,轮询操作是否完成,然后将数据传递回 Swift。
要将数据传递回 Swift 世界,请调用 SwiftScraper.postMessage()
,传递一个可以序列化回 Swift 对象的单个对象。
在本例中,我们将进行 Google 图片搜索,然后向下滚动到底部。此处使用的无限滚动模式将在我们执行此操作时加载更多图像,我们将计算滚动前后的图像数量。
var GoogleSearch = (function() {
// ...
function scrollAndCountImages() {
var firstCount = document.querySelectorAll('img').length;
window.scrollTo(0, document.body.scrollHeight);
setTimeout(function () {
var secondCount = document.querySelectorAll('img').length;
SwiftScraper.postMessage({'first': firstCount, 'second': secondCount});
}, 2000);
}
return {
assertGoogleTitle: assertGoogleTitle,
performSearch: performSearch,
assertSearchResultTitle: assertSearchResultTitle,
getSearchResults: getSearchResults,
scrollAndCountImages: scrollAndCountImages
};
})()
对于熟悉
WKWebView
的人来说,SwiftScraper.postMessage()
函数只是webkit.messageHandlers.swiftScraperResponseHandler.postMessage()
的别名
在 Swift 中,使用 AsyncScriptStep
,它的使用方式与 ScriptStep
相同,不同之处在于,直到调用 SwiftScraper.postMessage
后才调用处理程序。预期 JavaScript 函数本身不返回任何内容。
let step1 = OpenPageStep(path: "https://www.google.com.au/search?tbm=isch")
let step2 = PageChangeStep(
functionName: "performSearch",
params: "ankylosaurus")
let step3 = AsyncScriptStep(functionName: "scrollAndCountImages") { response, _ in
if let json = response as? JSON {
if let first = json["first"], let second = json["second"] {
print("first: ", first, "second: ", second)
}
}
return .proceed
}
当您需要一个需要执行一些自定义操作的步骤时,请使用 ProcessStep
。
let processStep = ProcessStep { model in
// perform some custom action here
return .proceed
}
此处需要注意的两个主要概念是
model
参数,用于在步骤之间传递模型数据这些概念适用于 ProcessStep
、ScriptStep
和 AsyncScriptStep
。我们将在接下来的两节中探讨它们。
如果您需要执行异步操作,请改用 AsyncProcessStep
let processStep = AsyncProcessStep { model, completion in
// perform some custom action here
completion(model, .proceed)
}
ProcessStep
、ScriptStep
和 AsyncScriptStep
都具有用于执行处理的处理程序闭包,并且这些处理程序都具有 inout JSON
类型的 model 参数。修改此 JSON 字典以在一个步骤中保存数据,然后在另一个步骤中读取它。
让我们修改上一节中的 AsyncScriptStep
,将滚动前后的计数保存到字典中。
let step3 = AsyncScriptStep(functionName: "scrollAndCountImages") { response, model in // notice the model param
if let json = response as? JSON {
if let first = json["first"], let second = json["second"] {
print("first: ", first, "second: ", second)
// Save the data to the model dictionary
model["first"] = first
model["second"] = second
}
}
return .proceed
}
返回值是一个枚举,可用于基本的控制流程。我们已经看到了 .proceed
,它表示转到下一步。.jumpToStep(n)
允许您跳转到另一个步骤,无论是在当前步骤之前还是之后。这允许您定义循环(通过向后跳转)以及条件(通过向前跳转)。
让我们继续无限滚动图像搜索示例,并添加一个 ProcessStep
,它将保持循环回到 step3
,直到滚动前计数和滚动后计数相同,这意味着页面上没有更多图像要加载。
将此步骤添加为要运行的最后一步。当您运行此代码时,您应该看到屏幕不断向下滚动,直到找不到更多图像为止。
let conditionStep = ProcessStep { model in
if let first = model["first"] as? Int,
let second = model["second"] as? Int,
first == second {
return .proceed
} else {
return .jumpToStep(2) // This is a zero-based index, i.e. step3
}
}
此技术对于重复一系列步骤最有用。虽然它也可以用于模拟 IF-THEN 样式的条件,但它本质上是一个
GOTO
结构,很容易导致难以维护的意大利面条🍝步骤。
您还可以提前退出步骤。.finish
的返回值将停止执行并显示成功,而 .failure(Error)
将停止执行并显示失败。
一个下载给定 URL 的内容并将其作为字符串返回的步骤。
let downloadCSV = DownloadStep(url: myURL) { response, model in
model["fileContent"] = response as? String // For example, save the content into the model
return .proceed // you can use any control flow logic you like (see above)
}
如果您想从非 HTML 页面(如 CSV 文档)获取内容,则此步骤很有帮助。在 HTML 文档上,您可以使用普通的 ScriptStep
来读取文档内容,但是 JavaScript 无法在 CSV 等文档上运行。
一个等待设定时间段的步骤。
let waitStep = WaitStep(waitTimeInSeconds: 0.5)
这是一个等待条件变为真才继续的步骤,如果超时发生时条件仍然为假,则会失败。
在本例中,iOS 代码将重复调用 JavaScript 函数 testThatStuffIsReady
,一旦它返回 true 就继续,或者如果在 2 秒内未返回 true 则超时失败。
let waitForConditionStep = WaitForConditionStep(
assertionName: "testThatStuffIsReady",
timeoutInSeconds: 2)
对于接受 params
的步骤,您可以选择使用 paramsKeys
并传入模型键的数组。然后,JavaScript 函数将接收与模型字典中此键的当前值相对应的参数。如果创建步骤时参数的值尚不清楚,则可以使用此方法。例如,使用 AsyncProcessStep
要求用户输入一个值,然后将其保存在模型中。
以下是上面讨论的步骤的完整列表
OpenPageStep
- 加载页面,并可选择执行 JavaScript 断言函数以查看页面是否正确加载PageChangeStep
- 调用一个 JavaScript 函数,该函数应导航到不同的页面。可选择执行 JavaScript 断言函数以查看新页面是否正确加载ScriptStep
- 运行 JavaScript 函数并返回返回值AsyncScriptStep
- 运行异步 JavaScript 函数并返回返回值ProcessStep
- 运行 Swift 代码,允许您在抓取器外部执行操作或修改控制流程AsyncProcessStep
- 运行 Swift 代码,允许您在抓取器外部执行异步操作或修改控制流程DownloadStep
- 从 URL 下载内容并将其作为字符串返回WaitStep
- 等待固定的秒数WaitForConditionStep
- 重复调用 JavaScript 函数,直到它返回 true(或超时)如果您想阅读更多示例代码
我收到错误:“发生 SSL 错误,无法建立与服务器的安全连接。”
应用传输安全 (ATS) 规则也适用于 Web 视图。如果您加载的网站不是 HTTPS,或使用过时的安全协议,macOS / iOS 将拒绝加载它。
快速解决方法是在您的 Info.plist
中添加以下设置来禁用 ATS
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
但是,在未来的某个时候,Apple 可能会要求提交到 App Store 的所有应用都支持 ATS。