Há algum tempo, escrevi um aplicativo c ++ / Qt que enviava grandes quantidades de dados no formato JSON pela rede. O QJsonDocument padrão foi usado . Durante a implementação, encontrei baixo desempenho, além de um design de classe inconveniente, que não permitia a detecção normal de erros durante a operação. O resultado foi a biblioteca JsonWriterSax , que permite gravar documentos JSON no estilo SAX com alta velocidade, que publico no github.com sob a licença MIT. Quem se importa - peço um gato.
Pouco de teoria
JSON (JavaScript Object Notation) é um formato de dados de texto estruturado desenvolvido por Douglas Crockford e um subconjunto da linguagem ECMAScript (JavaScript, JScript, etc. foram criados com base). O JSON substitui o XML, expandindo os recursos de aninhamento e adicionando tipos de dados. Atualmente, é usado ativamente na Internet.
Mas existem falhas no JSON. Na minha opinião, entre os tipos padrão, claramente não existe o tipo DateTime suficiente - você precisa transferir o valor na forma de um número ou uma string e, ao analisar, tomar uma decisão dependendo do contexto. Mas vale a pena notar que no ECMAScript, o tipo Data foi criado há muito tempo, não foi pensado e, no mundo js, bibliotecas de terceiros são usadas para trabalhar com datas.
Existem 2 abordagens principais para analisar e criar documentos estruturados - SAX e DOM. Eles apareceram para XML, mas podem ser usados como padrões para criar manipuladores de outros formatos.
SAX (API simples para XML)
É usado para processamento seqüencial de dados e permite processar documentos grandes em um fluxo. Ao ler, ele retorna às informações do aplicativo sobre o elemento ou erro encontrado, mas a preservação das informações e o controle de aninhamento estão no próprio aplicativo. Durante a gravação, as etapas no estilo são geralmente indicadas: iniciar um elemento, iniciar um subelemento, escrever um número, escrever uma linha, fechar um subelemento, fechar um elemento. As desvantagens incluem o fato de que o programador é obrigado a escrever o código mais detalhadamente, para entender melhor a estrutura do documento e a falta ou limitação extrema da edição de um documento existente.
DOM (Modelo de Objeto de Documento)
Com esse método, uma árvore de documentos é construída na memória que pode ser serializada, desserializada e modificada. A principal desvantagem é o alto consumo de memória e o aumento do tempo de processamento. Sob o capô, geralmente é usado um manipulador SAX.
Problemas QJsonDocument
O QJsonDocument padrão usa a abordagem DOM. Ao criar um documento, a velocidade é baixa - você pode ver as referências no final do artigo. Mas o maior problema para mim foi o projeto de retorno de erro mal concebido.
auto max = std::numeric_limits<int>::max(); QJsonArray ja; for(auto i = 0; i < max; ++i) { ja.append(i); if(ja.size() - 1 != i) { break; } }
Neste exemplo, se não houver memória suficiente, a mensagem será gravada no fluxo de erros.
QJson: Document too large to store in data structure
e os dados não serão mais adicionados. No caso de uma matriz, você pode verificar a condição
ja.size() - 1 != i
Mas o que fazer ao trabalhar com um objeto? Verificar constantemente se uma nova chave foi adicionada? Analisar o log em busca de um erro?
A biblioteca
A biblioteca JsonWriterSax permite gravar um documento JSON em um QTextStream no estilo SAX e está disponível no github sob a licença MIT. O controle de memória é do aplicativo. A biblioteca controla a integridade do JSON - se o elemento for adicionado incorretamente, a função de gravação retornará um erro. Para controle, é usada uma gramática KS. Os testes foram escritos, mas talvez algum caso tenha sido deixado sem assistência. Se alguém corrigir a operação incorreta da verificação e relatar para corrigir o erro - ficarei muito grato.
Eu acho que a melhor descrição de uma biblioteca para um programador é exemplo de código =)
Exemplos
Criação de matriz
QByteArray ba; QTextStream stream(&ba); stream.setCodec("utf-8"); JsonWriterSax writer(stream); writer.writeStartArray(); for(auto i = 0; i < 10; ++i) { writer.write(i); } writer.writeEndArray(); if(writer.end()) { stream.flush(); } else { qWarning() << "Error json"; }
Como resultado, obtemos
[0,1,2,3,4,5,6,7,8,9]
Criação de Objetos
QByteArray ba; QTextStream stream(&ba); stream.setCodec("utf-8"); JsonWriterSax writer(stream); writer.writeStartObject(); for(auto i = 0; i < 5; ++i) { writer.write(QString::number(i), i); } for(auto i = 5; i < 10; ++i) { writer.write(QString::number(i), QString::number(i)); } writer.writeKey("arr"); writer.writeStartArray(); writer.writeEndArray(); writer.writeKey("o"); writer.writeStartObject(); writer.writeEndObject(); writer.writeKey("n"); writer.writeNull(); writer.write(QString::number(11), QVariant(11)); writer.write("dt", QVariant(QDateTime::fromMSecsSinceEpoch(10))); writer.writeEndObject(); if(writer.end()) { stream.flush(); } else { qWarning() << "Error json"; }
Como resultado, obtemos
{"0":0,"1":1,"2":2,"3":3,"4":4,"5":"5","6":"6","7":"7","8":"8","9":"9","arr":[],"o":{},"n":null,"11":11,"dt":"1970-01-01T03:00:00.010"}
Criando um documento com aninhamento e tipos diferentes
QByteArray ba; QTextStream stream(&ba); stream.setCodec("utf-8"); JsonWriterSax writer(stream); writer.writeStartArray(); for(auto i = 0; i < 1000; ++i) { writer.writeStartObject(); writer.writeKey("key"); writer.writeStartObject(); for(auto j = 0; j < 1000; ++j) { writer.write(QString::number(j), j); } writer.writeEndObject(); writer.writeEndObject(); } writer.writeEndArray(); if(writer.end()) { stream.flush(); } else { qWarning() << "Error json"; }
Benchmarks
Usado pelo QBENCHMARK durante a compilação do release. A funcionalidade é implementada na classe JsonWriterSaxTest .
OS 5.0 elementar Juno, kernel 4.15.0-38-genérico, CPU CPU Intel® Core (TM) 2 Quad 9550 @ 2,83GHz, 4G RAM, Qt 5.11.2 GCC 5.3.1
Matriz de números longos
- QJsonDocument: 42 msegs por iteração (total: 85, iterações: 2)
- JsonWriterSax: 23 msegs por iteração (total: 93, iterações: 4)
Objeto grande de um nível
- QJsonDocument: 1.170 msegs por iteração (total: 1.170, iterações: 1)
- JsonWriterSax: 53 msegs por iteração (total: 53, iterações: 1)
Documento complexo grande
- QJsonDocument: 1.369 msegs por iteração (total: 1.369, iterações: 1)
- JsonWriterSax: 463 msegs por iteração (total: 463, iterações: 1)
OS 5.0 elementar Juno, kernel 4.15.0-38-genérico, CPU CPU Intel® Core (TM) i7-7500U a 2.70GHz, 8G RAM, Qt 5.11.2 GCC 5.3.1
Matriz de números longos
- QJsonDocument: 29,5 msegs por iteração (total: 118, iterações: 4)
- JsonWriterSax: 13 msegs por iteração (total: 52, iterações: 4)
Objeto grande de um nível
- QJsonDocument: 485 msegs por iteração (total: 485, iterações: 1)
- JsonWriterSax: 31 msegs por iteração (total: 62, iterações: 2)
Documento complexo grande
- QJsonDocument: 734 msegs por iteração (total: 734, iterações: 1)
- JsonWriterSax: 271 msegs por iteração (total: 271, iterações: 1)
MS Windows 7 SP1, CPU Intel® Core (TM) i7-4770 CPU a 3.40GHz, 8G RAM, Qt 5.11.0 GCC 5.3.0
Matriz de números longos
- QJsonDocument: 669 msegs por iteração (total: 669, iterações: 1)
- JsonWriterSax: 20 ms por iteração (total: 81, iterações: 4)
Objeto grande de um nível
- QJsonDocument: 1.568 msegs por iteração (total: 1.568, iterações: 1)
- JsonWriterSax: 44 msegs por iteração (total: 88, iterações: 2)
Documento complexo grande
- QJsonDocument: 1.167 msegs por iteração (total: 1.167, iterações: 1)
- JsonWriterSax: 375 msegs por iteração (total: 375, iterações: 1)
MS Windows 7 SP1, CPU Intel® Core (TM) i3-3220 CPU a 3,30GHz, 8G RAM, Qt 5.11.0 GCC 5.3.0
Matriz de números longos
- QJsonDocument: 772 msegs por iteração (total: 772, iterações: 1)
- JsonWriterSax: 26 msegs por iteração (total: 52, iterações: 2)
Objeto grande de um nível
- QJsonDocument: 2.029 msegs por iteração (total: 2.029, iterações: 1)
- JsonWriterSax: 59 ms por iteração (total: 59, iterações: 1)
Documento complexo grande
- QJsonDocument: 1.530 msegs por iteração (total: 1.530, iterações: 1)
- JsonWriterSax: 495 msegs por iteração (total: 495, iterações: 1)
Perspectivas
Em versões futuras, planejo adicionar a capacidade de descrever o formato dos dados do usuário por meio de funções lambda usando QVariant, adicionar a capacidade de usar separadores para formatar o documento (documento bonito) e, se a comunidade estiver interessada, posso adicionar um analisador SAX.
A propósito, minha biblioteca me ajudou a encontrar o erro de estouro, que permite qInfo (), qDebug (), qWarning () definir o formato e a saída no estilo do módulo de log do Python. Também pretendo postar esta biblioteca em código aberto - se alguém estiver interessado - escreva nos comentários.