СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

| сохранено

H Парсинг Json на C# в черновиках Из песочницы

ASP, C#, .NET
imageПрикручивая авторизацию с помощью популярных сайтов я столкнулся с проблемой. Согласно стандарта OAuth ответ от сервера авторизации приходит в формате Json, а в .net нет удобной функции для его парсинга. Существует конечно JavaScriptSerializer но он наследует все проблемы сериализеров. Во первых, если будет расхождение в названии полей, тогда будут появляться исключения. Во вторых чисто лень создавать класс, пусть и содержащий только название полей, под каждый ответ. Я подключал 8 систем авторизации, в среднем мне пришлось бы написать по 2-3 класса на каждую. Т.е. где то 20 классов. Я уже не говорю о тех, кто делает глубокую интеграцию. Зачем нагромождать код? Ну и естественно в веб особо остро стоит вопрос производительности, а сериализаторы производительностью не блещут. Исходя из всех этих рассуждений я написал довольно простую функцию, которая превращает строку json в словарь типа Dictionary<string,string>.




        static Dictionary<string, string> ParseJson(string res)
        {
            var lines = res.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            var ht = new Dictionary<string, string>(20);
            var st = new Stack<string>(20);

            for (int i = 0; i < lines.Length; ++i)
            {
                var line = lines[i];
                var pair = line.Split(":".ToCharArray(), 2, StringSplitOptions.RemoveEmptyEntries);

                if (pair.Length == 2)
                {
                    var key = ClearString(pair[0]);
                    var val = ClearString(pair[1]);

                    if (val == "{")
                    {
                        st.Push(key);
                    }
                    else
                    {
                        if (st.Count > 0)
                        {
                            key = string.Join("_", st) + "_" + key;
                        }

                        if (ht.ContainsKey(key))
                        {
                            ht[key] += "&" + val;
                        }
                        else
                        {
                            ht.Add(key, val);
                        }
                    }
                }
                else if (line.IndexOf('}') != -1 && st.Count > 0)
                {
                    st.Pop();
                }
            }

            return ht;
        }

        static string ClearString(string str)
        {
            str = str.Trim();

            var ind0 = str.IndexOf("\"");
            var ind1 = str.LastIndexOf("\"");
            
            if (ind0 != -1 && ind1 != -1)
            {
                str = str.Substring(ind0 + 1, ind1 - ind0 - 1);
            }
            else if (str[str.Length-1] == ',')
            {
                str = str.Substring(0, str.Length - 1);
            }

            str = HttpUtility.UrlDecode(str);

            return str;
        }


Рассмотрим пример json строка полученная от системы Windows LiveId:

{
   "id": "cb6e111111aaaaaa", 
   "name": "Code Tester", 
   "first_name": "Code", 
   "last_name": "Tester", 
   "link": "http://profile.live.com/cid-cb6e111111aaaaaa/", 
   "gender": "male", 
   "emails": {
      "preferred": "tester@gmail.com", 
      "account": "tester@gmail.com", 
      "personal": null, 
      "business": null
   }, 
   "locale": "en_US", 
   "updated_time": "2012-05-21T21:40:43+0000"
}


Превращается в словарь:
id,                 cb6e111111aaaaaa
name,               Code Tester
first_name,         Code
last_name,          Tester
link,               http://profile.live.com/cid-cb6e111111aaaaaa/
gender,             male
emails_preferred,   tester@gmail.com
emails_account,     tester@gmail.com
emails_personal,    null
emails_business,    null
locale,             en_US
updated_time,       2012-05-21T21:40:43+0000


Есть некоторые нюансы. Как видно из примера вложенные объекты получают префикс родителя. Таким образом мы получили emails_preferred и emails_account. Если же json будет содержать массив объектов, тогда значения будут разделены символом &. Надеюсь пригодится.

комментарии (43)

+13
lair ,  
Вы правда никогда не слышали про Json.NET?

JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
–11
Master_Dante ,   * (был изменён)
Честно говоря статья торчала долгое время в черновиках, недавно я про нее вспомнил… А во вторых я бы не стал таскать целую библиотеку за проектом только ради такой маленькой задачи. Библиотека больше подойдет для тех случаев, когда требуется много разной работы с JSON, например не только парсинг но и создание, хотя и создание JSON быстрее будет работать вручную, без сложения строк сразу отправлять в out stream.
+16
lair ,  
Вот из таких «маленьких задач» и складывается впустую потраченное время на каждом проекте и баги, которых можно было бы избежать.
–8
+1 –9
Master_Dante ,  
Простите но такими маленькими задачами я добиваюсь вполне конкретных целей полезных для проекта, а это высокая производительность в нашем случае. Хотите поспорить на этот счет?
+4
Moxa ,  
я бы поспорил насчет производительности, сделайте бенчмарки
–6
Master_Dante ,  
Ок, напишите что будем сравнивать.
+2
Moxa ,  
вот тут есть неплохие бенчмарки java json-парсеров, организуйте аналогичные)
–5
Master_Dante ,  
Ваша просьба более чем нескромная ;) Обойдемся черной консолью :)
+7
lair ,  
Конечно, хочу.

Во-первых, вы говорите, высокая производительность? Окей, покажите конкретное сравнение производительности вашего решения с json.net. Насколько ваше решение быстрее? А в процентах? А в процентах от IO-операции, в которой вы получили JSON?

Во-вторых, ваше решение просто содержит ошибки. Возьмем вот такой (совершенно корректный) JSON:
{"abc:def":"\\"}


Ваш парсер на нем падает с ошибкой
System.ArgumentOutOfRangeException was unhandled
HResult=-2146233086
Message=Length cannot be less than zero.


Окей, вы не учитываете, что ключ — это произвольная строка. Правим:
{
"abcdef":"\\"
}


Получаем…
abcdef=>\\


Но почему? А потому, что вы вообще не знаете, как в json отображаются строки.

Вот вам еще один пример:
{
"abcdef":"%57T%46?"
}


Результат?
abcdef=>WTF?


Потому что, как вам верно написали ниже HttpUtility.UrlDecode() к json никакого отношения не имеет.

Вывод напрашивается сам: вы потратили время (явно больше, чем скачать пакет из nuget) на решение, которое (а) некорректно и (б) не гарантирует большей производительности. М?
–3
Master_Dante ,  
Да объективности ради я завтра сделаю такой тест, как и положено считать будем время на миллион операций. И что бы зря не тратить время скажите я правильно понимаю мы будем тестить полный парсинг с помощью функции что вы показали JsonConvert.DeserializeObject(json);? Ошибки что вы указали я исправлю перед тестом.
+1
lair ,  
Неправильно понимаете. Поскольку речь идет об оптимизированном под производительность коде, тестировать надо JsonSerializer.Deserialize, с однократным созданием JsonSerializer.

Ну и да, не забудьте посчитать выигрыш в процентах от общего времени IO-операции.
–1
Master_Dante ,  
Однократное создание будет, при условии что это возможно для разных json ;). Насчет IO операций поясните. Если вы имеете ввиду запросы, то какой смысл это учитывать, если это не имеет отношения к целевому действию?
+1
lair ,  
Однократное создание будет, при условии что это возможно для разных json

Это, конечно же, возможно для разных json.

Если вы имеете ввиду запросы, то какой смысл это учитывать, если это не имеет отношения к целевому действию?

Да, я имею в виду запросы. Если в цепочке «прочитать данные — распарсить данные» первая операция занимает 99% времени, а вторая — 1%, оптимизировать вторую смысла нет.
–2
Master_Dante ,   * (был изменён)
Это совершенно не имеет отношение к предмету разговора. И зависит от того как именно вы организовали IO. Допустим синхронно или асинхронно, это совершенно другая тема. Потому что при асинхронном подходе потоки не блокируются при ожидании ответа.
0
lair ,  
Это имеет прямое отношение к предмету разговора, потому что не бывает абстрактной производительности в вакууме, бывает метрика на конкретном сценарии использования. В вашем случае этот сценарий очевиден — получи токен от OAuth-провайдера и распарси его. Если время получения на два-три порядка превосходит время парсинга, то оптимизировать парсинг достаточно бессмысленно.

И нет, блокировки потока тут ни при чем.

(Зато ваше решение принципиально не способно работать на потоковом IO — в отличие от json.net.)
+2
lair ,  
В тривиальном тесте
Тест, порядок вызовов сериализаторов не имеет значения
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;

