설명 없음

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-строка");

// и ВСЁ