问题 良好的图遍历算法


抽象问题:我有一个大约250,000个节点的图表,平均连接数大约为10.找到一个节点的连接是一个漫长的过程(10秒钟就可以了)。将节点保存到数据库也需要大约10秒钟。我可以非常快速地检查数据库中是否已存在节点。允许并发,但一次不超过10个长请求,您将如何遍历图表以获得最快的覆盖率。

具体问题:我正在尝试抓一个网站用户页面。为了发现新用户,我正在从已知用户那里获取好友列表。我已经导入了大约10%的图形但是我一直陷入循环或使用太多内存记住太多节点。

我目前的实施:

def run() :
    import_pool = ThreadPool(10)
    user_pool = ThreadPool(1)
    do_user("arcaneCoder", import_pool, user_pool)

def do_user(user, import_pool, user_pool) :
    id = user
    alias = models.Alias.get(id)

    # if its been updates in the last 7 days
    if alias and alias.modified + datetime.timedelta(days=7) > datetime.datetime.now() :
        sys.stderr.write("Skipping: %s\n" % user)
    else :
        sys.stderr.write("Importing: %s\n" % user)
        while import_pool.num_jobs() > 20 :
            print "Too many queued jobs, sleeping"
            time.sleep(15)

        import_pool.add_job(alias_view.import_id, [id], lambda rv : sys.stderr.write("Done Importing %s\n" % user))

    sys.stderr.write("Crawling: %s\n" % user)
    users = crawl(id, 5)
    if len(users) >= 2 :
        for user in random.sample(users, 2) :
            if (user_pool.num_jobs() < 100) :
                user_pool.add_job(do_user, [user, import_pool, user_pool])

def crawl(id, limit=50) :
    '''returns the first 'limit' friends of a user'''
    *not relevant*

当前实施的问题:

  • 陷入我已经导入的派系中,从而浪费时间并且导入线程处于空闲状态。
  • 当他们被指出时会添加更多。

因此,欢迎边际改进,以及完全重写。谢谢!


5899
2017-08-24 06:05


起源

与Robert Tarjan有关,他是几个着名的图论(!)算法的发现者? - Jim Lewis
:)可悲的是,只有匈牙利的小镇,我们都得到了我们的姓氏。但我们都热爱计算机和数学。 - Paul Tarjan
与问题无关,但请注意,sys.stderr.write(“... \ n”)可以替换为print >> sys.stderr,“...”(不需要“\ n”,并使用更常见的印刷声明)。 - Eric Lebigot
是的,但是我将stdout重定向到一个文件(你无法知道),并且仍然希望在我的控制台中显示错误消息。 - Paul Tarjan


答案:


要记住您已访问过的用户的ID,您需要一张长度为250,000个整数的地图。这远非“太多”。只需维护这样一个地图,只遍历导致已经未被发现的用户的边缘,在找到这样的边缘时将它们添加到该地图。

据我所知,您已接近实施广度优先搜索(BFS)。检查谷歌有关此算法的详细信息。当然,不要忘记互斥体 - 你需要它们。


7
2017-08-24 06:15



