【问题标题】:How to parse bigdata json file (wikidata) in C++ efficiently?如何有效地解析 C++ 中的大数据 json 文件(wikidata)?
【发布时间】:2015-04-08 02:38:41
【问题描述】:

我有一个大约 36 GB 的 json 文件(来自 wikidata),我想更有效地访问它。目前我在 C++ 中使用 rapidjsons SAX 风格的 API——但解析整个文件在我的机器上需要大约 7415200 毫秒(=120 分钟)。我想根据 json 对象内的两个主键(“名称”或“实体键”-> 即“堆栈溢出”或“Q549037”)之一访问此文件中的 json 对象。这意味着我必须在最坏的情况下解析当前的整个文件。

所以我想到了两种方法:

  • 将大文件拆分为数十亿个小文件 - 文件名指示名称/实体键(即 Q549037.json / Stack_Overflow.json 或 Q549037#Stack_Overflow.json) -> 不确定存储中的过载
  • 从主键到文件中的ftell() 位置建立某种索引。建立索引大约需要 120 分钟(就像现在解析一样),但访问应该更快
    • 即使用两个 std::unorderedmap 之类的东西(可能会再次遇到内存问题)
    • 索引文件 - 创建两个文件:一个具有按名称排序的条目,另一个按实体键排序(创建这些文件可能需要更长的时间,因为排序)

解决此类问题的最佳做法是什么? 我应该遵循哪种方法?还有其他想法吗?

【问题讨论】:

  • 如果您决定编写自己的想法,请考虑使用正确的解析树而不是无序映射。 boost::intrusive 可能会有所帮助。
  • 您还可以考虑使用 MongoDB 或其他一些 NoSQL 数据库来存储 JSON 数据。据我了解,这是 NoSQL 数据库的(初始)点。
  • 我刚刚意识到您的吞吐量只有大约 5MB/s。在我的机器上,内存中的 SAX 解析大约是 300MB/s。 DOM 约为 200MB/s。所以对于大文件,它应该是 I/O 绑定的。我怀疑出了什么问题。你用的是github的最新版本吗?
  • @MiloYip 是的,没错 - 300 MB/s 应该只需要大约 2 分钟:我想我必须再做一些检查,为什么它慢到 5 MB/s我的电脑。

标签: c++ json bigdata rapidjson wikidata


【解决方案1】:

我认为性能问题不是由于解析。使用 RapidJSON 的 SAX API 应该已经提供了良好的性能和内存友好。如果您需要访问 JSON 中的每个值,这可能已经是最好的解决方案了。

但是,从问题描述来看,一次读取 所有 值似乎不是您的要求。您想读取特定条件的 一些(可能是少量)值(例如,通过主键)。那么读取/解析一切都不适合这种情况。

您将需要一些索引机制。可以使用文件位置执行此操作。如果位置上的数据也是有效的 JSON,您可以查找并将其流式传输到 RapidJSON 以解析该 JSON 值(当解析完整的 JSON 时,RapidJSON 可以停止解析,kParseStopWhenDoneFlag)。

其他选项是将 JSON 转换为某种数据库,SQL 数据库、键值数据库或自定义数据库。使用提供的索引工具,您可以快速查询数据。这可能需要很长时间才能进行转换,但对于以后的检索来说性能很好。

请注意,JSON 是一种交换格式。它不是为对大数据进行快速个人查询而设计的。


更新:最近发现有一个项目semi-index可能适合你的需求。

【讨论】:

    【解决方案2】:

    编写您自己的 JSON 解析器,最大限度地减少分配和数据移动。也为直接的 ANSI 抛弃多字符。我曾经写过一个 XML 解析器来解析 4GB 的 Xml 文件。我试过 MSXML 和 Xerces 都有轻微的内存泄漏,当用于这么多数据时实际上会耗尽内存。一旦达到最大嵌套级别,我的解析器实际上会停止内存分配。

    【讨论】:

      【解决方案3】:

      您对问题的定义无法给出准确的答案。

      我想知道为什么您首先要坚持使用 JSON。它当然不是快速访问大数据的最佳格式。

      如果您大量使用 wikia 数据,为什么不将它们完全转换成更易于管理的格式?

      自动化与您的条目格式匹配的数据库定义应该很容易,并且一劳永逸地将大量 JSON 转换为数据库记录。

      您可以随时停止数据库转换(即将每个 JSON 块存储为纯文本或进一步细化)。
      在最小的情况下,您最终会得到一个数据库表,其中包含按名称和键索引的记录。
      肯定比将文件系统用作数据库(通过创建数百万个以名称+键命名的文件)或编写专用代码来查找记录要少。

      这也可能会为您节省大量磁盘空间,因为内部数据库存储通常比纯文本表示更有效。

      【讨论】:

        【解决方案4】:

        我对维基百科中的数据做了一些解析。我对提取方程特别感兴趣,所以我只对文件的一部分感兴趣。

        首先,如果您对它的维基媒体数据感兴趣,那么获得一个实验室帐户会容易得多。这需要大约一天的时间,它可以让你在他们的机器上运行大部分代码,避免下载数 GB 的需要。使用 Labs 帐户,您应该能够在相当最新的数据库复制上运行代码,而无需完全使用 json。

        我使用一个简单的 python 程序来解析数据,它基本上在每一行运行几个正则表达式;一个可以找到包含<title>...</title> 的行,所以我知道它是哪篇维基百科文章,还有一些可以找到命名空间和数学标签。它可以在 13 秒内处理 160MB 的文件,因此可能可以在一个小时内完成整个 36GB。

        此代码生成的文本文件仅包含我感兴趣的数据。如果您感兴趣,代码是

        import sys
        import re
        
        dump = len(sys.argv)>1 and sys.argv[1]=='-d'
        titleRE = re.compile('<title>(.*)</title>')
        nsRE = re.compile('<ns>(.*)</ns>')
        mathRE = re.compile('&lt;/?math(.*?)&gt;')
        pageEndRE = re.compile('</page>')
        supOc = 0
        supCc = 0
        subOc = 0
        subCc = 0
        
        title =""
        attr = ""
        ns = -1
        inEqn = 0
        for line in sys.stdin:
            m = titleRE.search(line)
            if m :
                title = m.group(1)
                expression = ""
                if dump : print line
                inEqn = 0
            m = nsRE.search(line)
            if m :
                ns = m.group(1)
            start = 0
            pos = 0
            m = mathRE.search(line,pos)
            while m :
                if m.group().startswith('&lt;math'):
                    attr = m.group(1)
                    start = m.end()
                    pos = start
                    expression = ""
                    inEqn = 1
                if m.group() == '&lt;/math&gt;' :
                    end = m.start()
                    expression = '    '.join([expression,line[start:end]])
                    print title,'\t',attr,'\t',expression.lstrip().replace('&lt;','<').replace('&gt;',
        '>').replace('&amp;','&')
                    pos = m.end()
                    expression = ""
                    start = 0
                    inEqn = 0
                m = mathRE.search(line,pos)
            if start > 0 :
                expression = line[start:].rstrip()
            elif inEqn :
                expression = '    '.join([expression,line.rstrip()])
        

        对不起,如果它有点神秘,但它不是供公众消费的。样本输出为

        Arithmetic mean         a_1,\ldots,a_n.
        Arithmetic mean         A
        Arithmetic mean         A=\frac{1}{n}\sum_{i=1}^{n} a_i
        Arithmetic mean         \bar{x}
        

        每一行都有文章的名称和乳胶方程式。这将我需要处理的数据减少到更易于管理的 500k。我不确定这样的策略是否适用于您的应用程序。

        对于主要的 enwiki 数据,将 xml 转储拆分为 27 个较小的文件,大小大致相同。您可能会发现一些大小合理的文件,比一个大文件或数百万个小文件更容易处理。可能很容易按文章标题中的第一个字母进行拆分,从而给出不到一百个文件,每个文件小于 1 GB。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-04-04
          • 2011-06-18
          • 2019-06-03
          • 2019-04-02
          • 1970-01-01
          • 1970-01-01
          • 2020-10-10
          • 1970-01-01
          相关资源
          最近更新 更多