【问题标题】:Replace quotes within quote encapsulated string using Perl regular expressions使用 Perl 正则表达式替换引号封装字符串中的引号
【发布时间】:2019-02-11 11:48:08
【问题描述】:

我正在尝试替换管道分隔和引号封装文件中的引号,而不替换提供封装的引号。

我尝试使用下面的 Perl 行将引号替换为反引号 `,但我不确定如何仅替换引号而不是整个组 1。

样本数据(test.txt):

"1"|"Text"|"a"\n
"2"|""Text in quotes""|"ab"\n
"3"|"Text "around" quotes"|"abc"\n

perl -pi.bak -e 's/(?<=\|")(.*)(?="\|)/\1`/' test.txt

这是正在发生的事情:

"1"|"`"|"a"\n
"2"|"`"|"ab"\n
"3"|"`"|"abc"\n

这是我想要实现的目标:

"1"|"Text"|"a"\n
"2"|"`Text in quotes`"|"ab"\n
"3"|"Text `around` quotes"|"abc"\n

【问题讨论】:

  • 我很惊讶你不想要 "3"|"Text ""around"" quotes"|"abc"\n3Text "around" quotesText "around" quotesabc 的正确 CSV)而不是破坏你的文本。
  • 您没有定义如何处理包含`的文本
  • Grasshopper,一旦您回来,请告诉我们您希望如何在您的字段中表示 literal 反引号。简而言之,如果你有"2"|""Text `in` quotes""|"ab",它应该是什么样子? in 应该用双反引号括起来吗?还是用反斜杠转义?还是其他方式?
  • @ikegami 我正在使用 SQL*Loader 将这些数据加载到数据库中,并且担心诸如“around”之类的文本会破坏负载。如果可行,这将是可取的。
  • @WiktorStribiżew 和 @ikegami 字段中的文字反引号应包含在 `` 双反引号中。

标签: regex perl replace


【解决方案1】:

对于 Perl 5.14 及更高版本,您可以使用

perl -pi.bak -e 's/(?:^|\|)(")?\K(.*?)(?=\1(?:$|\|))/$2=~s#"|(`)#`$1#gr/ge' test.txt

查看regex demoonline demo

这里的重点是您将字段与第一个正则表达式匹配,然后使用在匹配部分运行的第二个正则表达式处理双引号和反引号。

详情

  • (?:^|\|) - 匹配字符串的开头或|
  • (")? - 与 " 匹配的可选组 1
  • \K - 匹配重置运算符丢弃当前匹配缓冲区中的所有文本
  • (.*?) - 第 2 组:除换行符以外的任何 0+ 个字符
  • (?=\1(?:$|\|)) - 正向前瞻,确保与第 1 组中的值相同,然后是字符串的结尾或紧邻当前位置右侧的 |

因此,第 2 组是单元格内容,没有包含双引号。 $2=~s#"|()#$1#gr 将所有 " 替换为 ` 并复制第 2 组值中找到的所有文字反引号(请参阅 this regex demo)。 "|(`) 模式匹配 " 或反引号(将后者捕获到第 1 组),`$1 将匹配替换为反引号和第 1 组的内容。

【讨论】:

  • 你应该提到你决定如何处理已经包含`的文本。
  • @ikegami 没有办法将它添加到答案中,因为 OP 没有提供任何关于如何在字段内表示文字反引号的规范(用另一个反引号转义?反斜杠?)。一旦规格公开,它就可以很容易地修复。
  • 这就是我的观点。你做了一个假设,但没有记录下来。您认为 OP 的规范对此有何看法?
  • @ikegami 现在,它按预期工作。文字反引号must be doubled.
【解决方案2】:

更新   澄清已经存在的反引号应该加倍


一种方法是在| 上使用split 并去掉封闭的引号以使剩余的正则表达式变得简单,然后将字符串组合回去。与单个正则表达式相比,这可能会损失一些效率,但维护起来要简单得多

perl -F"\|" -wlanE'
    say join "\|", 
        map { s/^"|"$//g; s/`/``/g; s/"([^"]+)"/`$1`/g; qq("$_") } @F
' data.txt

-a 选项使其“自动拆分”每一行,因此在程序中,行标记在 @F 中可用,-F 指定要拆分的模式(默认值除外)。 -l 处理换行符。见Command switches in perlrun

map 中,封闭的"s 被删除,任何现有的反引号加倍;然后" 周围的模式被全局更改。然后将引号放回并返回列表join-ed。 join 中的 | 被转义,以便通过 shell 潜入 Perl 程序;如果这进入脚本(而不是单行),我总是建议将 \| 更改为 |

