【问题标题】:Validate content of csv file c#验证csv文件c#的内容
【发布时间】:2018-02-14 13:43:35
【问题描述】:

我有一个要求,用户将上传以下格式的 csv 文件,其中包含大约 1.8 到 200 万条记录

SITE_ID,HOUSE,STREET,CITY,STATE,ZIP,APARTMENT  
44,545395,PORT ROYAL,CORPUS CHRISTI,TX,78418,2  
44,608646,TEXAS AVE,ODESSA,TX,79762,  
44,487460,EVERHART RD,CORPUS CHRISTI,TX,78413,  
44,275543,EDWARD GARY,SAN MARCOS,TX,78666,4  
44,136811,MAGNOLIA AVE,SAN ANTONIO,TX,78212  

我要做的是,首先验证文件,然后将其保存在数据库中,如果其验证成功并且没有错误。我必须应用的验证对于每列都不同。例如,

SITE_ID: it can only be an integer and it is required.  
HOUSE: integer, required  
STREET: alphanumeric, required  
CITY: alphabets only, required  
State: 2 alphabets only, required  
zip: 5 digits only, required  
APARTMENT: integer only, optional  

我需要一种将这些验证应用于各个列的通用方法。到目前为止,我尝试将 csv 文件转换为 dataTable,并且我计划尝试通过正则表达式验证每个单元格,但这对我来说似乎不是一个通用或好的解决方案。任何人都可以在这方面帮助我并指出正确的方向吗?

【问题讨论】:

  • 我会用 csv 的属性做我自己的类,然后自定义属性来指定属性的验证(所以我可以稍后更改或扩展它们)或在映射方法期间验证它们。
  • 下面的回答对你有帮助吗? stackoverflow.com/a/16608967/4222487
  • 如果这个文件有固定的结构(每次相同的列) - 为什么需要通用方法?
  • 在以下帖子中查看我的代码:stackoverflow.com/questions/30129406/…
  • @Evk 通用方法在有多种类型的 CSV 文件时非常有用,每种类型都有自己定义的规范。我的回答完全涵盖了这一点。

标签: c# csv file-upload


【解决方案1】:

这是一种有效的方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.OleDb;
using System.Text.RegularExpressions;
using System.IO;


namespace ConsoleApplication23
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.csv";
        static void Main(string[] args)
        {
            CSVReader csvReader = new CSVReader();
            DataSet ds = csvReader.ReadCSVFile(FILENAME, true);
            RegexCompare compare = new RegexCompare();
            DataTable errors = compare.Get_Error_Rows(ds.Tables[0]);
        }
    }
    class RegexCompare
    {
        public static Dictionary<string,RegexCompare> dict =  new Dictionary<string,RegexCompare>() {
               { "SITE_ID", new RegexCompare() { columnName = "SITE_ID", pattern = @"[^\d]+", positveNegative = false, required = true}},
               { "HOUSE", new RegexCompare() { columnName = "HOUSE", pattern = @"[^\d]+", positveNegative = false, required = true}}, 
               { "STREET", new RegexCompare() { columnName = "STREET", pattern = @"[A-Za-z0-9 ]+", positveNegative = true, required = true}}, 
               { "CITY", new RegexCompare() { columnName = "CITY", pattern = @"[A-Za-z ]+", positveNegative = true, required = true}},
               { "STATE", new RegexCompare() { columnName = "STATE", pattern = @"[A-Za-z]{2}", positveNegative = true, required = true}},
               { "ZIP", new RegexCompare() { columnName = "ZIP", pattern = @"\d{5}", positveNegative = true, required = true}},
               { "APARTMENT", new RegexCompare() { columnName = "APARTMENT", pattern = @"\d*", positveNegative = true, required = false}},
            };


        string columnName { get; set;}
        string pattern { get; set; }
        Boolean positveNegative { get; set; }
        Boolean required { get; set; }

        public DataTable Get_Error_Rows(DataTable dt)
        {
            DataTable dtError = null;
            foreach (DataRow row in dt.AsEnumerable())
            {
                Boolean error = false;
                foreach (DataColumn col in dt.Columns)
                {
                    RegexCompare regexCompare = dict[col.ColumnName];
                    object colValue = row.Field<object>(col.ColumnName);
                    if (regexCompare.required)
                    {
                        if (colValue == null)
                        {
                            error = true;
                            break;
                        }
                    }
                    else
                    {
                        if (colValue == null)
                            continue;
                    }
                    string colValueStr = colValue.ToString();
                    Match match = Regex.Match(colValueStr, regexCompare.pattern);
                    if (regexCompare.positveNegative)
                    {
                        if (!match.Success)
                        {
                            error = true;
                            break;
                        }
                        if (colValueStr.Length != match.Value.Length)
                        {
                            error = true;
                            break;
                        }
                    }
                    else
                    {
                        if (match.Success)
                        {
                            error = true;
                            break;
                        }
                    }

                }

                if(error)
                {
                    if (dtError == null) dtError = dt.Clone();
                    dtError.Rows.Add(row.ItemArray);
                }
            }
            return dtError;
        }
    }

    public class CSVReader
    {

        public DataSet ReadCSVFile(string fullPath, bool headerRow)
        {

            string path = fullPath.Substring(0, fullPath.LastIndexOf("\\") + 1);
            string filename = fullPath.Substring(fullPath.LastIndexOf("\\") + 1);
            DataSet ds = new DataSet();

            try
            {
                if (File.Exists(fullPath))
                {
                    string ConStr = string.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}" + ";Extended Properties=\"Text;HDR={1};FMT=Delimited\\\"", path, headerRow ? "Yes" : "No");
                    string SQL = string.Format("SELECT * FROM {0}", filename);
                    OleDbDataAdapter adapter = new OleDbDataAdapter(SQL, ConStr);
                    adapter.Fill(ds, "TextFile");
                    ds.Tables[0].TableName = "Table1";
                }
                foreach (DataColumn col in ds.Tables["Table1"].Columns)
                {
                    col.ColumnName = col.ColumnName.Replace(" ", "_");
                }
            }

            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            return ds;
        }
    }

}

