TStringList分割错误

31

最近我从一个声誉良好的SO用户那里得知,TStringList存在分割错误,这会导致其无法解析CSV数据。我没有被告知这些错误的性质,互联网上的搜索(包括Quality Central)也没有产生任何结果,所以我想问一下。什么是TStringList分割错误

请注意,我不对基于未经证实的观点的答案感兴趣。


我所知道的:

不多...其中一个是,这些错误很少在测试数据中出现,但在实际情况下却不那么少见。

另一个是,正如所述,它们阻止了CSV的正确解析。考虑到很难通过测试数据重现这些错误,我(可能)正在寻求那些在生产代码中尝试使用字符串列表作为CSV解析器的人的帮助。

无关问题:

我在一个标记为“Delphi-XE”的问题上获取了信息,因此由于具有特点,空格字符被视为分隔符而导致的解析失败不适用。因为 Delphi 2006 引入了 StrictDelimiter 属性来解决这个问题。我自己正在使用 Delphi 2007。

另外,由于字符串列表只能保存字符串,它只负责拆分字段。任何涉及到字段值(例如日期、浮点数等)的转换困难,由于区域设置的差异等原因,都不在范围之内。

基本规则:

CSV 没有标准规范,但是可以从各种规范中推断出基本规则。

下面演示了 TStringList 如何处理这些规则。规则和示例字符串来自Wikipedia。测试代码通过在字符串周围添加括号([ ])来显示前导或尾随空格(如果相关)。


空格被视为字段的一部分,不应该被忽略。

测试字符串:[1997, Ford , E350]
项:[1997] [ Ford ] [ E350]


带有嵌入逗号的字段必须用双引号字符括起来。

测试字符串:[1997,Ford,E350,“超级豪华卡车”]
项:[1997] [Ford] [E350] [Super, luxurious truck]


带有嵌入双引号字符的字段必须用双引号字符括起来,并且每个嵌入的双引号字符都必须由一对双引号字符表示。

测试字符串:[1997,Ford,E350,“超级,“豪华”卡车”]
项:[1997] [Ford] [E350] [Super, "luxurious" truck]


带有嵌入换行符的字段必须用双引号字符括起来。

测试字符串:[1997,Ford,E350,“现在去拿一个
他们很快就会走了”]
项:[1997] [Ford] [E350] [Go get one now
they are going fast]


在修剪前导或尾随空格的CSV实现中,具有这些空格的字段必须用双引号字符括起来。

测试字符串:[1997,Ford,E350,“ 超级豪华卡车 ”]
项:[1997] [Ford] [E350] [ Super luxurious truck ]


字段可以始终用双引号字符括起来,无论是否必要。

测试字符串:[“1997”,“Ford”,“E350”]
项:[1997] [Ford] [E350]



测试代码:

var
  SL: TStringList;
  rule: string;

  function GetItemsText: string;
  var
    i: Integer;
  begin
    for i := 0 to SL.Count - 1 do
      Result := Result + '[' + SL[i] + '] ';
  end;

  procedure Test(TestStr: string);
  begin
    SL.DelimitedText := TestStr;
    Writeln(rule + sLineBreak, 'Test string: [', TestStr + ']' + sLineBreak,
            'Items: ' + GetItemsText + sLineBreak);
  end;