我不知道有关引用的典型数据和可能的边缘情况,但如果可能存在松散(单个、未配对)的引用,上述将出现问题并可能产生错误的输出,并且悄悄地;就像任何需要成对引号的程序一样,不需要非常详细的分析。

简单地将所有"s(除了封闭的)替换为

可能总体上更安全
map { s/^"|"$//g; s/`/``/g; s/"/`/g; qq("$_") }

(或使用tr 而不是正则表达式s///g)。这也增加了一些效率。


获取数据“肉”的另一种方法是使用Text::CSV,它允许使用(默认)逗号以外的分隔符并吸收封闭的引号。在字段中包含引号被认为是错误的 CSV,但该模块也可以很好地解析它,并提供以下选择。

use warnings;
use strict;
use feature 'say';

use Text::CSV;

my $file = shift || 'data.txt';
my $outfile = 'new_' . $file;

my $csv = Text::CSV->new( { binary => 1, sep_char => '|', 
    allow_loose_quotes => 1, escape_char => '',     # quotes inside fields
    always_quote => 1                               # output as desired
} ) or die "Can't do CSV: ", Text::CSV->error_diag;

open my $fh,     '<', $file    or die "Can't open $file: $!";
open my $out_fh, '>', $outfile or die "Can't open $outfile: $!";

while (my $row = $csv->getline($fh)) {
    s/`/``/g for @$row;
    tr/"/`/  for @$row;
    $csv->say($out_fh, $row);
}

要在字段内使用引号,escape_char 需要不同于 quote_char;我只是在这里将其设置为''。输出也由模块处理,always_quote 属性用于此(引用所有字段,无论是否需要)。请参阅文档。

当然,这个模块可以做更多的事情。

如果问题的目的正是为了清理文件格式,在该文件格式中,字段和字段内部都使用相同的引号,我建议使用模块来完成所有操作。这种方法允许人们干净、一致地设置各种选项,包括输入和输出,并且是可维护的。


几个问题

  • 有什么样的数据,是否有可能出现杂散引用?然后呢?这甚至会影响最佳方法的选择,因为它可能需要详细分析。

  • 如果这里的任务是理顺 CSV 样式的数据,那么为什么不将字段内的引号加倍,就像 CSV 中常见和正确的那样,而不是替换它们(并可能损害它们的文本含义)?例如,请参阅模块的文档。

【讨论】:

  • 请注意,与更复杂的单个正则表达式相比,拆分和多个正则表达式不一定会降低效率。如果你真的在乎,你应该对它们进行基准测试。
  • @Grinnz 正确!起初我“可能会输”……但是,在这种情况下,针对编写良好的正则表达式,我认为它应该会输掉“一些”。确实,应该标杆。 ......实际上,我把那个“可能”放回去了。谢谢
  • Split 可以匹配包含在引号封装字符串中的管道。
  • @robartsd 好吧......数据有多种可能性会破坏任何处理。在这个问题中,管道不能出现在不应该出现的位置,或者我们应该将分隔符视为"|"——这也是可能的(并且可能是split 中的分隔符模式)。这取决于真实数据是什么样的,并且 OP 可能需要对我们提供的任何方法进行改进。
  • @ikegami heh,没有决定,只是拿起他们的。它确实需要一个很好的提及,谢谢。已编辑
【解决方案3】:

Perl 使用 $1 作为正则表达式替换部分中第一个捕获组的占位符,而不是 \1(用于正则表达式的匹配部分)。您的正则表达式与内引号不匹配,并且无法匹配管道分隔数据的第一个或最后一个字段。您的替换也未能在捕获的组之前包含引号字符。

试试:

perl -pi.bak -e 's/(?<=(?:^|\|)")"([^"]*)"(?="(?:$|\|))/`$1´/' test.txt

【讨论】:

    【解决方案4】:

    另一个 Perl。用数组@F分割后,检查“不是在元素的开头/结尾。

     perl -F"\|"  -lane   ' for(@F) { s/(?<!^)"(?!$)/`/g }; print join("|",@F) ' 
    

    使用给定的输入

    $ cat grasshopper.txt
    "1"|"Text"|"a"
    "2"|""Text in quotes""|"ab"
    "3"|"Text "around" quotes"|"abc"
    $  perl -F"\|"  -lane   ' for(@F) { s/(?<!^)"(?!$)/`/g }; print join("|",@F) ' grasshopper.txt
    "1"|"Text"|"a"
    "2"|"`Text in quotes`"|"ab"
    "3"|"Text `around` quotes"|"abc"
    $
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-11-24
      • 1970-01-01
      • 1970-01-01
      • 2021-03-16
      • 1970-01-01
      • 2012-09-10
      • 1970-01-01
      相关资源
      最近更新 更多