意图:小型应用程序来学习Haskell:下载维基百科文章,然后下载所有链接到它的文章,然后下载所有链接到它们的文章,等等……直到达到指定的递归深度为止。结果被保存到一个文件中。
方法:使用StateT跟踪下载队列、下载文章和更新队列。我递归地构建了一个列表IO [WArticle],然后打印它。
问题:在分析时,我发现使用中的内存总量与下载的文章数量成正比。
分析:根据文献,我认为这是一个懒惰和/或严格的问题。BangPatterns减少了内存消耗,但没有解决比例问题。此外,我知道所有文章都是在文件输出启动之前下载的。
可能的解决方案:
1)函数getNextNode :: StateT CrawlState IO WArticle (以下)已经具有IO。一种解决方案是只编写其中的文件,并且只返回状态。这意味着文件被写成了非常小的块。感觉不太好..。
2)具有函数buildHelper :: CrawlState -> IO [WArticle] (以下)返回[IO WArticle]。虽然我不知道如何重写该代码,并在评论中建议我不要这样做。
这些建议的解决方案是否比我认为的更好,还是有更好的选择?
import GetArticle (WArticle, getArticle, wa_links, wiki2File) -- my own
type URL = Text
data CrawlState =
CrawlState ![URL] ![(URL, Int)]
-- [Completed] [(Queue, depth)]
-- Called by user
buildDB :: URL -> Int -> IO [WArticle]
buildDB startURL recursionDepth = buildHelper cs
where cs = CrawlState [] [(startURL, recursionDepth)]
-- Builds list recursively
buildHelper :: CrawlState -> IO [WArticle]
buildHelper !cs@(CrawlState _ queue) = {-# SCC "buildHelper" #-}
if null queue
then return []
else do
(!article, !cs') <- runStateT getNextNode cs
rest <- buildHelper cs'
return (article:rest)
-- State manipulation
getNextNode :: StateT CrawlState IO WArticle
getNextNode = {-# SCC "getNextNode" #-} do
CrawlState !parsed !queue@( (url, depth):queueTail ) <- get
article <- liftIO $ getArticle url
put $ CrawlState (url:parsed) (queueTail++ ( if depth > 1
then let !newUrls = wa_links article \\ parsed
!newUrls' = newUrls \\ map fst queue
in zip newUrls' (repeat (depth-1))
else []))
return article
startUrl = pack "https://en.wikipedia.org/wiki/Haskell_(programming_language)"
recursionDepth = 3
main :: IO ()
main = {-# SCC "DbMain" #-}
buildDB startUrl recursionDepth
>>= return . wiki2File
>>= writeFile "savedArticles.txt"https://gitlab.com/mattias.br/sillyWikipediaSpider的完整代码。当前版本仅限于从每个页面下载前八个链接,以节省时间。如果不改变它,就可以下载大约600 MB堆使用的55页。
谢谢你的帮助!
发布于 2018-06-25 21:07:29
( 2)在这种情况下,IO WArticle我想要吗?
不完全是。问题是,一些IO WArticle操作依赖于以前操作的结果:指向未来页面的链接位于先前获得的页面中。[IO Warticle]不能提供这样的信息:从某种意义上说,它是纯的,您可以在列表中找到一个操作,而不执行前面的操作。
我们需要的是一种“有效列表”,它让我们一个接一个地提取文章,逐步实现必要的效果,而不是强迫我们一次就完全生成清单。
有几个库提供了这些类型的“有效列表”:流式传输、喉管、导管。在返回最终结果之前,他们定义了扩展基monad的monad转换器,并将其扩展到产出量中间值。通常,最终结果是与生成的值不同的类型;它可能只是单元()。
注意:这些库的 Functor、Applicative和Monad实例与纯列表的对应实例不同。Functor 实例映射的结果是最终值,而不是生成的中间值。为了映射生成的值,它们提供了分离函数。Monad实例对有效列表进行排序,而不是尝试所有组合。为了尝试所有的组合,他们提供分离函数。
使用流式传输库,我们可以将buildHelper修改为如下所示:
import Streaming
import qualified Streaming.Prelude as S
buildHelper :: CrawlState -> Stream (Of WArticle) IO ()
buildHelper !cs@(CrawlState _ queue) =
if null queue
then return []
else do (article, cs') <- liftIO (runStateT getNextNode cs)
S.yield article
buildHelper cs'然后我们可以使用像mapM_这样的函数(来自Streaming.Prelude,而不是Control.Monad!)一个接一个地处理这些文章,因为它们是生成的。
发布于 2018-06-27 17:21:21
在danidiaz的回答基础上添加一个进一步的解释和代码构建。下面是最后的代码:
import Streaming
import qualified Streaming.Prelude as S
import System.IO (IOMode (WriteMode), hClose, openFile)
buildHelper :: CrawlState -> Stream (Of WArticle) IO ()
buildHelper cs@( CrawlState _ queue ) =
if null queue
then return ()
else do
(article, cs') <- liftIO (runStateT getNextNode cs)
S.yield article
buildHelper cs'
main :: IO ()
main = do outFileHandle <- openFile filename WriteMode
S.toHandle outFileHandle . S.show . buildHelper $
CrawlState [] [(startUrl, recursionDepth)]
hClose outFileHandleoutFileHandle是一个常见的文件输出句柄。
S.toHandle接受一个字符串流并将它们写入指定的句柄。
S.show将show :: WArticle -> String映射到流上。
这是一种优雅的解决方案,尽管它是由一系列IO操作(即下载网站)生成的,并在结果可用时将其写入文件。在我的机器上,它在执行过程中仍然使用大量内存(相对于任务),但从未超过450 MB。
https://stackoverflow.com/questions/51013774
复制相似问题