begin
  SL := TStringList.Create;
  SL.Delimiter := ',';        // default, but ";" is used with some locales
  SL.QuoteChar := '"';        // default
  SL.StrictDelimiter := True; // required: strings are separated *only* by Delimiter

  rule := 'Spaces are considered part of a field and should not be ignored.';
  Test('1997, Ford , E350');

  rule := 'Fields with embedded commas must be enclosed within double-quote characters.';
  Test('1997,Ford,E350,"Super, luxurious truck"');

  rule := 'Fields with embedded double-quote characters must be enclosed within double-quote characters, and each of the embedded double-quote characters must be represented by a pair of double-quote characters.';
  Test('1997,Ford,E350,"Super, ""luxurious"" truck"');

  rule := 'Fields with embedded line breaks must be enclosed within double-quote characters.';
  Test('1997,Ford,E350,"Go get one now'#10#13'they are going fast"');

  rule := 'In CSV implementations that trim leading or trailing spaces, fields with such spaces must be enclosed within double-quote characters.';
  Test('1997,Ford,E350," Super luxurious truck "');

  rule := 'Fields may always be enclosed within double-quote characters, whether necessary or not.';
  Test('"1997","Ford","E350"');

  SL.Free;
end;

如果你已经阅读完了所有内容,那么问题是:什么是"TStringList分割错误"?

1
+1,我以前听过这个,但从未验证过。 - Robert Love
在使用TStringList 13年后,我从未遇到过任何问题(除了早期版本中缺少StrictDelimiter的问题)。它可能不会完全满足每个人对其功能的期望,并且在输入不符合其规范的情况下“失败”,但是它内部是一致的。请注意,我编写的RTL代码的第一个子类是重新实现CommaText以处理无引号字段中的空格以处理格式不良的输入。 - Gerry Coll
1
+1 加入“豪华”、“卡车”和“福特”成为一个实体 :-) - Premature Optimization
我认为这里的主要问题是缺乏严格的CSV格式以及你会在不同情况下看到的各种变化。同时,作为Delphi7用户,这就是TStringList无法正确工作的地方,可能也促使了那位声誉良好的用户最初提出“分裂错误”的评论。 - Simon
1
@Mason,原始问题在这里:http://stackoverflow.com/questions/6385736/restoring-dataset-from-delimiter-separated-values-file/6419269#6419269 - Johan
显示剩余7条评论
4个回答

13

并不多... 其中一个问题是,这些bug很少出现在测试数据中,但在现实世界中却不那么罕见。

只需要一个案例。测试数据并非随机数据,一个用户有一个失败案例应该提交数据,然后我们就有了一个测试用例。如果没有人提供测试数据,也许就没有bug/故障吗?

CSV没有标准规范。

这确实有助于混淆。没有标准规范,如何证明某个东西是错误的?如果这留给个人直觉,你可能会遇到各种麻烦。以下是我与政府发行的软件进行愉快互动时遇到的一些问题;我的应用程序应该以CSV格式导出数据,而政府应用程序应该导入数据。以下是我们连续几年遇到的问题:

  • 如何表示空数据?由于没有CSV标准,有一年我的友好政府决定任何东西都可以,包括什么也没有(两个连续逗号)。接下来他们决定只有连续逗号是可以的,也就是说,Field,"",Field不合法,应该是Field,,Field。一周内政府应用程序改变了验证规则,解释给我的客户很有趣...
  • 是否导出零整数数据?这可能是一个更大的滥用,但我的“政府应用程序”也决定对此进行验证。有一段时间必须包括0,然后必须包括0。也就是说,有一段时间Field,0,Field有效,下一步只能使用Field,,Field...

这里是另一个测试用例,其中(我)的直觉失败了:

1997, Ford, E350, "Super, luxurious truck"

请注意,"Super之间的空格,以及紧随其后的非常幸运的逗号。由TStrings使用的解析器只在它紧接着分隔符时才看到引号字符。该字符串被解析为:

