正则表达式:分割CSV文件

64

我知道类似的问题已经被问了很多次,但是尝试了许多可能性后,我仍然没有找到一个百分百有效的正则表达式。

我有一个CSV文件,试图将其拆分成数组,但遇到两个问题:带引号的逗号和空元素。

CSV文件长这样:

123,2.99,AMO024,Title,"Description, more info",,123987564

我尝试使用的正则表达式是:

thisLine.split(/,(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/)

唯一的问题是在我的输出数组中,第5个元素输出为123987564而不是一个空字符串。


2
看起来像是JS。使用一个适当的解析器怎么样?另外,请指定您正在使用的语言,这将避免很多猜测工作。 - HamZa
3
除了出于好奇心,你为什么想到使用正则表达式? - Tony Hopkinson
这是经典的ASP(使用JScript)代码,我认为在处理数据之前使用正则表达式来提取数据是最简单的方法。 - Code Ninja
2
“我一直没有找到一个百分之百有效的正则表达式”,这主要是因为这是CSV解析器的工作,你应该使用一个。例如,可以尝试使用这个:https://github.com/gkindel/CSV-JS - Tomalak
请在数组中添加一个示例元素,其中使用双引号,因为自由格式文本可能会经常出现这种情况。例如:a "b" c 最终将以CSV格式呈现为 "a ""b"" c"。任何优秀的CSV解析器都需要能够处理这种情况。 - Thomas Tempelmann
显示剩余2条评论
18个回答

78

Description

不使用分割字符串的方法,可以考虑直接匹配并处理所有找到的匹配项。

此表达式将:

  • 按逗号分隔您的样本文本
  • 处理空值
  • 忽略双引号中的逗号,只要双引号没有嵌套即可
  • 从返回的值中修剪定界逗号
  • 从返回的值中修剪周围的引号
  • 如果字符串以逗号开头,则第一个捕获组将返回一个null值

正则表达式:(?:^|,)(?=[^"]|(")?)"?((?(1)[^"]*|[^,"]*))"?(?=,|$)

enter image description here

Example

示例文本

123,2.99,AMO024,Title,"Description, more info",,123987564

使用非Java表达式的ASP示例

Set regEx = New RegExp
regEx.Global = True
regEx.IgnoreCase = True
regEx.MultiLine = True
sourcestring = "your source string"
regEx.Pattern = "(?:^|,)(?=[^""]|("")?)""?((?(1)[^""]*|[^,""]*))""?(?=,|$)"
Set Matches = regEx.Execute(sourcestring)
  For z = 0 to Matches.Count-1
    results = results & "Matches(" & z & ") = " & chr(34) & Server.HTMLEncode(Matches(z)) & chr(34) & chr(13)
    For zz = 0 to Matches(z).SubMatches.Count-1
      results = results & "Matches(" & z & ").SubMatches(" & zz & ") = " & chr(34) & Server.HTMLEncode(Matches(z).SubMatches(zz)) & chr(34) & chr(13)
    next
    results=Left(results,Len(results)-1) & chr(13)
  next
Response.Write "<pre>" & results

使用非 Java 表达式进行匹配

Group 0 获取包括逗号在内的整个子字符串
Group 1 获取引号(如果有)
Group 2 获取不包括逗号的值

[0][0] = 123
[0][1] = 
[0][2] = 123

[1][0] = ,2.99
[1][1] = 
[1][2] = 2.99

[2][0] = ,AMO024
[2][1] = 
[2][2] = AMO024

[3][0] = ,Title
[3][1] = 
[3][2] = Title

[4][0] = ,"Description, more info"
[4][1] = "
[4][2] = Description, more info

[5][0] = ,
[5][1] = 
[5][2] = 

[6][0] = ,123987564
[6][1] = 
[6][2] = 123987564

编辑

正如Boris指出的,CSV格式将把双引号"转义为双倍双引号""。虽然这不是原帖中提到的要求,但如果您的文本包含双倍双引号,则需要使用这个修改后的表达式:

正则表达式:(?:^|,)(?=[^"]|(")?)"?((?(1)(?:[^"]|"")*|[^,"]*))"?(?=,|$)

另请参阅:https://regex101.com/r/y8Ayag/1

还应指出,正则表达式是一种模式匹配工具,而不是解析引擎。因此,如果您的文本包含双倍双引号,则在模式匹配完成后仍将包含双倍双引号。使用这个解决方案后,您仍需要搜索双倍双引号并替换捕获的文本中的它们。


11
请问您用的是哪个软件或网站来生成这些图表? - Ibrahim Najjar
1
正确的做法不会考虑转义引号,但从技术上讲这超出了范围。 - Ro Yo Mi
1
@ReiMiyasaka,你只是部分正确,这个方法无法处理嵌套引号,但是嵌套引号并不是原始问题的一部分。事实上,在我的第三个要点中已经提到了这个方法无法处理嵌套引号的情况,“只要双引号没有嵌套即可”。 - Ro Yo Mi
1
在C#.net中使用此字符串文字:(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^\"]*))\"?(?=,|$)。可以在http://regexstorm.net/tester上进行验证,并且请注意,Visual Studio喜欢“格式化”它,通过输入大量不必要的空格来破坏它。 - Sean Munson
1
值得注意的是,awk和sed无法处理这种级别的正则表达式,但perl可以,因此您可以在bash中执行以下操作。cat mydata.csv|perl -ne 's/(?:^|,)(?=[^"]|(")?)"?((?(1)(?:[^"]|"")*|[^,"]*))"?(?=,|$)/<data>\2<\/data>/g; print;' - user1754036
显示剩余9条评论

45

我在这方面做了一些工作,并想出了这个解决方案:

(?:,|\n|^)("(?:(?:"")*[^"]*)*"|[^",\n]*|(?:\n|$))

