출력 파일에 사용하기 위해 객체를 사용자 정의 문자열 형식으로 직렬화하는 모범 사례

StackOverflow https://stackoverflow.com/questions/1179816

문제

나중에 선택하여 처리할 출력 파일에 쓰기 위한 Excel 친화적인 형식을 생성하기 위해 특정 비즈니스 클래스에 ToString() 재정의를 구현하려고 했습니다.데이터는 다음과 같습니다.

5555555 "LASTN SR, FIRSTN"  5555555555  13956 STREET RD     TOWNSVILLE  MI  48890   25.88   01-003-06-0934

형식 문자열을 만들고 재정의하는 것은 나에게 큰 문제가 아닙니다. ToString(), 하지만 그러면 의 동작이 변경됩니다. ToString() 모든 객체에 대해 이런 방식으로 직렬화하기로 결정하여 다음을 구현합니다. ToString() 도서관 전체에 걸쳐 모두 너덜 너덜했습니다.

지금까지 계속해서 읽어봤습니다 IFormatProvider, 그리고 이를 구현하는 클래스는 좋은 생각처럼 들리지만 이 모든 논리가 어디에 있어야 하는지와 포맷터 클래스를 구축하는 방법에 대해 여전히 약간 혼란스럽습니다.

개체에서 CSV, 탭으로 구분된 문자열 또는 기타 XML이 아닌 임의 문자열을 만들어야 할 때 어떻게 하시나요?

도움이 되었습니까?

해결책

다음은 반사를 사용하여 개체 목록에서 CSV를 만드는 일반적인 방식입니다.

    public static string ToCsv<T>(string separator, IEnumerable<T> objectlist)
    {
        Type t = typeof(T);
        FieldInfo[] fields = t.GetFields();

        string header = String.Join(separator, fields.Select(f => f.Name).ToArray());

        StringBuilder csvdata = new StringBuilder();
        csvdata.AppendLine(header);

        foreach (var o in objectlist) 
            csvdata.AppendLine(ToCsvFields(separator, fields, o));

        return csvdata.ToString();
    }

    public static string ToCsvFields(string separator, FieldInfo[] fields, object o)
    {
        StringBuilder linie = new StringBuilder();

        foreach (var f in fields)
        {
            if (linie.Length > 0)
                linie.Append(separator);

            var x = f.GetValue(o);

            if (x != null)
                linie.Append(x.ToString());
        }

        return linie.ToString();
    }

Tocsv ()의 파일에 직접 쓰기 또는 StringBuilder를 ienumerable 및 수율 명세서로 대체하는 것과 같은 많은 변형이 이루어질 수 있습니다.

다른 팁

다음은 Per Hejndorf의 CSV 아이디어의 단순화된 버전입니다(각 라인을 차례로 생성하므로 메모리 오버헤드 없음).대중적인 요구로 인해 다음을 사용하여 필드와 단순 속성을 모두 지원합니다. Concat.

2017년 5월 18일 업데이트

이 예는 완전한 솔루션을 의도한 것이 아니며 단지 Per Hejndorf가 게시한 원래 아이디어를 발전시킨 것입니다.유효한 CSV를 생성하려면 텍스트 내의 모든 텍스트 구분 문자를 일련의 2개 구분 문자로 바꿔야 합니다.예를 들어간단한 .Replace("\"", "\"\"").

2016년 2월 12일 업데이트

오늘 프로젝트에서 내 코드를 다시 사용한 후에 다음 예제에서 시작할 때 아무것도 당연하게 여겨서는 안 된다는 것을 깨달았습니다. @Per Hejndorf.기본 구분 기호 ","(쉼표)를 가정하고 구분 기호를 두 번째로 만드는 것이 더 합리적입니다. 선택 과목, 매개변수.내 라이브러리 버전도 3번째 라이브러리를 제공합니다. header 때로는 데이터만 원하는 경우 헤더 행을 반환할지 여부를 제어하는 ​​매개변수입니다.

예를 들어

public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    if (header)
    {
        yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    }
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

그런 다음 쉼표로 구분하여 다음과 같이 사용합니다.

foreach (var line in ToCsv(objects))
{
    Console.WriteLine(line);
}

또는 다른 구분 기호에 대해 이와 같습니다(예:탭):

foreach (var line in ToCsv(objects, "\t"))
{
    Console.WriteLine(line);
}

실제 사례

쉼표로 구분된 CSV 파일에 목록 쓰기

using (TextWriter tw = File.CreateText("C:\testoutput.csv"))
{
    foreach (var line in ToCsv(objects))
    {
        tw.WriteLine(line);
    }
}

또는 탭으로 구분하여 작성하세요.