【讨论】:

  • 那么CSVReader 是什么?它来自哪个命名空间/包?
  • 我在using 中引用了System.DataSystem.Data.OleDb,但它不知道任何此类。事实上,命名空间不包含任何不以“OleDb”开头的内容。那是哪个 .Net 版本?
  • 我刚刚注意到我修复了一些错误。我不得不重新排列一些 If/Else 语句。
  • 从菜单尝试:项目:添加参考:网络:System.Data。如果有 System.Data.Oledb 添加。某些版本的 VS 添加了不同数量的库以保持较低的内存使用率。加载数据库将添加所有类。
  • 是的,不,那里真的什么都没有。 MSDN 上也不存在该类。请检查它的命名空间。
【解决方案2】:

这是一个设计过度但非常有趣的通用方法,您可以在其中为您的类提供属性以将它们与 CSV 列标题匹配:

第一步是解析您的 CSV。有多种方法,但我最喜欢的是TextFieldParser that can be found in the Microsoft.VisualBasic.FileIO namespace。使用它的好处是它是 100% 原生的;您需要做的就是将Microsoft.VisualBasic 添加到引用中。

完成后,您的数据为List&lt;String[]&gt;。现在,事情变得有趣了。看,现在我们可以创建一个自定义属性并将其添加到我们的类属性中:

属性类:

[AttributeUsage(AttributeTargets.Property)]
public sealed class CsvColumnAttribute : System.Attribute
{
    public String Name { get; private set; }
    public Regex ValidationRegex { get; private set; }

    public CsvColumnAttribute(String name) : this(name, null) { }

    public CsvColumnAttribute(String name, String validationRegex)
    {
        this.Name = name;
        this.ValidationRegex = new Regex(validationRegex ?? "^.*$");
    }
}

数据类:

public class AddressInfo
{
    [CsvColumnAttribute("SITE_ID", "^\\d+$")]
    public Int32 SiteId { get; set; }

    [CsvColumnAttribute("HOUSE", "^\\d+$")]
    public Int32 House { get; set; }

    [CsvColumnAttribute("STREET", "^[a-zA-Z0-9- ]+$")]
    public String Street { get; set; }

    [CsvColumnAttribute("CITY", "^[a-zA-Z0-9- ]+$")]
    public String City { get; set; }

    [CsvColumnAttribute("STATE", "^[a-zA-Z]{2}$")]
    public String State { get; set; }

    [CsvColumnAttribute("ZIP", "^\\d{1,5}$")]
    public Int32 Zip { get; set; }

