Разобрать подстроку как JSON, используя QJsonDocument
У меня есть строка, которая содержит (не является) данные в кодировке JSON, как в этом примере:
foo([1, 2, 3], "some more stuff")
| |
start end (of JSON-encoded data)
Полный язык, который мы используем в нашем приложении, содержит данные в кодировке JSON, а остальная часть языка тривиальна (просто рекурсивный материал). При разборе таких строк слева направо в рекурсивном парсере я знаю, когда сталкиваюсь с JSON-кодированным значением, как здесь [1, 2, 3]
начиная с индекса 4. После анализа этой подстроки мне нужно знать конечную позицию, чтобы продолжить синтаксический анализ оставшейся части строки.
Я хотел бы передать эту подстроку в хорошо протестированный JSON-парсер, такой как QJsonDocument
в Qt5. Но, читая документацию, невозможно проанализировать только подстроку как JSON, что означает, что, как только закончится анализ данных (после использования ]
здесь) управление возвращается без сообщения об ошибке разбора. Кроме того, мне нужно знать конечную позицию, чтобы продолжить анализ моего собственного материала (здесь оставшаяся строка , "some more stuff")
).
Для этого я использовал собственный анализатор JSON, который берет текущую позицию по ссылке и обновляет ее после завершения анализа. Но так как это важная часть безопасности бизнес-приложения, мы больше не хотим придерживаться моего собственного парсера. Я имею в виду, что есть QJsonDocument
так почему бы не использовать его. (Мы уже используем Qt5.)
В качестве обходного пути, я думаю об этом подходе:
- Позволять
QJsonDocument
проанализировать подстроку, начиная с текущей позиции (которая не является допустимой JSON) - Ошибка сообщает о неожиданном символе, это некоторая позиция за пределами JSON
- Позволять
QJsonDocument
проанализируйте снова, но на этот раз подстрока с правильной конечной позицией
Вторая идея состоит в том, чтобы написать "конечный сканер JSON", который берет всю строку, начальную позицию и возвращает конечную позицию данных в кодировке JSON. Это также требует синтаксического анализа, так как в строковых значениях могут появляться несоответствующие скобки / круглые скобки, но писать (и использовать) такой класс должно быть намного проще (и безопаснее) по сравнению с полностью обработанным вручную JSON-парсером.
У кого-нибудь есть идея получше?
1 ответ
Я применил быстрый анализатор [*] на основе http://www.ietf.org/rfc/rfc4627.txt с использованием Spirit Qi.
На самом деле он не анализируется в AST, но он анализирует всю полезную нагрузку JSON, которая на самом деле немного больше, чем требуется здесь.
Пример http://liveworkspace.org/code/3k4Yor%242 выводит:
Non-JSON part of input starts after valid JSON: ', "some more stuff")'
На основании теста, данного ОП:
const std::string input("foo([1, 2, 3], \"some more stuff\")");
// set to start of JSON
auto f(begin(input)), l(end(input));
std::advance(f, 4);
bool ok = doParse(f, l); // updates f to point after the start of valid JSON
if (ok)
std::cout << "Non-JSON part of input starts after valid JSON: '" << std::string(f, l) << "'\n";
Я проверил с несколькими другими более вовлеченными документами JSON (включая многострочный).
Несколько замечаний:
- Я сделал синтаксический анализатор на основе итератора, поэтому он, вероятно, будет легко работать со строками Qt (?)
- Если вы хотите запретить многострочные фрагменты, измените шкипер с
qi::space
вqi::blank
- Существует сокращение соответствия для разбора номера (см. TODO), которое не влияет на достоверность этого ответа (см. Комментарий).
[*] технически, это скорее заглушка синтаксического анализатора, поскольку она не переводится во что-то другое. Это в основном лексер, взявший на себя слишком много работы:)
Полный код образца:
// #define BOOST_SPIRIT_DEBUG
#include <boost/spirit/include/qi.hpp>
namespace qi = boost::spirit::qi;
template <typename It, typename Skipper = qi::space_type>
struct parser : qi::grammar<It, Skipper>
{
parser() : parser::base_type(json)
{
// 2.1 values
value = qi::lit("false") | "null" | "true" | object | array | number | string;
// 2.2 objects
object = '{' >> -(member % ',') >> '}';
member = string >> ':' >> value;
// 2.3 Arrays
array = '[' >> -(value % ',') >> ']';
// 2.4. Numbers
// Note out spirit grammar takes a shortcut, as the RFC specification is more restrictive:
//
// However non of the above affect any structure characters (:,{}[] and double quotes) so it doesn't
// matter for the current purpose. For full compliance, this remains TODO:
//
// Numeric values that cannot be represented as sequences of digits
// (such as Infinity and NaN) are not permitted.
// number = [ minus ] int [ frac ] [ exp ]
// decimal-point = %x2E ; .
// digit1-9 = %x31-39 ; 1-9
// e = %x65 / %x45 ; e E
// exp = e [ minus / plus ] 1*DIGIT
// frac = decimal-point 1*DIGIT
// int = zero / ( digit1-9 *DIGIT )
// minus = %x2D ; -
// plus = %x2B ; +
// zero = %x30 ; 0
number = qi::double_; // shortcut :)
// 2.5 Strings
string = qi::lexeme [ '"' >> *char_ >> '"' ];
static const qi::uint_parser<uint32_t, 16, 4, 4> _4HEXDIG;
char_ = ~qi::char_("\"\\") |
qi::char_("\x5C") >> ( // \ (reverse solidus)
qi::char_("\x22") | // " quotation mark U+0022
qi::char_("\x5C") | // \ reverse solidus U+005C
qi::char_("\x2F") | // / solidus U+002F
qi::char_("\x62") | // b backspace U+0008
qi::char_("\x66") | // f form feed U+000C
qi::char_("\x6E") | // n line feed U+000A
qi::char_("\x72") | // r carriage return U+000D
qi::char_("\x74") | // t tab U+0009
qi::char_("\x75") >> _4HEXDIG ) // uXXXX U+XXXX
;
// entry point
json = value;
BOOST_SPIRIT_DEBUG_NODES(
(json)(value)(object)(member)(array)(number)(string)(char_));
}
private:
qi::rule<It, Skipper> json, value, object, member, array, number, string;
qi::rule<It> char_;
};
template <typename It>
bool tryParseAsJson(It& f, It l) // note: first iterator gets updated
{
static const parser<It, qi::space_type> p;
try
{
return qi::phrase_parse(f,l,p,qi::space);
} catch(const qi::expectation_failure<It>& e)
{
// expectation points not currently used, but we could tidy up the grammar to bail on unexpected tokens
std::string frag(e.first, e.last);
std::cerr << e.what() << "'" << frag << "'\n";
return false;
}
}
int main()
{
#if 0
// read full stdin
std::cin.unsetf(std::ios::skipws);
std::istream_iterator<char> it(std::cin), pte;
const std::string input(it, pte);
// set up parse iterators
auto f(begin(input)), l(end(input));
#else
const std::string input("foo([1, 2, 3], \"some more stuff\")");
// set to start of JSON
auto f(begin(input)), l(end(input));
std::advance(f, 4);
#endif
bool ok = tryParseAsJson(f, l); // updates f to point after the end of valid JSON
if (ok)
std::cout << "Non-JSON part of input starts after valid JSON: '" << std::string(f, l) << "'\n";
return ok? 0 : 255;
}