[1997]
[ Ford]
[ E350]
[ "Super]
[ luxurious truck"]

直观上,我会期望:

[1997]
[ Ford]
[ E350]
[Super luxurious truck]

但是猜猜看,Excel和Delphi的处理方式是一样的...

结论

  • TStrings.CommaText非常好且实现得很好,至少我查看过的Delphi 2010版本相当有效(避免多次字符串分配,使用PChar“遍历”解析的字符串),并且与Excel的解析器大致相同。
  • 在现实世界中,您需要与其他软件交换数据,这些软件使用其他库编写(或根本没有库),人们可能已经误解了某些(缺失的?)CSV规则。您将不得不进行适应,并且可能不是正确或错误的问题,而是“我的客户需要导入这些垃圾”。如果发生这种情况,您将不得不编写自己的解析器,以适应您将要处理的第三方应用程序的要求。在那之前,您可以安全地使用TStrings。而当它发生时,这可能不是TString的错!

1
Cosmin,如果我们按照基本规则作为规范的话,我想你的测试用例就不符合规范了。因为空格不应该被忽略(规则#1),双引号嵌入到字段中,因此违反了规则#3(它应该由双引号转义,并且该字段应该由双引号包围)。尽管如此,您的直觉是有道理的,并展示了潜在的混淆。因此,感谢您提供的所有其他有用信息! - Sertac Akyuz
我认为Cosmin的观点是,在现实世界中没有“基本规则”-因为没有明确的CSV规范可以指出;)非常好的文章,Cosmin。 - reiniero

4

我敢说,最常见的失败情况是嵌入式换行符。我知道我大多数CSV解析都会忽略它。我将使用两个TStringLists,一个用于解析的文件,另一个用于当前行。因此,我的代码类似于以下内容:

procedure Foo;
var
    CSVFile, ALine: TStringList;
    s: string;

begin
    CSVFile := TStringList.Create;
    ALine := TStringList.Create;
    ALine.StrictDelimiter := True;
    CSVFile.LoadFromFile('C:\Path\To\File.csv');
    for s in CSVFile do begin
        ALine.CommaText := s;
        DoSomethingInteresting(ALine);
    end;
end;

当然,由于我没有注意确保每一行“完整”,所以在字段中输入包含引号的换行符并且我错过了它的情况下,可能会出现问题。但在我遇到真实的数据问题之前,我不会费心去修复它。:-P

1
Delphi 2010解析器在引用的CSV中处理换行符非常好。这里是一个PasteBin控制台应用程序,展示了它的效果。 - Cosmin Prund
@Cosmin:请仔细阅读我的代码。如果你将文件读入字符串列表,然后逐行处理它,你将无法捕获嵌入的换行符。 - afrazier
我认为这不太可能是一个常见的故障情况,毕竟你还没有遇到过这种在现实世界中会成为问题的数据.. ;) 此外,我相信任何遇到这种情况的开发人员都会意识到这不是 TStringList 的问题,因为它只是在处理给定的数据。 - Sertac Akyuz
1
Delphi 2010/XE的TStringList修复了许多CSV引用问题,在2009年使用它们时无法解决。这意味着,我必须停止说这个特定方面在Delphi中是有问题的,因为现在它可以正常工作了。 - Warren P

0

已经尝试使用TArray<String>进行分割了吗?

var
text: String;
arr: TArray<String>;
begin
text := '1997,Ford,E350';
arr := text.split([',']);

所以arr将是:

arr[0] = 1997;
arr[1] = Ford;
arr[2] = E350;

它属于System.SysUtils中的TStringHelper,而不是TArray - Serhii Kheilyk

0

另一个例子... 这个TStringList.CommaText的bug存在于Delphi 2009中。

procedure TForm1.Button1Click(Sender: TObject);
var
  list : TStringList;
begin
  list := TStringList.Create();
  try
    list.CommaText := '"a""';
    Assert(list.Count = 1);
    Assert(list[0] = 'a');
    Assert(list.CommaText = 'a'); // FAILS -- actual value is "a""
  finally
    FreeAndNil(list);
  end;
end;

TStringList.CommaText的setter和相关方法会破坏保存a项的字符串的内存(其空终止字符被一个“"”覆盖)。


我认为你的例子不符合RTL似乎遵循的规则。你有一个嵌入式引用,但它没有被加倍(应该这样做),代码可以自由地按照自己的意愿输出。 - Sertac Akyuz
是的,我向RTL提供了无效的CSV。但根据查看TStringList源代码,RTL的意图是处理任何输入并仅返回有效的输出。我的“错误”违反了这一点,我认为。 - Nathan Schubkegel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接