SwiftScraper

CI Status Documentation percentage License: MIT Latest version platforms supported: macOS | iOS SPM compatible

Swift 的 Web 抓取库。这是 cweatureapps/SwiftScraper 的一个分支,旨在将该库作为 Swift 包提供。

概述

此框架提供了一种简单的方式,以声明式地定义 Swift 中的一系列步骤,这些步骤代表如何抓取网站,从而使应用程序能够读取此网页数据。

特性

教程

在本教程中,我们将通过执行 Google 搜索来介绍此框架的基本用法。

通过 Swift Package Manager 添加包

将此依赖项添加到您的 Package.swift 文件中

.package(url: "https://github.com/Nef10/SwiftScraper.git", .exact("X.Y.Z")),

注意:根据语义版本控制,所有 < 1.0.0 的版本更改都可能是破坏性的,因此现在请使用 .exact

JavaScript 设置

按照惯例,所有步骤都将使用在单个 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
}

此处需要注意的两个主要概念是

这些概念适用于 ProcessStepScriptStepAsyncScriptStep。我们将在接下来的两节中探讨它们。

如果您需要执行异步操作,请改用 AsyncProcessStep

let processStep = AsyncProcessStep { model, completion in
    // perform some custom action here
    completion(model, .proceed)
}

传递模型数据

ProcessStepScriptStepAsyncScriptStep 都具有用于执行处理的处理程序闭包,并且这些处理程序都具有 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 要求用户输入一个值,然后将其保存在模型中。

步骤列表

以下是上面讨论的步骤的完整列表

示例

如果您想阅读更多示例代码

FAQ

我收到错误:“发生 SSL 错误,无法建立与服务器的安全连接。”

应用传输安全 (ATS) 规则也适用于 Web 视图。如果您加载的网站不是 HTTPS,或使用过时的安全协议,macOS / iOS 将拒绝加载它。

快速解决方法是在您的 Info.plist 中添加以下设置来禁用 ATS

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

但是,在未来的某个时候,Apple 可能会要求提交到 App Store 的所有应用都支持 ATS。