如何在CSV中正确转义双引号?

307
我在我的CSV文件中有一行数据,像这样:
"Samsung U600 24"","10000003409","1","10000003427"
引号紧挨着数字24是用来表示英寸的,而接近该引号的另一个引号则是用来关闭字段的。我正在使用fgetcsv函数读取这一行数据,但解析器却出现了错误,将该值读取为:
Samsung U600 24",10000003409"
我尝试在英寸引号前面加上反斜杠,但结果只是得到了一个反斜杠在名称中的情况:
Samsung U600 24\"
有没有办法在CSV文件中正确转义这个值,使其变为Samsung U600 24",或者我必须在处理器中使用正则表达式来处理它?
7个回答

515
使用2个引号:
"Samsung U600 24"""

RFC-4180 说:“如果用双引号括起字段,那么字段内出现的双引号必须通过在其前面加上另一个双引号进行转义。”


190
如果使用双引号来包含字段,那么在字段内出现的双引号必须通过在其前面加上另一个双引号进行转义。——摘自RFC-4180的一段话。 - tommed
6
像tommed所说的那样,您只需要添加一个双引号来转义另一个双引号。您可以使用一个命令行工具称为csvfix来检测不符合规范的行:csvfix check -nl -v [文件名] - Sam Critchley
5
我看到这里只使用了一个双引号进行转义。用户4035说的“Use 2 quotes”是指用两个引号替换一个引号。通过用双引号转义双引号,你实际上创建了一对双引号(2个双引号)。最后看到的引号是用来终止字段的。 - Zenexer
非常好的方法。但这需要我在解析之前修改CSV文件。 - remy727
1
@GarfieldCat 试试这个:"world,"",hello" - user4035
显示剩余3条评论

7