在这里试一下!

这个解决方案可以处理“好”的CSV数据,如:

"a","b",c,"d",e,f,,"g"

0: "a"
1: "b"
2: c
3: "d"
4: e
5: f
6:
7: "g"

还有更丑陋的事物,比如

"""test"" one",test' two,"""test"" 'three'","""test 'four'"""

0: """test"" one"
1: test' two
2: """test"" 'three'"
3: """test 'four'"""

这里有一个它是如何工作的解释:

(?:,|\n|^)      # all values must start at the beginning of the file,  
                #   the end of the previous line, or at a comma  
(               # single capture group for ease of use; CSV can be either...  
  "             # ...(A) a double quoted string, beginning with a double quote (")  
    (?:         #        character, containing any number (0+) of  
      (?:"")*   #          escaped double quotes (""), or  
      [^"]*     #          non-double quote characters  
    )*          #        in any order and any number of times  
  "             #        and ending with a double quote character  

  |             # ...or (B) a non-quoted value  

  [^",\n]*      # containing any number of characters which are not  
                # double quotes ("), commas (,), or newlines (\n)  

  |             # ...or (C) a single newline or end-of-file character,  
                #           used to capture empty values at the end of  
  (?:\n|$)      #           the file or at the ends of lines  
)

2
很棒的解决方案!也可以与.NET的Regex类正常工作。 - Herman Cordes
2
感谢这个解决方案。提醒一下,如果您有一个空的第一列(例如“,foo,bar”),它将不会被捕获。一个解决方法是在解析之前在这样的行前面添加空引号“”。 - adu
2
完美。所有其他答案即使是简单的示例也会产生错误的结果,但这个答案对我所有的情况都有效(不一致的封闭符号+值内逗号)。 - afilina
1
非常有价值的答案,谢谢!这对于值中的换行符和(最重要的是)由双引号("")转义的引号非常有效。 - BurninLeo
1
好的,这是唯一一个对我有效的。但是我如何更改它以便删除引号? - Gener4tor
显示剩余4条评论

15

我来晚了,但以下是我所使用的正则表达式:

(?:,"|^")(""|[\w\W]*?)(?=",|"$)|(?:,(?!")|^(?!"))([^,]*?)(?=$|,)|(\r\n|\n)

这个模式有三个捕获组:

  1. 带引号单元格的内容
  2. 不带引号单元格的内容
  3. 换行符

此模式处理以下所有内容:

  • 没有任何特殊功能的普通单元格内容:one,2,three
  • 包含双引号(“被转义为“”)的单元格:no quote,"a ""quoted"" thing",end
  • 单元格包含换行符:one,two\nthree,four
  • 具有内部引号的正常单元格内容:one,two"three,four
  • 单元格包含逗号后跟引号:one,"two ""three"", four",five

查看此模式的使用情况。

如果您正在使用更强大的正则表达式,具有命名组和回溯,则我更喜欢以下内容:

(?<quoted>(?<=,"|^")(?:""|[\w\W]*?)*(?=",|"$))|(?<normal>(?<=,(?!")|^(?!"))[^,]*?(?=(?<!")$|(?<!"),))|(?<eol>\r\n|\n)

查看此模式的使用示例。

编辑

(?:^"|,")(""|[\w\W]*?)(?=",|"$)|(?:^(?!")|,(?!"))([^,]*?)(?=$|,)|(\r\n|\n)

只要您不使用JavaScript,这个稍作修改的模式可以处理第一列为空的行。由于某些原因,使用JavaScript会忽略该模式中的第二列。我无法正确处理这种边缘情况。


在所有发布的解决方案中,这个对我来说是最好的。它可以处理各种边缘情况。然而,它无法处理以逗号开头的字符串。例如,“,second,third”应该产生3个匹配,但只产生2个。 - Suraj
@bubleboy - 我很喜欢你的正则表达式,但是它似乎不能处理 CSV 的最后一列为空的情况 ..., column5,。有没有办法调整那个正则表达式来匹配这些尾列呢? - RHarris
@RHarris - 谢谢。我测试了这个模式,它获取了最后一个空列。你使用的是什么语言?不是所有的正则表达式实现都是相同的。该语言可能有不同的要求。 - bublebboy
@RHarris - 我在 C# 中尝试了这个代码,它能如预期般工作: var pat = new System.Text.RegularExpressions.Regex(@"(?:^""|,"")(""""|[\w\W]*?)(?="",|""$)|(?:^(?!"")|,(?!""))([^,]*?)(?=$|,)|(\r\n|\n)", System.Text.RegularExpressions.RegexOptions.Multiline); var all = pat.Matches(",one,two,\"lets test, some \"\"quotes\"\"\",three,"); 这将导致 MatchCollection(5) { [], [,two], [,"lets test, some ""quotes""], [,three], [,] } - bublebboy
抱歉,我意识到是我的TextReader.ReadLine()导致了这个问题。倒数第二列有\r\n(例如:"...,three\r\n和一些进一步的文本,")。我其实从来没有得到过最后一列。感谢您的帮助。 - RHarris
你的正则表达式在这个简单的测试 ",","value2" 上失败了。 - Nickolay Savchenko

13
几个月前,我为一个项目创建了这个。
 ".+?"|[^"]+?(?=,)|(?<=,)[^"]+

Regular expression visualization

它在C#中可以工作,当我选择Python和PCRE时,Debuggex很高兴。Javascript不认识这种形式的Proceeded By?<=...

对于您的值,它将创建匹配项

123
,2.99
,AMO024
,Title
"Description, more info"
,
,123987564

请注意,引号中的内容不包含前导逗号,但是为了处理空值情况,需要尝试匹配前导逗号。完成后,请根据需要修剪值。
我使用RegexHero.Net来测试我的正则表达式。

2
虽然问题中的示例没有提到,但完美的正则表达式算法还需要处理字段内的引号字符,例如:`single,“quoted”,“with”“quotes”“,end”。你的算法还没有做到这一点。 - Thomas Tempelmann
我也对加号后面的第一个“?”感到困惑 - 对我来说看起来是多余的。我不得不将其更改为"[^"]+"|[^"]+?(?=,)|(?<=,)[^"]+,否则它无法正确扫描带引号的字段(在我的正则表达式版本中,即Real Studio 2012)。 - Thomas Tempelmann
2
"+" 后面的 "?" 分配了非贪婪状态,它会尽可能地获取到下一个字符的第一个实例。如果原始帖子有两个引用值,例如,不使用问号可能会抓取引号之间的所有文本。双引号很难处理,我会看看能否找到解决方案。 - scott.smart
如果双引号在里面,就可以用公式 (?:") 轻松地扫过它们,但不幸的是,在引号结束时这并不起作用。查看链接的 Q 我发现通过加倍转义引号也并非普适的- Apple 的 Numbers 这样做,但其他应用程序可能会使用 \" 转义。此外:开头或结尾的空字段不起作用。对于开头,我在我的代码中使用了一个特殊情况,对于结尾,我将 |,$ 添加到正则表达式中。 - Thomas Tempelmann
只需要进行一点小修改,这个程序现在运行得非常好! - zerocool

10

我也需要这个答案,但是我发现虽然这些答案很有用,但是有点难以理解和复制到其他语言中。下面是我想出来的最简单的表达式,用于从CSV行中提取单个列。我没有进行分割。我正在构建一个正则表达式来匹配CSV中的一列,因此我不会对行进行分割:

("([^"]*)"|[^,]*)(,|$)

这个正则表达式用于匹配CSV行中的单个列。表达式的第一个部分"([^"]*)"用于匹配带引号的条目,第二个部分[^,]*用于匹配不带引号的条目。然后可以跟随一个,或行尾$

以下是用于测试该表达式的debuggex工具链接。

https://www.debuggex.com/r/s4z_Qi2gZiyzpAhx


3
它可以在JavaScript中使用(这不是提问者所询问的,但了解这一点很有帮助)。 - Michael Plautz
如果您正在使用此功能,您应确保该行不以\r或\n(或\r\n)结尾。 - Mr.WorshipMe
2
不处理转义的双引号("")。 - Tamir Daniely
它可以处理双引号("")和转义(")引号,使得这个正则表达式返回一个完整的字段,无论是带引号还是不带引号,用逗号分隔。在获取字段后仍然需要处理双引号和转义引号,但是主要任务——获取完整字段,可以通过这个简洁的正则表达式正确地实现。如果你在编程代码中使用这个正则表达式,你可以很容易地对双引号或转义引号进行替换操作(但只有在找到的字符串以引号符号开头时)。 - Thomas Tempelmann

5

我个人尝试了许多正则表达式,但没有找到能够匹配所有情况的完美正则表达式。

我认为正则表达式很难正确配置以匹配所有情况。虽然有些人可能不喜欢命名空间(我曾经也是其中之一),但我提出了一个在 .Net 框架中的东西,每次都能以正确的方式管理每个双引号情况,且在所有情况下都给我正确的结果:

Microsoft.VisualBasic.FileIO.TextFieldParser

在这里找到它:StackOverflow

使用示例:

TextReader textReader = new StringReader(simBaseCaseScenario.GetSimStudy().Study.FilesToDeleteWhenComplete);
Microsoft.VisualBasic.FileIO.TextFieldParser textFieldParser = new TextFieldParser(textReader);
textFieldParser.SetDelimiters(new string[] { ";" });
string[] fields = textFieldParser.ReadFields();
foreach (string path in fields)
{
    ...

希望这能有所帮助。


4
在Java中,这个模式",(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))"对我来说几乎可行:
String text = "\",\",\",,\",,\",asdasd a,sd s,ds ds,dasda,sds,ds,\"";
String regex = ",(?=([^\"]*\"[^\"]*\")*(?![^\"]*\"))";
Pattern p = Pattern.compile(regex);
String[] split = p.split(text);
for(String s:split) {
    System.out.println(s);
}

输出:

","
",a,,"

",asdasd a,sd s,ds ds,dasda,sds,ds,"

缺点:当列中引号数量为奇数时,无法工作 :(

3
使用JScript编写经典ASP页面的优势在于,您可以使用已经为JavaScript编写的众多库之一。例如:https://github.com/gkindel/CSV-JS。下载它,将其包含在您的ASP页面中,使用它来解析CSV文件。
<%@ language="javascript" %>

<script language="javascript" runat="server" src="scripts/csv.js"></script>
<script language="javascript" runat="server">

var text = '123,2.99,AMO024,Title,"Description, more info",,123987564',
    rows = CSV.parse(line);

    Response.Write(rows[0][4]);
</script>

不幸的是,我需要在我的ASP脚本中完成解析。 - Code Ninja
4
但上述内容确实是ASP。你有没有读过我回答中的文字? - Tomalak

3

这里又有一个答案。 :) 由于我不能使其他答案完美工作。

我的解决方案既处理了转义引号(双重出现),也没有在匹配中包含定界符。

请注意,我一直在根据我的情况匹配'而不是",但只需在模式中替换它们即可获得相同的效果。

代码如下(如果您使用下面的注释版本,请记得使用“忽略空格”标志/x):

# Only include if previous char was start of string or delimiter
(?<=^|,)
(?:
  # 1st option: empty quoted string (,'',)
  '{2}
  |
  # 2nd option: nothing (,,)
  (?:)
  |
  # 3rd option: all but quoted strings (,123,)
  # (included linebreaks to allow multiline matching)
  [^,'\r\n]+
  |
  # 4th option: quoted strings (,'123''321',)
  # start pling
  ' 
    (?:
      # double quote
      '{2}
      |
      # or anything but quotes
      [^']+
    # at least one occurance - greedy
    )+
  # end pling
  '
)
# Only include if next char is delimiter or end of string
(?=,|$)

单行版本:

(?<=^|,)(?:'{2}|(?:)|[^,'\r\n]+|'(?:'{2}|[^']+)+')(?=,|$)

正则表达式可视化(如果它可以工作,debux现在似乎有问题-否则请按照下一个链接)

Debuggex演示

regex101示例


2

如果您知道没有空字段(,,),那么这个表达式就很有效:

("[^"]*"|[^,]+)

就像以下示例中所示...

Set rx = new RegExp
rx.Pattern = "(""[^""]*""|[^,]+)"
rx.Global = True
Set col = rx.Execute(sText)
For n = 0 to col.Count - 1
    if n > 0 Then s = s & vbCrLf
    s = s & col(n)
Next

然而,如果你预计会出现空字段,并且你的文本相对较小,那么你可能考虑在解析之前用空格替换空字段以确保它们被捕获。例如...
...
Set col = rx.Execute(Replace(sText, ",,", ", ,"))
...

如果您需要维护字段的完整性,可以在循环内恢复逗号并测试空格。这可能不是最有效的方法,但它能完成任务。


这个解决了我的问题。我不支持空字段,所以它让我抓取到了所有的内容,而如果之前在任何字段中有引号,就会导致复杂情况。 - Imbaker1234

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