    [CsvColumnAttribute("APARTMENT", "^\\d*$")]
    public Int32? Apartment { get; set; }
}

如您所见,我在这里所做的是将每个属性链接到一个 CSV 列名,并给它一个正则表达式来验证内容。在不需要的东西上,您仍然可以使用正则表达式,但允许使用空值,如公寓一所示。

现在,要真正将列与 CSV 标题匹配,我们需要获取 AddressInfo 类的属性,检查每个属性是否有 CsvColumnAttribute,如果有,将其名称与CSV 文件数据的列标题。一旦我们有了它,我们就会得到一个PropertyInfo 对象列表,它可以用来动态填充为所有行创建的新对象的属性。

此方法是完全通用的,允许在 CSV 文件中以任何顺序给出列,一旦将 CsvColumnAttribute 分配给要填写的属性,解析将适用于任何类。它将自动验证数据,您可以随心所欲地处理故障。不过,在这段代码中,我所做的只是跳过无效行。

public static List<T> ParseCsvInfo<T>(List<String[]> split) where T : new()
{
    // No template row, or only a template row but no data. Abort.
    if (split.Count < 2)
        return new List<T>();
    String[] templateRow = split[0];
    // Create a dictionary of rows and their index in the file data.
    Dictionary<String, Int32> columnIndexing = new Dictionary<String, Int32>();
    for (Int32 i = 0; i < templateRow.Length; i++)
    {
        // ToUpperInvariant is optional, of course. You could have case sensitive headers.
        String colHeader = templateRow[i].Trim().ToUpperInvariant();
        if (!columnIndexing.ContainsKey(colHeader))
            columnIndexing.Add(colHeader, i);
    }
    // Prepare the arrays of property parse info. We set the length
    // so the highest found column index exists in it.
    Int32 numCols = columnIndexing.Values.Max() + 1;
    // Actual property to fill in
    PropertyInfo[] properties = new PropertyInfo[numCols];
    // Regex to validate the string before parsing
    Regex[] propValidators = new Regex[numCols];
    // Type converters for automatic parsing
    TypeConverter[] propconverters = new TypeConverter[numCols];
    // go over the properties of the given type, see which ones have a
    // CsvColumnAttribute, and put these in the list at their CSV index.
    foreach (PropertyInfo p in typeof(T).GetProperties())
    {
        object[] attrs = p.GetCustomAttributes(true);
        foreach (Object attr in attrs)
        {
            CsvColumnAttribute csvAttr = attr as CsvColumnAttribute;
            if (csvAttr == null)
                continue;
            Int32 index;
            if (!columnIndexing.TryGetValue(csvAttr.Name.ToUpperInvariant(), out index))
            {
                // If no valid column is found, and the regex for this property
                // does not allow an empty value, then all lines are invalid.
                if (!csvAttr.ValidationRegex.IsMatch(String.Empty))
                    return new List<T>();
                // No valid column found: ignore this property.
                break;
            }
            properties[index] = p;
            propValidators[index] = csvAttr.ValidationRegex;
            // Automatic type converter. This function could be enhanced by giving a
            // list of custom converters as extra argument and checking those first.
            propconverters[index] = TypeDescriptor.GetConverter(p.PropertyType);
            break; // Only handle one CsvColumnAttribute per property.
        }
    }
    List<T> objList = new List<T>();
    // start from 1 since the first line is the template with the column names
    for (Int32 i = 1; i < split.Count; i++)
    {
        Boolean abortLine = false;
        String[] line = split[i];
        // make new object of the given type
        T obj = new T();
        for (Int32 col = 0; col < properties.Length; col++)
        {
            // It is possible a line is not long enough to contain all columns.
            String curVal = col < line.Length ? line[col] : String.Empty;
            PropertyInfo prop = properties[col];
            // this can be null if the column was not found but wasn't required.
            if (prop == null)
                continue;
            // check validity. Abort buildup of this object if not valid.
            Boolean valid = propValidators[col].IsMatch(curVal);
            if (!valid)
            {
                // Add logging here? We have the line and column index.
                abortLine = true;
                break;
            }
            // Automated parsing. Always use nullable types for nullable properties.
            Object value = propconverters[col].ConvertFromString(curVal);
            prop.SetValue(obj, value, null);
        }
        if (!abortLine)
            objList.Add(obj);
    }
    return objList;
}