不仅需要双引号,你还需要使用单引号 (')、双引号 (")、反斜杠 (\) 和 NUL(空字节)。

使用fputcsv()来写入数据,使用fgetcsv()来读取数据,这两个函数会自动处理上述所有情况。


26
@Angelin Nadar,请问你能否在提到需要将单引号、反斜杠和NUL字符加倍的说法时添加一个来源?我在RFC-4180中没有找到相关内容。 - Petr 'PePa' Pavel
4
一个正确的CSV文件甚至不需要在只包含单引号的字段周围添加双引号。如果CSV阅读器实现得当,它应该能够正确读取文件,即使有这些符号存在,你不需要真正转义单引号等符号。 - xji
15
为什么这个回答曾经被投票赞成?关于转义字符的评论从未得到证实,而原问题也没有涉及PHP。这只在字符串分隔符(仅限选择的分隔符)为程序所允许更改时才是真实的,比如Open Office。 - Dave F
5
如果你真正阅读RFC4180,你会发现其中提到CSV格式有各种不同的规范和实现,并列举了至少4种。原帖并没有指定具体格式,因此我认为基于对特定文档的假设而对这个答案进行投票反对是不公平的。 - c z
3
@cz 这里的答案明显是错误的。它既没有回答原问题,也没有提供合理的其他上下文。例如,通常的“其他CSV格式”中没有一个需要在双引号内转义单引号,但是一些常见的CSV读取器不会取消转义转义的单引号。在某些读取器中可能需要转义反斜杠,但并非所有读取器都需要。 - Kai Petzke
显示剩余3条评论

5
CSV在理论上是一种简单的格式(由逗号分隔的表格数据),但遗憾的是没有正式的规范,因此存在许多细微差异的实现。这就需要在导入/导出时要小心处理。我将引用RFC 4180来描述“常见实现”:
2.  Definition of the CSV Format

   While there are various specifications and implementations for the
   CSV format (for ex. [4], [5], [6] and [7]), there is no formal
   specification in existence, which allows for a wide variety of
   interpretations of CSV files.  This section documents the format that
   seems to be followed by most implementations:

   1.  Each record is located on a separate line, delimited by a line
       break (CRLF).  For example:

       aaa,bbb,ccc CRLF
       zzz,yyy,xxx CRLF

   2.  The last record in the file may or may not have an ending line
       break.  For example:

       aaa,bbb,ccc CRLF
       zzz,yyy,xxx

   3.  There maybe an optional header line appearing as the first line
       of the file with the same format as normal record lines.  This
       header will contain names corresponding to the fields in the file
       and should contain the same number of fields as the records in
       the rest of the file (the presence or absence of the header line
       should be indicated via the optional "header" parameter of this
       MIME type).  For example:

       field_name,field_name,field_name CRLF
       aaa,bbb,ccc CRLF
       zzz,yyy,xxx CRLF


   4.  Within the header and each record, there may be one or more
       fields, separated by commas.  Each line should contain the same
       number of fields throughout the file.  Spaces are considered part
       of a field and should not be ignored.  The last field in the
       record must not be followed by a comma.  For example:

       aaa,bbb,ccc

   5.  Each field may or may not be enclosed in double quotes (however
       some programs, such as Microsoft Excel, do not use double quotes
       at all).  If fields are not enclosed with double quotes, then
       double quotes may not appear inside the fields.  For example:

       "aaa","bbb","ccc" CRLF
       zzz,yyy,xxx

   6.  Fields containing line breaks (CRLF), double quotes, and commas
       should be enclosed in double-quotes.  For example:

       "aaa","b CRLF
       bb","ccc" CRLF
       zzz,yyy,xxx

   7.  If double-quotes are used to enclose fields, then a double-quote
       appearing inside a field must be escaped by preceding it with
       another double quote.  For example:

       "aaa","b""bb","ccc"

通常情况下:
- 一个字段可以有也可以没有双引号包围。(2005年的RFC说Excel不使用双引号,但我测试了Excel 2016,它确实使用了。) - 包含换行符(CRLF)、双引号和逗号的字段应该用双引号包围。(特别地,CSV文件可能有多行,因为在文本编辑器中显示的多行对应一行数据。) - 如果使用双引号来包围字段,那么字段内出现的双引号必须通过在其前面加上另一个双引号进行转义。 - 因此,在原始CSV字段中,""表示空字符串,而""""表示单引号"。 (通常不是问题:CRLF(Windows风格)或LF(Unix风格)换行符;最后一行是否以换行符结束)
然而,您可能会遇到一些数据,它们使用转义字符(如\)来转义引号或其他字符(分隔符、换行符、转义字符本身)。例如,在readr的read_csv()函数中,可以通过escape_doubleescape_backslash参数来控制这种情况。有些不寻常的数据可能使用注释字符,比如#(在R的read.table函数中是默认值,但在read.csv函数中不是)。

2

我用Java编写了代码。

public class CSVUtil {
    public static String addQuote(
            String pValue) {
        if (pValue == null) {
            return null;
        } else {
            if (pValue.contains("\"")) {
                pValue = pValue.replace("\"", "\"\"");
            }
            if (pValue.contains(",")
                    || pValue.contains("\n")
                    || pValue.contains("'")
                    || pValue.contains("\\")
                    || pValue.contains("\"")) {
                return "\"" + pValue + "\"";
            }
        }
        return pValue;
    }

    public static void main(String[] args) {
        System.out.println("ab\nc" + "|||" + CSVUtil.addQuote("ab\nc"));
        System.out.println("a,bc" + "|||" + CSVUtil.addQuote("a,bc"));
        System.out.println("a,\"bc" + "|||" + CSVUtil.addQuote("a,\"bc"));
        System.out.println("a,\"\"bc" + "|||" + CSVUtil.addQuote("a,\"\"bc"));
        System.out.println("\"a,\"\"bc\"" + "|||" + CSVUtil.addQuote("\"a,\"\"bc\""));
        System.out.println("\"a,\"\"bc" + "|||" + CSVUtil.addQuote("\"a,\"\"bc"));
        System.out.println("a,\"\"bc\"" + "|||" + CSVUtil.addQuote("a,\"\"bc\""));
    }
}

2
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community
所有的.Contains()都会每次扫描整个字符串吗?为什么不总是用双引号括起文本呢?此外,第一个“pValue.contains("""))”似乎是无用的(替换将检查双引号是否存在)。 - Alex 75

-2

由于没有人提到我通常的做法,我就简单介绍一下。当遇到一个棘手的字符串时,我甚至不会费心去转义它。

我的方法是只需使用base64_encodebase64_decode,也就是在写入CSV行之前将值编码为Base64,在读取时解码。

以您的示例为例,假设使用PHP:

$csvLine = [base64_encode('Samsung U600 24"'),"10000003409","1","10000003427"];

当我想要获取值时,我执行相反的操作。

$value = base64_decode($csvLine[0])

我只是不喜欢经历痛苦。


-3

我知道这是一个旧帖子,但这是我使用扩展方法在C#中解决它的方式(以及将null值转换为空字符串)。

创建一个静态类,其中包含以下内容:

    /// <summary>
    /// Wraps value in quotes if necessary and converts nulls to empty string
    /// </summary>
    /// <param name="value"></param>
    /// <returns>String ready for use in CSV output</returns>
    public static string Q(this string value)
    {
        if (value == null)
        {
            return string.Empty;
        }
        if (value.Contains(",") || (value.Contains("\"") || value.Contains("'") || value.Contains("\\"))
        {
            return "\"" + value + "\"";
        }
        return value;
    }

然后对于每个要写入CSV的字符串,不要:

stringBuilder.Append( WhateverVariable );

你只需要做:

stringBuilder.Append( WhateverVariable.Q() );

10
这是否遗漏了嵌套引号的加倍? - Martin Smith

-3

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