测试已测试 | ✓ |
语言语言 | SwiftSwift |
许可证 | MIT |
发布日期最后发布 | 2017年5月 |
SwiftSwift 版本 | 3.1 |
SPM支持 SPM | ✗ |
由 CweatureApps 维护。
Swift 的网络爬取库。
此框架通过在 Swift 中声明性地定义一系列步骤来提供一种简单的方法,这些步骤代表如何抓取网页,允许应用程序读取该网页数据。
在这个教程中,我们将通过在谷歌网站上执行搜索来覆盖此框架的基本用法。
在您的 Podfile 中参考此 pod
pod "SwiftScraper", git: "https://github.com/cweatureapps/SwiftScraper.git"
按照约定,所有步骤都将使用在单个模块中公开的函数,该模块在单个 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()
}
运行后,您将看到一个打开谷歌主页的网页视图。
网页视图通常需要有一个可见的框架大小,因为网站通常会使用响应式断点,有时甚至会根据页面尺寸更改 HTML 结构。
insertWebViewIntoView
方法可以帮助您轻松地将网页视图插入到您拥有的任何UIView
中。设置父视图尺寸由您自己决定,或者您甚至可以将其隐藏在用户看不见的地方。
我们可以在页面加载时运行一些断言代码,以确保加载的页面是预期的。我们可以通过引用模块暴露的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 对象的 state
属性来观察执行进度。
stepRunner.state.afterChange.add { change in
print("-----", change.newValue, "-----")
switch change.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以提交谷歌搜索。这是 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对象的单一对象。
在此示例中,我们将进行谷歌图片搜索,然后滚动到页面底部。这里使用的无限滚动模式在我们这样做时将加载更多图片,并且我们将统计滚动前后的图片数量。
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
。我们将在下一节中探讨它们。
ProcessStep
、ScriptStep
和 AsyncScriptStep
都有一个执行处理的处理闭包,这些处理闭包都有一个类型为 inout JSON
的模型参数。修改这个 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)
会因错误而停止执行。
一个等待设定时间的步骤。
let waitStep = WaitStep(waitTimeInSeconds: 0.5)
这是一个等待条件变为真之前继续执行的步骤,或者如果条件在超时时仍然为假,它将失败。
在此示例中,iOS 代码将反复调用 JavaScript 函数 testThatStuffIsReady
,一旦返回真,就会继续执行;如果在两秒钟内没有返回真,则会由于超时而失败。
let waitForConditionStep = WaitForConditionStep(
assertionName: "testThatStuffIsReady",
timeoutInSeconds: 2)
我遇到了错误:"发生SSL错误,无法与服务器建立安全连接。"
应用程序传输安全(ATS)规则同样适用于网页视图。如果您加载的网站不是HTTPS,或者使用的是过时的安全协议,则iOS将拒绝加载该网站。
快速解决方案是在您的 Info.plist
中设置以下选项来禁用ATS。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
然而,在未来的某个时刻,苹果可能要求所有提交到App Store的应用程序都支持ATS。
更多信息,请参考以下链接
SwiftScraper遵循MIT许可证。请参阅LICENSE文件以获取更多信息。