要在您的 CSV 文件上使用,只需这样做

// the function using VB's TextFieldParser
List<String[]> splitData = SplitFile(datafile, new UTF8Encoding(false), ',');
// The above function, applied to the AddressInfo class
List<AddressInfo> addresses = ParseCsvInfo<AddressInfo>(splitData);

就是这样。自动解析和验证,全部通过类属性上的一些添加属性。

注意,如果提前拆分数据会对大数据造成太大的性能影响,那并不是真正的问题; TextFieldParserStream 包裹在 TextReader 中工作,因此您可以提供一个流并在 ParseCsvInfo 函数内动态执行 csv 解析,而不是提供 List&lt;String[]&gt;,只需读取每个 CSV直接来自TextFieldParser

我在这里没有这样做,因为我将阅读器写给 List&lt;String[]&gt; 的原始 csv 读取用例包括自动编码检测,这无论如何都需要读取整个文件。

【讨论】:

    【解决方案3】:

    我建议使用 CSV 库来读取文件。
    例如你可以使用 LumenWorksCsvReader:https://www.nuget.org/packages/LumenWorksCsvReader

    您的正则表达式验证方法实际上是可以的。 例如,您可以创建一个“验证字典”并根据正则表达式检查每个 CSV 值。

    然后您可以构建一个函数,该函数可以使用这样的“验证字典”验证 CSV 文件。

    看这里:

    string lsInput = @"SITE_ID,HOUSE,STREET,CITY,STATE,ZIP,APARTMENT
    44,545395,PORT ROYAL,CORPUS CHRISTI,TX,78418,2
    44,608646,TEXAS AVE,ODESSA,TX,79762,
    44,487460,EVERHART RD,CORPUS CHRISTI,TX,78413,
    44,275543,EDWARD GARY,SAN MARCOS,TX,78666,4
    44,136811,MAGNOLIA AVE,SAN ANTONIO,TX,78212";
    
    Dictionary<string, string> loValidations = new Dictionary<string, string>();
    loValidations.Add("SITE_ID", @"^\d+$"); //it can only be an integer and it is required.
    //....
    
    bool lbValid = true;
    using (CsvReader loCsvReader = new CsvReader(new StringReader(lsInput), true, ','))
    {
        while (loCsvReader.ReadNextRecord())
        {
            foreach (var loValidationEntry in loValidations)
            {
                if (!Regex.IsMatch(loCsvReader[loValidationEntry.Key], loValidationEntry.Value))
                {
                    lbValid = false;
                    break;
                }
            }
            if (!lbValid)
                break;
        }
    }
    Console.WriteLine($"Valid: {lbValid}");
    

    【讨论】:

    • 实际上,.Net 中有一个本地可用的 CSV 库。他们只是把它隐藏得很好,在 VB 命名空间中。
    【解决方案4】:

    这是使用Cinchoo ETL(一个开源文件帮助程序库)来满足您的需求的另一种方法。

    首先定义一个带有 DataAnnonations 验证属性的 POCO 类,如下所示

    public class Site
    {
        [Required(ErrorMessage = "SiteID can't be null")]
        public int SiteID { get; set; }
        [Required]
        public int House { get; set; }
        [Required]
        public string Street { get; set; }
        [Required]
        [RegularExpression("^[a-zA-Z][a-zA-Z ]*$")]
        public string City { get; set; }
        [Required(ErrorMessage = "State is required")]
        [RegularExpression("^[A-Z][A-Z]$", ErrorMessage = "Incorrect zip code.")]
        public string State { get; set; }
        [Required]
        [RegularExpression("^[0-9][0-9]*$")]
        public string Zip { get; set; }
        public int Apartment { get; set; }
    }
    

    然后将此类与 ChoCSVReader 一起使用,使用 Validate()/IsValid() 方法加载并检查文件的有效性,如下所示

    using (var p = new ChoCSVReader<Site>("*** YOUR CSV FILE PATH ***")
        .WithFirstLineHeader(true)
        )
    {
        Exception ex;
        Console.WriteLine("IsValid: " + p.IsValid(out ex));
    }
    

    希望对你有帮助。

    免责声明:我是这个库的作者。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-05-16
      • 1970-01-01
      • 2014-02-26
      • 2011-07-02
      • 1970-01-01
      • 1970-01-01
      • 2019-04-23
      相关资源
      最近更新 更多