3

在我构建英语语言数据库的工作中,我经常处理来自不同来源的文本内容,并且需要合并共享相同第一个字段的行。我经常在文本编辑器中使用捕获第一个字段的正则表达式来破解它,搜索“\n”,但我的文本文件通常>10GB,因此命令行流式解决方案优于内存。

样本输入:

apple|pear 
apple|quince 
apple cider|juice
banana|plantain
cherry|cheerful, crimson
cherry|ruddy
cherry|cerise

期望的输出:

apple|pear|quince 
apple cider|juice
banana|plantain
cherry|cheerful, crimson|ruddy|cerise

逻辑是连接(用“|”连接)具有相同第一个字段的所有行。

唯一的分隔符是“|”,并且分隔符在每个输入行只出现一次。即它实际上是一个 2 列文本文件。文件排序无关紧要,唯一关心的是具有相同第一个字段的连续行。

我有很多解决方案和单行代码(通常在 awk 或 ruby​​ 中)来处理同一行内容,但是在处理多行内容时我遇到了问题,希望能得到帮助。出于某种原因,多行处理总是让我陷入困境。

我确信这可以用 awk 简洁地完成。

4

6 回答 6

3

在每个 Unix 机器上的任何 shell 中使用任何 awk 并假设您的输入按示例输入中所示的第一个字段分组,并且您在某些行的末尾实际上没有尾随空格:

$ cat tst.awk
BEGIN { FS=OFS="|" }
$1 != prev {
    if ( NR>1 ) {
        print out
    }
    out = prev = $1
}
{ out = out OFS $2 }
END { print out }

$ awk -f tst.awk file
apple|pear|quince
apple cider|juice
banana|plantain
cherry|cheerful, crimson|ruddy|cerise

如果它没有被分组然后做sort file | awk -f tst.awk并且如果有尾随空格然后添加{ sub(/ +$/,"") }作为脚本的第一行。

于 2022-02-18T21:09:51.703 回答
3

假设/理解:

  • 整个文件可能未排序(按第一个字段)
  • 第一个字段中具有相同字符串的所有行将连续列出;这应该消除了在内存中维护大量数据的需要,但我们需要更多的输入
  • 第二个字段可能包含尾随空格(每个样本输入);这将需要删除
  • 输出不需要排序(按第一个字段)

一个awk想法:

awk '

function print_line() {
    if (prev != "")
       print prev,data
}

BEGIN { FS=OFS="|" }

      { if ($1 != prev) {
           print_line()
           prev=$1
           data=""
        }
        gsub(/[[:space:]]+$/,"",$2)              # strip trailing white space
        data= data (data=="" ? "" : OFS) $2      # concatentate 2nd fields with OFS="|"
      }

END   { print_line() }                           # flush last set of data to stdout
' pipe.dat

这会产生:

apple|pear|quince
apple cider|juice
banana|plantain
cherry|cheerful, crimson|ruddy|cerise
于 2022-02-18T19:44:57.997 回答
2

这是一个逐行读取文件的 Ruby 解决方案。最后,我展示了如果可以将文件吞入字符串,解决方案会变得多么简单。

让我们首先创建一个要使用的输入文件。

str =<<~_
  apple|pear 
  apple|quince 
  apple cider|juice
  banana|plantain
  cherry|cheerful, crimson
  cherry|ruddy
  cherry|cerise
_
file_name_in = 'file_in'
File.write(file_name_in, str)
  #=> 112

逐行读取文件时的解决方案

我们可以使用以下方法生成所需的输出文件。

def doit(file_name_in, file_name_out)  
  fin = File.new(file_name_in, "r")
  fout = File.new(file_name_out, "w")
  str = ''
  until fin.eof?
    s = fin.gets.strip
    k,v = s.split(/(?=\|)/)
    if str.empty?
      str = s
      key = k
    elsif k == key
      str << v
    else
      fout.puts(str)
      str = s
      key = k
    end
  end
  fout.puts(str)
  fin.close
  fout.close
end

让我们试试看。

file_name_out = 'file_out'
doit(file_name_in, file_name_out)
puts File.read(file_name_out)

打印以下内容。

apple|pear|quince
apple cider|juice
banana|plantain
cherry|cheerful, crimson|ruddy|cerise

注意

"apple|pear".split(/(?=\|)/)
  #=> ["apple", "|pear"]

正则表达式包含与和之间的零宽度位置匹配的正前瞻(?=\|)'e''|'

文件被吞入字符串时的解决方案

OP 不想将文件吞入一个字符串(因此我在上面的解决方案),但我想说明如果可以这样做,问题会变得多么简单。这是执行此操作的众多方法之一。