using Newtonsoft.Json;

namespace json
{
	class Program
	{
		private static void Main(string[] args)
		{
			const string json = @"{
   ""id"": ""cb6e111111aaaaaa"", 
   ""name"": ""Code Tester"", 
   ""first_name"": ""Code"", 
   ""last_name"": ""Tester"", 
   ""link"": ""http://profile.live.com/cid-cb6e111111aaaaaa/"", 
   ""gender"": ""male"", 
   ""emails"": {
      ""preferred"": ""tester@gmail.com"", 
      ""account"": ""tester@gmail.com"", 
      ""personal"": null, 
      ""business"": null
   }, 
   ""locale"": ""en_US"", 
   ""updated_time"": ""2012-05-21T21:40:43+0000""
}";
			var sw1 = new Stopwatch();
			var sw2 = new Stopwatch();
			var ser = JsonSerializer.CreateDefault();
			const int inner = 100000;
			const int outer = 10;
			for (var i = 0; i < outer; i++)
			{
				sw2.Start();
				for (var j = 0; j < inner; j++)
				{
					ParseJson(json);
				}
				sw2.Stop();
				sw1.Start();
				for (var j = 0; j < inner; j++)
				{
					using (var r = new StringReader(json))
					{
						ser.Deserialize<Dictionary<string, object>>(new JsonTextReader(r));
					}
				}
				sw1.Stop();
			}
			Console.WriteLine("json.net: {0} total, {1} per iteration", sw1.ElapsedMilliseconds, sw1.ElapsedMilliseconds/outer/inner );
			Console.WriteLine("handwritten: {0} total, {1} per iteration", sw2.ElapsedMilliseconds, sw2.ElapsedMilliseconds / outer / inner);
		}

		static Dictionary<string, string> ParseJson(string res)
		{
			var lines = res.Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
			var ht = new Dictionary<string, string>(20);
			var st = new Stack<string>(20);

			for (int i = 0; i < lines.Length; ++i)
			{
				var line = lines[i];
				var pair = line.Split(":".ToCharArray(), 2, StringSplitOptions.RemoveEmptyEntries);

				if (pair.Length == 2)
				{
					var key = ClearString(pair[0]);
					var val = ClearString(pair[1]);

					if (val == "{")
					{
						st.Push(key);
					}
					else
					{
						if (st.Count > 0)
						{
							key = string.Join("_", st) + "_" + key;
						}

						if (ht.ContainsKey(key))
						{
							ht[key] += "&" + val;
						}
						else
						{
							ht.Add(key, val);
						}
					}
				}
				else if (line.IndexOf('}') != -1 && st.Count > 0)
				{
					st.Pop();
				}
			}

			return ht;
		}

		static string ClearString(string str)
		{
			str = str.Trim();

			var ind0 = str.IndexOf("\"");
			var ind1 = str.LastIndexOf("\"");

			if (ind0 != -1 && ind1 != -1)
			{
				str = str.Substring(ind0 + 1, ind1 - ind0 - 1);
			}
			else if (str[str.Length - 1] == ',')
			{
				str = str.Substring(0, str.Length - 1);
			}

			str = HttpUtility.UrlDecode(str);

			return str;
		}
	}
}



ваше решение в три раза проигрывает json.net.

Причем львиную долю времени занимает ClearString, и это банально из-за того, что вы не знаете разницы между string.[Last]IndexOf(string) и string.[Last]IndexOf(char), хотя эта разница стоит вам двухкратного изменения производительности. Так что нет, применительно к вашему коду ни о какой оптимизации производительности речи вести не стоит.
–3
Master_Dante ,  
Я признаю что json.net неимоверно крута, вы позволите мне немного переписать мою функцию до теста? Что бы она во первых соответствовала замечанию dtestyk о json в одной строке, во вторых замечание Moxa про HttpUtility.UrlDecode(str), ну и в третьих я обойдусь без CrearString, а также добавлю пул объектов.
+5
lair ,   * (был изменён)
А смысл? Когда вас спросили, почему вы не используете json.net, вы сослались на высокую производительность. Теперь мы видим, что ни о какой производительности в вашем коде речи не идет. Несомненно, ваш код можно сделать корректным и быстрым, но время, которое вы на это затратите (если вы, конечно, не возьмете где-нибудь готовый исходник) будет совершенно не пропорционально (потенциальному) выигрышу в производительности.

