我在F#中构建了一个网络抓取工具,我遇到了如何存储我已经去过的网页以及我尚未访问的网页的问题。
我目前的实施涉及使用记录列表跟踪状态
type Page = {url:Uri; visited:bool; redirects:bool}
let createCrawlLink (url: Uri) = {url=url; visited=false; redirects=false}
let initialize url = [createCrawlLink(url)]
let uriInList(data:Page list)(uri:Uri) = List.exists (fun x -> x.url.AbsoluteUri = uri.AbsoluteUri) data
let add (data:Page list) (url) =
let uri = new Uri(url)
match uriInList data uri with
| true -> data
| false -> (createCrawlLink uri) :: data
现在,当我从列表中删除第一个项目并访问它时,我想做一些事情。
我对改变所访问的记录/重定向属性的功能方式感到困惑。到目前为止,似乎我必须找到记录,使用我想要更改的属性制作副本,然后将整个列表复制到新列表中,删除旧记录并添加新记录。
这似乎很多工作,但谷歌没有找到任何好的数据结构(或者我不知道要搜索的词)。有更好的清洁方式吗?
答案 0 :(得分:4)
您正在使用列表,但正如ildjarn在评论中所说,您应该使用一个集合。但是,如果你需要跟踪每个URI的多个标记(已经访问过这个标记吗?这个重定向吗?),那么你必须跟踪多个集合(visitedURIs
和{{1例如)。
因此,您可能需要的数据结构是PersistentHashMap
from FSharpx.Collections。它是一个持久的数据结构,所以每当你在其中进行更新时它都是非破坏性的,你会得到一个带有更改的 new 哈希映射,但旧的哈希映射仍然存在不变,以便任何其他仍然具有引用功能的函数仍然可以看到数据的一致视图(当您开始尝试并行化代码时,这是一个巨大的优势!)
另请注意,对于列表,如果需要在现有列表的中间进行频繁更新,PersistentVector
类型(也来自FSharpx.Collections)非常适合。
答案 1 :(得分:3)
我认为将页面存储为与访问的页面分开访问会使其更简单,更高效,无论它是否有效。
我会将访问过的网页存储在Map<string, Page>
中,其中string
是网址,以便您可以持续访问访问过的网页。
然后从列表的头部开始使用排队的URL进行模式匹配,并在地图中构建结果。
type Page = { url:Uri; redirects:bool }
type PagesVisited = Map<string, Page>
let rec crawl (urisToVisit:Uri list) (visited:PagesVisited) : PagesVisited =
match urisToVisit with
| uri :: remainingUris ->
if Map.containsKey (uri:Uri).AbsoluteUri visited then
crawl remainingUris visited
else
let (redirects, newUris) = visit uri
let visited' = Map.add uri.AbsoluteUri {url=uri; redirects = redirects} visited
crawl (newUris @ urisToVisit) visited'
| [] ->
printfn "Finished the internet"
visited
// Kick it off
crawl [Uri("https://stackoverflow.com")] Map.empty
这向您展示了执行此循环的可能功能方式。我已经离开了visit
的实施。
请注意,在列表前面添加新项目非常有效。它不会在内存中创建列表的新副本。所以我使用列表连接运算符@
将可能更短的列表放在可能更长的前面。
同样,即使每个实例都是不可变的,也不会在每个循环上复制PagesVisited
映射。使用结构共享,以便可以添加和删除项目,同时仍保留对先前版本的地图的引用。这比完整副本要快得多。
如果您关心的是使其快速有效而不是保持其功能,您可能会使用可变集合ResizeArray
和Dictionary
。