Недавно для моего проекта понадобилась мне библиотека для архивирования. С полгода назад по работе я пользовался библиотекой zlibnet и впечатления остались не очень приятные, так что решил поискать альтернативу. После недолгих поисков наткнулся на обзор библиотек для архивации, которая и сподвигла меня написать этот обзор.
Участники
Я буду тестировать четыре библиотеки: ZLibNet, #ZipLib, DotNetZip и ZipStorer. Теперь о каждой поподробней:
ZLibNet
Лицензия: Свободная
Размер: 35 кБ + 137 кБ(ZLib)
Эта библиотека представляет собой обёртку над широко известной сишной библиотекой ZLib. Так как большую часть составляет unmanaged код, то авторы обещают высокую производительность.
Пример кода для архивирования:
Zipper zip = new Zipper();
zip.ItemList.Add(inPath);
zip.ZipFile = outPath;
zip.PathInZip = enPathInZip.None;
zip.Zip();
#ZipLib
Лицензия: Модифицированный GPL. Можно использовать в коммерческих проектах
Размер: 196 кБ
Библиотека полностью написана на C#. Заявлена поддержка так же GZip, Tar, BZip2 форматов.
Пример кода для архивирования:
using (ZipOutputStream s = new ZipOutputStream(File.Create(outPath)))
{
s.UseZip64 = UseZip64.Off;
if (level != -1)
s.SetLevel(level);
byte[] buffer = new byte[4096];
ZipEntry entry = new ZipEntry(Path.GetFileName(inPath));
s.PutNextEntry(entry);
using (FileStream fs = File.OpenRead(inPath))
{
int sourceBytes;
do
{
sourceBytes = fs.Read(buffer, 0, buffer.Length);
s.Write(buffer, 0, sourceBytes);
}
while (sourceBytes > 0);
}
s.Finish();
s.Close();
}
DotNetZip
Лицензия: Ms-Pl
Размер: 277 кБ(урезанная версия)
Библиотека позиционируется как самая удобная для использования в .net и моно проектах. Заявлена поддержка AES шифрования. Полезным так же может оказаться наличие многих версий: полная, уменьшенная версия(оставлен основной функционал и существенно уменьшен размер), для компакт фреймворка, для SilverLight. Имеется также отдельные версии поддерживающие bzip2 и обёртка на ZLib.
Пример кода для архивирования:
using (ZipFile zip = new ZipFile())
{
zip.CompressionLevel = compressionLevel;
ZipEntry ze = zip.AddFile(inPath, "");
zip.Save(outPath);
}
ZipStorer
Лицензия: Ms-Pl
Размер: 33 кБ(исходник)
Строго говоря является не библиотекой, а отдельным классом, в следствии чего крайне просто интегрируется в проект, да и изменения в случае чего произвести будет несложно. Возможности крайне ограничены, но учитывая размер, на это можно закрыть глаза. Старая версия может работать в Silverlight.
Вот вкратце и всё, что можно сказать об испытуемых.
Пример кода для архивирования:
ZipStorer zip = ZipStorer.Create(outPath, "About");
zip.AddFile(compressionLevel, inPath, inputFileName, "");
// Updates and closes the zip file
zip.Close();
Тестирование
Тестирование будет проводиться следующим образом: делается 100 прогонов каждого метода, измеряется время, убираются 20 худших и 20 лучших времён, по остальным рассчитывается среднее. Платформа: основные тесты проводились на процессоре E8400(3.0GHz) с четырьмя гигабайтами DDRII памяти. Все четыре библиотеки тестируются в одной программе, которая логирует все свои действия и отписывает результаты в csv файл, который потом можно легко открыть в редакторе таблиц. IDE — VS2010. Использовался .net 4.0.
Архивирование
Тесты будут проводиться на 3х файлах: первый — большой текстовый файл, второй — база данных SQLite, и третий — книга «История государства Российского» Карамзина в формате pdf.
Для каждого файла проводятся два теста на первом будут тестироваться все библиотеки с настройками обеспечивающими максимальное быстродействие, на втором будет включено максимальное сжатие, к сожалению функцию уровня сжатия поддерживают только #ZipLib и DotNetZip, поэтому во втором этапе будут участвовать только они.
TXT файл.
Это текстовый файл размером в 9 373 180 байт. Документ по структуре — логи переписки по аське из квипа.
Итак, результаты с максимальной скоростью:
Итак, по скорости #ZipLib и zlibnet одинаковы, однако, вторая библиотека показывает куда лучший результат. Остальные библиотеки показывают серьёзное отставание по скорости. Теперь результаты теста на максимальное сжатие:
Тут результаты более ровные.
DB файл.
База данных, размером 19 407 754 байт. Содержимое базы — большое количество строк.
Ситуация идентична текстовому файлу, теперь результаты теста на максимальное сжатие:
И опять результаты первого тесты повторяются: одна библиотека чуть быстрее, другая немного лучше сжимает.
PDF файл.
ПДФка с отсканированными страницами, размером 19 407 754 байт. Можно ожидать, что сжатие особого толка не даст, однако, давайте это проверим:
Разрывы по размеру оказались несущественны, однако же скорость отличается очень значительно. На этот раз в лидерах ZipStorer, что объяснимо — у него худшее сжатие. DotNetZip и #ZipLib показывают практически одинаковые результаты. Теперь результаты теста на максимальное сжатие:
Ситуация изменилась несильно, разве что, несколько увеличилась разница по времени.
Стоит отметить что изначально название файла содержало кириллицу и только ZipStorer достойно справилась с ним(для этого нужно было добавить строчку: zip.EncodeUTF8 = true), ZlibNet вообще отказалась с ним работать(выбрасывало исключение в котором говорилось что поддерживаются только ASCII символы в названии), а #ZipLib и DotNetZip упаковали странно: стандартный windows просмотрщик показывал пустой архив. Распаковка дала исходный файл, но в названии все кириллические символы были заменены.
Разархивирование.
Для начала я проверил все архивы на чтение. Все библиотеки могут открывать архивы, созданные другими библиотеками, никаких неожиданностей не возникло.
Непосредственно само разархивирование я буду тестировать на двух архивах: первый — база из первого теста запакованная быстрейшим способом, второй — тот же файл, но с ультра сжатием. Оба архива получены с помощью 7-zip. Рассмотрим результаты:
Парадоксально, но факт: все библиотеки распаковывали архив с максимальным сжатием быстрей, чем с минимальным. Удивляют так же гигансткие разрывы между библиотеками.
Сравнение процессоров.
Теперь запущу тест на архивацию тестового файла ещё на двух компьютерах. Первый — годовалый чертырёхядерный i7-870(2.93GHz) 16Gb, и второй — трёхлетний ноутбук Dell 1525 T2370(1.73GHz) 2Gb.
Архивирование базы данных:
Результаты удивляют. Значительный прирост показала только DotNetZip. ZlibNet и ZipStorer показали двухкратный рост производительности при переходе от ноутбука к двухядерному процессору, но вот уже 4х ядерник особого прироста не дал, т.е. можно сделать вывод что они зависят скорее от частоты, чем от количества ядер. Но наиболее поразительным является результат #ZipLib — на i7-870 сжатие занимает больше времени чем на старом ноутбучном процессоре. Объяснить подобную разницу я затрудняюсь.
А вот результаты распаковки архива:
Тут ситуация тоже необычная: и #ZipLib и DotNetZip на i7-870 работают медленней, чем на E8400.
Напоследок, приведу скриншоты диспетчера задач во время работы приложения:
Тут красным выделен период работы ZLibNet, синим — #ZipLib, жёлтым — DotNetZip, и фиолетовым — ZipStorer. Хорошо заметно, что DotNetZip грузит оба ядра на полную катушку. Что и подтверждает результаты теста на сравние.
Итоги.
Тесты получились неоднозначными. У каждой из библиотек имеется свои плюсы и минусы. Лично для себя я выбрал DotNetZip, мне понравился её простой интерфейс и предсказуемый рост производительности относительно мощи процессора.
Исходный код можно посмотреть тут.
вторник, 13 марта 2012 г.
ASP.NET приложение с богатым web-интерфейсом на qooxdoo? Легко!
На просторах Интернета есть очень хороший Javascript фреймворк под названием qooxdoo. Наверное, многие знают о нем и видели его в действии. А если кто не знает, это фреймворк с большим количеством классов, в том числе отвечающих за UI. Подробности можно почитать и посмотреть там. Хорош он всем, но при разработке ASP.NET приложений его использовать крайне сложно. Так было раньше. Теперь я рад поделиться новостью: появился новый проект, который позволяет просто и быстро создавать ASP.NET приложения с пользовательским интерфейсом на qooxdoo, при этом писать на javascript совсем не придется. Проект называется qxdotnet и распространяется под лицензией LGPL.
Главная идея проекта заключается в том, чтобы создать классы-обертки на C# для всех основных классов qooxdoo, отвечающих за пользовательский интерфейс. Экземпляры этих классов-оберток генерирую javascript-код, который создает аналогичные объекты в браузере. Сообщения о событиях (нажатие на кнопку, ввод данных в текстовое поле) передаются на сервер через AJAX.
Иными словами, Вы просто создаете ASP.NET приложение, пишите на C# примерно следующий код:
btn = new qxDotNet.UI.Form.Button();
btn.Label = "push me";
root.Add(btn);
Далее запускаете приложение и видите свою кнопочку в браузере. Набор свойств и методов класса qxDotNet.UI.Form.Button почти такой же, как у javascript-ового класса qx.ui.form.Button. При нажатии на кнопку на сервер передается сообщение и вызывается соответствующее событие.
Исходники и пример можно скачать тут. Для компиляции потребуется MS Visual Studio 2008. Можно использовать VS 2010, но потребуется конвертировать файлы решения.
Проект qxdotnet реализован как приложение для qooxdoo, поэтому для компиляции такого приложения (как и любого другого приложения для qooxdoo) потребуется исполнить ряд скриптов, написанных на Python-е. Для выполнения этих скриптов нужно, чтобы в системе был установлен ActivePython. Скачать можно тут. Вызов Python-а производится самой VS во время компиляции решения.
Ну и третье, что потребуется для компиляции проекта, это qooxdoo 1.6 SDK. Его нужно скачать отсюда и распаковать в папку рядом с файлом решения. Т.е. в итоге должно получиться в одной папке следующие папки и файлы: DemoApplication, qooxdoo-1.6-sdk, source, qxDotNet.sln…
Далее можно открыть файл решения qxDotNet.sln в VS, установить в качестве начального проект DemoApplication и запустить его. Код главной формы демо-приложения можно посмотреть в файле MainForm.cs.
В заключении стоит отметить, что, хотя проект находится на стадии Alpha, почти все элементы пользовательского интерфейса уже работают. Есть на что посмотреть и попробовать в действии.
Главная идея проекта заключается в том, чтобы создать классы-обертки на C# для всех основных классов qooxdoo, отвечающих за пользовательский интерфейс. Экземпляры этих классов-оберток генерирую javascript-код, который создает аналогичные объекты в браузере. Сообщения о событиях (нажатие на кнопку, ввод данных в текстовое поле) передаются на сервер через AJAX.
Иными словами, Вы просто создаете ASP.NET приложение, пишите на C# примерно следующий код:
btn = new qxDotNet.UI.Form.Button();
btn.Label = "push me";
root.Add(btn);
Далее запускаете приложение и видите свою кнопочку в браузере. Набор свойств и методов класса qxDotNet.UI.Form.Button почти такой же, как у javascript-ового класса qx.ui.form.Button. При нажатии на кнопку на сервер передается сообщение и вызывается соответствующее событие.
Исходники и пример можно скачать тут. Для компиляции потребуется MS Visual Studio 2008. Можно использовать VS 2010, но потребуется конвертировать файлы решения.
Проект qxdotnet реализован как приложение для qooxdoo, поэтому для компиляции такого приложения (как и любого другого приложения для qooxdoo) потребуется исполнить ряд скриптов, написанных на Python-е. Для выполнения этих скриптов нужно, чтобы в системе был установлен ActivePython. Скачать можно тут. Вызов Python-а производится самой VS во время компиляции решения.
Ну и третье, что потребуется для компиляции проекта, это qooxdoo 1.6 SDK. Его нужно скачать отсюда и распаковать в папку рядом с файлом решения. Т.е. в итоге должно получиться в одной папке следующие папки и файлы: DemoApplication, qooxdoo-1.6-sdk, source, qxDotNet.sln…
Далее можно открыть файл решения qxDotNet.sln в VS, установить в качестве начального проект DemoApplication и запустить его. Код главной формы демо-приложения можно посмотреть в файле MainForm.cs.
В заключении стоит отметить, что, хотя проект находится на стадии Alpha, почти все элементы пользовательского интерфейса уже работают. Есть на что посмотреть и попробовать в действии.
Производительность: LINQ to XML vs XmlDocument vs XmlReader на Desktop и Windows Phone
Не так давно мне пришлось делать приложение для Windows Phone работающее с xml-файлами. Всё было неплохо, но когда в файле стало ~100.000 записей, чтение их занимало ну уж очень много времени. И я решил сравненить производительность различных способов чтения данных из xml возможных на платформе .Net.
Подробности под катом.
Оборудование
Для лучшего понимания показателей проведенных тестов стоить рассказать на чём они были проведены. Тесты из разряда «Desktop» я выполял на домашнем компьютере:
Процессор: Pentium Dual-Core T4300 2100 Mhz
RAM: DDR2 2048Mb
Тесты на Windows Phone были выполнены на HTC 7 Mozart.
Подготовка к тестированию
Для тестирования использовался простой xml-файл. ID для каждого элемента генерировались рандомно, а количество записей различалось в зависимости от теста и составляло: 1, 10, 100, 1 000, 100 000 штук соответственно. Итоговый файл выглядел примерно следующим образом:
........
* This source code was highlighted with Source Code Highlighter.
Для уменьшение погрешностей каждый тест был выполен 100 раз и полученные данные усреднены. А для имитации некоторых действий над записью вызывался пустой метод ProcessId(id).
XmlDocument.Load
На мой взгляд реализация чтения данных этим способом наиболее простая и понятная. Но, как мы увидим в конце, достигается это уж очень большой ценой (в конце статьи приведена реализация этого способа без использования XPath, но результаты, лично у меня, не сильно отличаются). Код метода следующий:
private static void XmlDocumentReader(string filename)
{
var doc = new XmlDocument();
doc.Load(filename);
XmlNodeList nodes = doc.SelectNodes("//item");
if (nodes == null)
throw new ApplicationException("invalid data");
foreach (XmlNode node in nodes)
{
string id = node.Attributes["id"].Value;
ProcessId(id);
}
}
* This source code was highlighted with Source Code Highlighter.
LINQ to XML
Использование Linq-to-XML также оставляет реализацию метода довольно простой и понятной.
private static void XDocumentReader(string filename)
{
XDocument doc = XDocument.Load(filename);
if (doc == null || doc.Root == null)
throw new ApplicationException("invalid data");
foreach (XElement child in doc.Root.Elements("item"))
{
XAttribute attr = child.Attribute("id");
if (attr == null)
throw new ApplicationException("invalid data");
string id = attr.Value;
ProcessId(id);
}
}
* This source code was highlighted with Source Code Highlighter.
XmlReader
Ну и наконец последний способ чтения данных из XML — использование XmlTextReader. Стоит сказать, что этот метод самый сложный для понимания. В процессе чтения xml-файла вы двигаетесь по нему сверху вниз (без возможности движения в обратном направлении), и вам каждый раз необходимо проверять, те ли данные вам нужно извлечь? Соответственно, код метода выглядит так:
private static void XmlReaderReader(string filename)
{
using (var reader = new XmlTextReader(filename))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name == "item")
{
reader.MoveToAttribute("id");
string id = reader.Value;
ProcessId(id);
}
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
* Для упрощения, в методах были опущены проверки.
Результаты для Desktop
Ниже представлены результаты тестирования. Для запуска каждого теста время измерялось отедельно и затем усреднялось. Время в таблице в миллисекундах.
1 10 100 1 000 10 000 100 000
XmlDocument 0,59 мс 0,5 мс 0,67 мс 2,49 мс 21,73 мс 398,91 мс
XmlReader 0,51 мс 0,47 мс 0,55 мс 1,31 мс 8,62 мс 79,65 мс
Linq to XML 0,57 мс 0,59 мс 0,64 мс 2,09 мс 15,6 мс 192,66 мс
Как видно из таблицы, XmlReader при чтении больших xml файлов, выигрывает в производительности Linq To XML в 2,42 раза, а XmlDocument в более чем 5 раз!
Тестирование на Windows Phone
Теперь настало время провести тесты на телефоне. Стоит заметить, что на Windows Phone установлена более старая версия .Net Framework'а, поэтому метод с использованием XmlDocument.Load не работает, а код для XmlReader пришлось немного переписать:
private static void XmlReaderReader(string filename)
{
using (var reader = XmlReader.Create(filename)) {
while (reader.Read()) {
if (reader.NodeType == XmlNodeType.Element) {
if (reader.Name == "item") {
reader.MoveToAttribute("id");
string id = reader.Value;
ProcessId(id);
}
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
Результаты для Windows Phone
Предсказуемо, что и на телефоне быстрее оказался XmlReader. Но в отличии от настольного компьютера, разница в производительности на больших файлах у них различна. На телефоне XmlReader быстрее LINQ to XML в 1,91 раз, а на десктопе в 2,42 раза.
1 10 100 1 000 10 000 100 000
XmlReader 1,67 мс 1,74 мс 3,19 мс 19,5 мс 173,84 мс 1736,18 мс
Linq to XML 1,73 мс 2,21 мс 4,75 мс 31,39 мс 314,39 мс 3315,13 мс
Разница в скорости чтения 100 элементов из файла на Desktop и Windows Phone.
Разница в скорости чтения 100 000 элементов из файла на Desktop и Windows Phone.
Как можно видеть, скорость чтения данных на телефоне и настольном компьютере, в зависимости от объема данных, изменяется нелинейно. Интересно узнать почему это так?
Заключение
Как мы вяснили, самым произоводительным способом чтения данных из xml является использование XmlReader'a вне зависимости от платформы. Но неудобство его использования заключается в довольно сложном способое выборки данных — нам каждый раз приходиться проверять на каком элементе стоит указатель.
Если же для вас производительность не является краеугольным камнем, а главное — ясность и простота сопровождаемости кода, то наиболее подходящим является использование LINQ to XML. Также необходимо стараться избегать использования XmlDocument.Load в рабочих проектах из-за его низкой производительности.
P.S. Стоит упомянуть, что на написание всего этого меня вдохновила эта статья.
Update: по предложению alex_rusсделал тест для XmlDocument без использования XPath. Результыты получились лучше, но все равно этот способ остался самым медленным.
Таблица № 3. Сравнение производительности XmlDocument с и без использования XPath.
1 10 100 1 000 10 000 100 000
XmlDocument (c XPath) 0,59 мс 0,5 мс 0,67 мс 2,49 мс 21,73 мс 398,91 мс
XmlDocument (без XPath) 0,56 мс 0,5 мс 0,65 мс 2,24 мс 19,47 мс 362,75 мс
Как видно из таблицы (и рисунка) производительность увеличилась только на 10%. Хотя были предположения, что это значение будет гораздо выше.
Собственно, код для XmlDocument без XPath ниже. Надеюсь, знающие люди покажут где у меня ошибки, в результате которых скорость обработки увеличилась всего лишь на 10%, а не «в разы».
private static void XmlDocumentReader2(string filename)
{
var doc = new XmlDocument();
doc.Load(filename);
XmlElement root = doc.DocumentElement;
foreach (XmlElement el in root.ChildNodes)
{
if (el.Name != "item") continue;
string id = el.Attributes["id"].Value;
ProcessId(id);
}
}
* This source code was highlighted with Source Code Highlighter.
Подробности под катом.
Оборудование
Для лучшего понимания показателей проведенных тестов стоить рассказать на чём они были проведены. Тесты из разряда «Desktop» я выполял на домашнем компьютере:
Процессор: Pentium Dual-Core T4300 2100 Mhz
RAM: DDR2 2048Mb
Тесты на Windows Phone были выполнены на HTC 7 Mozart.
Подготовка к тестированию
Для тестирования использовался простой xml-файл. ID для каждого элемента генерировались рандомно, а количество записей различалось в зависимости от теста и составляло: 1, 10, 100, 1 000, 100 000 штук соответственно. Итоговый файл выглядел примерно следующим образом:
........
* This source code was highlighted with Source Code Highlighter.
Для уменьшение погрешностей каждый тест был выполен 100 раз и полученные данные усреднены. А для имитации некоторых действий над записью вызывался пустой метод ProcessId(id).
XmlDocument.Load
На мой взгляд реализация чтения данных этим способом наиболее простая и понятная. Но, как мы увидим в конце, достигается это уж очень большой ценой (в конце статьи приведена реализация этого способа без использования XPath, но результаты, лично у меня, не сильно отличаются). Код метода следующий:
private static void XmlDocumentReader(string filename)
{
var doc = new XmlDocument();
doc.Load(filename);
XmlNodeList nodes = doc.SelectNodes("//item");
if (nodes == null)
throw new ApplicationException("invalid data");
foreach (XmlNode node in nodes)
{
string id = node.Attributes["id"].Value;
ProcessId(id);
}
}
* This source code was highlighted with Source Code Highlighter.
LINQ to XML
Использование Linq-to-XML также оставляет реализацию метода довольно простой и понятной.
private static void XDocumentReader(string filename)
{
XDocument doc = XDocument.Load(filename);
if (doc == null || doc.Root == null)
throw new ApplicationException("invalid data");
foreach (XElement child in doc.Root.Elements("item"))
{
XAttribute attr = child.Attribute("id");
if (attr == null)
throw new ApplicationException("invalid data");
string id = attr.Value;
ProcessId(id);
}
}
* This source code was highlighted with Source Code Highlighter.
XmlReader
Ну и наконец последний способ чтения данных из XML — использование XmlTextReader. Стоит сказать, что этот метод самый сложный для понимания. В процессе чтения xml-файла вы двигаетесь по нему сверху вниз (без возможности движения в обратном направлении), и вам каждый раз необходимо проверять, те ли данные вам нужно извлечь? Соответственно, код метода выглядит так:
private static void XmlReaderReader(string filename)
{
using (var reader = new XmlTextReader(filename))
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
if (reader.Name == "item")
{
reader.MoveToAttribute("id");
string id = reader.Value;
ProcessId(id);
}
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
* Для упрощения, в методах были опущены проверки.
Результаты для Desktop
Ниже представлены результаты тестирования. Для запуска каждого теста время измерялось отедельно и затем усреднялось. Время в таблице в миллисекундах.
1 10 100 1 000 10 000 100 000
XmlDocument 0,59 мс 0,5 мс 0,67 мс 2,49 мс 21,73 мс 398,91 мс
XmlReader 0,51 мс 0,47 мс 0,55 мс 1,31 мс 8,62 мс 79,65 мс
Linq to XML 0,57 мс 0,59 мс 0,64 мс 2,09 мс 15,6 мс 192,66 мс
Как видно из таблицы, XmlReader при чтении больших xml файлов, выигрывает в производительности Linq To XML в 2,42 раза, а XmlDocument в более чем 5 раз!
Тестирование на Windows Phone
Теперь настало время провести тесты на телефоне. Стоит заметить, что на Windows Phone установлена более старая версия .Net Framework'а, поэтому метод с использованием XmlDocument.Load не работает, а код для XmlReader пришлось немного переписать:
private static void XmlReaderReader(string filename)
{
using (var reader = XmlReader.Create(filename)) {
while (reader.Read()) {
if (reader.NodeType == XmlNodeType.Element) {
if (reader.Name == "item") {
reader.MoveToAttribute("id");
string id = reader.Value;
ProcessId(id);
}
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
Результаты для Windows Phone
Предсказуемо, что и на телефоне быстрее оказался XmlReader. Но в отличии от настольного компьютера, разница в производительности на больших файлах у них различна. На телефоне XmlReader быстрее LINQ to XML в 1,91 раз, а на десктопе в 2,42 раза.
1 10 100 1 000 10 000 100 000
XmlReader 1,67 мс 1,74 мс 3,19 мс 19,5 мс 173,84 мс 1736,18 мс
Linq to XML 1,73 мс 2,21 мс 4,75 мс 31,39 мс 314,39 мс 3315,13 мс
Разница в скорости чтения 100 элементов из файла на Desktop и Windows Phone.
Разница в скорости чтения 100 000 элементов из файла на Desktop и Windows Phone.
Как можно видеть, скорость чтения данных на телефоне и настольном компьютере, в зависимости от объема данных, изменяется нелинейно. Интересно узнать почему это так?
Заключение
Как мы вяснили, самым произоводительным способом чтения данных из xml является использование XmlReader'a вне зависимости от платформы. Но неудобство его использования заключается в довольно сложном способое выборки данных — нам каждый раз приходиться проверять на каком элементе стоит указатель.
Если же для вас производительность не является краеугольным камнем, а главное — ясность и простота сопровождаемости кода, то наиболее подходящим является использование LINQ to XML. Также необходимо стараться избегать использования XmlDocument.Load в рабочих проектах из-за его низкой производительности.
P.S. Стоит упомянуть, что на написание всего этого меня вдохновила эта статья.
Update: по предложению alex_rusсделал тест для XmlDocument без использования XPath. Результыты получились лучше, но все равно этот способ остался самым медленным.
Таблица № 3. Сравнение производительности XmlDocument с и без использования XPath.
1 10 100 1 000 10 000 100 000
XmlDocument (c XPath) 0,59 мс 0,5 мс 0,67 мс 2,49 мс 21,73 мс 398,91 мс
XmlDocument (без XPath) 0,56 мс 0,5 мс 0,65 мс 2,24 мс 19,47 мс 362,75 мс
Как видно из таблицы (и рисунка) производительность увеличилась только на 10%. Хотя были предположения, что это значение будет гораздо выше.
Собственно, код для XmlDocument без XPath ниже. Надеюсь, знающие люди покажут где у меня ошибки, в результате которых скорость обработки увеличилась всего лишь на 10%, а не «в разы».
private static void XmlDocumentReader2(string filename)
{
var doc = new XmlDocument();
doc.Load(filename);
XmlElement root = doc.DocumentElement;
foreach (XmlElement el in root.ChildNodes)
{
if (el.Name != "item") continue;
string id = el.Attributes["id"].Value;
ProcessId(id);
}
}
* This source code was highlighted with Source Code Highlighter.
Альтернативная проверка предусловий в Code Contracts
При попытке использования библиотеки Code Contracts в реальном проекте может возникнуть небольшая сложность: хотя сам класс Contract с методами проверки предусловий и постусловий, располагается в mscorlib начиная с 4-й версии .NET Framework, но без установки самой библиотеки Code Contracts, они не попадают в результирующую сборку.
Это может вызвать определенные сложности в крупных распределенных командах, поскольку для нормального использования контрактов всем разработчикам придется установить дополнительное расширение. А поскольку у ключевых людей проекта может не быть четкой уверенности в том, а нужно ли вообще нам это добро, то такой переход может быть затруднительным.
Однако Code Contracts поддерживает дополнительный «режим совместимости», который позволяет «жестко зашить» проверки предусловий в результирующий код, так что они будут видны всем, не зависимо от того, установлены контракты на машине разработчика или нет.
Постановка проблемы
Давайте вначале рассмотрим пример, который более четко покажет, в чем проблема.
class SimpleClass
{
public int Foo(string s)
{
Contract.Requires(s != null);
return s.Length;
}
}
С этим кодом совершенно все в порядке и при попытке вызова метода Foo с null, мы получим нарушение контракта, что при установленной библиотеке Code Contracts и включенной проверке предусловий приведет к генерации исключения 'System.Diagnostics.Contracts.__ContractsRuntime.ContractException'.
Да, именно этого мы и ждем, но особенность заключается в том, что код генерации исключения генерируется не компилятором, а отдельным процессом, который запускается сразу после компиляции. А это значит, что без библиотеки Code Contracts, выполнение этого кода приведет к генерации NullReferenceExcpetion, поскольку никакой дополнительной валидации аргументов не останется и в помине. Я неоднократно сталкивался с тем, что такое поведение вызывало примерно такую реакцию: «WTF? Куда делась моя проверка!»
Поскольку мы не хотим слышать подобные “WTF?!?” от наших коллег, у которых контракты не установлены, то хотелось бы иметь способ зашить проверку предусловий более основательным образом.
Ручная проверка предусловий
Библиотека Code Contracts позволяет использовать предусловия в старом формате. Это значит, что если существующий метод уже содержит проверку входных параметров (т.е. проверку предусловий), то для преобразования их в полноценные предусловия после них достаточно добавить вызов Contract.EndContractBlock:
public class SimpleClass
{
public int Foo(string s)
{
if (s == null)
throw new ArgumentNullException("s");
Contract.EndContractBlock();
return s.Length;
}
}
Добавление вызова Contract.EndContractBlock превращает одну (или несколько) проверок входных параметров в полноценные предусловия. Теперь, для любого разработчика, у которого контракты не установлены, этот код будет выглядеть, как и раньше. В то время, как обладатели контрактов, смогут пользоваться всеми их преимуществами, такими как проверка валидности программы с помощью Static Checker-а, автоматическая генерация документации, возможность отлова всех нарушений контрактов (подробнее об этом будет ниже). Отличие этого способа проверки лишь в том, что их нельзя отключить и выпилить из кода полностью.
Данный подход можно совмещать с более продвинутыми техниками использования контрактов. Так, например, можно совмещать old-style проверку предусловий совместно с проверкой постусловий и инвариантов. Но поскольку постусловия и инварианты в большей мере касаются самого класса, а не его клиентов, то это никак не затронет всех тех разработчиков, у которых контракты не установлены.
ПРИМЕЧАНИЕ
Библиотека Code Contracts позволяет настраивать, какие проверки должны оставаться в результирующем коде. Если разработчики достаточно уверены в своем коде, то они могут убрать все проверки из кода и сэкономить несколько тактов процессора на каждой из них. Более подробно об уровнях мониторинга и возможностях по их управлению можно почитать в статье: «Мониторинг утверждений в период выполнения».
Использование существующих методов проверки
Еще одним стандартным способом валидации аргументов является использование специальных классов (guard-ов) с набором разных методов, типа NotNull, NotNullOrEmpty и т.п. Библиотека Code Contracts поддерживает возможность превращения подобных методов в полноценные контракты: для этого методы класса валидатора нужно пометить атрибутом ContractArgumentValidatorAttribute.
ПРИМЕЧАНИЕ
К сожалению атрибут ContractArgumentValidatorAttribute не входит в состав .NET Framework версии 4.0, он появится только в версии 4.5. Разруливается эта ситуация путем добавления в ваш проект файла ContractExtensions.cs, который появится в %ProgramFiles%\Microsoft\Contracts\Language\CSharp после установки библиотеки Code Contracts.
public static class Guard
{
[ContractArgumentValidatorAttribute]
public static void IsNotNull(T t) where T : class
{
if (t == null)
throw new ArgumentNullException("t");
Contract.EndContractBlock();
}
}
Теперь мы можем использовать старый добрый метод IsNotNull для проверки предусловий:
public int Foo(string s)
{
Guard.IsNotNull(s);
return s.Length;
}
Отступление от темы. Contract.ContractFailed
Возможно, вы обращали внимание на существование двух версии метода Contract.Requires, одна из которых является обобщенной (generic) и может использоваться для генерации нужного типа исключения, нарушение же необобщенной версии приводит к генерации внутреннего (internal) исключение типа ContractException.
Причина, по которой по умолчанию генерируется внутреннее исключение, заключается в том, что нарушение контракта не может быть восстановлено программным путем. Это баг в коде и для его устранения необходимо изменение этого кода. Однако при использовании любого подхода к проверке предусловий (Contract.Requires + 2 рассмотренных сегодня подхода), пользователь может «захавать» исключение, перехватив базовый тип исключения.
В классе Contract есть событие ContractFailed, которое будет вызываться при нарушении предусловий/постусловий/инвариантов. Например, перед запуском интеграционного или юнит-теста можно подписаться на это событие, и если падает предусловие, но тест остается зеленым, то можно закатывать рукава и идти искать того нерадивого программиста, который ловит исключения, не предназначенные для обработки.
Заключение
Использование одного из описанных здесь подходов для перехода к контрактному программированию позволяет плавно мигрировать код на контракты, не ломая жизнь остальной части команды. При этом вы можете использовать старые средства валидации со всеми достоинствами контрактов (включая возможность узнать о нарушении предусловий, описанную в предыдущем разделе), не заставляя всех и каждого устанавливать дополнительные утилиты на свои машины.
Это может вызвать определенные сложности в крупных распределенных командах, поскольку для нормального использования контрактов всем разработчикам придется установить дополнительное расширение. А поскольку у ключевых людей проекта может не быть четкой уверенности в том, а нужно ли вообще нам это добро, то такой переход может быть затруднительным.
Однако Code Contracts поддерживает дополнительный «режим совместимости», который позволяет «жестко зашить» проверки предусловий в результирующий код, так что они будут видны всем, не зависимо от того, установлены контракты на машине разработчика или нет.
Постановка проблемы
Давайте вначале рассмотрим пример, который более четко покажет, в чем проблема.
class SimpleClass
{
public int Foo(string s)
{
Contract.Requires(s != null);
return s.Length;
}
}
С этим кодом совершенно все в порядке и при попытке вызова метода Foo с null, мы получим нарушение контракта, что при установленной библиотеке Code Contracts и включенной проверке предусловий приведет к генерации исключения 'System.Diagnostics.Contracts.__ContractsRuntime.ContractException'.
Да, именно этого мы и ждем, но особенность заключается в том, что код генерации исключения генерируется не компилятором, а отдельным процессом, который запускается сразу после компиляции. А это значит, что без библиотеки Code Contracts, выполнение этого кода приведет к генерации NullReferenceExcpetion, поскольку никакой дополнительной валидации аргументов не останется и в помине. Я неоднократно сталкивался с тем, что такое поведение вызывало примерно такую реакцию: «WTF? Куда делась моя проверка!»
Поскольку мы не хотим слышать подобные “WTF?!?” от наших коллег, у которых контракты не установлены, то хотелось бы иметь способ зашить проверку предусловий более основательным образом.
Ручная проверка предусловий
Библиотека Code Contracts позволяет использовать предусловия в старом формате. Это значит, что если существующий метод уже содержит проверку входных параметров (т.е. проверку предусловий), то для преобразования их в полноценные предусловия после них достаточно добавить вызов Contract.EndContractBlock:
public class SimpleClass
{
public int Foo(string s)
{
if (s == null)
throw new ArgumentNullException("s");
Contract.EndContractBlock();
return s.Length;
}
}
Добавление вызова Contract.EndContractBlock превращает одну (или несколько) проверок входных параметров в полноценные предусловия. Теперь, для любого разработчика, у которого контракты не установлены, этот код будет выглядеть, как и раньше. В то время, как обладатели контрактов, смогут пользоваться всеми их преимуществами, такими как проверка валидности программы с помощью Static Checker-а, автоматическая генерация документации, возможность отлова всех нарушений контрактов (подробнее об этом будет ниже). Отличие этого способа проверки лишь в том, что их нельзя отключить и выпилить из кода полностью.
Данный подход можно совмещать с более продвинутыми техниками использования контрактов. Так, например, можно совмещать old-style проверку предусловий совместно с проверкой постусловий и инвариантов. Но поскольку постусловия и инварианты в большей мере касаются самого класса, а не его клиентов, то это никак не затронет всех тех разработчиков, у которых контракты не установлены.
ПРИМЕЧАНИЕ
Библиотека Code Contracts позволяет настраивать, какие проверки должны оставаться в результирующем коде. Если разработчики достаточно уверены в своем коде, то они могут убрать все проверки из кода и сэкономить несколько тактов процессора на каждой из них. Более подробно об уровнях мониторинга и возможностях по их управлению можно почитать в статье: «Мониторинг утверждений в период выполнения».
Использование существующих методов проверки
Еще одним стандартным способом валидации аргументов является использование специальных классов (guard-ов) с набором разных методов, типа NotNull, NotNullOrEmpty и т.п. Библиотека Code Contracts поддерживает возможность превращения подобных методов в полноценные контракты: для этого методы класса валидатора нужно пометить атрибутом ContractArgumentValidatorAttribute.
ПРИМЕЧАНИЕ
К сожалению атрибут ContractArgumentValidatorAttribute не входит в состав .NET Framework версии 4.0, он появится только в версии 4.5. Разруливается эта ситуация путем добавления в ваш проект файла ContractExtensions.cs, который появится в %ProgramFiles%\Microsoft\Contracts\Language\CSharp после установки библиотеки Code Contracts.
public static class Guard
{
[ContractArgumentValidatorAttribute]
public static void IsNotNull
{
if (t == null)
throw new ArgumentNullException("t");
Contract.EndContractBlock();
}
}
Теперь мы можем использовать старый добрый метод IsNotNull для проверки предусловий:
public int Foo(string s)
{
Guard.IsNotNull(s);
return s.Length;
}
Отступление от темы. Contract.ContractFailed
Возможно, вы обращали внимание на существование двух версии метода Contract.Requires, одна из которых является обобщенной (generic) и может использоваться для генерации нужного типа исключения, нарушение же необобщенной версии приводит к генерации внутреннего (internal) исключение типа ContractException.
Причина, по которой по умолчанию генерируется внутреннее исключение, заключается в том, что нарушение контракта не может быть восстановлено программным путем. Это баг в коде и для его устранения необходимо изменение этого кода. Однако при использовании любого подхода к проверке предусловий (Contract.Requires + 2 рассмотренных сегодня подхода), пользователь может «захавать» исключение, перехватив базовый тип исключения.
В классе Contract есть событие ContractFailed, которое будет вызываться при нарушении предусловий/постусловий/инвариантов. Например, перед запуском интеграционного или юнит-теста можно подписаться на это событие, и если падает предусловие, но тест остается зеленым, то можно закатывать рукава и идти искать того нерадивого программиста, который ловит исключения, не предназначенные для обработки.
Заключение
Использование одного из описанных здесь подходов для перехода к контрактному программированию позволяет плавно мигрировать код на контракты, не ломая жизнь остальной части команды. При этом вы можете использовать старые средства валидации со всеми достоинствами контрактов (включая возможность узнать о нарушении предусловий, описанную в предыдущем разделе), не заставляя всех и каждого устанавливать дополнительные утилиты на свои машины.
Маленькие чудеса C#/.NET – структура DateTimeOffset
Рассмотрим некоторые части .Net Framework'a, выглядящие тривиальными, но вполне способными сделать ваш код более простым как в написании, так и в сопровождении.
Пишущие на .NET (а если вы этого не делаете, то зря читаете этот пост) наверняка время от времени используют для своих нужд структуру DateTime. Эта структура удобна для хранения дат, времени или даты/времени, относящихся к локальной временной зоне (или же к UTC).
Однако, бывают случаи, когда вам необходимо сохранить время в виде смещения, а не конвертировать его в локальное время. И вот здесь вам на помощь придёт структура, впервые появившаяся в .NET 3.5 — DateTimeOffset.
Проблема: парсинг DateTime может привести к конвертации в локальное время
Представим себе, что вы используете файл, веб-сервис и т.п. некой сторонней фирмы, чьи сервера находятся в другой временной зоне. Более того, у них есть несколько полей, в возвращаемых данных, которые должны содержать даты, но на самом деле содержат сериализованные экземпляры структуры DateTime, время в которых установлено в полночь. Например, дату рождения пациента они передают вот в таком виде:
2012-03-01 00:00:00-05:00
Такая запись говорит о том, что человек родился 1 марта 2012 года в неуказанное время (в конце концов, большая часть форм не требует от вас заполнения времени вашего рождения). Но поскольку экземпляр структуры DateTime был сериализован «в лоб», то он и содержит время, установленное в полночь, согласно своей временной зоне.
Итак, зная, что эта дата совместима с Восточной временной зоной (Eastern Time Zone), а мы находимся в Центральной временной зоне (Central Time Zone) мы парсим её так:
// ясно что здесь выполняется чтение файла/потока/и т.п.
var dateString = "2012-03-01 00:00:00-05:00";
// парсим в DateTime
var birthDay = DateTime.Parse(dateString);
Выглядит идеально, не так ли? Но тут кроется проблемка. Если мы проверим содержимое объекта DateTime на нашей локальной машине, где выставлена Центральная временная зона, то увидим вот что:
2012-02-29 11:00:00 PM
Что случилось? (Или как говорил один персонаж — Кто это сделал?) Да, метод DateTime.Parse() конвертировал дату в локальную временную зону поскольку оригинальная дата рождения хранилась с указанным смещением. Вам просто оказали услугу — конвертировали указанную дату и время в ваши локальные дату и время. Это не так и плохо, если бы речь не шла о дне рождения, которое с 1 марта переместилось на 29 февраля.
Конечно, мы можем созвониться с третьей стороной и попросить перестать включать время в строку с датой или же перестать высылать смещение вместе со временем (в этом случае она перестанет конвертироваться в локальное время, но будет отмечена как DateTimeKind.Unspecified).
Однако бывает, что у нас нет возможности таким образом изменить ситуацию.
Бывают случаи, когда вы хотите считывать дату и время со смещением, но не конвертировать его в локальную временную зону. И вот тут вам пригодится DateTimeOffset.
DateTimeOffset – хранит DateTime и Offset
Так что там про DateTimeOffset? Структура так же проста, как и её имя, DateTimeOffset это дата+время+смещение. Именно поэтому она представляет намного более точную точку во времени, поскольку включает информацию о смещении, по которому были установлены текущие дата и время.
По правде говоря, функциональность DateTime и DateTimeOffset во многом перекрывается, а поскольку у Microsoft есть руководство по выбору того или другого, то я рекомендую ознакомиться с ней в MSDN. Статья называется «Choosing Between DateTime, DateTimeOffset, and TimeZoneInfo».
В целом, вы можете использовать DateTime, если вы «прикреплены» к одной временной зоне или используете только универсальное время в формате UTC. Но если вы хотите использовать даты и время из разных временных зон, а также хотите сохранить информацию о смещении без конвертации в локальное время, то лучше использовать DateTimeOffset.
В структуре DateTimeOffset есть много таких же свойств, как и в структуре DateTime (Day, Month, Year, Hour, Minute, Second, и т.п.), потому здесь их я описывать не стану. Главное отличие состоит в нескольких новых свойствах:
DateTime
Возвращает DateTime без учёта смещения.
LocalDateTime
Возвращает конвертированный DateTime, с учётом смещения (т.е. в локальной временной зоне).
Offset
Возвращает смещение относительно UTC.
UtcDateTime
Возвращает DateTime как время UTC.
Свойство DateTime возвращает вам DateTime (не приведенное к локальной временной зоне), а свойство Offset имеет формат TimeSpan, представляющее смещение от времени UTC. Также есть свойства LocalDateTime и UtcDateTime, конвертирующие данный DateTimeOffset в DateTime для локальной временной зоны или UTC.
Также замечу, что свойства Now и UtcNow структуры DateTimeOffset возвращают не тип DateTime, а DateTimeOffsets с соответствующим смещением от UTC. Конечно, как и DateTime, DateTimeOffset обладает методами, оперирующими с датой/временем, возвращая тип DateTimeOffset вместо DateTime.
Так как нам всё это поможет в выше приведенном примере? Теперь мы знаем, что третья сторона высылает нам дату и время своей временной зоны, которое не нужно конвертировать в локальные дату/время. Поэтому можно использовать DateTimeOffset.Parse() (или TryParse()) и выбрать только дату:
// ясно что здесь выполняется чтение файла/потока/и т.п.
var dateString = "2012-03-01 00:00:00-05:00";
// парсим день рождения как смещение даты/времени (без конвертирования в локальные даты/время)
var dtOffset = DateTimeOffset.Parse(dateString);
// теперь если нам надо сравнить результат с другими объектами типа локального DateTime
// мы просто используем свойство Date для получения даты
// без времени или смещения
var theDay = dtOffset.Date;
Таким образом, вы можете легко парсить даты без необходимости отслеживания “полночного сдвига” или же использовать его там, где необходимо иметь дату, время и смещение без конвертирования в локальное время.
Итоги
Хоть структура DateTime и является достаточно мощной в плане парсинга, манипулирования и сравнения дат/времени, она может доставить немало неприятных минут в работе с датами в формате разных временных зон. DateTimeOffset в этом случае проявляет себя куда более гибкой, поскольку использует смещение от UTC.
Вольный перевод (с) В.Ф.Чужа ака hDrummer, оригинал здесь
Пишущие на .NET (а если вы этого не делаете, то зря читаете этот пост) наверняка время от времени используют для своих нужд структуру DateTime. Эта структура удобна для хранения дат, времени или даты/времени, относящихся к локальной временной зоне (или же к UTC).
Однако, бывают случаи, когда вам необходимо сохранить время в виде смещения, а не конвертировать его в локальное время. И вот здесь вам на помощь придёт структура, впервые появившаяся в .NET 3.5 — DateTimeOffset.
Проблема: парсинг DateTime может привести к конвертации в локальное время
Представим себе, что вы используете файл, веб-сервис и т.п. некой сторонней фирмы, чьи сервера находятся в другой временной зоне. Более того, у них есть несколько полей, в возвращаемых данных, которые должны содержать даты, но на самом деле содержат сериализованные экземпляры структуры DateTime, время в которых установлено в полночь. Например, дату рождения пациента они передают вот в таком виде:
2012-03-01 00:00:00-05:00
Такая запись говорит о том, что человек родился 1 марта 2012 года в неуказанное время (в конце концов, большая часть форм не требует от вас заполнения времени вашего рождения). Но поскольку экземпляр структуры DateTime был сериализован «в лоб», то он и содержит время, установленное в полночь, согласно своей временной зоне.
Итак, зная, что эта дата совместима с Восточной временной зоной (Eastern Time Zone), а мы находимся в Центральной временной зоне (Central Time Zone) мы парсим её так:
// ясно что здесь выполняется чтение файла/потока/и т.п.
var dateString = "2012-03-01 00:00:00-05:00";
// парсим в DateTime
var birthDay = DateTime.Parse(dateString);
Выглядит идеально, не так ли? Но тут кроется проблемка. Если мы проверим содержимое объекта DateTime на нашей локальной машине, где выставлена Центральная временная зона, то увидим вот что:
2012-02-29 11:00:00 PM
Что случилось? (Или как говорил один персонаж — Кто это сделал?) Да, метод DateTime.Parse() конвертировал дату в локальную временную зону поскольку оригинальная дата рождения хранилась с указанным смещением. Вам просто оказали услугу — конвертировали указанную дату и время в ваши локальные дату и время. Это не так и плохо, если бы речь не шла о дне рождения, которое с 1 марта переместилось на 29 февраля.
Конечно, мы можем созвониться с третьей стороной и попросить перестать включать время в строку с датой или же перестать высылать смещение вместе со временем (в этом случае она перестанет конвертироваться в локальное время, но будет отмечена как DateTimeKind.Unspecified).
Однако бывает, что у нас нет возможности таким образом изменить ситуацию.
Бывают случаи, когда вы хотите считывать дату и время со смещением, но не конвертировать его в локальную временную зону. И вот тут вам пригодится DateTimeOffset.
DateTimeOffset – хранит DateTime и Offset
Так что там про DateTimeOffset? Структура так же проста, как и её имя, DateTimeOffset это дата+время+смещение. Именно поэтому она представляет намного более точную точку во времени, поскольку включает информацию о смещении, по которому были установлены текущие дата и время.
По правде говоря, функциональность DateTime и DateTimeOffset во многом перекрывается, а поскольку у Microsoft есть руководство по выбору того или другого, то я рекомендую ознакомиться с ней в MSDN. Статья называется «Choosing Between DateTime, DateTimeOffset, and TimeZoneInfo».
В целом, вы можете использовать DateTime, если вы «прикреплены» к одной временной зоне или используете только универсальное время в формате UTC. Но если вы хотите использовать даты и время из разных временных зон, а также хотите сохранить информацию о смещении без конвертации в локальное время, то лучше использовать DateTimeOffset.
В структуре DateTimeOffset есть много таких же свойств, как и в структуре DateTime (Day, Month, Year, Hour, Minute, Second, и т.п.), потому здесь их я описывать не стану. Главное отличие состоит в нескольких новых свойствах:
DateTime
Возвращает DateTime без учёта смещения.
LocalDateTime
Возвращает конвертированный DateTime, с учётом смещения (т.е. в локальной временной зоне).
Offset
Возвращает смещение относительно UTC.
UtcDateTime
Возвращает DateTime как время UTC.
Свойство DateTime возвращает вам DateTime (не приведенное к локальной временной зоне), а свойство Offset имеет формат TimeSpan, представляющее смещение от времени UTC. Также есть свойства LocalDateTime и UtcDateTime, конвертирующие данный DateTimeOffset в DateTime для локальной временной зоны или UTC.
Также замечу, что свойства Now и UtcNow структуры DateTimeOffset возвращают не тип DateTime, а DateTimeOffsets с соответствующим смещением от UTC. Конечно, как и DateTime, DateTimeOffset обладает методами, оперирующими с датой/временем, возвращая тип DateTimeOffset вместо DateTime.
Так как нам всё это поможет в выше приведенном примере? Теперь мы знаем, что третья сторона высылает нам дату и время своей временной зоны, которое не нужно конвертировать в локальные дату/время. Поэтому можно использовать DateTimeOffset.Parse() (или TryParse()) и выбрать только дату:
// ясно что здесь выполняется чтение файла/потока/и т.п.
var dateString = "2012-03-01 00:00:00-05:00";
// парсим день рождения как смещение даты/времени (без конвертирования в локальные даты/время)
var dtOffset = DateTimeOffset.Parse(dateString);
// теперь если нам надо сравнить результат с другими объектами типа локального DateTime
// мы просто используем свойство Date для получения даты
// без времени или смещения
var theDay = dtOffset.Date;
Таким образом, вы можете легко парсить даты без необходимости отслеживания “полночного сдвига” или же использовать его там, где необходимо иметь дату, время и смещение без конвертирования в локальное время.
Итоги
Хоть структура DateTime и является достаточно мощной в плане парсинга, манипулирования и сравнения дат/времени, она может доставить немало неприятных минут в работе с датами в формате разных временных зон. DateTimeOffset в этом случае проявляет себя куда более гибкой, поскольку использует смещение от UTC.
Вольный перевод (с) В.Ф.Чужа ака hDrummer, оригинал здесь
Теряем посетителей — много и каждый день
По какой-то причине огромное количество проектов с радостью теряет самых хлебных посетителей — белых воротничков в рабочее время.
Большинство работодателей закрывают все социальные сети скопом, начиная, естественно, с контакта. Несмотря на это — у крупных и очень крупных проектов в начале страницы( уже не очень хорошо) можно встретить простую строчку:
или
В итоге пользователи с закрытым контактом не могут открыть сайт, перейти по ссылке и в конце концов — принести проекту прибыль.
Примеры c посещаемостью больше 50к в день:
vesti.ru
kp.ru
smotri.ru
oktogo.ru
lifehacker.ru
povarenok.ru
Будьте аккуратны, используйте асинхронный код, доступный для всех соц. сетей, в крайнем случае — его можно смастерить самому.
UPD: в комментариях просят пояснить — почему один скрипт помешает всему.
Встречая script или link браузер тут же начинает грузить содержимое(в несколько потоков). До тех пор пока загрузка не окончена — браузер не может отрендерить страницу и «виснет» пока от запрос не отвалится по таймауту(минута-две).
Согласен, что если нормальные админы настраивают файрвол так, что этой проблемы не будет, но как показывает практика многие об этом не задумываются.
UPD2: Самый массовая «блокировка рунета» случилась в прошлом году, когда пару часов лежал Яндекс и вместе с ним — все сайты с синхронным кодом Директа, Метрики и других виджетов яндекса.
Большинство работодателей закрывают все социальные сети скопом, начиная, естественно, с контакта. Несмотря на это — у крупных и очень крупных проектов в начале страницы( уже не очень хорошо) можно встретить простую строчку:
или
В итоге пользователи с закрытым контактом не могут открыть сайт, перейти по ссылке и в конце концов — принести проекту прибыль.
Примеры c посещаемостью больше 50к в день:
vesti.ru
kp.ru
smotri.ru
oktogo.ru
lifehacker.ru
povarenok.ru
Будьте аккуратны, используйте асинхронный код, доступный для всех соц. сетей, в крайнем случае — его можно смастерить самому.
UPD: в комментариях просят пояснить — почему один скрипт помешает всему.
Встречая script или link браузер тут же начинает грузить содержимое(в несколько потоков). До тех пор пока загрузка не окончена — браузер не может отрендерить страницу и «виснет» пока от запрос не отвалится по таймауту(минута-две).
Согласен, что если нормальные админы настраивают файрвол так, что этой проблемы не будет, но как показывает практика многие об этом не задумываются.
UPD2: Самый массовая «блокировка рунета» случилась в прошлом году, когда пару часов лежал Яндекс и вместе с ним — все сайты с синхронным кодом Директа, Метрики и других виджетов яндекса.
DevBar — питейное заведение для работников IT-индустрии и не только (Петербург)
Добрый день, господа и дамы.
Долгое время мы с партнером вынашивали идею организации бара, ориентированного на IT-специалистов.
Мы разработчики, живем в Санкт-Петербурге и частенько после работы любим пропустить пару (если повезет) бокалов каких-либо алкогольных напитков, обсуждая различные рабочие и не очень вопросы.
В один момент мы поймали себя на том, что на рынке не хватает заведений, ориентированных конкретно на нас. Есть ирландские пабы, спортивные бары, байкерские бары. Но работников такой важной индустрии, почему-то, все забыли. Когда я был в Голландии, я заметил питейные заведения для медиков, полицейских, пожарных, садоводов, ассенизаторов, и еще кучу мест с профессиональной концепцией. В нашей стране подобная ориентированность пока не развита.
Собственно, пораскинув мозгами и записав все на бумагу, получилось следующее.
Фишки
У любого уважающего себя заведения должны быть свои «фишки», которые заставляют посетителей приходить снова, рассказывать о них друзьям. Да и вообще, на этом автоматически строится лицо заведения в обществе.
В IT за этим далеко ходить не нужно.
Коктейльная карта
Вот несколько примеров из списка, который мы составили с опытным барменом (который делает так, чтобы было не только интересно, но и вкусно).
Basic — простой коктейль, для разогрева
PHP — лонг, идет легко, но лучше не увлекаться
Java — лонг, вкусный, пьется долго и не зависит от сосуда
С++ — шот, намешано кучу всего, вставляет не по-детски
Полный список (а вы видели только восьмую его часть) и составы вскоре можно будет посмотреть на нашем сайте.
Мы ничего не имеет против одноименных языков программирования. Партнер, например, работает с PHP.
Конкурсы
Круглосуточно можно подойти к бармену, пройти тест на знание какой-либо технологии или тематики.
Также, в главном зале стоит сервер. При желании, можно попробовать его взломать на определенных условиях и получить вечер бесплатного алкоголя.
Сцена для докладов
Я всегда считал, что большинство IT-конференций — это скучно. Разумеется, бывают интересные доклады, но зачастую нужно все-таки совмещать полезное с приятным.
Поэтому планируется организовать небольшую сцену для докладчиков, которые за определенные бонусы могут поделиться своими знаниями с посетителями. Все оборудование, разумеется, предоставляется.
И в догонку
Карточные игры. Бесплатный Wi-Fi, разумеется. Розетки для ноутбуков, зарядные кабели для телефонов у каждого столика.
Соответствующий интерьер (со спектрумами и денди). Негромкая музыка. В общем все, что нужно для работы (если вы придете к нам за этим).
Ценовая политика
Представители IT-индустрии бывают разные. В какой-то мере это студенты, в большинстве же — трудоустроенные специалисты, т.е. достаточно платежеспособная аудитория. Но мы решили не становиться элитным дорогим баром и держаться между самыми дешевыми представителями рынка и средними игроками. Дабы и студентам было, где хорошо провести время в кругу интересных людей, и матерым спецам не приходилось уходить с пустым бумажником и кредиткой в овердрафте. По плану, наш «средний чек» на 30% ниже, чем в среднем по больнице.
Местоположение
Партнер изначально предлагал разместиться где-нибудь в спальном районе по принципу «раз уж так круто, доедут». Но поговорив с опытными людьми мы решили, что нужно сделать умнее. Мы посмотрели, где находится большинство IT-компаний в городе (буквально, на карте), прикинули вектор пути домой большинства сотрудников и выбрали оптимальное, достаточно проходное место. Как ни странно, это оказался не центр, а Петроградский район (сдвиг на север). Там сейчас подыскиваем подходящую площадь.
Привлечение аудитории
Собственно, подобному заведению, на наш взгляд, намного проще раскрутиться, чем очередному «Irish super pub». Неординарность заведения и узкая целевая аудитория сделает свое дело (был изучен опыт тех же Голландских коллег). Да и сарафанное радио никто не отменял.
Привлекать будем через социальные сети, легкую рекламу в IT-компаниях и тематические (в плане технологий) вечера. А специалисты на утро сами расскажут коллегам, почему у них такое бледное лицо.
Многие скажут, что подобными развлечениями мы подрываем индустрию изнутри. Мы же искренне верим, что хороший досуг и народные способы опохмеления обеспечивают повышение производительности каждого посетителя.
Тем более, как было описано выше, мы обеспечиваем условия не только для распития напитков, но и для работы. Отчасти, это даже коворкинг-центр с хай-лоу сплит покером и прекрасными дамами.
Говорить — не мешки ворочать
На данный момент у нас готовы расчеты затрат на запуск, списки начальных закупок (от туалетной бумаги до барной стойки) и подробный бизнес-план. Также имеется часть стартового капитала и связи с некоторыми представителями рынка. Сейчас мы активно ищем инвестора.
Что же мы хотим от вас?
Мы в любом случае будем воплощать эту идею, но нам интересно, как ее прокомментируют представители крупнейшего IT-сообщества нашей страны. Вероятно, проскочат полезные предложения или очевидные недостатки нашего плана, которые мы не усмотрели замыленным взглядом.
***
Через неделю мы опубликуем конкретные цифры плана, учтя комментарии и пожелания (к слову, публикуем мы подобный пресс-релиз не только здесь).
На данный момент следить за развитием проекта можно в твиттере.
Дабы не было рекламы:
Твиттер
Спасибо за внимание!
Долгое время мы с партнером вынашивали идею организации бара, ориентированного на IT-специалистов.
Мы разработчики, живем в Санкт-Петербурге и частенько после работы любим пропустить пару (если повезет) бокалов каких-либо алкогольных напитков, обсуждая различные рабочие и не очень вопросы.
В один момент мы поймали себя на том, что на рынке не хватает заведений, ориентированных конкретно на нас. Есть ирландские пабы, спортивные бары, байкерские бары. Но работников такой важной индустрии, почему-то, все забыли. Когда я был в Голландии, я заметил питейные заведения для медиков, полицейских, пожарных, садоводов, ассенизаторов, и еще кучу мест с профессиональной концепцией. В нашей стране подобная ориентированность пока не развита.
Собственно, пораскинув мозгами и записав все на бумагу, получилось следующее.
Фишки
У любого уважающего себя заведения должны быть свои «фишки», которые заставляют посетителей приходить снова, рассказывать о них друзьям. Да и вообще, на этом автоматически строится лицо заведения в обществе.
В IT за этим далеко ходить не нужно.
Коктейльная карта
Вот несколько примеров из списка, который мы составили с опытным барменом (который делает так, чтобы было не только интересно, но и вкусно).
Basic — простой коктейль, для разогрева
PHP — лонг, идет легко, но лучше не увлекаться
Java — лонг, вкусный, пьется долго и не зависит от сосуда
С++ — шот, намешано кучу всего, вставляет не по-детски
Полный список (а вы видели только восьмую его часть) и составы вскоре можно будет посмотреть на нашем сайте.
Мы ничего не имеет против одноименных языков программирования. Партнер, например, работает с PHP.
Конкурсы
Круглосуточно можно подойти к бармену, пройти тест на знание какой-либо технологии или тематики.
Также, в главном зале стоит сервер. При желании, можно попробовать его взломать на определенных условиях и получить вечер бесплатного алкоголя.
Сцена для докладов
Я всегда считал, что большинство IT-конференций — это скучно. Разумеется, бывают интересные доклады, но зачастую нужно все-таки совмещать полезное с приятным.
Поэтому планируется организовать небольшую сцену для докладчиков, которые за определенные бонусы могут поделиться своими знаниями с посетителями. Все оборудование, разумеется, предоставляется.
И в догонку
Карточные игры. Бесплатный Wi-Fi, разумеется. Розетки для ноутбуков, зарядные кабели для телефонов у каждого столика.
Соответствующий интерьер (со спектрумами и денди). Негромкая музыка. В общем все, что нужно для работы (если вы придете к нам за этим).
Ценовая политика
Представители IT-индустрии бывают разные. В какой-то мере это студенты, в большинстве же — трудоустроенные специалисты, т.е. достаточно платежеспособная аудитория. Но мы решили не становиться элитным дорогим баром и держаться между самыми дешевыми представителями рынка и средними игроками. Дабы и студентам было, где хорошо провести время в кругу интересных людей, и матерым спецам не приходилось уходить с пустым бумажником и кредиткой в овердрафте. По плану, наш «средний чек» на 30% ниже, чем в среднем по больнице.
Местоположение
Партнер изначально предлагал разместиться где-нибудь в спальном районе по принципу «раз уж так круто, доедут». Но поговорив с опытными людьми мы решили, что нужно сделать умнее. Мы посмотрели, где находится большинство IT-компаний в городе (буквально, на карте), прикинули вектор пути домой большинства сотрудников и выбрали оптимальное, достаточно проходное место. Как ни странно, это оказался не центр, а Петроградский район (сдвиг на север). Там сейчас подыскиваем подходящую площадь.
Привлечение аудитории
Собственно, подобному заведению, на наш взгляд, намного проще раскрутиться, чем очередному «Irish super pub». Неординарность заведения и узкая целевая аудитория сделает свое дело (был изучен опыт тех же Голландских коллег). Да и сарафанное радио никто не отменял.
Привлекать будем через социальные сети, легкую рекламу в IT-компаниях и тематические (в плане технологий) вечера. А специалисты на утро сами расскажут коллегам, почему у них такое бледное лицо.
Многие скажут, что подобными развлечениями мы подрываем индустрию изнутри. Мы же искренне верим, что хороший досуг и народные способы опохмеления обеспечивают повышение производительности каждого посетителя.
Тем более, как было описано выше, мы обеспечиваем условия не только для распития напитков, но и для работы. Отчасти, это даже коворкинг-центр с хай-лоу сплит покером и прекрасными дамами.
Говорить — не мешки ворочать
На данный момент у нас готовы расчеты затрат на запуск, списки начальных закупок (от туалетной бумаги до барной стойки) и подробный бизнес-план. Также имеется часть стартового капитала и связи с некоторыми представителями рынка. Сейчас мы активно ищем инвестора.
Что же мы хотим от вас?
Мы в любом случае будем воплощать эту идею, но нам интересно, как ее прокомментируют представители крупнейшего IT-сообщества нашей страны. Вероятно, проскочат полезные предложения или очевидные недостатки нашего плана, которые мы не усмотрели замыленным взглядом.
***
Через неделю мы опубликуем конкретные цифры плана, учтя комментарии и пожелания (к слову, публикуем мы подобный пресс-релиз не только здесь).
На данный момент следить за развитием проекта можно в твиттере.
Дабы не было рекламы:
Твиттер
Спасибо за внимание!
Открытие кредитного сервиса Conpay.ru
Пройдя сквозь огонь, воду и медные трубы, набив себе шишек о подводные камни и получив бесценный опыт поднятия интернет-проекта с нуля до единицы смысла, мы объявляем об открытии кредитного сервиса Conpay.ru.
Пролог
Почти полгода назад, 3 сентября 2011 года мы начали разработку кредитного сервиса Conpay.ru. Задача, поставленная перед командой, была определена очень просто: дать возможность покупателям интернет-магазинов приобретать товары в кредит. Но, не смотря на простоту в определении этой задачи, для ее решения нам пришлось на полгода выпасть из нормальной жизни и с головой уйти в мутную воду финансовых стартапов. На старте проекта команда состояла всего из 3 человек: полтора программиста и полтора менеджера. Тем не менее, даже поняв сложность задачи и ограниченность собственных ресурсов, мы продолжали работу на энтузиазме и вдохновении (и бодреньких книжках типа Фрайда и Кавасаки). И вот, наконец, после нескольких месяцев трудов и испытаний нам удалось запустить сервис.
Во-первых, мы создали действительно работающую и проверенную модель взаимодействия с банками и магазинами. Сложно передать, чего это нам стоило – в этой статье об этом сказано очень мало.
Во-вторых, мы написали сам кредитный сервис. Теперь это действительно удобная и полезная штука. Кроме кнопки «Купить в кредит», она включает в себя 4 приложения.
Первое приложение: это кредитный сервис для продавца (интернет-магазина), позволяющий не только размещать на сайте кнопку, но и управлять кредитными покупками, заявками на кредит, доставкой кредитных договоров, контактами покупателей, кредитными продуктами и т. п…
Второе приложение: кредитный калькулятор – это удобный и наглядный сервис для выбора выгодного кредита. Именно кредитный калькулятор открывает пользователь, нажав на кнопку «Купить в кредит» на сайте интернет-магазина.
Третье приложение: личный кабинет покупателя, где он может получить информацию о своих заявках и кредитах. Здесь же покупатель вводит свои анкетные данные и подтверждает согласие на выдачу кредита.
И, наконец, четвертое приложение – это админка для управления всей этой системой.
Модель
Когда мы только начали разработку и написали об этом в блогах и на Хабре, к нам проявили умеренный интерес несколько десятков не самых крупных магазинов. К сожалению, им пришлось ждать полгода, пока наш сервис заработает, и некоторые из них так и не дождались. Тогда же, в самом начале работы, нам удалось выйти на нужных людей в двух банках, еще два представителя других банков оказались постоянными читателями Хабра и связались с нами после первой публикации об идее сервиса (спасибо тебе, Хабрахабр – ибо достучаться до банкиров прес-релизами или информационными письмами было бы невозможно).
Настроение у всех было восторженное. Владельцы магазинов полагали, что на их счета уже скоро пойдут суммы кредитов, выданных на их товары, а им останется только подсчитывать десятые доли в показателях роста продаж. Представители банков ждали от нас миллионов заявок, из которых можно будет выбирать добросовестных заемщиков и продавать им кредиты под 90% годовых. Оказалось, что все не так просто. Оказалось, что все очень сложно.
Схема работы нашего сервиса на данный момент выглядит так:
0. Мы устанавливаем код кнопки «Купить в кредит» на сайт магазина (на страницы товаров, категорий товаров и на страницу выбора способа оплаты)
1. Покупатель нажимает кнопку «купить в кредит» на сайте магазина, выбирает подходящий кредит в кредитном калькуляторе и отправляет заявку на кредит в личном кабинете покупателя
2. Заявка вместе с информацией о товаре, о магазине и о выбранных покупателем условиях кредита передается в указанные покупателем банки
3. В случае одобрения заявки одним или несколькими банками, покупатель выбирает одно любое положительное решение и подтверждает согласие на выдачу кредита.
4. В личный кабинет продавца приходит извещение об одобрении кредита, предлагается выбрать способ доставки товара и договора, загружается и распечатывается кредитный договор.
5. Товар доставляется покупателю вместе с кредитным договором. Покупатель предъявляет паспорт для сверки, ставит подпись в договоре. Подписанный договор возвращается в магазин.
6. Подписанный кредитный договор доставляется в отделение банка. Сумма кредита переводится на счет магазина. Все счастливы.
Здесь можно выделить 4 основные темы, над которыми нам пришлось поломать голову, чтобы начать работать по описанной выше схеме.
Договоры
Во-первых, это договорная база. Юридически оформить схему взаимодействия между банком, магазином и нашим сервисом оказалось очень сложно. Для этого нам пришлось провести более 50 встреч с представителями банков, написать и отправить более 100 официальных писем, нанять собственного юриста (слава богу, он оказался хорошим парнем), подготовить и не согласовать несколько десятков редакций договоров, актов и дополнительных соглашений. Но в итоге мы все-таки добились своего. На данный момент у нас есть полностью рабочая и детально проработанная процедурная модель, описывающая схему работы сервиса от нажатия кнопки «купить в кредит» до момента доставки товара покупателю и перевода суммы кредита на счет магазина. Если кому-нибудь будет настолько интересно, что захочется почитать эти договора, напишите мне об этом по почте, и я Вам их пришлю (но увлекательного чтения, к сожалению, пообещать не могу).
Технология
Следующая тема, это техническое решение, позволяющее передавать и получать данные от банков-партнеров, каждый из которых имеет свой собственный технический регламент для получения и обработки заявок на кредит. Здесь нет смысла лукавить – эта задача была не самой сложной. Вообще, поработав с банками, я не без удивления обнаружил, что уровень развития их технологий в Сети отстает от текущих стандартов лет на 5. Тем не менее, пришлось еще основательно подумать и над защитой персональных данных, и над надежностью процесса авторизации покупателя и магазина.
Стоимость
Третья тема, это коммерческие условия сотрудничества. Прожив полгода в долг, нам хотелось верить в будущие миллионы, хотя бы наших, российских рублей. Оказалось, верить в них не стоило. Во всех вопросах, касающихся денег, нам пришлось руководствоваться, прежде всего, интересами покупателей и продавцов (владельцев интернет-магазинов). В итоге получить что-либо с банков оказалось еще сложнее, чем с магазинов.
Но благодаря упрямству этой стратегии нам удалось добиться 2-х важных уступок со стороны банков. Первая и главная из них – это отсутствие каких-либо комиссий для магазина и покупателя. Проще говоря, мы ничего не берем ни с магазинов, ни с покупателей. Так что наш сервис можно без всякого лукавства назвать совершенно бесплатным. Вторая уступка со стороны банков – это кредитные продукты. Нам удалось убедить банки в том, что покупатель в Интернете, мягко говоря, не готов переплачивать 90% годовых.
Разумеется, банк самостоятельно определяет набор кредитных продуктов, предлагаемых в каждом конкретном магазине (как вы понимаете, нельзя купить телефон или ноутбук по кредиту на мебель или стройматериалы). Тем не менее, мы добились того, что среди этих кредитных продуктов большинство относится к категории низкодоходных и среднедоходных, то есть эффективная процентная ставка составляет от 20% до 50% годовых.
Логистика
Четвертая тема оказалась самой сложной. Она касается доставки кредитного договора покупателю. Современные банковские технологии, к сожалению, не позволяют проводить удаленную идентификацию клиента. Цифровая подпись придет в массы не скоро (а в России, скорее всего, никогда). Поэтому для того, чтобы выдать кредит, заемщика нужно обязательно увидеть в лицо (мне, кстати, до сих пор непонятно, почему нет такой необходимости при эмитировании кредитной карты). А если заставить клиента идти в банк, теряется весь смысл нашего сервиса. Единственное решение – это доставка кредитного договора покупателю на дом.
Но дело в том, что ни один из банков, с которыми мы вели переговоры, не согласился использовать собственную логистику для доставки кредитного договора покупателю на подпись. Проблема еще и в том, что получить право на доставку договора покупателю может только аккредитованное банком лицо (человек, который прошел обучение в банке и имеет соответствующие статус и полномочия от банка). Это связано, прежде всего, с необходимостью проверки паспортных данных и внешнего вида покупателя. Кроме того, в качестве подписанта в кредитных договорах со стороны банка указан именно тот человек (продавец, экспедитор или курьер), который осуществляет оформление договора.
Так как мы собираемся обслуживать покупателей по всей России, мы не можем ограничиваться собственной курьерской службой. Да и денег на ее создание у нас на данный момент нет. Тем не менее, мы все-таки нашли решение: это доставка кредитного договора вместе с товаром. Вопрос о наделении продавца, курьера или экспедитора необходимыми полномочиями нам удалось решить с банками следующим образом: владелец или представитель магазина передает в банк паспортные и контактные данные тех лиц, которые будут заниматься доставкой проданных в кредит товаров. Это могут быть как собственные курьеры магазина, так и представители курьерских служб. Банк самостоятельно и за свой счет связывается с ними, приглашает в отделение банка, проводит обучение (это занимает не более 1 часа) и наделяет необходимыми полномочиями. Хотя обучение – это очень условное название для данного процесса. На деле безопасник банка в шутливой форме рассказывает курьерам о том, что их ждет, если они задумают каким-либо образом кинуть банк. Я сам слушал. Очень познавательно.
Прочие сложности
На данный момент основные сложности у нас возникают с поиском партнеров. Причем эти сложности связаны не с отсутствием интереса к сервису с их стороны (интерес есть, иногда даже слишком большой), а с элементарной нехваткой ресурсов на маркетинг и сэйлз.
Разумеется, наибольший интерес для банков представляют крупные торговые сети и самые популярные магазины. Но для того, чтобы достучаться до людей, принимающих решения в этих организациях, нам нужно прыгнуть выше головы. И как часто это бывает в странах с низкой среднегодовой температурой, далеко не факт, что принимающий решение будет руководствоваться интересами компании, а не своими собственными.
Сдержанность интереса банков объяснить проще. Пока у нас за спиной всего лишь несколько десятков мелких магазинов, мы им интересны не более чем обычная торговая точка со средним трафиком. Поэтому, для того чтобы достучаться до других банков, нам нужны крупные магазины и торговые сети. А дальше см. предыдущий абзац.
Эпилог
В заключение, немного лирики: мы хотим создать кредитную торговую площадку в виде агрегатора торговых предложений от интернет-магазинов, подключенных к кредитному сервису. Это будет огромный интернет-магазин, где все можно будет купить в кредит.
Еще мы хотим попробовать продавать в Интернете кредитные продукты для образования, здоровья, туризма и отдыха и т. п… То есть для того, чтобы подключиться к сети Conpay.ru, не обязательно быть интернет-магазином. И даже сайт свой иметь необязательно (если будет очень нужно, всегда можно сделать). Важно лишь иметь товар или услугу, которая будет пользоваться спросом и привлекать покупателей.
Отдельный сервис мы хотим сделать для B2B-рынка. Там процесс покупки намного сложнее, имеется своя специфика рабочих процессов, вместо банков работают лизинговые компании, стоимость привлеченного клиента намного выше и много других особенностей. B2B-компании, кстати, сами заинтересовались возможностью прикрутить аналог нашего сервиса к своему бизнесу, так что интерес к сервису для B2B определенно есть.
Ну и последнее – это офлайн. Теоретически мы можем заменить одним нетбуком десяток кредитных инспекторов, занимающих место в торговом зале. Проще говоря, для точки продаж (без сайта или с сайтом без интернет-магазина) наш сервис так же может быть полезен.
В общем, открытие сервиса – это, безусловно, значимое событие, но уже сейчас понятно, что допиливать мы его будем бесконечно.
UPD: Открытие произошло на самом деле еще вчера, но я по глупости своей закинул свой рекламный-пререкламный топик в тематический блог «Электронная коммерция» и за это немедленно был переведен в режим read-only (читай забанен) на 1000 дней. Но хабр, кажется, простил меня и прислал инвайт, за что ему огромное спасибо.
Пролог
Почти полгода назад, 3 сентября 2011 года мы начали разработку кредитного сервиса Conpay.ru. Задача, поставленная перед командой, была определена очень просто: дать возможность покупателям интернет-магазинов приобретать товары в кредит. Но, не смотря на простоту в определении этой задачи, для ее решения нам пришлось на полгода выпасть из нормальной жизни и с головой уйти в мутную воду финансовых стартапов. На старте проекта команда состояла всего из 3 человек: полтора программиста и полтора менеджера. Тем не менее, даже поняв сложность задачи и ограниченность собственных ресурсов, мы продолжали работу на энтузиазме и вдохновении (и бодреньких книжках типа Фрайда и Кавасаки). И вот, наконец, после нескольких месяцев трудов и испытаний нам удалось запустить сервис.
Во-первых, мы создали действительно работающую и проверенную модель взаимодействия с банками и магазинами. Сложно передать, чего это нам стоило – в этой статье об этом сказано очень мало.
Во-вторых, мы написали сам кредитный сервис. Теперь это действительно удобная и полезная штука. Кроме кнопки «Купить в кредит», она включает в себя 4 приложения.
Первое приложение: это кредитный сервис для продавца (интернет-магазина), позволяющий не только размещать на сайте кнопку, но и управлять кредитными покупками, заявками на кредит, доставкой кредитных договоров, контактами покупателей, кредитными продуктами и т. п…
Второе приложение: кредитный калькулятор – это удобный и наглядный сервис для выбора выгодного кредита. Именно кредитный калькулятор открывает пользователь, нажав на кнопку «Купить в кредит» на сайте интернет-магазина.
Третье приложение: личный кабинет покупателя, где он может получить информацию о своих заявках и кредитах. Здесь же покупатель вводит свои анкетные данные и подтверждает согласие на выдачу кредита.
И, наконец, четвертое приложение – это админка для управления всей этой системой.
Модель
Когда мы только начали разработку и написали об этом в блогах и на Хабре, к нам проявили умеренный интерес несколько десятков не самых крупных магазинов. К сожалению, им пришлось ждать полгода, пока наш сервис заработает, и некоторые из них так и не дождались. Тогда же, в самом начале работы, нам удалось выйти на нужных людей в двух банках, еще два представителя других банков оказались постоянными читателями Хабра и связались с нами после первой публикации об идее сервиса (спасибо тебе, Хабрахабр – ибо достучаться до банкиров прес-релизами или информационными письмами было бы невозможно).
Настроение у всех было восторженное. Владельцы магазинов полагали, что на их счета уже скоро пойдут суммы кредитов, выданных на их товары, а им останется только подсчитывать десятые доли в показателях роста продаж. Представители банков ждали от нас миллионов заявок, из которых можно будет выбирать добросовестных заемщиков и продавать им кредиты под 90% годовых. Оказалось, что все не так просто. Оказалось, что все очень сложно.
Схема работы нашего сервиса на данный момент выглядит так:
0. Мы устанавливаем код кнопки «Купить в кредит» на сайт магазина (на страницы товаров, категорий товаров и на страницу выбора способа оплаты)
1. Покупатель нажимает кнопку «купить в кредит» на сайте магазина, выбирает подходящий кредит в кредитном калькуляторе и отправляет заявку на кредит в личном кабинете покупателя
2. Заявка вместе с информацией о товаре, о магазине и о выбранных покупателем условиях кредита передается в указанные покупателем банки
3. В случае одобрения заявки одним или несколькими банками, покупатель выбирает одно любое положительное решение и подтверждает согласие на выдачу кредита.
4. В личный кабинет продавца приходит извещение об одобрении кредита, предлагается выбрать способ доставки товара и договора, загружается и распечатывается кредитный договор.
5. Товар доставляется покупателю вместе с кредитным договором. Покупатель предъявляет паспорт для сверки, ставит подпись в договоре. Подписанный договор возвращается в магазин.
6. Подписанный кредитный договор доставляется в отделение банка. Сумма кредита переводится на счет магазина. Все счастливы.
Здесь можно выделить 4 основные темы, над которыми нам пришлось поломать голову, чтобы начать работать по описанной выше схеме.
Договоры
Во-первых, это договорная база. Юридически оформить схему взаимодействия между банком, магазином и нашим сервисом оказалось очень сложно. Для этого нам пришлось провести более 50 встреч с представителями банков, написать и отправить более 100 официальных писем, нанять собственного юриста (слава богу, он оказался хорошим парнем), подготовить и не согласовать несколько десятков редакций договоров, актов и дополнительных соглашений. Но в итоге мы все-таки добились своего. На данный момент у нас есть полностью рабочая и детально проработанная процедурная модель, описывающая схему работы сервиса от нажатия кнопки «купить в кредит» до момента доставки товара покупателю и перевода суммы кредита на счет магазина. Если кому-нибудь будет настолько интересно, что захочется почитать эти договора, напишите мне об этом по почте, и я Вам их пришлю (но увлекательного чтения, к сожалению, пообещать не могу).
Технология
Следующая тема, это техническое решение, позволяющее передавать и получать данные от банков-партнеров, каждый из которых имеет свой собственный технический регламент для получения и обработки заявок на кредит. Здесь нет смысла лукавить – эта задача была не самой сложной. Вообще, поработав с банками, я не без удивления обнаружил, что уровень развития их технологий в Сети отстает от текущих стандартов лет на 5. Тем не менее, пришлось еще основательно подумать и над защитой персональных данных, и над надежностью процесса авторизации покупателя и магазина.
Стоимость
Третья тема, это коммерческие условия сотрудничества. Прожив полгода в долг, нам хотелось верить в будущие миллионы, хотя бы наших, российских рублей. Оказалось, верить в них не стоило. Во всех вопросах, касающихся денег, нам пришлось руководствоваться, прежде всего, интересами покупателей и продавцов (владельцев интернет-магазинов). В итоге получить что-либо с банков оказалось еще сложнее, чем с магазинов.
Но благодаря упрямству этой стратегии нам удалось добиться 2-х важных уступок со стороны банков. Первая и главная из них – это отсутствие каких-либо комиссий для магазина и покупателя. Проще говоря, мы ничего не берем ни с магазинов, ни с покупателей. Так что наш сервис можно без всякого лукавства назвать совершенно бесплатным. Вторая уступка со стороны банков – это кредитные продукты. Нам удалось убедить банки в том, что покупатель в Интернете, мягко говоря, не готов переплачивать 90% годовых.
Разумеется, банк самостоятельно определяет набор кредитных продуктов, предлагаемых в каждом конкретном магазине (как вы понимаете, нельзя купить телефон или ноутбук по кредиту на мебель или стройматериалы). Тем не менее, мы добились того, что среди этих кредитных продуктов большинство относится к категории низкодоходных и среднедоходных, то есть эффективная процентная ставка составляет от 20% до 50% годовых.
Логистика
Четвертая тема оказалась самой сложной. Она касается доставки кредитного договора покупателю. Современные банковские технологии, к сожалению, не позволяют проводить удаленную идентификацию клиента. Цифровая подпись придет в массы не скоро (а в России, скорее всего, никогда). Поэтому для того, чтобы выдать кредит, заемщика нужно обязательно увидеть в лицо (мне, кстати, до сих пор непонятно, почему нет такой необходимости при эмитировании кредитной карты). А если заставить клиента идти в банк, теряется весь смысл нашего сервиса. Единственное решение – это доставка кредитного договора покупателю на дом.
Но дело в том, что ни один из банков, с которыми мы вели переговоры, не согласился использовать собственную логистику для доставки кредитного договора покупателю на подпись. Проблема еще и в том, что получить право на доставку договора покупателю может только аккредитованное банком лицо (человек, который прошел обучение в банке и имеет соответствующие статус и полномочия от банка). Это связано, прежде всего, с необходимостью проверки паспортных данных и внешнего вида покупателя. Кроме того, в качестве подписанта в кредитных договорах со стороны банка указан именно тот человек (продавец, экспедитор или курьер), который осуществляет оформление договора.
Так как мы собираемся обслуживать покупателей по всей России, мы не можем ограничиваться собственной курьерской службой. Да и денег на ее создание у нас на данный момент нет. Тем не менее, мы все-таки нашли решение: это доставка кредитного договора вместе с товаром. Вопрос о наделении продавца, курьера или экспедитора необходимыми полномочиями нам удалось решить с банками следующим образом: владелец или представитель магазина передает в банк паспортные и контактные данные тех лиц, которые будут заниматься доставкой проданных в кредит товаров. Это могут быть как собственные курьеры магазина, так и представители курьерских служб. Банк самостоятельно и за свой счет связывается с ними, приглашает в отделение банка, проводит обучение (это занимает не более 1 часа) и наделяет необходимыми полномочиями. Хотя обучение – это очень условное название для данного процесса. На деле безопасник банка в шутливой форме рассказывает курьерам о том, что их ждет, если они задумают каким-либо образом кинуть банк. Я сам слушал. Очень познавательно.
Прочие сложности
На данный момент основные сложности у нас возникают с поиском партнеров. Причем эти сложности связаны не с отсутствием интереса к сервису с их стороны (интерес есть, иногда даже слишком большой), а с элементарной нехваткой ресурсов на маркетинг и сэйлз.
Разумеется, наибольший интерес для банков представляют крупные торговые сети и самые популярные магазины. Но для того, чтобы достучаться до людей, принимающих решения в этих организациях, нам нужно прыгнуть выше головы. И как часто это бывает в странах с низкой среднегодовой температурой, далеко не факт, что принимающий решение будет руководствоваться интересами компании, а не своими собственными.
Сдержанность интереса банков объяснить проще. Пока у нас за спиной всего лишь несколько десятков мелких магазинов, мы им интересны не более чем обычная торговая точка со средним трафиком. Поэтому, для того чтобы достучаться до других банков, нам нужны крупные магазины и торговые сети. А дальше см. предыдущий абзац.
Эпилог
В заключение, немного лирики: мы хотим создать кредитную торговую площадку в виде агрегатора торговых предложений от интернет-магазинов, подключенных к кредитному сервису. Это будет огромный интернет-магазин, где все можно будет купить в кредит.
Еще мы хотим попробовать продавать в Интернете кредитные продукты для образования, здоровья, туризма и отдыха и т. п… То есть для того, чтобы подключиться к сети Conpay.ru, не обязательно быть интернет-магазином. И даже сайт свой иметь необязательно (если будет очень нужно, всегда можно сделать). Важно лишь иметь товар или услугу, которая будет пользоваться спросом и привлекать покупателей.
Отдельный сервис мы хотим сделать для B2B-рынка. Там процесс покупки намного сложнее, имеется своя специфика рабочих процессов, вместо банков работают лизинговые компании, стоимость привлеченного клиента намного выше и много других особенностей. B2B-компании, кстати, сами заинтересовались возможностью прикрутить аналог нашего сервиса к своему бизнесу, так что интерес к сервису для B2B определенно есть.
Ну и последнее – это офлайн. Теоретически мы можем заменить одним нетбуком десяток кредитных инспекторов, занимающих место в торговом зале. Проще говоря, для точки продаж (без сайта или с сайтом без интернет-магазина) наш сервис так же может быть полезен.
В общем, открытие сервиса – это, безусловно, значимое событие, но уже сейчас понятно, что допиливать мы его будем бесконечно.
UPD: Открытие произошло на самом деле еще вчера, но я по глупости своей закинул свой рекламный-пререкламный топик в тематический блог «Электронная коммерция» и за это немедленно был переведен в режим read-only (читай забанен) на 1000 дней. Но хабр, кажется, простил меня и прислал инвайт, за что ему огромное спасибо.
Подписаться на:
Комментарии (Atom)