【问题标题】:F# Api crawler using recursion or loop使用递归或循环的 F# Api 爬虫
【发布时间】:2020-07-29 08:04:24
【问题描述】:

我正在尝试构建一个简单的 API 爬虫,它会遍历分页内容并返回聚合结果。这是我第一次尝试编写递归函数:

namespace MyProject

open FSharp.Data

type MyJson = JsonProvider<"./Resources/Example.json">

module ApiClient = 
    let rec getAllPagesRec baseUrl (acc: MyJson.ItemList[]) (page: int) = 
        async {
            let pageUrl = baseUrl + "&page=" + page.ToString()
            let! response = Http.AsyncRequestString(pageUrl)
            let content = MyJson.Parse(response)

            let mergedArray = Array.concat [ content.ItemList; acc ]

            if content.PageNumber < content.PageCount
            then return! getAllPagesRec baseUrl mergedArray (page+1)
            else return mergedArray
        }

但是,我在Microsoft Docs 中读到这样的代码浪费了内存和处理器时间。所以我将我的函数重写为一个循环:

    let getAllPagesLoop baseUrl =
        async {
        let mutable continueLoop = true
        let mutable page = 1
        let mutable acc = [||]
        
        while continueLoop do
            let pageUrl = baseUrl + "&page=" + page.ToString()
            let! response = Http.AsyncRequestString(pageUrl)
            let content = MyJson.Parse(response)
            acc <- Array.concat [ content.ItemList; acc ]
            
            if content.PageNumber < content.PageCount
            then page <- page + 1
            else continueLoop <- false
        
        return acc
    }

第二个函数看起来更像 C#-y 并且包含很多突变,这似乎与 F# 的哲学相矛盾。有没有其他方法可以“优化”第一个功能?也许使用yield/yield! 关键字?还是两个功能都足够好?

【问题讨论】:

    标签: f#


    【解决方案1】:

    您的第一段代码很好*。 Microsoft 文档所说的是避免使用递归依赖于先前值的递归。斐波那契定义为

    Fib n = fin (n-1) + Fib(n-2)
    

    因此,如果您计算 Fib 5,您就可以了

    Fib 4 + Fib 3
    

    但是当你做 Fib 4 时,你会做

    Fib 3 + Fib 2
    

    这意味着您正在再次计算Fib 3Fib 2. 的同义词对于较大的 n 会有很多重新计算。他们的意思是在这些场景中,您希望缓存这些结果。

    在您的场景中,大概第 1 页有指向第 10、11、12 页的链接,但没有指向第 1 页,第 10 页有指向第 100,101,102 页的链接,但没有指向第 1..、10.. 等页面的链接。那么就没有浪费了计算,因为你没有重新计算任何东西。

    如果不是这种情况并且可能存在循环链接,那么您需要跟踪访问过的页面,例如传递一个包含访问过的页面的列表,每次访问时将访问过的页面添加到列表中,如果列表已经在访问过的列表中,则避免获取该列表。

    另一方面,您的代码可能没有利用并行化。如果您可以提前获得页数(可能是第一页的一次调用)。您可以像这样并行下载页面:

    let getPage baseUrl (page: int) : MyJson.ItemList[] = 
        async {
            let pageUrl = baseUrl + "&page=" + page.ToString()
            let! response = Http.AsyncRequestString(pageUrl)
            let content = MyJson.Parse(response)
    
            return content.ItemList
        }
    
     [| 1..nPages |]
     |>Array.map (getPage baseUrl)
     |>Async.Parallel
     |>Async.RunSynchronously
     |>Array.concat
    

    【讨论】:

    • 感谢您的解释。我认为 Microsoft Docs 应该更明确地说明它们所指的内容,因为目前它们可能给人的印象是所有递归函数都对性能不利。顺便问一下,fine后面的星号是什么意思?
    • @Storm 我担心并行化只是用我的想法更新了我的答案。
    • 我刚刚阅读了编辑后的帖子。非常感谢,我一定会添加并行化。
    猜你喜欢
    • 2019-04-28
    • 1970-01-01
    • 2018-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多