从大型JSON文件中在PHP中读取大数组

10

我知道我的问题在互联网上已经有很多回答,但似乎我找不到一个好的答案,所以我会尝试解释我的问题并希望得到最佳答案。

我想要做的事情是读取一个可能比这个更复杂的大型JSON文件,“嵌套对象和大型数组”,以下是一个简单的示例:

{
  "data": {
    "time": [
      1,
      2,
      3,
      4,
      5,
       ...
    ],
    "values": [
      1,
      2,
      3,
      4,
      6,
       ...
    ]
  }
}

这个文件可能有200M或更多的数据,我正在使用 file_get_contents()json_decode() 从文件中读取数据,

然后将结果放入变量中,并循环遍历时间并使用当前索引获取时间值以从值数组中获取相应的值,然后将时间和值保存在数据库中,但这样会占用大量的CPU和内存,是否有更好的方法来处理这个问题?

有没有更好的函数可以使用,更好的JSON结构可以使用,或者使用比JSON更好的数据格式来实现这一点。

我的代码:

$data = json_decode(file_get_contents(storage_path("test/ts/ts_big_data.json")), true);
        
foreach(data["time"] as $timeIndex => timeValue) {
    saveInDataBase(timeValue, data["values"][timeIndex])
}

感谢任何帮助的提前

更新于06/29/2020:

我有另一个更复杂的JSON结构示例

{
      "data": {
        "set_1": {
          "sub_set_1": {
            "info_1": {
              "details_1": {
                "data_1": [1,2,3,4,5,...],
                "data_2": [1,2,3,4,5,...],
                "data_3": [1,2,3,4,5,...],
                "data_4": [1,2,3,4,5,...],
                "data_5": 10254552
              },
              "details_2": [
                [1,2,3,4,5,...],
                [1,2,3,4,5,...],
                [1,2,3,4,5,...],
              ]
            },
            "info_2": {
              "details_1": {
                "data_1": {
                  "arr_1": [1,2,3,4,5,...],
                  "arr_2": [1,2,3,4,5,...]
                },
                "data_2": {
                 "arr_1": [1,2,3,4,5,...],
                  "arr_2": [1,2,3,4,5,...]
                },
                "data_5": {
                  "text": "some text"
                }
              },
              "details_2": [1,2,3,4,5,...]
            }
          }, ...
        }, ...
      }
    } 

该 JSON 文件的文件大小可能约为 500MB 或更大,其内部的数组可能包含约 100MB 或更多的数据。

我的问题是如何以最有效的方式获得任何数据片段并在节点之间导航,而不会占用过多的 RAM 和 CPU。我不能逐行读取文件,因为我需要在必要时获取任何数据片段。

Python 是否比 PHP 更适合处理这种大型数据且效率更高?

如果可能,请提供详细答案,我认为这将对每个想要使用 PHP 处理大型数据的人都有很大的帮助。


你能分享一些代码吗?尝试使用 next 来访问数据,而不是使用索引。 - Ôrel
你可能想看一下这个:https://stackify.com/a-guide-to-streams-in-php-in-depth-tutorial-with-examples/ - opensource-developer
一个常见的解决方案是将复杂数据存储在多个简单结构中。你真正的问题可能是存储文件的大小过大,而且无法逐行解析它,因为它是二进制的。此外,请确保 saveInDataBase() 没有每次重新连接到数据库(登录是非常耗时的操作)。还要考虑生成修改500或1000行的INSERT/UPDATE查询,然后运行这些查询,而不是运行只修改1行的INSERTS/UPDATES。(每次运行都是一个新的TCP请求到数据库,也很慢) - Nicholas Summers
这个可能会有帮助吗?https://dev59.com/Nm855IYBdhLWcg3w_5cQ - bestprogrammerintheworld
这个 JSON 文件是从哪里来的?你能够操作 JSON 节点的格式或结构吗?JSON 文件的大小是否有上限? - Vinay
显示剩余6条评论
7个回答

10

JSON是一种很棒的格式,比XML更好的替代方案。最终,JSON几乎可以一对一地转换成XML。

大文件可能会变得更大,因此我们不想将所有内容都读入内存,也不想解析整个文件。我曾经遇到过XXL大小的JSON文件,也有同样的问题。

我认为问题不在于特定的编程语言,而在于这些格式的实现和细节。

我为您提供了3个解决方案:

  1. PHP原生实现首选

几乎与流式XMLReader一样快,有一个名为JsonReader的库可供使用。例如:

use pcrov\JsonReader\JsonReader;

$reader = new JsonReader();
$reader->open("data.json");

while ($reader->read("type")) {
    echo $reader->value(), "\n";
}
$reader->close();

这个库不会将整个文件读入内存或解析所有行。它按照命令逐步遍历JSON对象树。

  1. 转化格式 (缺点:多次转换)

将文件预处理成其他格式,如XML或CSV。 有非常轻量级的nodejs库,例如https://www.npmjs.com/package/json2csv可以将JSON转换为CSV。

  1. 使用一些NoSQL数据库 (缺点:需要安装和维护额外的复杂软件)