Что, в итоге, демонстрирует мой изначальный тезис: ваша «маленькая задача» привела к багам (как функциональным, так и потере производительности) и бессмысленной потере времени. Если бы вы сразу взяли json.net, ни одной из этих проблем бы не было.
–1
Master_Dante ,   * (был изменён)
Так же я сказал вам, что функция была написана несколько лет назад ;). Теперь я перепишу ее с учетом своего текущего опыта и сделаю бенчмарк, благодаря вашей критике я сделаю ее лучше :).

>> Несомненно, ваш код можно сделать корректным и быстрым, но время, которое вы на это затратите

Вы не поверите моя работа почти на 100% состоит в том, что бы делать код быстрее ведь последние годы, я занимаюсь высоко нагруженным проектом, где количество запросов от 2000 в секунду. И наверно для обычных проектов ваше замечание справедливо, однако для высоких нагрузок любые тормоза это дополнительные сервера, а так же зарплата админам, что вылевается в хороший ценник.
0
lair ,   * (был изменён)
Так же я сказал вам, что функция была написана несколько лет назад

… и до сих пор используется на «высоконагруженном проекте, где количество запросов от 2000 в секунду»?

Вы не поверите моя работа почти на 100% состоит в том, что бы делать код быстрее

Ну, как я уже показал, сделать ваш код быстрее — невелика трудность. Четыре замененных кавычки — увеличение скорости в два раза.

Какими инструментами вы пользуетесь при анализе производительности кода?
–3
Master_Dante ,  
>> и до сих пор используется на «высоконагруженном проекте, где количество запросов от 2000 в секунду»?

Да представляете какой ужас, хорошо что никто не знает :)
+2
lair ,  
Мне кажется, это многое говорит о качестве проекта (особенно учитывая, что код банально ошибочный) и аккуратности профилирования на нем. Ну и отдельно служит хорошей иллюстрацией того факта, что ваша экономия на спичках никому не нужна — если ваш проект несколько лет не замечает трехкратной разницы в производительности json-сериализатора, то и полуторакратную разницу в производительности словаря на 20 объектах он не заметит.
–3
Master_Dante ,  
В вас столько желчи, будьте осторожны и не испачкайте ею штаны :). Я не обязан перед вами оправдываться. Эта функция действительно не так идеальна, она так же не так плоха, да и вобще все отлично солнце светит работа кипит.
+4
lair ,   * (был изменён)
она так же не так плоха

Она как раз очень плоха. И еще хуже, что вы этого не видите, несмотря на заявляемые вами годы на (неназываемом) высоконагруженном проекте.

Впрочем, заведомо некорректные утверждения в статье (например, «ответ от сервера авторизации приходит в формате Json, а в .net удобной функции для его парсинга нет») даже похуже будут.
+2
dordzhiev ,  
lair ведь все по делу говорит, а вы ему еще и грубите. Прислушались бы лучше, чем вот так вот спорить и оправдываться.
+2
+3 –1
mayorovp ,  
Маленькая библиотека решает маленькую задачу. Что тут неправильного?
+1
lair ,  
создание JSON быстрее будет работать вручную, без сложения строк сразу отправлять в out stream.

И про JsonSerializer.Serialize(TextWriter,object) вы тоже не знаете.
+1
maseal ,  
Справедливости ради, в микрософтовских шаблонах проектов Visual Studio для WebAPI, уже по умолчанию в зависимостях используется JSON.Net
+2
dtestyk ,  
чисто лень создавать класс, пусть и содержащий только название полей, под каждый ответ

у JavaScriptSerializer есть метод DeserializeObject
+3
joher ,  
К чему эти магические цифры
var ht = new Dictionary<string, string>(20);

Можно же получить нормальное число полей.
Почему <string,string>? Лучше уж тогда <string,object> и вместо вложенных объектов класть теже словари. А главная проблема в том что словарь не понацея в данном случае. Вам придеться либо проверять каждый раз на наличие ключа либо ловить исключения, по аналогии со стандартным парсером. Стандартный парсер хотя бы заполнит поля null'ам которые проверять в разы приятней.
–5
Master_Dante ,   * (был изменён)
К чему эти магические цифры


