【问题标题】:How to convert comma-delimited file to pipe-delimited in vb.net如何在 vb.net 中将逗号分隔的文件转换为管道分隔
【发布时间】:2014-12-12 18:46:42
【问题描述】:

网络上有很多搜索结果(以及在 SO 中)与我需要做的事情相似,但我还没有找到适合我的特殊情况的解决方案。

我有一个逗号分隔的文件,其中只有包含逗号的列在它们周围有双引号。其他没有逗号的字段用逗号简单分隔。

举个例子:

123,"box,toy",phone,"red,car,cat,dog","bike,pencil",man,africa,yellow,"jump,rope"

该行的输出需要是:

123|box,toy|phone|red,car,cat,dog|bike,pencil|man|africa|yellow|jump,rope

我目前有这个代码:

Using sr As New StreamReader(csvFilePath)
    Dim line As String = ""
    Dim strReplacerQuoteCommaQuote As String = Chr(34) & "," & Chr(34)
    Dim strReplacerQuoteComma As String = Chr(34) & ","
    Dim strReplacerCommaQuote As String = "," & Chr(34)

    Do While sr.Peek <> -1
        line = sr.ReadLine
        line = Replace(line, strReplacerQuoteCommaQuote, "|")
        line = Replace(line, strReplacerQuoteComma, "|")
        line = Replace(line, strReplacerCommaQuote, "|")
        line = Replace(line, Chr(34), "")

        Console.WriteLine("line: " & line)
    Loop
End Using

这个过程的问题是当我到达第四个 Replace() 行时,字符串看起来像这样:

123|box,toy|phone|red,car,cat,dog|bike,pencil|man,africa,yellow|jump,rope

所以 man 和 africa 需要在它们之后使用管道,但显然我不能只对所有逗号进行替换。

我该怎么做?有没有可以处理这个问题的 RegEx 语句?

使用工作代码更新

Avinash 评论中的link 给出了我所接受的答案。我导入了 System.Text.RegularExpressions 并使用了以下内容:

Using sr As New StreamReader(csvFilePath)
    Dim line As String = ""
    Dim strReplacerQuoteCommaQuote As String = Chr(34) & "," & Chr(34)
    Dim strReplacerQuoteComma As String = Chr(34) & ","
    Dim strReplacerCommaQuote As String = "," & Chr(34)

    Do While sr.Peek <> -1
        line = sr.ReadLine
        Dim pattern As String = "(,)(?=(?:[^""]|""[^""]*"")*$)"
        Dim replacement As String = "|"
        Dim regEx As New Regex(pattern)

        Dim newLine As String = regEx.Replace(line, replacement)
        newLine = newLine.Replace(Chr(34), "")

        Console.WriteLine("newLine: " & newLine)
    Loop
End Using

【问题讨论】:

  • 我认为最好使用 csv 文件解析器。如果您想使用正则表达式,那么这个 answer 将帮助您匹配出现在双引号之外的所有逗号。最后将所有匹配的逗号替换为|
  • 在未来,我会远离任何像"(,)(?=(?:[^""]|""[^""]*"")*$)" 这样的正则表达式,因为它每次匹配逗号时都必须向前看字符串的末尾,比如 n 阶乘。
  • 果然:我目前正在测试最终用户将转换的完整“生产”版本文件。它有大约 90k 行,并且需要花费 LOOOOOOOOONG 时间来转换!对此还有什么其他的攻击角度?
  • 听起来您还有其他问题。我刚刚在不到 2 秒的时间内处理了一个包含 90k 行 123,"box,toy",phone,"red,car,cat,dog","bike,pencil",man,africa,yellow,"jump,rope" 的文本文件。您实际上对提取的数据做了什么?
  • @Blue Dog 有趣的是,您的处理速度如此之快。我使用的实际文件有 14 个字段,每行最多一到两个字段中大约 200 个字符,但我认为这不会有太大的不同。我将不得不设置一个仅使用转换代码的测试应用程序,看看我是否可以追踪可能减慢它的任何东西。不过,感谢您对此进行检查。

标签: regex vb.net csv replace substring


【解决方案1】:

这似乎适用于您的示例:

Dim result = Regex.Replace(input, ",(?=([^""]*""[^""]*"")*[^""]*$)", Function(m) m.Value.Replace(",", "|"))
result = result.Replace(Chr(34), "")

查看接受的答案here 以获得正则表达式的解释,并确保在您使用它的时候在那里投票@mathematical.coffee's answer,因为我基本上只是偷了他的正则表达式。

编辑: 关于您的性能问题,我创建了一个包含 90k 行的文件:

abcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz",abcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz","abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz",abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,yellow,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz"

大约等于 35MB 的文件大小,我的笔记本电脑(没什么特别的)将在大约 6.5 秒内解析它。