例如Redis或CouchDB(将json文件导入到couch db-


1
@JEY同意。根据我的经验,我已经使用了所有三个选项。使用流的JsonReader是最好的。我已经更改了顺序。(请更改您的评论,以便读者不会感到困惑) - Max Kaps 4bis.nl
我不知道有JSON的流解析器,好知道了!我认为这绝对是一个很好的选择。 - Chris Haas
将数据导入Redis是可以的,但在读取时会遇到问题(其他请求也会等待)。由于它将成为一个巨大的字符串(200MB),你必须等待/传输整个网络中的所有数据。 - Ersoy
1
@MaxKaps4bis.nl,感谢您的帮助。我认为您的解决方案并不是100%完美的,但JsonReader是目前最好的答案。我将接受您的答案以便您获得奖励,并且我将在本周对JsonReader进行一些测试并提供一些示例作为答案的补充。 - Fadi
另一个流解析器是json-streaming-parser,它也是PHP实现的,每个解析器都有自己的优缺点。 - Nigel Ren
显示剩余2条评论

5
你的问题基本上与使用特定编程语言来访问一个巨大(存储目的)文件中的数据相关的内存管理有关。
例如,当你使用下面的代码进行操作时: $data = json_decode(file_get_contents(storage_path("test/ts/ts_big_data.json")), true);
发生的情况是运行时Zend引擎使用的内存增加了很多,因为它必须分配一定的内存单元来存储涉及代码语句中每个进行中的文件处理的引用 - 比如还要在内存中保留指针,而不仅仅是实际打开的文件-除非该文件最终被覆盖并且内存缓冲区再次释放(释放)。难怪如果你强制执行同时使用file_get_contents()函数读取字符串和json_decode()函数,你就会迫使解释器在内存中保持所有3个“东西”:文件本身,已创建的引用(字符串)以及结构(json文件)。
相反,如果你把声明从一条语句中分解成几个,那么第一个数据结构(文件)所占用的内存堆栈将在“获取其内容”的操作完成后被卸载,然后将其写入另一个变量(或文件)。只要你没有定义一个变量来保存数据,它就会仍然停留在内存中(作为一个blob - 没有名称,没有存储地址,只有内容)。因此,在处理大数据时,将所有东西分解成小步骤会更加CPU和RAM有效。
因此,你首先需要简单地重写代码,如下所示:
$somefile = file_get_contents(storage_path("test/ts/ts_big_data.json"));

$data = json_decode($somefile, true);


第一行执行时,ts_big_data.json占用的内存被释放(可以将其视为被清除并再次提供给其他进程使用)。
第二行执行时,$somefile的内存缓冲区也被释放。从中得出的要点是,相对于始终使用3个内存缓冲区来存储数据结构,每次只有2个被使用,当然忽略用于实际构建文件的其他内存。当使用数组时(JSON 文件就是数组),动态分配的内存会急剧增加,而不是线性增加,这是我们可能倾向于认为的。底线是,与仅仅管理处理这些巨大文件的函数相比,仅仅在文件存储分配方面就会损失50% 的性能(3个大文件比其中的2个多占用50%的空间),我们最好采用更小的步骤来处理它们的执行。
为了理解这一点,想象一下你只在某个时间点访问所需的内容(这也是一个原则,称为YAGNI-You Aren't Gonna Need It——或类似于极限编程实践背景下的原则,参见此处的参考资料https://wiki.c2.com/?YouArentGonnaNeedIt,自C或Cobol时代传承下来)。
接下来要采用的方法是将文件分成更多块,但是以结构化方式(关系型数据结构)进行。显然,你必须再次将数据碎片保存为 blobs 在数据库中。优势在于,在 DB 中检索数据比在文件中快得多(由于 SQL 在生成和更新表时分配索引)。一个有 1 或 2 个索引的表可以通过结构化查询以闪电般的速度访问。同样,索引是指向数据主存储的指针。
然而,一个重要的问题是,如果你仍然希望使用 JSON(内容和数据存储类型),则不能在本地更新它而不影响全局。我不确定你所指的是读取 JSON 文件中与时间相关的函数值。你是指你的 JSON 文件正在不断变化吗?最好将其拆分为多个表,这样每个单独的表都可以更改而不影响所有数据的大结构。更易于管理,更易于维护,更容易定位变化。 我的理解是,最好的解决方案是将相同的文件拆分为几个 JSON 文件,其中剥离不需要的值。顺便问一句,你真的需要存储的全部数据吗? 我不会立即提供代码,除非您向我解释上述问题(以便我们可以进行交谈),然后我将相应地编辑我的答案。昨天我写了一个与处理blob相关的问题-并将其存储在服务器中-以加速使用cron进程在服务器中执行数据更新。我的数据大约为25MB +,而不是像您的情况一样是500+,但我必须了解您的情况下的用例。
另外,那个你必须处理的文件是如何创建的?为什么你只管理它的最终形式而不干预进一步向其中添加数据?我的看法是你可能应该停止以前的方式将数据存储到其中(从而停止增加你的痛苦),而是将其今天的目的转变为历史数据存储,然后转而在更具弹性的东西中存储未来的数据(如MongoDB或NoSQL数据库)。
可能您并不需要太多的代码,而是需要一种稳健且实用的策略以及首先处理您的数据的工作方式。 编程是最后一步,在决定Web项目的所有详细架构之后。

1
不一定是Fadi投了反对票,我希望OP能看到有时候可能是战术性的负评来让答案看起来不好。但是正如你所看到的,没有任何反馈,这个负评是毫无意义的,虽然可能很烦人,但这就是有些人玩的游戏。 - Nigel Ren

1
我的方法是分块读取JSON文件。
如果这些JSON对象具有一致的结构,您可以轻松地检测到文件中JSON对象的开始和结束。
一旦收集到整个对象,将其插入数据库,然后继续下一个对象。并没有更多的要做了。根据您的数据源,检测JSON对象的开头和结尾的算法可能会变得复杂,但我以前用过更复杂的结构(XML)来完成类似的工作,效果很好。
以上答案摘自 => 解析大型JSON文件 请参阅以下参考资料,它可能对您的情况有所帮助
=> https://laracasts.com/discuss/channels/general-discussion/how-to-open-a-28-gb-json-file-in-php

你能提供一份可用的代码吗?并附带示例数据?比如说,你能在代码中说明需要执行哪些步骤,如何使用解析器、如何循环访问数据以及如何导航节点等吗? - Fadi

0
我的问题是如何以最有效的方式获取数据的任何部分并在节点之间导航,而不会占用太多RAM和CPU。我无法逐行读取文件,因为我需要在必要时获取数据的任何部分。
这是纯文本JSON,没有索引,因此无法在不逐行迭代的情况下解析数据。解决方案是将数据序列化一次并存储在数据库中(我考虑使用SQLite进行快速设置)。
如果您强制不能将数据存储在数据库中,或者无法以SQLite格式检索它,则除了创建一个队列作业来解析它之外,您别无选择。

0

正如你所说,逐行阅读是无法避免的。使用 SQL 只是将问题转移到另一个环境中。我个人会这样做:

  1. 当新的 JSON 文件到来时,将其存储起来,最简单的方法是使用 S3 和 Storage::disk('s3')->put(...);https://laravel.com/docs/7.x/filesystem),并将其放入队列中。你可以使用 Laravel 队列或者我更喜欢的 RabbitMQ。向队列添加一个新条目,例如 {'job': 'parseMyJSON', 'path': 'https://path-on.s3'}
  2. 创建一个可以访问队列的新服务器实例
  3. 编写应用程序的工作实例,可以从队列中获取作业。在第 2 步的新服务器上运行它。每当你将作业放入队列时,它将从 S3 获取 JSON 文件并执行必要的作业。然后它将逐个从队列中取出下一个作业。
如果这个工作实例是用Python或PHP编写的,你需要测试哪种方式更快。优点是,你可以根据需要扩展工作实例,而不会影响Web应用程序的性能。希望这能帮到你。

感谢您的帮助,我已经在使用Laravel队列和工作进程,所以我可以在后台完成任务,这样主应用程序就不会受到太大影响。您提供的解决方案听起来很棒,但我认为它并不适用于我的问题,因为例如如果我不能做到“s3”或使用2个服务器,我正在寻找一种处理大型JSON文件而不占用太多内存和CPU的方法或数据结构,这是我的情况,再次感谢您的帮助。 - Fadi

-1

尝试减少大量数据的复杂性以实现更快的文件I/O

JSON是一种存储数据的好格式,但需要读取整个文件才能解析它。

使您的数据结构更简单,但分散在几个文件中可以让您逐行读取文件,这比一次性读取要快得多。这也带来了不需要一次性将整个文件存储在RAM中的好处,因此更适合资源有限的环境。

可能看起来像这样:

objects.json

{
  "data": {
    "times_file": "/some/path/objects/object-123/object-123-times.csv",
    "values_file": "/some/path/objects/object-123/object-123-times.csv"
  }
}

object-123-times.csv

1
2
3
4
...

这将允许您将大量数据存储在更简单但更易于访问的格式中。然后,您可以使用类似{{link1:fgetcsv()}}的东西来解析每一行。


-2

您可以使用 array_chunk() 函数将数组分成块

array_chunk() 函数是 PHP 中的内置函数,用于根据传递给函数的参数将数组拆分为给定大小的部分或块。最后一个块可能包含比块的期望大小少的元素。

请查看此 链接 中的示例


2
谢谢你的帮助,但我认为第一个问题在于file_get_contents()json_decode(),对吧?它们需要大量的CPU和内存来打开并将JSON文件转换为数组,请告诉我你的想法,你会如何在这种情况下使用array_chunk() - Fadi
1
这个建议只会生成一个更深/更复杂的数据结构。 - mickmackusa
如果有什么问题,这可能会使它花费更长的时间。 - Nicholas Summers
@mickmackusa,感谢您的评论,请您能否提供更多的解释? - Foued MOUSSI

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接