Comments 60
JSON — это не тот формат данных, на который можно слепо полагаться
Нет в мире совершенства.
предложения по расширению формата json в соответствии с синтаксисом ECMAScript 5. Цитата из википедии:
Некоторые нововведения:
Поддерживаются как однострочные //, так и многострочные /* */ комментарии.
Объекты и списки могут иметь запятую после последнего элемента (удобно при копипейсте элементов).
Ключи объекта могут быть без кавычек, если они являются валидными идентификаторами ECMAScript 5.
Строки могут заключаться как в одинарные, так и в двойные кавычки.
Числа могут быть в шестнадцатеричном виде, начинаться или заканчиваться десятичной точкой, включать Infinity, -Infinity, NaN и -NaN, начинаться со знака +.
ИМХО вполне разумные предложения, надо бы чтобы их приняли. Сам по себе формат действительно очень красивый, лучше многословного xml и всех форматов основанных на отступах.
Запятые в конце — для простой автоматической генерации списков (не нужно думать последний элемент или нет).
Разные кавычки… не знаю, кавычки существуют всего двух типов, может это и имеет смысл а может и нет.
Шестнадцатеричный вид (0x) это общепринятая вещь. Неплохо бы еще двоичный сразу добавить (0b).
Так что но никакого зоопарка, только все самое необходимое для определения данных.
включать Infinity, -Infinity, NaN и -NaN,
профита от этого не понял, если честно
Лично я, хотя и прекрасно понимаю, что JSON это отдельный стандарт, все равно, «в душе» всегда воспринимал JSON как просто объектный литерал JavaScript, из которого, по понятным причинам, убрали тип function. ИМХО, к этому он и должен стремиться.
Вообще, главная проблема JSON в том, что он, с одной стороны, очень похож на JavaScript, а с другой — имеет ряд мелких отличий. Проблемой это является потому, что многие люди обманываются большой схожестью, и не всегда уделяют должное внимание отличиям. К тому же, если отсутствие function вполне понятно, то смысл остальных расхождений с JavaScript не вполне ясен. Как можно уменьшить путаницу? Сделать JSON не похожим на JavaScript просто не получится — это был бы уже совершенно другой формат. А вот привести его в большее соответствие с JavaScript, в принципе можно. Включение Infinity и NaN отлично в эту идею вписывается. И польза тут не столько от самих Infinity и NaN, сколько от большей совместимости с JavaScript.
В то же время, хоть я и назвал вышеописанное «главной проблемой», это не стоит читать как «фатальный недостаток». JSON и в текущем виде вполне неплох. Просто, если что-то и стоит в нем улучшить, то именно совместимость с JavaScript. Так мне кажется. Возможно это от того, что мне часто приходится работать с JSON именно в JavaScript. С другой стороны JSON это ведь «JavaScript Object Notation», а содержимое должно соответствовать названию.
А по поводу .1, разных кавычек и прочего — это просто грязь и бессмысленное усложнение стандарта. Нормальные сериализаторы такого никогда не выдадут, а у разработчика ручки не отсохнут аккуратно написать.
Пожалуй, только комментарии кажутся полезным дополнением.
Согласен насчёт .1
и кавычек. Но не согласен насчёт inf и NaN — в большинстве систем, где есть числа с плавающей точкой (напомню — таких чисел вообще‐то может и не быть), есть и inf с NaN, иногда даже с двумя NaN. Для получения «неопределённого поведения» по причине «отсутствует представление» совершенно не нужно искать где‐то платформу, на которой есть числа с плавающей точкой, но нет inf и/или NaN — достаточно взглянуть в статью и найти там различные вещи вроде «слишком большое целое», «число с плавающей точкой со слишком большой мантиссой», и т.д. То, что к списку непредставимых чисел с плавающей точкой добавятся ещё два не изменит того, что парсеру хорошо бы как‐то обрабатывать непредставимые числа, и хорошо бы их обрабатывать согласованно (consistently).
Я предпочту лучше иметь возможность сериализовать любое число с плавающей точкой, какое мне может попасться, чем как‐то обрабатывать ситуацию «есть число с плавающей точкой, но в JSON его сериализовать нельзя». Что делать парсеру в проблемных случаях обычно понятнее, чем что делать генератору.
{ "a": ".1", "b": "NaN", "c": 0.1 }
Может, проще вообще взять и написать свой формат?
- Такой вариант точно медленнее, но может быть совершенно медленным. Особенно, если вы делаете что‐то общего назначения и не можете сказать «ключу
"b"
всегда соответствуетfloat
» (т.е. здесь вам придётся как‐то отдельно кодировать NaN, которыйfloat
и строку"NaN"
и проверять все строки), а основная часть парсера написана не вами и на C в целях его ускорения (с этим вы столкнётесь, если, к примеру, попытаетесь расширить стандартный JSON парсер/генератор в Python: самая критичная к производительности часть на C). - Строка, в которой нарушается типизация (а
"timeout": inf
куда логичнее"timeout": 0.0
), в глазах других разработчиков, которые будут её писать, выглядит как‐то костыльно. - Везде, куда вы хотите подсунуть такой JSON, вам нужен свой парсер и свой генератор.
- И это совершенно не отменяет того, что в наиболее используемом стандарте представления чисел с плавающей точкой две бесконечности и не‐число есть, почти все конвертеры string→float понимают строки вроде
inf
илиnan
(потому как это понимает даже libc’шнаяstrtod
— обязана понимать по стандарту, хотя и может понять как «самое большое/маленькое представимое число» (inf) и «что‐то вроде ошибки» (возвращается 0, но во второй аргумент записывается первый, аerrno
не устанавливается)), а конкурирующие форматы зачастую позволяют записать inf и nan.
Если комментарии сохранять при десериализации, то придётся заводить для них новый тип (как в XML) или, скажем, временно запихивать комментарии в «скрытые» поля вроде { "__comment": «This is my comment» }. Но последнее можно делать и самому, если сильно приспичит.
{
my/*Avesome*/Val : "this is awesome value",
strangeNumber : 10/*000*/00,
}
И если комментарий внутри значения еще можно воспринимать, как что-то вроде:
strangeNumber : [
10,
/*000*/,
00
]
То комментарий в середине ключа приводит нас к тому, что уже и ключ теперь не является простым значением. Он тоже становится массивом из кусков строки и комментариев. По-моему это уж слишком.
Даже если сохранять только комментарии, которые расположены между значениями, возникают дополнительные трудности:
{
//my first parameter
"a" : 1,
//my second parameter
"c" : 2,
//my third parameter
"b" : 3
}
Если в ходе десериализации и последующей новой сериализации параметры будут отсортированы по алфавиту, то комментарии перепутаются и начнут «врать». Хотя, по идее, порядок должен быть не важен, ведь у нас не массив. Но как-то так получается, что все, чего коснулись комментарии становится массивом.
Так что, остается их просто игнорировать. Да, при машинной обработке они потеряются, но это меньшее из зол. Тем более, если конфигурационный файл создается программой, то комментарии должны быть уже в коде этой программы, а не в самом файле конфигурации. Более того, описание параметров конфигурации разумно включать в документацию. Там им будет самое место. Так же описания можно хранить в виде отдельного json, и тогда можно будет генерировать документацию автоматом, и отображать подсказки прямо в визуальном редакторе конфигурации. Все эти методы на порядок лучше, чем писать пояснения в комментариях. Комментарии же уместны и удобны в процессе активной разработки, когда json пишется вручную. Краткие пояснения, TODO, не используемые куски конфига, к которым возможно еще вернешься и т.д. Для всего этого вполне достаточно самой простой реализации, которая просто удалит все комментарии, перед тем, как парсить остальной код.
Интересно было бы посмотреть на результаты ставшего стандартным даже для майкросовтовских библиотек дотнетного Newtonsoft.Json. Он в дотнете благополучно выжил все остальные парсеры из дикой природы.
Есть ещё один не приянтный момент, связанный с мегапопулярностью JSON: его используют всюду, даже там,
где явно не стоило бы, видимо, надеясь на лёгкую переносимость данных(в любом ЯП есть либа для работы с JSON).
Как пример из личного опыта, могу привести такой случай: человеку надо было перегнать данные из JSON в базу данных, подвох был в том, что json файлы были по 20-40 Gb в одну строку. Помню я очень тогда удивился увидев "однострочный" тридцатигиговый json файл, думаю, что такие объёмы никакая стдлиба ЯП не прожуёт, разве только у тебя не оч. много оперативки на машине где работает разбор подобного файла.
Пришлось писать свой "парсер", который посимвольно/блочно разбивает эту жижу на отдельные json'ы, из которых потом извлекается нужная информация и вставляется в БД.
Мне повезло: это были то ли твиты, то ли месаги с какого-то форума и в них не было "},{".
Вообще нормальные парсеры (тот же JSON.NET) поддерживают потокенный разбор (JsonReader) из байтстрима, а десериализатор, соответственно, можно заставить работать в поточном режиме, вызывая в нужных состояниях парсера. Так что тут проблема не файла, проблема вашего парсера. А так я тоже видел, как люди трёхгигабайтный xml в память полностью грузили для вставки в базу.
ага, тогда смотрел обычный json из стандартной библиотеки python(https://docs.python.org/2/library/json.html)
и что-то я не нашёл тогда ничего альтернативного, потому и пилил свой, видимо, плохо искал,
сейчас вот вижу, на http://stackoverflow.com/questions/10382253/reading-rather-large-json-files-in-python подсказывают, что есть https://github.com/isagalaev/ijson
"a module that will work with JSON as a stream, rather than as a block file."
Кстати говоря, в случае перегонки больших данных из json файлов в БД с целью получения возможности
последующей выборки/анализа онных, можно рассмотреть такой вариант: просто импортнуть в MongoDb.
Этого должно быть достаточно для простенькой выборки, вроде "все коменнты/посты такого-то опльзователя за такой-то период" и т.п.
В общем случае, когда неизвестно, что делать с разобранными данными, придётся всё дерево держать в памяти.
Потоковые парсеры практически всегда есть, как уже сказали.
Для Ruby, к примеру, Yajl-ruby (насколько помню, основано на потоковом C-парcере Yajl).
Слишком «тяжёлый» формат: занимает больше места (и, следовательно, дольше передаётся по сети), дольше разбирается (если, конечно, вы не решите выбрать подмножество XML и написать специальный парсер для него), обычно дольше создаётся (генерация имеет куда бо́льший потенциал оптимизации, чем разбор), сложнее читать человеку, код для работы с разобранной XML обычно длиннее (если только парсер не ваш под ваше подмножество и вы не можете воспользоваться одним из таких парсеров, которые умеют (де)сериализовывать ваши объекты — для Java это вроде обычное дело, а про JavaScript не скажу), много ненужных (в тех случаях, когда выбирают JSON, а не «не нужных» в принципе) возможностей.
Было бы интересно увидеть результаты ещё и этих парсеров:
https://dlang.org/phobos/std_json.html
http://vibed.org/api/vibe.data.json/
https://code.dlang.org/packages/json
Могу помочь с кодом на D, если нужно.
Кстати говоря, BSON, я так понимаю, лишён всех этих подводных камней JSON(если грубо сравнивать), потому что он бинарный: http://bsonspec.org/spec.html
Отсюда мораль: текстовые протоколы передачи данных хорошо, а бинарные ещё лучше(экономичнее, эффективнее, меньше ошибок).
Предвидя, минусующих, скажу: я знаю, что JSON это не протокол передачи данных, а формат, я иммел ввиду в более широком смысле(этот формат в основном и используется для обмена данными между разными компонентами систем, в этом смысле можно провести аналогию с "протоколом передачи данных" работающем поверх http)
Эцсамое, где-то видел сравнение, на котором BSON при всей своей бинарности жрёт больше места чем неотформатированный JSON на выборке "обычных данных".
Вот примеры нелогичных для меня моментов:
1. Длины кусков данных представляются в виде int32. С одной стороны, вылезает ограничение в 2ГБ, с другой — на описание длины коротких списков (а их большинство) все равно приходится тратить по 4 байта. Почему нельзя использовать способ с переменной длиной поля длины, например, так?
2. Почему имя элемента не может содержать символ 0x00?
3. Почему нельзя было boolean false и true сделать просто разными типами? Сэкономили бы целый байт!
4. Зачем тратить байты на символ 0x00 в конце строк?
5. Почему массивы представляются как объекты? Похоже на костыль: ведь для каждого элемента нужно заводить по неспользуемому ключу.
6. Зачем хранить длину массивов и объектов в байтах, когда логичнее хранить количество элементов? Значение меньше — меньше места будет занимать при кодировании с переменной длиной.
Резюме: если разработчики MongoDB решили, что именно такой формат удобен для внутреннего представления данных — да пожалуйста, их право. Только не стоит в этом случае этот формат вытаксивать наружу. На роль компактного формата для передачи данных он не очень годится.
По всему не отвечу ибо не сильно разбираюсь, но по вот этому:
- Почему имя элемента не может содержать символ 0x00?
- Зачем тратить байты на символ 0x00 в конце строк?
по-моему, очевидно, используется как разделитель/спец символ чтобы в потоке байтов
одно можно было отличить от другого (границы строк "String — The int32 is the number bytes in the (byte*) + 1 (for the trailing '\x00')" )
Резюме: если разработчики MongoDB решили, что именно такой формат удобен для внутреннего представления данных — да пожалуйста, их право. Только не стоит в этом случае этот формат вытаксивать наружу.
Не согласен с Вами, почему не стоит? Стоит хотя бы потому что у них "получилось". Посмотрите на спецификацию — она в сравнении с JSON спецификациями и рфц приведёнными в статье, в разы меньше, лаконичнее и одна. Да, есть какие-то сырые моменты, ну так никто и не говорит, что это идеал.
На роль компактного формата для передачи данных он не очень годится.
Годится: https://en.wikipedia.org/wiki/BSON, раздел Efficiency:
Сompared to JSON, BSON is designed to be efficient both in storage space and scan-speed. Large elements in a BSON document are prefixed with a length field to facilitate scanning. In some cases, BSON will use more space than JSON due to the length prefixes and explicit array indices.
Но да, есть случаи когда выигрыша по сравнению с обычным JSON нет — бсон больше ест (последнее предложение).
Кстати есть же ещё http://msgpack.org/ если BSON не нравится...
С учетом выявленных проблем того и другого в результате был разработан CBOR (https://cbor.io) и стандартизирован в RFC 7049 (а за годы уже и схему подвезли).
по-моему, очевидно, используется как разделитель/спец символ чтобы в потоке байтов
одно можно было отличить от другого (границы строк «String — The int32 is the number bytes in the (byte*) + 1 (for the trailing '\x00')» )
Зачем нужен разделитель, если длина строки передаётся отдельно? А вот невозможность использования строки, содержащей символ 0x00, в качестве ключа, может выйти боком.
Годится: https://en.wikipedia.org/wiki/BSON, раздел Efficiency: ...
Так я предложил идеи, как добиться ещё более компактного представления без изменения функциональности. Впрочем, проще в этом случае уже использовать protobuf — и компактнее, и быстрее.
Так я предложил идеи, как добиться ещё более компактного представления без изменения функциональности. Впрочем, проще в этом случае уже использовать protobuf — и компактнее, и быстрее.
https://github.com/mongodb/mongo/tree/v0.8/db, февраль 2009, уже содержит надпись BSON в разных файлах. https://en.wikipedia.org/wiki/Protocol_Buffers: публичный релиз в июле 2008. Учитывая, что до этой даты никакого релиза не было, писать Mongo начали в октябре 2007 (первое изменение в репозитории), а первое упоминание BSON (в репозитории) было в октябре 2008, в изменении с говорящим комментарием «rename classes» (т.е. BSON в каком‐то виде был и раньше), вы действительно ожидаете использование protobuf в mongoDB? Там уже на момент релиза protobuf было «работает — не трогай».
Кроме того, BSON предоставляет готовую схему с готовым набором типов, а protobuf требует разработки своего набора типов и только так даст вам схему.
Они нелогичны только если вы оптимизируете «по длине». «По скорости» наоборот: я не видел исходников MongoDB, но для меня как C разработчика всё выглядит логичным:
- При этом чтение длины — один memcpy. В худшем случае ещё и с преобразованием LE→BE (насколько я знаю, одна инструкция в ряде процессоров, за такты не скажу, но точно быстрее ветвлений, неизбежных при использовании переменной длины любого толка).
- C’шные строки заканчиваются на 0.
- Если вы знаете, что BSON корректный, то просто копируете один следующий байт в bool. Он почти наверняка уже будет в кэше. Хотя, скорее всего, два типа будут всё же быстрее.
- Не нужно выделять буфер под строки: если у вас уже есть в памяти буфер с BSON, то когда вам нужно вернуть C’шную строку вы просто возвращаете указатель внутрь этого буфера. Если нулевого байта там не будет, вам придётся либо выделять память, либо отказаться от строковых функций из стандартной библиотеки, даже если вас вполне устраивает, что C’шная строка не переживёт буфера с BSON.
- Выглядит как особенность, связанная с реализацией MongoDB.
- Логичнее хранить и то, и то: длина в байтах нужна, когда вы хотите найти какой‐то элемент и знаете, что этот — не то, что вам нужно. С длиной в байтах можно быстро сделать
seek
, сразу увеличить указатель или как‐то ещё пропустить ненужный кусок: при чтении BSON что черезread()
, что черезmmap()
, что просто получивchar*
от какой‐то функции мы можем скакать по байтам, но не по структуре. Без длины в байтах вам придётся прочитать весь массив, чтобы определить, где он заканчивается. Но если же вам нужен именно этот элемент и вы хотите превратить его, скажем, вstruct BSONData[]
, то проще выделить память под возвращаемое значение, имея длину в элементах.
Резюме: если разработчики MongoDB решили, что именно такой формат удобен для внутреннего представления данных — да пожалуйста, их право. Только не стоит в этом случае этот формат вытаксивать наружу. На роль компактного формата для передачи данных он не очень годится.
Бинарные форматы нужны не только потому, что они компактные, но и потому что они быстрые. Разные форматы имеют разный баланс память/скорость, и я не вижу, почему вы считаете, что причины, по которым разработчики BSON частично пожертвовали компактностью, не применимы к другим проектам других разработчиков: кому‐то другому тоже может быть нужен достаточно (для его проекта) компактный формат, который при этом быстр.
4. Не нужно выделять буфер под строки ...
А теперь смотрите нюанс: в отличие от строк-ключей, BSON допускает использование символа 0x00 в строках-значениях. Если этот символ встретится посередине строки, то при работе со строкой как в C-строкой (она же заканчивается на 0!) она будет обрезана.
Бинарные форматы нужны не только потому, что они компактные, но и потому что они быстрые. Разные форматы имеют разный баланс память/скорость, и я не вижу, почему вы считаете, что причины, по которым разработчики BSON частично пожертвовали компактностью, не применимы к другим проектам других разработчиков: кому‐то другому тоже может быть нужен достаточно (для его проекта) компактный формат, который при этом быстр.
Скорость передачи данных по сети существенно меньше скорости парсинга данных (гусары c highload — молчите), поэтому для ориентированного на передачу по сети формата предпочтительнее использовать более компактное представление.
Если же формат разрабатывался для локального использования с упором на скорость, то не вижу ничего плохого в принесении компактности в жертву.
А теперь смотрите нюанс: в отличие от строк-ключей, BSON допускает использование символа 0x00 в строках-значениях. Если этот символ встретится посередине строки, то при работе со строкой как в C-строкой (она же заканчивается на 0!) она будет обрезана.
Смотря какой строкой. Если мы точно знаем, что корректный BSON именно в этой строке не допускает нулевого байта, то эту строку можно использовать и так. Если запрошенный целевой формат в принципе не допускает нулевого байта внутри (а вызывающему может быть глобально пофиг на такое обрезание), то также можно работать и так. Ещё возможно, что написанный код предполагает ветвление — какой‐то более эффективный код в отсутствие нулевого байта внутри и менее в присутствии.
Я замечу, что в проекте Neovim со структурой String поступают также по схожим причинам: хотя String — это и {size_t; char *;}
, и иногда может содержать и нулевой байт внутри, char *
всё равно оканчивается на нулевой байт просто, потому что так удобнее, а наличие нулевого байта внутри невозможно, допустимо обрабатывать обрезанием или вообще проходит по статье «garbage in — garbage out» (оно же «нам послали строку с нулевым байтом — автор ССЗБ»). Когда нужно ситуация обрабатывается и на наличие/отсутствие нулевого байта в конце всем наплевать (а я лично пишу так, как будто его там нет).
От XML отошёл полностью.
Как вы думаете, куда ведёт ссылка «Nicolas Seriot»?
(Надо, кстати, мне проголосовать.)
Может он и непонятный, но Wayfarer15 тут уже 4 года. Ссылка не бросается в глаза, но чтобы её найти мне достаточно было просто предположить, что перевод оформлен как перевод (полоска около заголовка не особо запоминается).
Если используешь assume — читай законы Мерфи.
![image](https://habrastorage.org/getpro/habr/comment_images/494/51e/c52/49451ec52f2f9062365a57385e8ddafa.jpg)
А это тут при чём? У вас была гипотеза, что «ссылка на первоисточник не указана». Первым логичным шагом её проверки должно быть предположение, что ссылка‐таки есть, за чем следует поиск ссылки. Т.к. я на хабре тоже не вчера появился, то быстро её нашёл (тем более, что её не прятали от тех, кто в общих чертах понимает логическое разбиение страницы на блоки), но в общем случае можно было применить <C-f>
на исходном коде страницы, а не катить бочку на автора, который как раз сделал всё правильно (указал, что статья перевод и где находится оригинал; конкретное место размещения информации уже определялось не им, а дизайнерами TM).
Парсинг JSON — это минное поле