Тайлбар байхгүй

vivanov ed50ed40ea second commit 10 сар өмнө
.gitignore.txt 429e5ab52f first commit 11 сар өмнө
readme.md ed50ed40ea second commit 10 сар өмнө

readme.md

Конспект "Форматы файлов"

CSV

CSV (от англ. Comma-Separated Values — значения, разделённые запятыми) — текстовый формат, предназначенный для представления табличных данных. Строка таблицы соответствует строке текста, которая содержит одно или несколько полей, разделенных запятыми.

Спецификация

  • Каждая строка файла — это одна строка таблицы.
  • Разделителем (англ. delimiter) значений колонок является символ запятой (,). Однако на практике часто используются другие разделители, то есть формат путают с DSVruen и TSV (см. ниже).
  • Значения, содержащие зарезервированные символы (двойная кавычка, запятая, точка с запятой, новая строка) обрамляются двойными кавычками ("). Если в значении встречаются кавычки — они представляются в файле в виде двух кавычек подряд.

Чтение данных из существующего файла

using System.Globalization;
using CsvHelper;
using CsvHelper.Configuration;

using (var reader = new StreamReader("./test.csv")) {
    using (var csv = new CsvReader(
        reader, CultureInfo.InvariantCulture))
    {
        var records = csv.GetRecords<Foo>();
        foreach (var record in records) {
            Console.WriteLine("{0}, {1}", record.description, record.value);
        }
    }
}

public class Foo
{
    public string description { get; set; }
    public double value { get; set; }
}

Вывод:

строка с пробелами, 123,15
строка      
с переносом 
строки, 456
  • Сначала считывается содержимое файла обычным StreamReader (путь ./ означает, что чтение из текущего каталога)

  • Затем содержимое отдаётся классу CsvReader

  • При чтении данных (GetRecords) используются класс Foo (просто как описание структуры). Классы мы пока не проходили, но тут пока ничего сложного

  • В классе у нас определены поля, соответствующие колонкам в нашем файле

  • В цикле перебираем записи (строки) нашего файла и выводим содержимое в консоль.

Если у нас данные без заголовков или с другими разделителями, то можно задать конфигурацию:

...

var config = new CsvConfiguration(
    CultureInfo.InvariantCulture) { 
        Delimiter = ";", 
        HasHeaderRecord = false };

using (var reader = new StreamReader("./test2.csv")) {
    using (var csv = new CsvReader(reader, config))

...

Запись в CSV

// использую не стандартный формат
var config = new CsvConfiguration(
    CultureInfo.InvariantCulture) { 
        Delimiter = ";", 
        HasHeaderRecord = false };

// создаю тестовые данные
var records = new List<Foo>
{
    new Foo { 
        description ="тест записи\nмногострочного текста",
        value = 12.34 },
    new Foo { 
        description = "просто, текст, с запятыми", 
        value = 0 }
};

using (var writer = new StreamWriter("./test3.csv"))
using (var csv = new CsvWriter(writer, config))
{
    csv.WriteRecords(records);
}

Вывод:

"тест записи
многострочного текста";12.34
просто, текст, с запятыми;0

Различия в культурной среде

В русской культурной среде в качестве разделителя разрядов в числах с плавающей запятой используется запятая. Поэтому вполне может попасться csv файл, в котором в числах разделитель запятая.

"строка с пробелами";123,15
"строка
с переносом 
строки";456

Разделитель полей мы менять умеем, но как быть с форматом чисел?

Нужно задать нужную культурную среду:

var config = new CsvConfiguration(
    new CultureInfo("ru-RU")) { 
        Delimiter = ";", 
        HasHeaderRecord = false };

JSON

JSON (JavaScript Object Notation). Можно перевести как способ записи объектов в JavaScript. Формат оказался настолько удобен, что его стали поддерживать практически все популярные языки программирования.

Как устроен этот формат

Допустим, у нас есть магазин с системой бонусов, которые начисляются по скидочной карте. Когда продавец считывает карту, он должен получить от сервера такие данные:

  • имя,
  • фамилию,
  • телефон,
  • город,
  • возраст,
  • количество бонусных баллов,
  • три предыдущие покупки (чтобы порекомендовать к ним что-то подходящее).

А теперь посмотрите на JSON-ответ, который получит продавец после считывания карты:

{
  "firstname": "Михаил",
  "lastname": "Максимов",
  "phone": "+79201234567",
  "city": "Москва",
  "age": 37,
  "bonus": 2000,
  "prev": [
    "Кроссовки",
    "Турник",
    "Зимняя куртка"
  ]
}

Общее правило такое: сначала всегда идёт название какого-то поля, а через двоеточие — его значение. Названия всегда берутся в двойные кавычки, строковые значения — тоже.

Ещё есть такое:

  • вложенные объекты берутся в фигурные скобки;

  • массивы берутся в прямоугольные скобки;

  • после каждой пары «свойство: значение» должна стоять запятая (в самом конце — не ставится).

Так как JSON — универсальный формат передачи данных, то он может работать только с теми данными, которые есть в большинстве языков:

  • строки — тоже, как и названия, берутся в двойные кавычки; числа, можно дробные;

  • логические значения true или false;

  • массивы или объекты.

То, что не входит в этот список, JSON не обработает и не выдаст сообщение об ошибке, потому что JSON — это просто формат данных и за его правильностью должен следить программист.

Newtonsoft.Json

Эта библиотека является стандартом де-факто для работы с JSON в C#.

Пример сериализации (преобразование объекта в JSON-строку)

var person = new Person
{
    name = "Имя",
    age = 18,
    date = "2024-03-07"
};

string json = JsonConvert.SerializeObject(
    person, Formatting.Indented);

Console.WriteLine(json);

Должны получить что-то подобное:

{
    "name": "Имя",
    "age":18,
    "date":"2024-03-07"
}

Работа с JSON. DataContractJsonSerializer.

Сериализация

var PersonList = new List<Person>() {
    new Person {name="Иванов", age=25, date=new DateTime(2021,1,1)},
    new Person {name="Петров", age=35, date=new DateTime(2021,1,2)}
};

// создаем объект сериализатора, указав, что на входе 
// будет МАССИВ объектов Person
var Serializer = new DataContractJsonSerializer(typeof(Person[]));

using (var streamWriter = new StreamWriter("test1.json"))
{
    Serializer.WriteObject(
        streamWriter.BaseStream,
        // Преобразуем список (List) объектов в МАССИВ
        PersonList.ToArray()
    );
}

Вывод:

[{"age":25,"date":"\/Date(1609448400000+0300)\/","name":"Иванов"},{"age":35,"date":"\/Date(1609534800000+0300)\/","name":"Петров"}]

Замечания по коду:

Первым параметром метод Serializer.WriteObject ждет поток данных (Stream). Передавать ему переменную streamWriter нельзя, т.к. у неё другой тип (StreamWriter).

В инете почему-то для получения потока из файла используют многоступенчатую схему:

сначала читают содержимое файла в объект типа MemoryStream затем передают этот объект в сериализатор Но при первом же вызове InteliSense видно, что у StreamWriter-а есть публичное свойство BaseStream, которое вполне можно использовать в сериализаторе.

var PersonList = (Person[])Serializer.ReadObject(
   new MemoryStream(
       Encoding.UTF8.GetBytes(
           "[{\"name\":\"Иванов\",\"age\":20},{\"name\":\"Петров\",\"age\":20}]"
       )
   )
);

Десериализация

Десериализовать будем такой файл, в нём используется наиболее распространенный формат даты:

[
    {"age":25,"name":"Иванов","date":"2021-03-21"},
    {"age":35,"name":"Петров","date":"2021-03-22"}
]

C# не поддерживает перегрузку свойств, поэтому мы не можем определить одноименные свойства с разными типами.

Решение есть в атрибутах контракта и в том, что сериализатор "видит" и приватные свойства класса:

  • [IgnoreDataMember] - этот атрибут скрывает отмеченное свойство от сериализатора
  • [DataMember(Name = "альтернативное название")] - можно задать альтернативное имя для сериализуемого свойства. Перепишем класс Person с учетом этих атрибутов:

    [DataContract]
    internal class Person
    {
    // создаем приватную переменную для хранения даты
    private DateTime privateDate;
    
    [DataMember]
    public string name { get; set; }
    
    [DataMember]
    public string age { get; set; }
    
    // создаем ПРИВАТНОЕ СТРОКОВОЕ свойство и с помощью атрибутов меняем ему имя для сериализатора
    [DataMember(Name = "date")]
    private string StringDate {
        get {
            return privateDate.ToString("yyyy-MM-dd");
        }
        set {
            // 2021-03-21
            // 0123456789
            privateDate = new DateTime(
                Convert.ToInt32(value.Substring(0, 4)),
                Convert.ToInt32(value.Substring(5, 2)),
                Convert.ToInt32(value.Substring(8, 2))
            );
        } 
    }
    
    // публичное свойство "дата" скрываем от сериализатора
    [IgnoreDataMember]
    public DateTime date { 
        get { return privateDate; }
        set { privateDate = value;  } 
    }
    }
    

Таким образом при десериализации при задании свойства "дата" будет вызван сеттер свойства StringDate. А при использовании объекта Person в коде его публичное свойство date.

Десериализация делается методом ReadObject, который на входе принимает поток с JSON-строкой.

var Serializer = new DataContractJsonSerializer(typeof(Person[]));
using(var sr = new StreamReader("test.json"))
{
    var PersonList = (Person[])Serializer.ReadObject(sr.BaseStream);
    ...

Вариант попроще

Можно использовать не только "голый" .NET Framework, но и библиотеки из других компонентов Visual Studio.

В пространстве имён System.Web.Script.Serialization есть класс JavaScriptSerializer, который выглядит попроще чем классическая реализация:

В пакет разработки C# не входит библиотека System.Web.Extensions (в которой и находится System.Web.Script.Serialization). Нужно в "Обозревателе решений" добавить в "Ссылки" библиотеку Сборки -> Платформа -> System.Web.Extensions

// целевые классы нам по прежнему нужны, но уже без всяких аннотаций
internal class MaterialTC
{
    public string Title { get; set; }
    public int Count { get; set; }
}

internal class Notice
{
    public Material[] data;
}

internal class Answer
{
    public Notice notice;
}


// в месте, где нам нужно распарсить JSON создаем сериализатор и разбираем строку
var serializer = new JavaScriptSerializer();
var answer = serializer.Deserialize<Answer>("тут ваша JSON-строка");

// и ВСЁ