使用sed和正则表达式替换逗号,但不包括字符串中的逗号。

3

我有以下模式的输入

10,0,'string1_string2,_string3','',8,0,0,0.59,'20140101205216','20140128074836',584266915,5934

我希望使用sed将所有逗号","替换为制表符。但约束条件是不要替换文本字符串内的","(即'string1_string2,_string3'中的逗号不应该被替换为制表符)。可以使用如下正则表达式实现:,(?!_)

然而,以下的sed命令无法实现此功能。我已经尝试了所有转义组合。

sed s/",\(\?\!,_\)"/"\t"/g 

有没有一种方法可以做到这一点?


我会使用Perl程序读取行,根据CSV字段进行分割,修改相关字段,然后重建该行。以非结构化方式处理结构化数据是一场长期的噩梦。 - Andy Lester
谢谢Johanthan和下面的所有人。是的,你们说得很对。我曾经有过许多噩梦,但我已经找到了像你建议的解决方案。 - Peyman
4个回答

3
在Mac OS X 10.9.1上,您可以使用:
sed -E -e "s/('[^']*'|[^,]*),/\1X/g"

除了您需要用实际的制表符替换X外,其他都相同。对于您的输入行,这将产生以下结果:

10X0X'string1_string2,_string3'X''X8X0X0X0.59X'20140101205216'X'20140128074836'X584266915X5934

如果您需要制表符的位置使用X代替,可以使用GNU sed中的-r选项(尽管它也识别-E)。但是Mac sed不会将\t扩展为制表符;而GNU sed会。如果您使用Bash,可以使用ANSI-C引用机制使Shell将制表符嵌入传递给sed的字符串中:

sed -E -e "s/('[^']*'|[^,]*),/\1"$'\t'"/g"

没有使用扩展正则表达式(通过-r-E激活),在sed中尝试匹配不值得;请使用awk代替。
正则表达式寻找单引号后跟零个或多个非引号和单引号, 或者是 零个或多个非逗号后跟一个逗号,并将其替换为被记作either/or字符串的内容加上一个“制表符”(使用X来表示制表符,因为它更可见)。
devnull 指出 上面的答案会将字符串末尾处的逗号一起替换掉。有一个解决方法:
sed -E -e "s/('[^']*'|[^,]*)(,|$)/\1"$'\t'"/g; s/"$'\t'"$//"

在分号之前的s///g在每行末尾添加一个制表符。在分号之后的s///删除刚刚添加的制表符。

在OSX上,使用 $'\t' 来插入制表符。这个方法同样适用于其他控制字符,例如 $'\n'。虽然看起来有些奇怪(没有 awk 的双关语意味),但它确实有效:sed -E -e "s/('[^']*'|[^,]*),/\1"$'\t'"/g" - mklement0
1
是的,那肯定行得通,可以使用 Bash 和 ANSI-C 引用机制,但处理\t的是 shell 而不是 sed。(我知道你知道;我只是确保后来者也知道。)使用 control-V control-I(或 tab)也可以实现。对于 GNU sedsed 本身处理 \t 到制表符的转换。 - Jonathan Leffler
1
我担心如果字符串以引号结尾,例如 a,'b,c',这可能会导致错误。 - devnull
@JonathanLeffler:说得好 - 感谢您向我介绍$'...'特性的名称。 - mklement0
@devnull:发现得好,是的,它将替换掉字符串中结束行的逗号。乍一看,用(,|$)替换,会有所帮助,但这会在行末添加一个制表符,这可能不是想要的。在sed中处理这个问题可能比它值得的麻烦。我需要冥想一下解决方案;目前我还没有头绪。 - Jonathan Leffler
1
对于这个问题,使用Perl或AWK更容易处理。 - devnull

1
我建议使用Perl的帮助,因为它提供了lookarounds功能。
s="10,0,'string1_string2,_string3','',8,0,0,0.59,'20140101205216','20140128074836',584266915,5934"

perl -pe "s/,(?=(([^']*'){2})*[^']*$)/\t/g" <<< "$s"

10\t0\t'string1_string2,_string3'\t''\t8\t0\t0\t0.59\t'20140101205216'\t'20140128074836'\t584266915\t5934

抱歉,您需要提供要翻译的具体文本。

+1; 它可以工作,但我仍然为了理解而感到头痛:
  • 由于前瞻性断言 (?=...),对于找到的每个,,匹配都会执行到行尾。
  • 括号中的整个表达式是前瞻表达式,只有在先前的,不在单引号字符串内时才匹配。它通过查找引号对来实现 - 这意味着如果行中剩余的引号(如果有)没有成对出现,则手头的,必须引号字符串内。
  • 净效应: 仅匹配并替换引号字符串外的,字符。
- mklement0
是的,它看起来有点棘手,但它确保在逗号后始终有偶数个单引号(0、2、4、6...)。前瞻正则表达式就是在做这件事(参见{2}部分)。 - anubhava

1
你可以使用 Text::ParseWords
perl -MText::ParseWords -n -l -e 'print join("\t", parse_line(",", 1, $_));' filename

您的输入将产生以下结果:

10      0       'string1_string2,_string3'      ''      8       0       0       0.59    '20140101205216'        '20140128074836'        584266915       5934

0

如果我正确理解了你的问题,这似乎可以工作:

sed -E 's/,([^_])/\t\1/g'

输出:

10  0   'string1_string2,_string3'  ''  8   0   0   0.59    '20140101205216'    '20140128074836'    584266915   5934

1
这段代码能够正常运行是因为字符串中的逗号刚好后面跟着一个下划线,并且其他逗号后面没有紧跟下划线。但它无法很好地处理变体情况,比如 'string1, string2, string3',_abc_ - Jonathan Leffler
@JonathanLeffler 我知道,但问题中没有要求处理所有变体。 Peyman建议使用 sed s /",\(\?\!,_\)"/"\t"/g,这是将 ,_ 替换为 \t 的方法。我有点困惑他真正想要什么。如果它应该适用于所有其他变体,则我的答案当然是无用的。 - Jakub Jirutka

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