В моих задачах json ответы не превышали 20 ключей. Экономит память.

Почему <string,string>?


Опять же все дело в памяти. Если создавать много Dictionary это во первых время на создание, во вторых память. Удобство разработки за счет производительности это не мой стиль. Наверно специфика моей работы делает такой отпечаток. :)

Вам придеться либо проверять каждый раз на наличие ключа


Да и для этого есть удобный ContainceKey. И этот метод работает очень быстро, уверен что быстрее чем аналогичная функция у парсера.
+3
bbmm ,  
В моих задачах json ответы не превышали 20 ключей. Экономит память.

Правда? Хотелось бы увидеть замер производительности.
0
lair ,  
В моих задачах json ответы не превышали 20 ключей. Экономит память.

Правда?

Вопрос номер один: сколько памяти выделит Dictionary, если его создать с явным количеством элементом в 20?
Правильный ответ: 23c.

Вопрос номер два: сколько памяти выделит Dictionary, если его создать с количеством элементов 0, а потом добавить 20 элементов?
После того, как отработает GC — те же 23c.

Наконец, вопрос номер три: сколько памяти выделит Dictionary, если его создать с количеством элементов 0, а потом добавить 10 элементов?
После того, как отработает GC — 11c.

А сколько памяти выделит Dictionary, если его создать с количеством элементов 20, а потом добавить 10 элементов?

(c везде — некая константа, описывающая внутренние расходы Dictionary на бакет+запись).
–1
Master_Dante ,  
Посмотрите сорцы Dictionary если памяти не хватает тогда вызывается функция

private void Resize()
{
this.Resize(HashHelpers.ExpandPrime(this.count), false);
}

И тогда вы поймете что память выделяется не линейно, а с запасом на будущее. Это раз. Во вторых, если я знаю характер ответа и заранее выделю память, тогда я избегаю потери времени на ее выделение в будущем.
+1
lair ,  
… если вы посмотрите те самые сорцы, в которые меня отправляете, то увидите, что память всегда (т.е. — и при первичной инициализации) выделяется не «линейно». Дело там, правда, не в «запасе», а в специфике реализации хэш-таблиц, но это уже детали.

Что же касается потерь времени — опять-таки, тесты в студию.
–3
Master_Dante ,   * (был изменён)
Вы знаете сколько стоит мое время? :) Хорошо специально для вас я сделаю и этот тест, будем агностиками и будем следовать принципам объективности :)
+3
lair ,  
Вы знаете сколько стоит мое время?

Лучше назовите компанию и проект, над которым вы работаете. Ну или хотя бы проект.
0
lam0x86 ,  
Да признайте уже, что ваше решение нежизнеспособно. Ну право же. Оно проигрывает стандартным библиотекам по всем параметрам. Вопрос про стоимость времени только усугубляет сомнение в Вашем профессионализме. Ни разу не слышал подобных речей ни от одного человека, добившегося чего-то в жизни.
+2
dtestyk ,  
а раньше писали парсер json на c#, потому что его там не было :)

а вы уверены, что всегда приходит и будет приходить
разделенно по строкам возвратом каретки и переносом строки?
–6
Master_Dante ,  
Уверен что нет :). Однако если кто нибудь попросит, я это учту.
+3
Moxa ,  
непонятно что в json-парсере делает HttpUtility.UrlDecode(str), json экранирует значения по-человечески и не использует URL-encoding
+2
Nagg ,  
Вредная статья. Слишком вредно называется — «Парсинг Json на C#». Будет новичок искать такой запрос и увидет вашу статью (остается надежда, что он увидет комментарии lair ).
0
lair ,   * (был изменён)
(не удержался, полез, проверил)

Существует, конечно, JavaScriptSerializer, но он наследует все проблемы сериализеров. [...] Ну и, естественно, в вебе особо остро стоит вопрос производительности, а сериализаторы производительностью не блещут.

Решение, описанное в статье, проигрывает JavaScriptSerializer в те же самые три раза (на самом деле, даже в три с половиной). И это, кстати, при том, что и MS, и json.net честно сохраняют иерархию объектов.

(если кому интересно — на этом тесте JsonSerializer быстрее JavaScriptSerializer процентов на 10, но зависит от числа итераций)