using (TextWriter tw = File.CreateText("C:\testoutput.txt"))
{
    foreach (var line in ToCsv(objects, "\t"))
    {
        tw.WriteLine(line);
    }
}

복잡한 필드/속성이 있는 경우 select 절에서 필터링해야 합니다.


이전 버전 및 세부 정보는 다음과 같습니다.

다음은 Per Hejndorf의 CSV 아이디어의 단순화된 버전입니다(각 라인을 차례로 생성하므로 메모리 오버헤드 없음). 코드는 4줄뿐입니다 :)

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    yield return String.Join(separator, fields.Select(f => f.Name).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString()).ToArray());
    }
}

다음과 같이 반복할 수 있습니다.

foreach (var line in ToCsv(",", objects))
{
    Console.WriteLine(line);
}

어디 objects 강력한 형식의 개체 목록입니다.

이 변형에는 공개 필드와 단순 공개 속성이 모두 포함됩니다.

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

경험의 규칙으로 나는 Tostring을 디버깅 도구로 우선적으로 만 옹호합니다. 비즈니스 로직의 경우 클래스/인터페이스의 명시 적 방법이어야합니다.

이와 같은 간단한 직렬화의 경우 CSV 출력 라이브러리에 대해 알고있는 별도의 클래스와 직렬화를 비즈니스 객체 자체로 밀지 않고 직렬화를 수행하는 비즈니스 객체에 대해 알고 있습니다.

이렇게하면 모델의보기를 생성하는 출력 형식 당 클래스로 끝납니다.

지속성을 위해 객체 그래프를 작성하려는보다 복잡한 직렬화를 위해서는 비즈니스 클래스에 넣는 것을 고려할 것입니다. 그러나 더 깨끗한 코드를 만드는 경우에만 가능합니다.

내가 지금까지 찾은 솔루션의 문제점은 속성의 하위 집합을 내보내지 않고 전체 객체 만 내보낼 수 있다는 것입니다. 대부분의 경우 CSV에서 데이터를 내보내야 할 때 정확한 방식으로 형식을 "재단"해야하므로 유형의 매개 변수 배열을 전달하여이를 수행 할 수있는 간단한 확장 방법을 만들었습니다. Func<T, string> 매핑을 지정합니다.

public static string ToCsv<T>(this IEnumerable<T> list, params Func<T, string>[] properties)
{
    var columns = properties.Select(func => list.Select(func).ToList()).ToList();

    var stringBuilder = new StringBuilder();

    var rowsCount = columns.First().Count;

    for (var i = 0; i < rowsCount; i++)
    {
        var rowCells = columns.Select(column => column[i]);

        stringBuilder.AppendLine(string.Join(",", rowCells));
    }

    return stringBuilder.ToString();
}

용법:

philosophers.ToCsv(x => x.LastName, x => x.FirstName)

생성 :

Hayek,Friedrich
Rothbard,Murray
Brent,David

나는 Hitech Magic의 변형이 같은 값을 가진 두 가지 속성이었고, 하나만 인구가 될 것입니다. 이것은 그것을 고쳐진 것 같습니다.

        public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
    {
        FieldInfo[] fields = typeof(T).GetFields();
        PropertyInfo[] properties = typeof(T).GetProperties();
        yield return String.Join(separator, fields.Select(f => f.Name).Union(properties.Select(p => p.Name)).ToArray());
        foreach (var o in objectlist)
        {
            yield return string.Join(separator, (properties.Select(p => (p.GetValue(o, null) ?? "").ToString())).ToArray());
        }
    }

Gone Coding의 답변은 매우 도움이되었습니다. 출력을 호스 할 텍스트 그레 린을 처리하기 위해 약간의 변경을했습니다.

 /******************************************************/
    public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
    {
       FieldInfo[] fields = typeof(T).GetFields();
       PropertyInfo[] properties = typeof(T).GetProperties();
       string str1;
       string str2;

       if(header)
       {
          str1 = String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p => p.Name)).ToArray());
          str1 = str1 + Environment.NewLine;
          yield return str1;
       }
       foreach(var o in objectlist)
       {
          //regex is to remove any misplaced returns or tabs that would
          //really mess up a csv conversion.
          str2 = string.Join(separator, fields.Select(f => (Regex.Replace(Convert.ToString(f.GetValue(o)), @"\t|\n|\r", "") ?? "").Trim())
             .Concat(properties.Select(p => (Regex.Replace(Convert.ToString(p.GetValue(o, null)), @"\t|\n|\r", "") ?? "").Trim())).ToArray());

          str2 = str2 + Environment.NewLine;
          yield return str2;
       }
    }
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top