是的,正则表达式很慢,而且 TextFieldParser 类也被广泛报道为不是最快的,但如果您仍在处理超过 5 分钟,您的代码显然还有其他一些瓶颈。请注意,我实际上并没有对解析结果做任何事情。

编辑 2: 好的,我想我会再做最后一次(今天早上我很无聊),但我仍然无法复制您延长的转化时间。

是时候变得残酷了,我创建了一个包含 150k 行的输入文件:

abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz",abcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz","abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz"

每行有 1140 个字符,总文件大小约为 167MB。

使用以下代码读取、转换和写回新文件需要 29 秒。

Dim line, result As String
Dim replace As String = ",(?=([^""]*""[^""]*"")*[^""]*$)"
Using sw As New StreamWriter("d:\output.txt")
    Using sr As New StreamReader("d:\input.txt")
        While Not sr.EndOfStream
            line = sr.ReadLine
            result = Regex.Replace(line, replace, Function(m) m.Value.Replace(",", "|"))
            sw.WriteLine(result.Replace(Chr(34), ""))
        End While
    End Using
End Using

编辑 3: 使用 @sln 的正则表达式,此代码将同一文件的处理时间缩短到 4 秒。

Dim line, result As String
Dim pattern As String = ",([^,""]*(?:""[^""]*"")?[^,""]*)(?=,|$)"
Dim replacement As String = "|$1"
Dim rgx As New Regex(pattern)
Using sw As New StreamWriter("d:\output.txt")
    Using sr As New StreamReader("d:\input.txt")
        While Not sr.EndOfStream
            line = sr.ReadLine
            result = rgx.Replace(line, replacement)
            sw.WriteLine(result.Replace(Chr(34), ""))
        End While
    End Using
End Using

所以你去吧,我认为你有一个赢家。正如 sln 所说,这是一个相对测试,因此机器速度无关紧要。

