SwiftScraper 0.3.0

SwiftScraper 0.3.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布日期最后发布2017年5月
SwiftSwift 版本3.1
SPM支持 SPM

CweatureApps 维护。



  • cweatureapps

Swift 的网络爬取库。

概览

此框架通过在 Swift 中声明性地定义一系列步骤来提供一种简单的方法,这些步骤代表如何抓取网页,允许应用程序读取该网页数据。

功能

  • 声明式 API - 清晰地定义运行步骤并避免 spaghetti 代码🍝使用 WebView 和代理模式带来的代码
  • 自定义 JavaScript 集成 - 简单地与自定义 JavaScript 集成以执行复杂的抓取,使用网络的 语言来处理网页
  • 在每个步骤中进行自定义处理
  • 在步骤之间传递数据
  • 控制流以确定下一个要运行的步骤,允许基本的条件判断和循环

教程

在这个教程中,我们将通过在谷歌网站上执行搜索来覆盖此框架的基本用法。

CocoaPod 集成

在您的 Podfile 中参考此 pod

pod "SwiftScraper", git: "https://github.com/cweatureapps/SwiftScraper.git"

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()
}

运行后,您将看到一个打开谷歌主页的网页视图。

网页视图通常需要有一个可见的框架大小,因为网站通常会使用响应式断点,有时甚至会根据页面尺寸更改 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 参数,用于在步骤之间传递模型数据
  • 返回值,它可以用于控制流

这些概念适用于 ProcessStepScriptStepAsyncScriptStep。我们将在下一节中探讨它们。

传递模型数据

ProcessStepScriptStepAsyncScriptStep 都有一个执行处理的处理闭包,这些处理闭包都有一个类型为 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)

常见问题解答(FAQ)

我遇到了错误:"发生SSL错误,无法与服务器建立安全连接。"

应用程序传输安全(ATS)规则同样适用于网页视图。如果您加载的网站不是HTTPS,或者使用的是过时的安全协议,则iOS将拒绝加载该网站。

快速解决方案是在您的 Info.plist 中设置以下选项来禁用ATS。

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

然而,在未来的某个时刻,苹果可能要求所有提交到App Store的应用程序都支持ATS。

更多信息,请参考以下链接

许可证

SwiftScraper遵循MIT许可证。请参阅LICENSE文件以获取更多信息。