在 .NET 中解析分隔的 CSV
Posted
技术标签:
【中文标题】在 .NET 中解析分隔的 CSV【英文标题】:Parse Delimited CSV in .NET 【发布时间】:2010-10-18 17:00:16 【问题描述】:我有一个以逗号分隔格式的文本文件,在大多数字段中由"
分隔。我正在尝试将其转化为可以枚举的内容(例如,通用集合)。我无法控制文件的输出方式,也无法控制用于分隔符的字符。
在这种情况下,字段用逗号分隔,文本字段用"
标记括起来。我遇到的问题是某些字段中包含引号(即 8"
Tray)并且不小心被选为下一个字段。对于数字字段,它们周围没有引号,但它们确实以 + 或 - 符号开头(表示正/负数)。
我正在考虑 RegEx,但我的技能不是那么好,所以希望有人能提出一些我可以尝试的想法。这个文件中有大约 19,000 条记录,所以我试图尽可能高效地完成它。以下是几行示例数据:
"00","000000112260 ","Pie Pumpkin ","RET","6.99 "," ","ea ",+0000000006.99000
"00","000000304078 ","Pie Apple caramel ","RET","9.99 "," ","ea ",+0000000009.99000
"00","StringValue here","8" Tray of Food ","RET","6.99 "," ","ea ",-00000000005.3200
还有很多字段,但你可以得到图片....
我正在使用 VB.NET,并且我有一个通用的列表设置来接受数据。我尝试过使用CSVReader,它似乎运行良好,直到您达到第三条记录(在文本字段中带有引号)。如果我能以某种方式让它处理额外的引号,那么 CSVReader 选项会很好用。
谢谢!
【问题讨论】:
从正确格式化 CSV 文件开始会有所帮助。 带引号的字符串中的双引号应该通过加倍进行转义。所以"8" Tray of Food" 是不允许出现在格式中的。这样的话,像hi","there 这样的字符串就可以存在了。转义和引用,变成"hi"",""there"。没有加倍,它变成 "hi","there" 看起来像两个字符串。 我同意上述两个 cmets,但不幸的是,我无法控制文件的导出方式。这就是它从软件中出来的方式。 请,请,请不要滚动您自己的 CSV 解析器,当然也不要为此使用 RegEx。使用免费、开源、久经考验的 FileHelpers 库。 filehelpers.com filehelpers 源不再可用 :( 【参考方案1】:CSV 文件至少有 ODBC 驱动程序。但是 CSV 有不同的风格。
是什么产生了这些文件?根据源应用程序的要求,不太可能存在匹配的驱动程序。
【讨论】:
这是一个旧的基于 DOS 的会计软件包,称为 Business Vision Delta。不幸的是,该公司已被出售给新的供应商,他们不再支持旧的 DOS 东西。这是我可以提取数据以集成到更新软件中的唯一方法。 你能告诉它使用什么样的数据表吗?也许是dbfs?此外,尝试使用 Excel、Access 以及任何其他可以导入 CSV 的应用程序打开 CSV 文件。尽量避免将编写软件作为第一选择。【参考方案2】:看看FileHelpers library。
【讨论】:
看起来不错,但我发现它使用起来非常令人沮丧。缺乏对自动属性而不是私有字段的支持非常笨拙。 这不是原始问题中的一个因素,但该页面说 FileHelpers 使用动态代码生成。这意味着它在某些受限环境中没有用(对我来说是 MonoTouch)。【参考方案3】:您对 CSVReader 的问题是第三条记录中的引号没有被另一个引号(也称为双引号)转义。如果您不转义它们,那么您希望如何处理文本字段中间的 "?
http://en.wikipedia.org/wiki/Comma-separated_values
(我最终不得不处理文件(使用不同的分隔符),但文本值中的引号字符没有被转义,我最终编写了自己的自定义解析器。我不知道这是否是绝对必要的,或者不是。)
【讨论】:
这是我的问题...我无法逃脱他们。我无法控制文件的导出方式。我试图避免编写一个逐字符检查引号后是否有逗号等的解析器,但它可能归结为这一点。 好吧,如果你走自己的路(我仍然相信有一个解决方案可以处理这种情况),请确保尽可能地验证字段计数和数据能够。 (我会发布我的,但我是在工作中发布的。)【参考方案4】:来自here:
Encoding fileEncoding = GetFileEncoding(csvFile);
// get rid of all doublequotes except those used as field delimiters
string fileContents = File.ReadAllText(csvFile, fileEncoding);
string fixedContents = Regex.Replace(fileContents, @"([^\^,\r\n])""([^$,\r\n])", @"$1$2");
using (CsvReader csv =
new CsvReader(new StringReader(fixedContents), true))
// ... parse the CSV
【讨论】:
这很好用,但由于某种原因,它搞砸了一个像这样的名称:产品“A”名称我确信它与正则表达式有关,但我似乎无法得到没错。 请参阅下面的答案,了解我是如何实现这一点的。 这是我使用的一个很好的解决方案,但 GetFileEncoding 函数不可用。如果有人需要,我稍后会发布。【参考方案5】:我建议查看 .Net 中的 TextFieldParserClass。你需要包括
Imports Microsoft.VisualBasic.FileIO.TextFieldParser
这是一个简单的示例:
Dim afile As FileIO.TextFieldParser = New FileIO.TextFieldParser(FileName)
Dim CurrentRecord As String() ' this array will hold each line of data
afile.TextFieldType = FileIO.FieldType.Delimited
afile.Delimiters = New String() ","
afile.HasFieldsEnclosedInQuotes = True
' parse the actual file
Do While Not afile.EndOfData
Try
CurrentRecord = afile.ReadFields
Catch ex As FileIO.MalformedLineException
Stop
End Try
Loop
【讨论】:
仅供参考:TextFieldParser 实现 IDisposable 并且应该包含在“使用”语句中或显式处置。 如果引用字段中有换行符,这似乎不起作用。无赖。 有谁知道为什么这样一个通用类会存在于 VisualBasic 命名空间中? 2021 签入——Avi 的答案仍然是我使用 Framework 4.6 解析某些字段嵌入逗号的 CSV 的最佳解决方案。来这里是因为其他方法不太好用。【参考方案6】:这种自定义方法的逻辑是:一次读取文件1行,用逗号分隔每一行,删除第一个和最后一个字符(删除外引号但不影响任何内引号),然后添加数据到您的通用列表。它很短,很容易阅读和使用。
Dim fr As StreamReader = Nothing
Dim FileString As String = ""
Dim LineItemsArr() as String
Dim FilePath As String = HttpContext.Current.Request.MapPath("YourFile.csv")
fr = New System.IO.StreamReader(FilePath)
While fr.Peek <> -1
FileString = fr.ReadLine.Trim
If String.IsNullOrEmpty(FileString) Then Continue While 'Empty Line
LineItemsArr = FileString.Split(",")
For Each Item as String In LineItemsArr
'If every item will have a beginning and closing " (quote) then you can just
'cut the first and last characters of the string here.
'i.e. UpdatedItems = Item. remove first and last character
'Then stick the data into your Generic List (Of String()?)
Next
End While
【讨论】:
或者在去掉外引号之前,用它作为测试来做字符串处理,或者数字处理(如果需要的话)。 这不处理数据中分隔符所在的数据【参考方案7】:我将此作为答案发布,以便我可以解释我是如何做到的以及为什么.... Mitch Wheat 的答案是为我提供此案例的最佳解决方案的答案,我只需要稍微修改一下即可导出此数据的格式。
这是 VB 代码:
Dim fixedContents As String = Regex.Replace(
File.ReadAllText(csvFile, fileEncoding),
"(?<!,)("")(?!,)",
AddressOf ReplaceQuotes)
我需要更改使用的 RegEx,因为某些字段中包含非转义引号,并且提供的 RegEx 似乎不适用于所有示例。这个使用“向前看”和“向后看”来查看引号是在逗号之后还是之前。在这种情况下,它们都是否定的(意味着告诉我双引号不在逗号之前或之后的位置)。这应该意味着引号在字符串的中间。
在这种情况下,我没有直接替换,而是使用函数 ReplaceQuotes 来为我处理。我使用它的原因是因为我需要一些额外的逻辑来检测它是否在行首。如果我会花更多时间在上面,我相信我可以调整 RegEx 以考虑行的开头(使用 MultiLine 等),但是当我快速尝试时,它似乎并没有在全部。
有了这个,在 32MB CSV 文件(大约 19000 行)上使用 CSV 阅读器,读取文件、执行正则表达式、将其加载到 CSV 阅读器、将所有数据添加到我的通用文件大约需要 2 秒上课并完成。真快!!
【讨论】:
【参考方案8】:试试这个网站。 http://kbcsv.codeplex.com/
我一直在寻找一个好的实用程序,这是我找到的最好的并且可以正常工作的实用程序。不要浪费时间尝试其他东西,这是免费的,而且很有效。
【讨论】:
【参考方案9】:正如这个链接所说...Don't roll your own CSV parser!
按照 Avi 的建议使用 TextFieldParser。 Microsoft 已经为您完成了这项工作。如果您最终编写了一个,并且发现其中存在错误,请考虑替换它而不是修复错误。我最近就这样做了,它为我节省了很多时间。
【讨论】:
【参考方案10】: public static Encoding GetFileEncoding(String fileName)
Encoding Result = null;
FileInfo FI = new FileInfo(fileName);
FileStream FS = null;
try
FS = FI.OpenRead();
Encoding[] UnicodeEncodings = Encoding.BigEndianUnicode, Encoding.Unicode, Encoding.UTF8 ;
for (int i = 0; Result == null && i < UnicodeEncodings.Length; i++)
FS.Position = 0;
byte[] Preamble = UnicodeEncodings[i].GetPreamble();
bool PreamblesAreEqual = true;
for (int j = 0; PreamblesAreEqual && j < Preamble.Length; j++)
PreamblesAreEqual = Preamble[j] == FS.ReadByte();
if (PreamblesAreEqual)
Result = UnicodeEncodings[i];
catch (System.IO.IOException)
finally
if (FS != null)
FS.Close();
if (Result == null)
Result = Encoding.Default;
return Result;
【讨论】:
【参考方案11】:排除第一个和最后一个引号的正则表达式是(?<!^)(?<!,)("")(?!,)(?!$)
。当然,你需要使用RegexOptions.Multiline。
这样就不需要评估器功能。我的代码用单引号替换了不需要的双引号。
完整的C#代码如下。
string fixedCSV = Regex.Replace(
File.ReadAllText(fileName),
@"(?<!^)(?<!;)("")(?!;)(?!$)", "'", RegexOptions.Multiline);
【讨论】:
【参考方案12】:你可以试试CsvHelper(我维护的一个库),它可以通过NuGet 获得。它遵循 CSV 的 RFC 4180 标准。它将能够处理字段内的任何内容,包括逗号、引号和换行符。
CsvHelper 使用简单,但也很容易配置它以处理许多不同类型的分隔文件。
CsvReader csv = new CsvReader( streamToFile );
IEnumerable<MyObject> myObjects = csv.GetRecords<MyObject>();
如果要读取较低级别的CSV文件,可以直接使用解析器,它将每一行作为字符串数组返回。
var parser = new CsvParser( myTextReader );
while( true )
string[] line = parser.ReadLine();
if( line == null )
break;
【讨论】:
另外,它是迄今为止我见过的最快的通用 csv 解析器。以上是关于在 .NET 中解析分隔的 CSV的主要内容,如果未能解决你的问题,请参考以下文章