,(?=([^"]*"[^"]*")*[^"]*$)          took 29 seconds
,([^,"]*(?:"[^"]*")?[^,"]*)(?=,|$)  took 4 seconds

最后(并且只是为了完整性)@jawood2005 提出的解决方案非常可行:

Dim line As String
Dim fields As String()
Using sw As New StreamWriter("d:\output.txt")
    Using tfp As New FileIO.TextFieldParser("d:\input.txt")
        tfp.TextFieldType = FileIO.FieldType.Delimited
        tfp.Delimiters = New String() {","}
        tfp.HasFieldsEnclosedInQuotes = True
        While Not tfp.EndOfData
            fields = tfp.ReadFields
            line = String.Join("|", fields)
            sw.WriteLine(line.Replace(Chr(34), ""))
        End While
    End Using
End Using

使用与正则表达式解决方案相同的 150k 行输入文件,这在 18 秒内完成,比我的要好,但 sln 赢得了最快解决问题的奖项。

【讨论】:

  • 似乎一切都结束了。 Avinash 从另一个链接到 MarcusQ 的答案获得了相同的正则表达式。不过,您会收到已回答的支票。 :)
  • 无论如何我都不是正则表达式专家,幸运的是 SO 有很多。最终所有代码都会被回收到一定程度,但我会始终相信我的来源。
  • 这种方法实际上很糟糕,对字符串末尾进行持续大量前瞻的开销是可怕的。
  • @sln:我只是想我从我之前问过的一个问题中想起了你。从那以后我的正则表达式没有任何改进!
  • 不,没关系。但延迟是一个问题。
【解决方案2】:

防弹方式。

 # Validate even quotes (one time match):  ^[^"]*(?:"[^"]*"[^"]*)*$   
 # Then ->
 # ----------------------------------------------
 # Find:  /,([^,"]*(?:"[^"]*")?[^,"]*)(?=,|$)/
 # Replace:  '|$1'

 ,
 (                             # (1 start)
      [^,"]*  
      (?: " [^"]* " )?
      [^,"]*  
 )                             # (1 end)
 (?= , | $ )

基准测试

自从@TheBlueDog 发布了一个基准(“编辑 2”),我想我会发布一个
基准也是。

它基于他的输入,目的是展示使用
'to-the-end-of-string'前瞻作为验证技术的弊端
(即这个 -> ^[^"]*(?:"[^"]*"[^"]*)*$

Blue Dog 的正则表达式替换方法受到不必要的回调的阻碍,所以我
想象一下,这是他的一些糟糕数字的原因。

不知道 Vb.net,所以这是在 Perl 中完成的。考虑了机器速度和语言
因为它是一个相对的测试。

总结:

,(?=([^"]*"[^"]*")*[^"]*$)          took 10 seconds
,([^,"]*(?:"[^"]*")?[^,"]*)(?=,|$)  took 2 seconds  

这代表了 5 倍的差异。

Perl 中的基准测试,150K 行(167MB 文件):

use strict;
use warnings;

use Benchmark ':hireswallclock';
my ($t0,$t1);
my ($infile, $outfile);

my $tstr = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz",abcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz","abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz,"abcdefghijklmnopqrstuvwxyz,abcdefghijklmnopqrstuvwxyz"
';

# =================================================
print "\nMaking 150K line (167MB file), csv_data_in.txt ...";

open( $infile, ">", 'csv_data_in.txt' ) or die "can't open 'csv_data_in.txt' for writing $!";
for (1 .. 150_000)
{
   print $infile $tstr;
}
close( $infile );

print "\nDone !\n\n";

# =================================================
print "Converting delimiters, writing to csv_data_out.txt ...";

open( $infile, "<", 'csv_data_in.txt' ) or die "can't open 'csv_data_in.txt' for readimg $!";
open( $outfile, ">", 'csv_data_out.txt' ) or die "can't open 'csv_data_out.txt' for writing $!";

my $line = '';

$t0 = new Benchmark;
while( $line = <$infile> )
{
    # Validation - Uncomment to check line for even quotes, otherwise don't
    # if ( $line =~ /^[^"]*(?:"[^"]*"[^"]*)*$/ )
    # {
        $line =~ s/,([^,"]*(?:"[^"]*")?[^,"]*)(?=,|$)/|$1/g;
    # }
    print $outfile $line;
}
$t1 = new Benchmark;

close( $infile );
close( $outfile );

print "\nDone !\n";
print "Conversion took: ", timestr(timediff($t1, $t0)), "\n\n";

输出:

Making 150K line (167MB file), csv_data_in.txt ...
Done !

Converting delimiters, writing to csv_data_out.txt ...
Done !
Conversion took: 2.1216 wallclock secs ( 1.87 usr +  0.17 sys =  2.04 CPU)

【讨论】:

  • 这太令人印象深刻了,你肯定证明了一点!我冒昧地在 VB 代码中使用了你的正则表达式,得到了 4.3 秒,见编辑。这很有趣!
【解决方案3】:

这可能不是最好的解决方案,但它应该可以工作......

我 99% 确定您正在使用 StreamReader ("sr") 来读取文件。尝试使用 FileIO.TextFieldParser 读取它,这将允许您将行拆分为字符串数组。

Dim aFile As FileIO.TextFieldParser = New FileIO.TextFieldParser(filePath)
Dim temp() As String ' this array will hold each line of data
Dim order As doOrder = Nothing
Dim orderID As Integer
Dim myDate As DateTime = Now.ToString

aFile.TextFieldType = FileIO.FieldType.Delimited
aFile.Delimiters = New String() {","}
aFile.HasFieldsEnclosedInQuotes = True

temp = aFile.ReadFields

' parse the actual file
Do While Not aFile.EndOfData...

在循环中,继续使用“aFile.ReadFields”读取下一行。拥有 String 数组后,您可以将每个字段与它们之间的管道连接起来。有点乱,不是正则表达式(不知道这是实际情况还是只是一个想法),但会完成工作。

另外,请注意“aFile.HasFieldsEnclosedInQuotes = True”,因为这是您列出的条件之一。

编辑:当我尝试输入时,我看到 The Blue Dog 给出了正则表达式的答案......您可能仍然想考虑使用 TextFieldParser,因为您正在阅读一个分隔文件。我现在就走了。

【讨论】:

  • 我刚刚对 Blue Dog 的回答发表了评论,即 RegEx 方法不起作用。我必须在 csv 中处理 90k 多行。我现在正在运行一个已经运行了 5 分钟的程序!我一直在查看 TextFieldParser 类,但我不确定它是否会起作用,因为它似乎不适用于我的情况,有些字段有引号,有些没有,有些字段会有逗号他们。是的,我正在使用 StreamReader/Writer
  • @marky 如果“HasFieldsEnclosedInQuotes”值为真,TextFieldParser 可以处理带引号或不带引号的行。此外,如果某个字段中有引号,则该字段将集中在一起,即使其中包含逗号。大约 6 个月前,我不得不将它用于一个项目(这段代码或多或少是从那里撕下来的)。我不保证它会满足你的目的,但它会满足你制定的标准。
  • 好的,我测试了 TextFieldParser,它的运行速度与 regEx 方法一样慢。 IE。在一个 90k 行的文件中花了大约 5 分钟来处理大约 20k 行 :( 还有其他想法吗?
  • 您只是更改分隔符并写入文件吗?如果是这样,如果您使用的是 streamwriter,请确保刷新它。这可能会大大提高速度。
  • 这个解决方案是可行的,实际上比我最初发布的正则表达式更快,请参阅我编辑的帖子。不过,@sln 在速度上仍然领先一英里。 :)
猜你喜欢
  • 2019-05-21
  • 2015-04-21
  • 2020-07-21
  • 1970-01-01
  • 1970-01-01
  • 2020-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多