如何有效地解析 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 对象内。这意味着我必须在最坏的情况下解析当前的整个文件。
所以我想了两个办法:
- 将大文件拆分为数十亿个小文件 - 文件名指示 name/entity-key(即 Q549037.json / Stack_Overflow.json 或 Q549037#Stack_Overflow.json)->不确定存储过载
- 构建某种从主键到文件中
ftell()
位置的索引。建立索引大约需要 120 分钟(就像现在解析一样),但访问速度应该会更快
- 即使用类似两个
std::unorderedmap
的东西(可能 运行 再次进入内存问题)
- 索引文件 - 创建两个文件:一个条目按名称排序,另一个条目按实体键排序(创建这些文件可能需要更长的时间,因为排序)
解决此类问题的最佳做法是什么?
我应该遵循哪种方法?还有其他想法吗?
编写您自己的 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('</?math(.*?)>')
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('<math'):
attr = m.group(1)
start = m.end()
pos = start
expression = ""
inEqn = 1
if m.group() == '</math>' :
end = m.start()
expression = ' '.join([expression,line[start:end]])
print title,'\t',attr,'\t',expression.lstrip().replace('<','<').replace('>',
'>').replace('&','&')
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。
我有一个大约 36 GB 的 json 文件(来自维基数据),我想更有效地访问它。目前我在 C++ 中使用 rapidjsons SAX-style API - 但解析整个文件在我的机器上花费了大约 7415200 毫秒(=120 分钟)。我想根据两个主键之一('name' 或 'entity-key' -> 即 'Stack Overflow' 或 'Q549037')访问此文件中的 json 对象在 json 对象内。这意味着我必须在最坏的情况下解析当前的整个文件。
所以我想了两个办法:
- 将大文件拆分为数十亿个小文件 - 文件名指示 name/entity-key(即 Q549037.json / Stack_Overflow.json 或 Q549037#Stack_Overflow.json)->不确定存储过载
- 构建某种从主键到文件中
ftell()
位置的索引。建立索引大约需要 120 分钟(就像现在解析一样),但访问速度应该会更快- 即使用类似两个
std::unorderedmap
的东西(可能 运行 再次进入内存问题) - 索引文件 - 创建两个文件:一个条目按名称排序,另一个条目按实体键排序(创建这些文件可能需要更长的时间,因为排序)
- 即使用类似两个
解决此类问题的最佳做法是什么? 我应该遵循哪种方法?还有其他想法吗?
编写您自己的 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('</?math(.*?)>')
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('<math'):
attr = m.group(1)
start = m.end()
pos = start
expression = ""
inEqn = 1
if m.group() == '</math>' :
end = m.start()
expression = ' '.join([expression,line[start:end]])
print title,'\t',attr,'\t',expression.lstrip().replace('<','<').replace('>',
'>').replace('&','&')
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。