【问题标题】:How to design a "web spider" with state in Haskell?如何在 Haskell 中设计一个带状态的“网络蜘蛛”?
【发布时间】:2011-01-19 07:23:15
【问题描述】:

经过多年的 OOP,我正在学习 Haskell。

我正在编写一个功能和状态很少的愚蠢网络蜘蛛。
我不确定如何在 FP 世界中正确地做到这一点。

在 OOP 世界中,这个蜘蛛可以这样设计(按用法):

Browser b = new Browser()
b.goto(“http://www.google.com/”)

String firstLink = b.getLinks()[0]

b.goto(firstLink)
print(b.getHtml())

这段代码加载http://www.google.com/,然后“点击”第一个链接,加载第二页的内容,然后打印内容。

class Browser {
   goto(url: String) : void // loads HTML from given URL, blocking
   getUrl() : String // returns current URL
   getHtml() : String // returns current HTML
   getLinks(): [String] // parses current HTML and returns a list of available links (URLs)

   private _currentUrl:String
   private _currentHtml:String
}

同时拥有 2 个或“浏览器”是可能的,它们有自己独立的状态:

Browser b1 = new Browser()
Browser b2 = new Browser()

b1.goto(“http://www.google.com/”)
b2.goto(“http://www.stackoverflow.com/”)

print(b1.getHtml())
print(b2.getHtml())

问题:展示你如何在 Haskell 中从 scracth 设计这样的东西(类似浏览器的 API,可能有几个独立的实例)?请给一个代码sn-p。

注意:为简单起见,请跳过有关 getLinks() 函数的详细信息(它琐碎且无趣)。
另外假设有一个 API 函数

getUrlContents :: String -> IO String

打开 HTTP 连接并返回给定 URL 的 HTML。


更新:为什么要有状态(或可能没有)?

API 可以有更多的功能,而不仅仅是单一的“加载和解析结果”。
我没有添加它们以避免复杂性。

它还可以关心 HTTP Referer 标头和 cookie,通过在每个请求中发送它们来模拟真实的浏览器行为。

考虑以下场景:

  1. 打开http://www.google.com/
  2. 在第一个输入区域输入“haskell”
  3. 点击“Google 搜索”按钮
  4. 点击链接“2”
  5. 点击链接“3”
  6. 打印当前页面的 HTML(“haskell”的谷歌结果页面 3)

手头有这样的场景,作为开发人员,我希望尽可能地将其转移到代码中:

Browser b = new Browser()
b.goto("http://www.google.com/")
b.typeIntoInput(0, "haskell")
b.clickButton("Google Search") // b.goto(b.finButton("Google Search"))
b.clickLink("2") // b.goto(b.findLink("2"))
b.clickLink("3")
print(b.getHtml())

此场景的目标是在一组操作后获取最后一页的 HTML。 另一个不太明显的目标是保持代码紧凑。

如果浏览器有状态,它可以发送 HTTP Referer 标头和 cookie,同时将所有机制隐藏在自身内部并提供漂亮的 API。

如果浏览器没有状态,开发人员很可能会传递所有当前的 URL/HTML/Cookie ——这会给场景代码增加噪音。

注意:我想有一些库可以在 Haskell 中抓取 HTML,但我的目的不是抓取 HTML,而是了解如何在 Haskell 中正确设计这些“黑盒”的东西。

【问题讨论】:

    标签: oop haskell io state


    【解决方案1】:

    正如你描述的问题,根本不需要状态:

    data Browser = Browser { getUrl :: String, getHtml :: String, getLinks :: [String]} 
    
    getLinksFromHtml :: String -> [String] -- use Text.HTML.TagSoup, it should be lazy
    
    goto :: String -> IO Browser
    goto url = do
                 -- assume getUrlContents is lazy, like hGetContents
                 html <- getUrlContents url 
                 let links = getLinksFromHtml html
                 return (Browser url html links)
    

    同时拥有 2 个或“浏览器”是可能的,它们有自己独立的状态:

    显然,您可以拥有任意数量,并且它们不能相互干扰。

    现在相当于你的 sn-ps。第一:

    htmlFromGooglesFirstLink = do
                                  b <- goto "http://www.google.com"
                                  let firstLink = head (links b)
                                  b2 <- goto firstLink -- note that a new browser is returned
                                  putStr (getHtml b2)
    

    第二个:

    twoBrowsers = do
                    b1 <- goto "http://www.google.com"
                    b2 <- goto "http://www.stackoverflow.com/"
                    putStr (getHtml b1)
                    putStr (getHtml b2)
    

    更新(回复您的更新):

    如果浏览器有状态,它可以发送 HTTP Referer 标头和 cookie,同时将所有机制隐藏在自身内部并提供漂亮的 API。

    仍然不需要状态,goto 可以只接受浏览器参数。首先,我们需要扩展类型:

    data Browser = Browser { getUrl :: String, getHtml :: String, getLinks :: [String], 
                             getCookies :: Map String String } -- keys are URLs, values are cookie strings
    
    getUrlContents :: String -> String -> String -> IO String
    getUrlContents url referrer cookies = ...
    
    goto :: String -> Browser -> IO Browser
    goto url browser = let
                         referrer = getUrl browser 
                         cookies = getCookies browser ! url
                       in 
                       do 
                         html <- getUrlContents url referrer cookies
                         let links = getLinksFromHtml html
                         return (Browser url html links)
    
    newBrowser :: Browser
    newBrowser = Browser "" "" [] empty
    

    如果浏览器没有状态,开发人员很可能会传递所有当前的 URL/HTML/Cookie ——这会给场景代码增加噪音。

    不,您只需传递 Browser 类型的值。以您为例,

    useGoogle :: IO ()
    useGoogle = do
                  b <- goto "http://www.google.com/" newBrowser
                  let b2 = typeIntoInput 0 "haskell" b
                  b3 <- clickButton "Google Search" b2
                  ...
    

    或者你可以去掉这些变量:

    (>>~) = flip mapM -- use for binding pure functions
    
    useGoogle = goto "http://www.google.com/" newBrowser >>~
                typeIntoInput 0 "haskell" >>=
                clickButton "Google Search" >>=
                clickLink "2" >>=
                clickLink "3" >>~
                getHtml >>=
                putStr
    

    这看起来够好吗?请注意,浏览器仍然是不可变的。

    【讨论】:

    【解决方案2】:

    不要尝试复制到许多面向对象。

    只需定义一个简单的 Browser 类型,它包含当前 URL(每个 IORef 以保证可变性)和一些 IO 函数来提供访问和修改功能。

    示例程序如下所示:

    import Control.Monad
    
    do
       b1 <- makeBrowser "google.com"
       b2 <- makeBrowser "stackoverflow.com"
    
       links <- getLinks b1
    
       b1 `navigateTo` (head links)
    
       print =<< getHtml b1
       print =<< getHtml b2
    

    请注意,如果您定义像 o # f = f o 这样的辅助函数,您将拥有更类似于对象的语法(例如 b1#getLinks)。

    完整的类型定义:

    data Browser = Browser { currentUrl :: IORef String }
    
    makeBrowser  :: String -> IO Browser
    
    navigateTo   :: Browser -> String -> IO ()
    getUrl       :: Browser -> IO String
    getHtml      :: Browser -> IO String
    getLinks     :: Browser -> IO [String]
    

    【讨论】:

    • 你为什么要让浏览器成为“对象”并模仿面向对象的设计/界面/语法?不是只需要一个简单的附加getLinks :: String -&gt; String -&gt; [String] 吗?
    • 恕我直言,即使您尝试过多地复制 OOP。对于此任务,可变性的唯一远程可能好处是缓存 HTML 和链接列表,您的答案没有这样做。即使在那里也不需要。
    【解决方案3】:

    getUrlContents 函数已经完成了 goto() 和 getHtml() 会做的事情,唯一缺少的是从下载页面中提取链接的函数。它可以接受一个字符串(页面的 HTML)和一个 URL(用于解析相对链接)并从该页面中提取所有链接:

    getLinks :: String -> String -> [String]
    

    通过这两个函数,您可以轻松构建其他执行爬虫的函数。例如,“获取第一个链接页面”示例可能如下所示:

    getFirstLinked :: String -> IO String
    getFirstLinked url =
       do page <- getUrlContents url
          getUrlContents (head (getLinks page url))
    

    下载从 URL 链接的所有内容的简单功能可能是:

    allPages :: String -> IO [String]
    allPages url =
       do page <- getUrlContent url
          otherpages <- mapM getUrlContent (getLinks page url)
          return (page : otherpages)
    

    (请注意,例如,这将无休止地跟随链接中的循环 - 真正使用的函数应该处理这种情况)

    这些函数使用的唯一“状态”是 URL,它只是作为参数提供给相关函数。

    如果所有浏览功能都需要更多信息,您可以创建一个新类型将它们组合在一起:

    data BrowseInfo = BrowseInfo
         { getUrl     :: String
         , getProxy   :: ProxyInfo
         , getMaxSize :: Int
         }
    

    使用此信息的函数可以简单地获取此类型的参数并使用包含的信息。拥有这些对象的多个实例并同时使用它们是没有问题的,每个函数都只会使用给定的对象作为参数。

    【讨论】:

      【解决方案4】:

      展示你如何在 Haskell 中从 scracth 设计这样的东西(类似浏览器的 API,可能有几个独立的实例)?请给一个代码sn-p。

      我会在每个点使用一个 (Haskell) 线程,让所有线程在 State monad 中运行,记录类型为它们需要的任何资源,并将结果通过通道传回主线程。

      添加更多并发!这就是FP方式。

      如果我没记错的话,这里有一个设计用于通过通道进行通信的链接检查线程群:

      此外,请确保不要使用字符串,而是使用文本或字节字符串——它们会快得多。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多