【问题标题】:ASCII files, how to deal with separator in quoted string?ASCII 文件,如何处理带引号的字符串中的分隔符?
【发布时间】:2018-04-27 03:57:35
【问题描述】:

我有一个使用 , 作为分隔符的 ASCII 文件(例如 csv),但是这个字符也出现在带引号的字符串中:

3,       "hh,1,foo",            foo
"5,,,5", "1,2,3d,,,something ", foo2
test,    "col3",                foo3

为了避免这种歧义,我想用; 替换, 分隔符。如何用命令行做到这一点? (在linux下)。


更新/额外问题

至少有两个使用 sedawk 的选项。

我的额外问题是哪个最快?(这对于 csv 文件可能很重要。

【问题讨论】:

  • 所以您想替换所有, 或仅替换特定的,您能否在此处提及更清晰的细节?
  • @RavinderSingh13 我想替换 我引用的字符串之外的“,”,那些具有字段“分隔符”角色的字符串。
  • 如果你想知道哪个是最快的,你可以用例如time awk '...'做基本的基准测试(记住要考虑缓存,所以运行测试几次直到结果稳定) .
  • 对于更有趣但仍符合标准的 CSV,您得到的所有答案都将失败,并且几乎所有这些都依赖于非标准工具和/或标准工具的专有扩展(例如 GNU sed)。如果这对您很重要,要使用所有 UNIX 平台上可用的标准 UNIX 工具稳健、高效且可移植地完成这项工作,请参阅stackoverflow.com/questions/45420535/…
  • 不客气。您的文件格式 CSV,即使您使用了逗号以外的其他字符作为分隔符。如果您的分隔符是字符串而不是单个字符 那么 您的格式将不是 CSV,但是您必须发布一个新问题,因为我怀疑您得到的任何答案是否有效。

标签: bash csv awk ascii


【解决方案1】:

我认为您的要求是在GNU Awk 中使用FPAT 的完美用例,

通常,当使用FS 时,gawk 将字段定义为出现在每个字段分隔符之间的记录部分。换句话说,FS 定义了字段不是什么,而不是字段是什么。但是,有时您真的想根据字段的内容来定义字段,而不是根据字段的本质来定义字段。

最臭名昭著的这种情况是所谓的逗号分隔值 (CSV) 数据。如果逗号只分隔数据,就不会有问题。当其中一个字段包含嵌入式逗号时,就会出现问题。在这种情况下,大多数程序会将该字段嵌入双引号中。

对于此处显示的 CSV 数据,每个字段要么是“非逗号的任何内容”,要么是“双引号、非双引号的任何内容以及结束双引号”。如果写成正则表达式常量(参见 Regexp),我们将有 ([^,]+)|([[:space:]]*\"[^\"]+\")。将其写成字符串需要我们转义双引号,从而导致:

FPAT = "([^,]+)|([[:space:]]*\"[^\"]+\")"

在你的文件上使用它来输出;字符上的分隔文件

awk -v OFS=';' 'BEGIN{FPAT = "([^,]+)|([[:space:]]*\"[^\"]+\")"}{$1=$1}1' file

您还可以通过在系统中设置locale 来加快速度。将语言环境设置强制为 C 将让您将字符单独匹配到 ASCII 数据集而不是 UTF-8,将其本地传递给命令

LC_ALL=C awk -v OFS=';' 'BEGIN{FPAT = "([^,]+)|([[:space:]]*\"[^\"]+\")"}{$1=$1}1' file

由于FPAT 涉及使用标准正则表达式解析器,它的执行速度可能比涉及非正则表达式替换的解析器慢。

【讨论】:

    【解决方案2】:

    对于这个不那么复杂的替换,使用sed

    $ cat file
    3,       "hh,1,foo",            foo
    "5,,,5", "1,2,3d,,,something ", foo2
    test,    "col3",                foo3
    $ sed -E 's/,([[:space:]]*")/;\1/g;s/("[[:space:]]*),/\1;/g' file
    3;       "hh,1,foo";            foo
    "5,,,5"; "1,2,3d,,,something "; foo2
    test;    "col3";                foo3
    

    甚至更短

    # sed -E 's/,(\s*"[^"]*"\s*),/;\1;/g' file
    3;       "hh,1,foo";            foo
    "5,,,5"; "1,2,3d,,,something "; foo2
    test;    "col3";                foo3
    

    perl 解决方案是

    # perl -ane  's/,(\s*"[^"]*"\s*),/;$1;/g;print' 47281774
    3;       "hh,1,foo";            foo
    "5,,,5"; "1,2,3d,,,something "; foo2
    test;    "col3";                foo3
    

    【讨论】:

    • 这也是一种解决方法。我还有另一个问题(也许是我最初问题的可能更新):我已经测试了两者(sed vs awk)。我的印象是 awk 解决方案在大文件上比 regexpr 更快。你也观察到了吗?
    • @PicaudVincent 对于较大的文件,sed 应该更快。但是对于非常大的文件,请使用perl,这是用于此目的的工具。
    • 你赢了!您的解决方案是最快的! :)
    • @PicaudVincent 我也添加了perl 解决方案。 :)
    【解决方案3】:

    考虑到您的 Input_file 将与显示的示例相同,如果是,那么关注 awk 也可能对您有所帮助。

    awk -F"\"" '{for(i=1;i<=NF;i+=2){gsub(/,/,";",$i)}} 1' OFS="\""   Input_file
    

    输出如下。

    3;       "hh,1,foo";            foo
    "5,,,5"; "1,2,3d,,,something "; foo2
    test;    "col3";                foo3
    

    编辑:在这里也添加了非单行解决方案的解释。

    awk -F"\"" '{           ##Making " as a field separator as all the liens in Input_file.
    for(i=1;i<=NF;i+=2){    ##Starting a for loop here from variable i value 1 to till value of NF(number of fields in a line) incrementing variable i by 2 here, so that we will NOT touch those commas which are coming in side " " here.
      gsub(/,/,";",$i)      ##Now using gsub functionality of awk which will globally substitute comma with semi colon in current fields value.
    }}                      ##closing the block of for loop here.
    1                       ##awk works on method of condition then action, so mentioning 1 means I am making condition as TRUE and NO action is defined so by default print of current line of Input_file will happen.
    ' OFS="\"" Input_file   ##Setting OFS(output field separator) as " and mentioning Inut_file name here.
    

    【讨论】:

      【解决方案4】:

      您可以使用awk 命令仅在" 引用的部分内进行搜索/替换。

      第一步是将,替换为_

      cat demo.txt | awk 'BEGIN{FS=OFS="\""} {for(i=2;i<NF;i+=2)gsub(",","_",$i)} 1' 
      

      给了

      3,       "hh_1_foo",            foo
      "5___5", "1_2_3d___something ", foo2
      test,    "col3",                foo3
      

      然后用更常用的tr 命令替换,;

      tr ',' ';' 
      

      最后一步以“反向”方式再次使用 awk 将临时的_ 占位符替换为初始的, 字符。

      把我们所有的东西放在一起:

      cat demo.txt | 
      awk 'BEGIN{FS=OFS="\""} {for(i=2;i<NF;i+=2)gsub(",","_",$i)} 1' | 
      tr ',' ';' | 
      awk 'BEGIN{FS=OFS="\""} {for(i=2;i<NF;i+=2)gsub("_",",",$i)} 1'
      

      这给了

      3;       "hh,1,foo";            foo
      "5,,,5"; "1,2,3d,,,something "; foo2
      test;    "col3";                foo3
      

      正如预期的那样。


      更新:最快的解决方案?

      我使用我得到的 3 个答案将它们放在一个 206Mb 的 csv 文件上(运行了几次来处理缓存效果......),这是我得到的典型结果:

      1/ 我最初的回答:

      time cat avec_vapeur.csv | awk 'BEGIN{FS=OFS="\""} {for(i=2;i<NF;i+=2)gsub(",","_",$i)} 1' |  tr ',' ';' |  awk 'BEGIN{FS=OFS="\""} {for(i=2;i<NF;i+=2)gsub("_",",",$i)} 1'  > /dev/null
      
      real    0m2.488s
      user    0m5.025s
      sys     0m0.242s
      

      2/ 基于 awk 的替代解决方案:ravindersingh13

      time cat avec_vapeur.csv | awk -F"\"" '{for(i=1;i<=NF;i+=2){gsub(/,/,";",$i)}} 1' OFS="\"" > /dev/null
      
      real    0m4.705s
      user    0m4.631s
      sys     0m0.111s
      

      3/ 基于 sed 的解决方案:sjsam

      time cat avec_vapeur.csv | sed -E 's/,([[:space:]]*")/;\1/g;s/("[[:space:]]*),/\1;/g' > /dev/null 
      
      real    0m0.174s
      user    0m0.118s
      sys     0m0.130s
      

      -> 明显的赢家是基于 sed 的解决方案!

      我得到的最后一个答案:inian

      time cat avec_vapeur.csv |  awk -v OFS=';' 'BEGIN{FPAT = "([^,]+)|([[:space:]]*\"[^\"]+\")"}{$1=$1}1' > /dev/null
      
      real    0m37.507s
      user    0m37.463s
      sys     0m0.122s
      

      这也是我测试过的最慢的(这里不做判断,只是为了好玩才做这些测试!)

      更新:我最初误读了 =inian=,抱歉。如果我理解你,我补充说

      LC_ALL=C
      

      加快速度。

      现在我明白了:

      real    0m20.268s
      user    0m20.008s
      sys     0m0.087s
      

      这比 sed 解决方案更快,但没有那么快。

      现在比赛结束了,我没有替补席了(我也得努力一点)

      获胜者的遗言,perl 解决方案:sjsam

      time cat avec_vapeur.csv | perl -ane  's/,(\s*"[^"]*"\s*),/;$1;/g;print' > /dev/null
      
      real    0m0.134s
      user    0m0.096s
      sys     0m0.104s
      

      它甚至比 sed 快一点(至少在我的测试中)!

      【讨论】:

      • 你错过了在我的逻辑中设置 LC_ALL 设置,这是 awk 加快速度的关键。也请不要在任何尝试中cat file | awk..。更新直接在文件上运行的基准测试结果。可以避免cat 的额外过程的原因,以显示更准确的结果。例如我的尝试你可以使用time LC_ALL=C awk -v OFS=';' 'BEGIN{FPAT = "([^,]+)|([[:space:]]*\"[^\"]+\")"}{$1=$1}1' file
      • @Inian,对不起,我误读了你,等一下,我会解决这个问题,然后再换板凳。对不起
      • 感谢您的基准测试,不建议强制跨 shell 进行区域设置,它可能会影响其他命令。只需在本地使用命令
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-16
      • 2020-09-23
      • 2019-07-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多