用户实际上是平均长度为15的字符串。我尝试使用{username1:True,username2:True}的字典,但很快达到了100%的记忆,机器被锁定了。也许在python中使用dict效率很低? - Paul Tarjan
一种可能的解决方案就是存储用户名的哈希值 - cobbal
另外,一个集合比dict更适合于那种类型的存储 - cobbal
是否设置O(1)查找? - Paul Tarjan
在文档中(docs.python.org/library/stdtypes.html#set-types-set-frozenset)它说,“设置元素,如字典键,必须是可以清洗的”,所以我假设如此 - cobbal


我真的很困惑为什么需要10秒钟才能将数据库添加到数据库中。这听起来像个问题。你用的是什么数据库?你有严格的平台限制吗?

有了现代系统和它们的大量内存,我会推荐一种漂亮的简单缓存。您应该能够创建一个非常快速的用户信息缓存,以避免重复工作。当您遇到节点时,请停止处理。这将避免在派系中永远骑自行车。

如果您需要在一段时间后允许重新散列现有节点,则可以使用last_visit_number,它将是dB中的全局值。如果节点具有该编号,则此爬网是遇到它的爬网。如果要自动重新访问任何节点,只需在开始爬网之前碰撞last_visit_number即可。

根据你的描述,我不太确定你是如何陷入困境的。

编辑------ 我刚刚注意到你有一个具体的问题。为了提高您提取新数据的速度,我会跟踪给定用户在您的数据中导入的次数(导入或尚未导入)。在选择要抓取的用户时,我会选择链接数较少的用户。我会特别选择链接数最少的链接或随机选择链接数最少的用户。

雅各


2
2017-08-24 06:15



10秒钟来自于必须抓取用户的几页信息,然后将其转换为我的数据库格式。大部分是网络时间。 - Paul Tarjan
至于新用户的选择,非常有趣。我会尝试为用户计算内联链接,并且只为低内核用户提供服务。 - Paul Tarjan
为什么这么少线程?你担心他们会阻止你吗?我打算为每个节点建议一个哈希值(a.la Pavel)。您可以做的一件事是创建递增ID并使用简单的映射表来交叉引用它们。 - TheJacobTaylor
是的,我担心远程站点会阻止我。礼貌和所有这些(在雅虎!我知道我们的爬虫可以随时瞄准2个requets / host)。通过内存集工作,我不需要优化我的爬行路径,但如果其他站点按数量级增长,我将不得不转向您的策略。谢谢! - Paul Tarjan
一个稍微扭曲的选择是通过他们的API利用搜索引擎的结果来获取你想要的数据。如果要构建初始数据库,则缓存页面(如果可用)可能会为您提供所需的数据。搜索引擎以极快的速度运行,可能不会介意更多的线程。 - TheJacobTaylor


没有特定的算法可以帮助您从头开始优化图形的构造。无论如何,您将不得不至少访问每个节点一次。你是否这样做 深度第一 要么 广度优先 从速度的角度来看是无关紧要的。 Theran 正确地在下面的评论中指出,通过首先探索更近的节点,广度优先搜索可以在整个图形完成之前立即为您提供更有用的图形;这可能是也可能不是你的担忧。他还指出,深度优先搜索的最新版本是使用递归实现的,这可能是您的问题。请注意,不需要递归;您可以将未完全探索的节点添加到堆栈中,并根据需要线性处理它们。

如果你做一个简单的存在检查新节点(如果你使用散列进行查找,则为O(1)),那么循环根本就不是问题。如果不存储完整的图形,则仅关注周期。您可以通过图表优化搜索,但构造步骤本身将始终采用线性时间。

我同意其他海报的说法,图表的大小应该不是问题。 250,000不是很大!

关于并发执行;图形由所有线程更新,因此它需要是同步的数据结构。由于这是Python,你可以使用 队列 模块,用于存储仍由线程处理的新链接。


2
2017-08-24 06:30



BFS可能更好,因为它将首先查看最初的节点,这可能会在早期提供有用的子集。 BFS还避免了递归250,000级别的递归风险,并且可以将其队列保持在与最终图形相同的DB中(假设为RDBMS)。 - Theran
你当然可以在没有深层堆栈跟踪问题的情况下制作DFS:DFS和BFS之间唯一真正的区别是在BFS中将节点添加到队列中;在DFS中,一个堆栈。相同的算法,不同的数据结构 - 因此,不同的语义。 - Michael Deardeuff
@Theran,迈克尔:+1谢谢 - 答案调整后做出澄清。 - ire_and_curses
听起来像一个简单的BFS(就像我有)。我和你一起100%,250,000很小,但是我原来的实现很快就用掉了{username1:True,username2:True}的字典。也许我应该将其作为另一个问题发布。 - Paul Tarjan


虽然你说得到朋友列表需要花费很多时间(10秒或更长时间),但是旧的Dijkstra算法的变体可能会起作用:

  1. 获取任何节点。
  2. 从已加载的任何节点获取连接。
  3. 如果尚未加载另一端,请将该节点添加到图形中。
  4. 转到第2步。

诀窍是以智能方式选择您在步骤2中加载的连接。关于此的一些简短评论:

  • 您应该以某种方式阻止相同的连接被加载两次或更多次。选择随机连接并在已经加载的情况下将其丢弃,如果您在所有连接之后,效率非常低。
  • 如果要最终加载所有连接,请同时加载节点的所有连接。

为了真正说明效率,请提供有关数据结构的更多详细信息。


0
2017-08-24 07:04