PHP中高效聚合多JSON文件数据并生成报表
在日常开发中,经常遇到需要从多个JSON文件中读取数据并进行聚合分析的场景。例如,分布式日志系统会将不同时段的日志分别存储为独立的JSON文件,或者多个业务模块各自生成数据快照文件。如果逐个文件读取后再逐条处理,当文件数量庞大(如数千个)或数据量较大时,容易导致内存溢出或处理速度过慢。本文将介绍一种高效聚合多个JSON文件数据并生成报表的PHP实现方案。
问题分析与设计思路
假设我们有数百个JSON文件位于同一个目录下,每个文件包含一组相同结构的记录(如用户行为数据)。我们需要统计所有记录中某个字段的总和、平均值,或者按某个维度分组计数。传统做法是使用file_get_contents()逐一读取文件,再通过json_decode()解析为数组,然后循环聚合。但这样做有以下痛点:
- 一次性加载整个文件到内存,大文件时消耗高
- 逐个文件的I/O开销较大,无法并行处理
- 聚合逻辑与文件读取紧密耦合,不利于维护
改进思路:
- 使用文件流逐行或逐块读取,避免一次性加载大JSON文件(若文件是单行JSON对象数组,可逐行解析)
- 利用
Generator实现惰性加载,仅按需迭代数据 - 将聚合操作封装为独立的函数或类,便于测试和复用
- 最终输出报表可以采用数组、CSV或简单的HTML表格
高效实现方案
下面我们实现一个聚合脚本,假设每个JSON文件内容为如下格式:
[
{"user":"Alice","amount":100,"category":"food"},
{"user":"Bob","amount":50,"category":"drink"},
{"user":"Alice","amount":30,"category":"drink"}
]我们需要统计每个用户的amount总和,以及每个类别的amount总和。以下是完整代码:
<?php
/**
* 从单个JSON文件中逐条读取记录(假设文件是JSON数组,每行一条记录)
* 使用生成器避免一次性加载整个文件
*
* @param string $filePath 文件路径
* @return Generator
*/
function readJsonRecords(string $filePath): Generator
{
// 检测文件是否存在
if (!file_exists($filePath) || !is_readable($filePath)) {
throw new RuntimeException("File {$filePath} not exist or not readable.");
}
// 读取整个文件内容(如果文件太大,可以改用流式读取,但这里为简化使用file_get_contents)
$content = file_get_contents($filePath);
if ($content === false) {
throw new RuntimeException("Failed to read file {$filePath}.");
}
$records = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException("JSON parse error in {$filePath}: " . json_last_error_msg());
}
// 逐个yield记录,外部循环不会重复加载
foreach ($records as $record) {
yield $record;
}
}
/**
* 批量聚合所有JSON文件的数据
*
* @param string $directory 包含JSON文件的目录路径
* @param string $pattern Glob匹配模式,如 "*.json"
* @return array ['user_totals' => [...], 'category_totals' => [...]]
*/
function aggregateJsonFiles(string $directory, string $pattern = '*.json'): array
{
$userTotals = []; // user => sum(amount)
$categoryTotals = []; // category => sum(amount)
$filePaths = glob($directory . DIRECTORY_SEPARATOR . $pattern);
if (empty($filePaths)) {
echo "No JSON files found in {$directory}" . PHP_EOL;
return ['user_totals' => $userTotals, 'category_totals' => $categoryTotals];
}
foreach ($filePaths as $filePath) {
try {
$records = readJsonRecords($filePath);
foreach ($records as $record) {
// 简单校验记录合法性
if (!isset($record['user'], $record['amount'], $record['category'])) {
echo "Skipping invalid record in {$filePath}" . PHP_EOL;
continue;
}
$user = $record['user'];
$category = $record['category'];
$amount = (float)$record['amount']; // 转浮点数确保计算准确
// 聚合用户总和
if (!isset($userTotals[$user])) {
$userTotals[$user] = 0.0;
}
$userTotals[$user] += $amount;
// 聚合类别总和
if (!isset($categoryTotals[$category])) {
$categoryTotals[$category] = 0.0;
}
$categoryTotals[$category] += $amount;
}
} catch (RuntimeException $e) {
echo "Error processing {$filePath}: " . $e->getMessage() . PHP_EOL;
continue;
}
}
return [
'user_totals' => $userTotals,
'category_totals' => $categoryTotals,
];
}
// ---------- 使用示例 ----------
$directory = '/var/logs/userdata'; // 请替换为真实路径
$result = aggregateJsonFiles($directory, '*.json');
// 生成报表(简单文本表格)
echo "=== 用户金额汇总 ===" . PHP_EOL;
printf("%-15s %s" . PHP_EOL, '用户', '总金额');
echo str_repeat('-', 25) . PHP_EOL;
arsort($result['user_totals']); // 按金额降序显示
foreach ($result['user_totals'] as $user => $total) {
printf("%-15s %.2f" . PHP_EOL, $user, $total);
}
echo PHP_EOL . "=== 类别金额汇总 ===" . PHP_EOL;
printf("%-15s %s" . PHP_EOL, '类别', '总金额');
echo str_repeat('-', 25) . PHP_EOL;
arsort($result['category_totals']);
foreach ($result['category_totals'] as $category => $total) {
printf("%-15s %.2f" . PHP_EOL, $category, $total);
}
?>上述代码中,readJsonRecords()函数作为生成器,每次仅yield一条记录,外层foreach循环依次处理,不会占用过多内存。聚合逻辑使用简单的数组累加,最后按金额排序输出文本格式的报表。如果需要输出为HTML表格,可替换printf部分为<table>标签。
优化与扩展
如果JSON文件非常大(超过几十MB),上述file_get_contents()仍然会一次性读入内存,可能会导致内存紧张。此时可以采用流式读取,例如使用fgets()逐行读取,前提是JSON文件是每行一条JSON对象(Line-delimited JSON)。示例:
function readJsonRecordsStream(string $filePath): Generator
{
$handle = fopen($filePath, 'rb');
if (!$handle) {
throw new RuntimeException("Cannot open file {$filePath}");
}
while (($line = fgets($handle)) !== false) {
$line = trim($line);
if (empty($line)) {
continue;
}
$record = json_decode($line, true);
if (json_last_error() !== JSON_ERROR_NONE) {
// 忽略解析失败的行,可记录日志
continue;
}
yield $record;
}
fclose($handle);
}此外,若数据量极大且需要实时报表,可以考虑将聚合结果缓存到APCu或Redis中,每次增量更新。或者使用PDO将数据导入数据库后执行SQL聚合,但这超出了纯PHP聚合的范畴。
性能注意事项
- 文件I/O:使用
glob()一次性获取文件列表,避免在循环内重复扫描目录。 - 解析开销:
json_decode()是CPU密集型操作,对于大文件建议使用流式逐行解析。 - 内存控制:生成器是惰性求值,每处理完一条记录即可释放,非常适合大数据量。
- 错误隔离:单个文件处理失败不应中断整个流程,使用
try-catch跳过并记录错误。
总结
本文介绍了一种利用生成器、文件遍历和简单聚合的PHP方案,能够高效处理多个JSON文件的数据汇总任务。通过将文件读取、解析和聚合逻辑解耦,代码易于维护和扩展。对于超大规模数据,可结合流式读取或数据库中间件使用。希望这篇文章能为你在实际项目中处理类似需求提供参考。