Введение
QJsonDocument::fromJson() загружает и разбирает весь JSON-документ в памяти целиком, что прекрасно работает для типичных ответов API размером в килобайты, но становится проблематичным для очень больших файлов — десятки и сотни мегабайт JSON, которые целиком в памяти могут занимать кратно больше из-за накладных расходов представления QJsonValue/QVariant. Для таких случаев нужен потоковый (SAX-подобный) разбор, обрабатывающий документ по частям без построения полного дерева в памяти. В статье рассмотрим подход к потоковому разбору больших JSON-файлов.
Концепция
Qt не предоставляет встроенного потокового JSON-парсера, аналогичного QXmlStreamReader для XML — для этой задачи обычно используются сторонние библиотеки, такие как RapidJSON с его SAX API, или ручная реализация инкрементального парсера для специфичного, заранее известного формата (например, построчного NDJSON — JSON Lines, где каждая строка файла — отдельный самостоятельный JSON-объект, что естественно поддаётся построчной потоковой обработке без сторонних библиотек).
Пример кода
// Вариант 1: построчный разбор NDJSON (JSON Lines) — без сторонних библиотек
#include <QFile>
#include <QTextStream>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
void processLargeNdjsonFile(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Не удалось открыть файл";
return;
}
QTextStream stream(&file);
qint64 processedLines = 0;
double totalRevenue = 0.0;
while (!stream.atEnd()) {
const QString line = stream.readLine();
if (line.trimmed().isEmpty()) continue;
const QJsonDocument doc = QJsonDocument::fromJson(line.toUtf8());
if (!doc.isObject()) continue;
const QJsonObject obj = doc.object();
totalRevenue += obj.value("amount").toDouble();
processedLines++;
// Каждая строка обрабатывается и тут же освобождается —
// в памяти одновременно находится только одна запись, а не весь файл
}
qDebug() << "Обработано записей:" << processedLines;
qDebug() << "Суммарная выручка:" << totalRevenue;
}
// Вариант 2: SAX-подобный разбор через RapidJSON для единого большого JSON-массива
#include <rapidjson/reader.h>
#include <rapidjson/filereadstream.h>
#include <cstdio>
#include <iostream>
struct OrderSumHandler : rapidjson::BaseReaderHandler<rapidjson::UTF8<>, OrderSumHandler>
{
double totalAmount = 0.0;
long long orderCount = 0;
bool inAmountField = false;
bool Key(const char *str, rapidjson::SizeType length, bool)
{
inAmountField = (std::string(str, length) == "amount");
return true;
}
bool Double(double value)
{
if (inAmountField) {
totalAmount += value;
orderCount++;
}
return true;
}
bool Int(int value) { return Double(static_cast<double>(value)); }
};
void processLargeJsonArrayWithRapidJson(const char *filePath)
{
FILE *file = fopen(filePath, "rb");
if (!file) return;
char readBuffer[65536];
rapidjson::FileReadStream inputStream(file, readBuffer, sizeof(readBuffer));
OrderSumHandler handler;
rapidjson::Reader reader;
reader.Parse(inputStream, handler);
std::cout << "Обработано заказов: " << handler.orderCount << std::endl;
std::cout << "Суммарная сумма: " << handler.totalAmount << std::endl;
fclose(file);
}
Пояснения к коду
Вариант с NDJSON читает файл построчно через QTextStream::readLine(), разбирая каждую строку как отдельный, самостоятельный JSON-документ через обычный QJsonDocument::fromJson() — поскольку каждая строка обрабатывается и сразу освобождается, в памяти в любой момент находится только текущая запись, а не весь файл целиком, что делает этот подход пригодным для файлов практически любого размера при условии, что формат файла действительно построчный.
Вариант с RapidJSON демонстрирует настоящий SAX-подход для единого большого JSON-документа (например, один огромный массив объектов, а не построчный формат) — OrderSumHandler реализует callback-методы (Key, Double, Int), вызываемые парсером по мере чтения потока без построения дерева объектов в памяти; в примере обрабатывается специфичная задача суммирования значений поля amount без хранения самих объектов целиком.
Подводные камни
- NDJSON-подход требует, чтобы формат файла действительно был построчным — попытка применить его к обычному единому JSON-документу (один большой объект или массив, отформатированный с произвольными разрывами строк) даст неверный результат, поскольку отдельные строки текста не будут являться самостоятельными валидными JSON-документами.
- SAX-парсинг требует написания специфичного обработчика для каждой конкретной задачи, в отличие от универсального построения дерева
QJsonDocument— обработчик жёстко завязан на конкретную структуру и цель обработки (как в примере — суммирование одного поля), и при изменении задачи (нужно посчитать что-то другое из того же файла) обработчик нужно переписывать, а не просто иначе обращаться к уже построенному дереву. - Отсутствие случайного доступа к данным при потоковой обработке. В отличие от полностью загруженного
QJsonDocument, где можно обращаться к любому полю в произвольном порядке, потоковый разбор по своей природе последовательный — если задача требует многократного обращения к разным частям документа в произвольном порядке, потоковый подход потребует либо нескольких проходов по файлу, либо сохранения нужных промежуточных данных вручную во время единственного прохода. - Дополнительная зависимость от сторонней библиотеки (RapidJSON или аналог) для полноценного SAX API, поскольку Qt не предоставляет его «из коробки» — это увеличивает поверхность сборки проекта и требует отдельного внимания к версионной совместимости и лицензированию используемой библиотеки.