def gulp_it(file_name_in, file_name_out)
  File.write(file_name_out,
    File.read(file_name_in).gsub(/^(.+)\|.*[^ ]\K *\r?\n\1/, ''))
end
gulp_it(file_name_in, file_name_out)
  #=> 98
puts File.read(file_name_out)

印刷

apple|pear|quince 
apple cider|juice
banana|plantain
cherry|cheerful, crimson|ruddy
cherry|cerise

考虑一下正则表达式引擎将要做什么,这可能是可以接受的快,当然取决于文件大小。

正则表达式演示

虽然链接使用 PCRE 引擎,但使用 Ruby 的正则表达式引擎 (Onigmo) 的结果将是相同的。我们可以通过以自由间距模式编写正则表达式自记录。

/
^        # match the beginning of a line
(.+)     # match one or more characters
\|.*[^ ] # match '|', then zero or more chars, then a non-space
\K       # resets the starting point of the match and discards
         # any previously-matched characters 
[ ]*     # match zero or more chars
\r?\n    # march the line terminator(s)
\1       # match the content of capture group 1
/x       # invoke free-spacing mode

(.+)匹配, 'apple','banana'并且'cherry'因为这些词在开头行。一个也可以写([^|]*)

于 2022-02-18T21:11:27.197 回答
1

假设您有以下 sample.txt

apple|pear 
apple|quince 
apple cider|juice
banana|plantain
cherry|cheerful, crimson
cherry|ruddy
cherry|cerise

我不确定您为什么要将解决方案作为“单线”,但以下内容将满足您的需求。

cat sample.txt | ruby -e 'puts STDIN.readlines.map {_1.strip}.group_by {_1.split("|").first}.map{|k,v| v.reduce("#{k}") {"#{_1}|#{_2.split("|").last}"}}'

一个更易读的版本,带有描述正在发生的事情的评论:

stripped_lines = STDIN.readlines.map { |l| l.strip } # remove leading and trailing whitespace

# create a hash where the keys are the value to the left of the |
# and the values are lines begining with that key ie 
# {
#      "apple"=>["apple|pear", "apple|quince"],
#      "apple cider"=>["apple cider|juice"],
#      "banana"=>["banana|plantain"],
#      "cherry"=>["cherry|cheerful, crimson", "cherry|ruddy", "cherry|cerise"]
# }

grouped_by_first_element = stripped_lines.group_by { |sl| sl.split('|').first }

# map to the desired result by starting with the key
# and then concatinating the part to the right of the | for each element
# ie start with apple then append |pear to get apple|pear then append quince to that to get
# apple|pear|quince

result = grouped_by_first_element.map do |key, values|
    values.reduce("#{key}") do |memo, next_element|
        "#{memo}|#{next_element.split('|').last}"
    end 
end

puts result 
于 2022-02-19T20:42:49.920 回答
0

纯 bash 解决方案可能如下所示:

unset out       # make sure we start fresh (important if this is in a loop)
declare -A out  # declare associative array
d='|'           # delimiter

# append all values to the key
while IFS=${d} read -r key val; do
    out[${key}]="${out[${key}]}${d}${val}"
done <file

# print desired output
for key in "${!out[@]}"; do
    printf '%s%s\n' "${key}" "${out[$key]}"
done | sort -t"${d}" -k1


### actual output
apple cider|juice
apple|pear|quince
banana|plantain
cherry|cheerful, crimson|ruddy|cerise

或者你可以用 awk 做到这一点。正如评论中提到的,纯 bash 不是一个很好的选择,主要是由于性能和可移植性。

awk -F'|' '{
        sub(/[[:space:]]*$/,"")  # only necessary if you wish to trim trailing whitespace, which existed in your example data
        a[$1]=a[$1] "|" $2       # append value to string
    } END {
        for(i in a) print i a[i] # print all recreated lines
    }' <file


### acutal output
apple|pear|quince
banana|plantain
apple cider|juice
cherry|cheerful, crimson|ruddy|cerise
于 2022-02-19T11:04:37.453 回答
0

如果我们假设s是一个包含文件中所有行的字符串。

s.split("\n").inject({}) { |h, x| k, v = x.split('|'); h[k] ||= []; h[k] << v.strip; h }

将产生:

{"apple"=>["pear", "quince"], "apple cider"=>["juice"], "banana"=>["plantain"], "cherry"=>["cheerful, crimson", "ruddy", "cerise"]}

然后:

s.split("\n").inject({}) { |h, x| k, v = x.split('|'); h[k] ||= []; h[k] << v.strip; h }.map { |k, v| "#{k}|#{v.join('|')}" }

产量:

["apple|pear|quince", "apple cider|juice", "banana|plantain", "cherry|cheerful, crimson|ruddy|cerise"]
于 2022-02-18T20:34:08.597 回答