如何有效地解析 C++ 中的大数据 json 文件(维基数据)?

How to parse bigdata json file (wikidata) in C++ efficiently?

我有一个大约 36 GB 的 json 文件(来自维基数据),我想更有效地访问它。目前我在 C++ 中使用 rapidjsons SAX-style API - 但解析整个文件在我的机器上花费了大约 7415200 毫秒(=120 分钟)。我想根据两个主键之一('name' 或 'entity-key' -> 即 'Stack Overflow' 或 'Q549037')访问此文件中的 json 对象在 json 对象内。这意味着我必须在最坏的情况下解析当前的整个文件。

所以我想了两个办法:

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

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

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

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

您将需要一些索引机制。用文件位置做这件事是可能的。如果位置上的数据也是有效的 JSON,您可以查找并将其流式传输到 RapidJSON 以解析该 JSON 值(RapidJSON 可以在完成 JSON 被 kParseStopWhenDoneFlag).

解析

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

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


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

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

我想知道你为什么要坚持 JSON 摆在首位。它当然不是快速访问大数据的最佳格式。

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

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

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

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

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

首先,如果您对其 WikiMedia 数据感兴趣,那么获得实验室帐户会容易得多。这需要大约一天的时间来完成,它会让你 运行 他们机器上的大部分代码,避免下载数 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()])

对不起,如果它有点神秘,但它不是为了 public 消费而准备的。样本输出是

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 个大小大致相等的较小文件。您可能会发现一些大小合理的文件,比一个大文件或数百万个小文件更容易处理。按文章标题中的第一个字母拆分可能很容易,给出少于 100 个文件,每个文件小于 1 GB。