【问题标题】:Is there an inverse of grep: finding short lines in long patterns?是否有 grep 的逆:在长模式中找到短线?
【发布时间】:2013-03-20 05:06:46
【问题描述】:

如果 grep 从查找文件的长行中的模式文件中找到一个短模式,我需要一个工具来提取可以在较长模式中找到的查找文件的短行。

换句话说,给定莎士比亚的作品,每行一个句子,说一本法语词典,我想找出在莎士比亚的哪一行中找到了哪些法语单词,从而可以检测到莎士比亚的一行可能包含多个法语单词,并且一个法语单词可能出现在不止一行的莎士比亚。

例如:

pattern_file={
"The sun is shining!"
"It is a beautiful day!"}

lookup_file={
"Rain"
"Sun"
"Cloud"
"Beautiful"
"Shining"}

我想要的是

function file pattern

给出在较长模式中找到的行和较长模式本身以逗号分隔,并检测到多个匹配项。

ideal_result_file={
"Sun","The sun is shining!"
"Beautiful","It is a beautiful day!",
"Shining", "The sun is shining!"}

目前,我使用 grep 逐行遍历整个查找文件:

    while read line
    do
      grep  -is $line pattern_file | sed 's/^/'"$line"'\,/g' >> result_file.csv
    done < lookup_file

这太慢了!我的 lookup_file 包含超过 50 000 行,而我的 pattern_file 包含 500 行。使用 grep 在我的 lookup_file 中查找更短的模式需要几秒钟,而使用我的循环方法的单次传递需要几天/几周。

我们将不胜感激任何语言的解决方案。

有点相关
Very slow loop using grep or fgrep on large datasets
Is Perl faster than bash?

解决方案需要与 GB 大小的循环和模式文件兼容。

【问题讨论】:

  • lookup_file 是由纯文本组成,还是有正则表达式?
  • lookup_file 是纯文本
  • 这个while循环可以向量化吗?或者翻译成另一种更有效的(编译)语言?
  • 我认为没有任何编译语言会比grep 更高效。无论如何,在 2000 字的文本文件上使用 grep -F -f /usr/share/dict/words(99000 字)在不到一秒的时间内运行,尽管它只产生最长的匹配(例如,anything 将产生匹配 anything 而不是 @ 987654331@)。您希望输出显示所有匹配项吗?
  • 是的,所有匹配项都是必需的。如果我可以在这里单独使用 grep,没有什么比 grep 更快的了,但也许使用 grep 的循环方法在另一种语言中会更快?

标签: bash search while-loop grep pattern-matching


【解决方案1】:

您可以使用-f 开关在 grep 中使用“模式文件”:

egrep -i -f lookup_file pattern_file >> result_file

这会更快,因为greplookup_file 编译成一个同时检查所有匹配项的状态机,而不是分别针对每一行检查每个模式。

如果您的 lookup_file 包含文本而不是正则表达式,您可以使用 fgrep,它会更快。

要获得理想的输出,您可以使用 -n 和 -o 开关并获得与每一行匹配的模式列表。

【讨论】:

  • 谢谢,但是我很清楚 -f 标志。问题不在于使用文件而不是循环,而是在这种情况下,如果我要使用 grep,我别无选择,只能循环。这是我的lookup_string 更短,是我想在更长的模式中找到的。诚然,我没有很好地解释这一点,但我还没有找到更好的方法来解释它。
  • 为什么必须使用循环? -f 不仅解决了循环的需要;它使grep 将整个文件编译成一个比循环更好的正则表达式。
  • Etienne,Joni 提出的这个解决方案似乎是最明智的。另一种方法是像数据库一样,创建一个模式表,并创建一个索引表,将每个模式进一步索引为更小的组件模式。顺便说一句,您的问题的语言非常混乱,您必须不再将您的模式视为模式(甚至认为,这就是我所说的),您必须从现在开始将您的模式视为数据(就像您会想到一本书的内容被编入索引以创建拼写检查器的字典)。
  • 我同意我的问题的措辞令人困惑,因为这是通常由 grep 解决的逆问题(文件长行中的短模式),请随时编辑。切换我所说的模式和查找文件(由 glenn jackman 提出)并使用 -f 标志(由 Joni 提出)确实让我更接近,但是,如下所述,这个简单的开关有很多困难,其中将整个 50 000 行文件作为模式加载使用了太多的内存,这不允许在长“模式”中检测到多个短段。
  • @EtienneLow-Décarie 加载文件需要多少内存?我已经将文件命名为“words”和“sentences”,这样它们的含义就更直接清楚了,这里是 'egrep -o -n -i -f words.txt sentence.txt' 的结果,看起来非常接近随心所欲:1:阳光灿烂 2:美丽
【解决方案2】:

既然您指出任何语言都可以接受,我将发布一个完全不同的方法:使用 shell 脚本,您永远不会超过内存工具或数据库的性能。如果您有大量数据,您应该使用专门用于此类操作的数据库,并且它的可扩展性要好得多。

这里是一个使用 sqlite (www.sqlite.org) 的简单示例。

您需要将模式和数据导入表格,例如(如果需要,您可以编写脚本):

CREATE TABLE patterns (pattern TEXT);
CREATE TABLE data (sentence TEXT);

BEGIN;

INSERT INTO patterns VALUES ('Sun');
INSERT INTO patterns VALUES ('Rain');
INSERT INTO patterns VALUES ('Cloud');
INSERT INTO patterns VALUES ('Beautiful');

INSERT INTO data VALUES ('The sun is shining');
INSERT INTO data VALUES ('It is a beautiful day');
INSERT INTO data VALUES ('It is cloudy and the sun shines');

COMMIT;

然后运行select 查询以获得所需的输出:

select pattern, group_concat(sentence) as doesmatch from (
    select pattern, sentence, lower(pattern) as lpattern, lower(sentence) as lsentence
    from patterns left outer join data
    where like('%' || lpattern || '%', lsentence)
) group by pattern;

如果您将第一个 sn-p 保存为 data.sql,将第二个保存为 query.sql,您可以在命令行中使用:

sqlite3 sentences.db < data.sql    # this imports your data, run once
sqlite3 sentences.db < query.sql

这给了你:

Beautiful|It is a beautiful day
Cloud|It is cloudy and the sun shines
Sun|The sun is shining,It is cloudy and the sun shines

我相信这就是你想要的。为了使它更花哨,请使用您最喜欢的更高级的工具和数据库库。我会为此选择python。

进一步改进的建议:

  • 使用regex 而不是like 过滤整个单词(即模式“sun”匹配“sun”但不匹配“sunny”),

  • 导入实用程序,

  • 输出格式,

  • 查询优化。

【讨论】:

  • 虽然我应该是,但我对数据库并不是很熟悉。不过仔细阅读,似乎将文件读入数据库可能会成为这里的新瓶颈 (stackoverflow.com/questions/5942402/python-csv-to-sqlite),因为我必须对我希望处理的所有模式和查找文件执行此操作。有关如何有效地做到这一点的任何建议?
  • @EtienneLow-Décarie 您指向的 Python 代码很慢,因为它没有将语句包装在事务中。刚刚编辑了我的示例以考虑到这一点。我不确定它会有多大帮助,但您可以使用 C 或 Java 等编译语言将数据导入/导出到数据库中,您可能会获得 50%,但您的里程可能会有所不同。根据数据库可能适合内存的数据量,这也会更快。
  • 开始处理这个问题,非常感谢!但是在您的代码中,您的表及其列的名称相同,您介意为了清楚起见进行编辑吗?
  • 太棒了!我尝试将我的数据加载到 sqlite 数据库中的表中(使用 SQLite 数据库浏览器让我开始),即使测试数据集(100 mb)远小于我的单个文件(> 1 gb),这个似乎是瓶颈,但一旦完成此步骤,它将在未来加快查询速度。
【解决方案3】:

您的解决方案实际上可能很慢,因为它创建了 50.000 个进程,所有进程都读取 500 行模式文件。

另一个“纯 bash 和 unix utils”解决方案可能是让 grep 做它最擅长的事情,并将输出与您的 pattern_file 匹配。

所以使用grep 来查找匹配的行和实际匹配的部分。

我在这里使用单词匹配,可以通过删除 grep 行中的 -w 开关来关闭它,并获得示例中所述的初始行为。

输出尚未重定向到result_file.csv.. 这很容易在以后添加 8)

#!/bin/bash
# open pattern_file
exec 3<> pattern_file

# declare and initialize integer variables
declare -i linenr
declare -i pnr=0

# loop for reading from the grep process
#
# grep process creates following output:
#   <linenumber>:<match>
# where linenumber is the number of the matching line in pattern_file
# and   match is the actual matching word (grep -w) as found in lookup_file
# grep output is piped through sed to actually get
#   <linenumber> <match>
while read linenr match ; do

   # skip line from pattern_file till we read the line
   # that contained the match
   while [[ ${linenr} > ${pnr} ]] ; do
       read -u 3 pline
       pnr+=1
   done

   # echo match and line from pattern_file
   echo "$match, $pline"
done < <( grep -i -w -o -n -f lookup_file pattern_file | sed -e 's,:, ,' )

# close pattern_file
exec 3>&-

结果是

sun, The sun is shining
shining, The sun is shining
beautiful, It is a beautiful day!

对于给出的示例。注意:匹配现在是保留大小写的完全匹配。所以这不会导致Sun, ...,而是sun, ...

结果是一个脚本,它使用 grep 读取一次 pattern_files,在最好的情况下读取一次 pattern_file 和 lookup_file - 取决于实际的实现。 它只会启动两个额外的进程:grepsed。 (如果需要,sed 可以在外循环中替换为一些 bash 替换)

我没有尝试使用 50.000 行 lookup_file 和 500 行 pattern_file。但我认为它可能和 grep 一样快。

只要grep 可以将lookup_file 保存在内存中,它可能是合理的快。 (谁知道)

无论它是否能解决您的问题,我都会对它与您的初始脚本相比的性能感兴趣,因为我确实缺少好的测试文件。

如果grep -f lookup_file 使用过多内存(正如您之前在评论中提到的),它可能是一种解决方案,可以将其拆分为实际适合内存的部分并多次运行脚本或使用不止一台机器,在这些机器上运行所有部分,然后收集并连接结果。只要 lookup_files 不包含欺骗,您就可以连接结果而不检查欺骗。如果排序很重要,您可以对所有单个结果进行排序,然后使用 sort -m 快速合并它们。

只要您只拆分lookup_file 一次并重新运行脚本,拆分lookup_file 应该不会显着影响运行时间,因为您的pattern_file 可能足够小,它的500 行无论如何都可以保留在内存缓存中!?如果您使用不止一台机器,lookup_file 可能也是如此 - 它的部分可能只保留在每台机器的内存中。

编辑:

正如我在评论中指出的那样,这不适用于开箱即用的重叠文件,因为grep -f 似乎只返回最长的匹配并且不会重新匹配,所以如果lookup_file 包含

Sun
Shining
is
S

结果是

sun, The sun is shining
is, The sun is shining
shining, The sun is shining

而不是

sun, The sun is shining
is, The sun is shining
shining, The sun is shining
s, The sun is shining
s, The sun is shining
s, The sun is shining

所以所有匹配的s(匹配3次)都不见了。

事实上这是这个解决方案的另一个问题:如果一个字符串被找到两次,它将被匹配两次,并返回相同的行,这可以被uniq删除。

可能的解决方法: 按搜索字符串的字符串长度拆分lookup_file。这将减少运行 grep 所需的最大内存,但也会稍微减慢整个过程。但是:然后您可以并行搜索(如果在同一台服务器上执行此操作,可能需要检查 greps --mmap 选项)。

【讨论】:

  • 感谢您的回答。我喜欢两步策略(获取匹配然后从匹配中获取数据)。目前,您使用提供的信息的方法会产生sun, Rain , Rain beautiful, Sun(有空匹配项)。我会尽力解决它以使其正常工作。干杯
  • 这与您提供的测试数据一致吗?它 (sun, Rain , Rain beautiful, Sun) 是否全部打印在一行中?这很奇怪。我在pattern_filelookup_file 中用空行进行了尝试。这总是我上面引用的结果。您使用的是哪个版本的 bash? (我目前使用的是 4.1.7)
  • 好的.. 阅读您最新的 cmets 到其他帖子后:是的,此解决方案在重叠匹配时失败。我认为 grep 仅返回最长的匹配项,然后忽略任何子匹配项,因此如果您的 lookup_file 包含 GATTACAGATTACGATGATTACAGGG 的匹配项将仅返回 GATTACA, CGATGATTACAGGGGATTA, ... 将丢失。我不认为grep 可以做到这一点——至少我没有找到命令行选项。 (但你得到的结果仍然没有被这个问题解释)它只是表明,grep -f根本没有帮助。
  • 编辑了答案以解决重叠匹配问题。
【解决方案4】:

使用哈希表或集合(取决于您的语言)以全小写形式存储字典。对于每一行,将行拆分为基于非字母字符的单词数组。根据这些单词构建一个微型哈希表,转换为小写,以消除重复。遍历该微型哈希表中的每个单词,验证它是否存在于您的字典哈希表中。如果存在,则打印该单词和整行。

这是在 Perl 中的一个实现。

#! /usr/bin/perl

my $dictFile=$ARGV[0];
my $srchFile=$ARGV[1];
(-f $dictFile and -f $srchFile) or die "Usage: $0 dictFile srchFile";

# Load dictionary into hash table
my %dict=();
open($df, "<$dictFile") or die "Cannot open $dictFile";
while (<$df>) {
  chomp;
  $dict{lc($_)}=1;
}

# Search file for your dictionary words
open($sf, "<$srchFile") or die "Cannot open $srchFile";
my $lineNo=0;
while ($line=<$sf>) {
  $lineNo++;
  chomp($line);
  my %words=();
  my @sentence=split(/[^a-zA-ZÀ-ÿ0-9]+/, $line);
  foreach $word (@sentence) {
    $words{lc($word)}=1;
  }
  while ( my ($key) = each(%words) ) {
    if ($dict{$key}) {
      print "$lineNo, $key, $line\n";
    }
  }
}

pattern.txt

The sun is shining!
It is a beautiful day!

lookup.txt

Rain
Sun
Cloud
Beautiful
Shining

$ ./deepfind lookup.txt pattern.txt

1, shining, The sun is shining!
1, sun, The sun is shining!
2, beautiful, It is a beautiful day!

编辑:根据您的 cmets,这是在“句子”中定义“单词”集的另一种方法。这准备了与字典中找到的任何序列长度相匹配的所有可行序列。

#! /usr/bin/perl
my $dictFile=$ARGV[0];
my $srchFile=$ARGV[1];
(-f $dictFile and -f $srchFile) or die "Usage: $0 dictFile srchFile";
# Load sequence dictionary into hash table
my %dict=();
my %sizes=();
open($df, "<$dictFile") or die "Cannot open $dictFile";
while (<$df>) {
  chomp;
  $dict{lc($_)}=1;
  $sizes{length($_)}=1;
}

# Search file for known sequences
open($sf, "<$srchFile") or die "Cannot open $srchFile";
my $lineNo=0;
while ($line=<$sf>) {
  $lineNo++;
  chomp($line);
  # Populate a hash table with every unique sequence that could be matched
  my %sequences=();
  while ( my ($size) = each(%sizes) ) {
    for (my $i=0; $i <= length($line)-$size; $i++) {
      $sequences{substr($line,$i,$size)}=1;
    }
  }
  # Compare each sequence with the dictionary of sequences.
  while ( my ($sequence) = each(%sequences) ) {
    if ($dict{$sequence}) {
      print "$lineNo, $sequence, $line\n";
    }
  }
}

【讨论】:

  • 这种方法的一个主要好处是它支持非常大的字典而不影响性能,前提是您分配了足够的 RAM 来将字典保存在内存中。正则表达式的复杂性没有改变——它仅用于将每一行标记为单词。
  • 这是一个了不起的解决方案!非常感谢!莎士比亚的例子是假设性的,我现在正试图让句子不能分解成单词(句子和单词实际上是我现实生活问题中的 DNA 代码)。
  • 搜索词必须适合完全不分隔的句子。
  • 这太迷人了!您的字典中表示的序列长度是否有很多变化?听起来好像“单词”是可以在任何时候开始和结束的子字符串。如果是这样,您的 %words,可以只填充一行中每个有效长度的每个可能的子字符串(其中有效长度由字典中找到的单词长度集定义)。
  • 是的,单词可以从任何地方开始,也可以在任何地方结束,因此使用了 grep。
【解决方案5】:

您需要交换“模式”和“查找”文件的含义,并使用 grep 的-o 开关。

$ cat patterns 
The sun is shining!
It is a beautiful day!

$ cat lookup 
Rain
Sun
Cloud
Beautiful

$ grep -iof lookup patterns 
sun
beautiful

【讨论】:

  • 这不适用于lookup=[sun,sunny]patterns=[sunny]
  • 感谢这个想法,但是这样的切换并不是那么简单。这个简单的开关有很多困难,其中包括加载整个 50 000 行文件作为模式使用太多内存,这不允许检测在长“模式”中检测到的多个短段。
【解决方案6】:

编辑:抱歉,前面的示例不起作用。

这似乎是 perl 的完美匹配。开始

#!/usr/bin/perl

open PATTERNS, "patterns";
open LOOKUP, "lookup";

my @l = <LOOKUP>;

while (chomp(my $re = <PATTERNS>)) {
     print "$re\n" if grep(/$re/, @l); 
}

请注意,我在这里切换了模式和查找的含义。图案就是图案。如果您想打印图案而不是线条,那很好,但我不会更改它们的名称。

【讨论】:

  • 诚然 perl 是解决方案。我还没有足够的知识来了解你的工作方法,但会继续尝试。
  • 你在什么时候失败了?如果您按字面意思复制上面的程序,我很确定它应该可以工作。
  • 已解决。谢谢!虽然 phatfingers 的哈希表看起来要快得多。
  • 哈希表自然不能做正则表达式,所以不能做“grep的逆”。
【解决方案7】:

使用后缀数组或后缀数组怎么样?您可以找到一个具有坚持使用类似于 grep 的选项 here 的优势的实现,尽管我从未使用过它,也无法证明它的效率和易用性。

后缀树/数组需要在 O(n) 到 O(n log n) 时间内对将要搜索的文件进行预处理(n 是查找文件的长度),并且后缀树/数组本身将是几个比原始文件大几倍(常数因子),但有磁盘绑定算法,它们经常用于搜索整个人类基因组(几 GB)。然后在文件中搜索字符串只需要 O(m) 时间,其中 m 是字符串的长度,这比 grep (O(n log m)?) 快得多。由于您似乎会多次搜索同一个文件,因此对后缀树/数组所需的预处理步骤的投资可能是值得的。

【讨论】:

    【解决方案8】:

    结合上面提到的一些想法,我想出了一个使用grep 并使用join 合并结果的两遍系统,如下所示:

    模式

    The sun is shining!
    It is a beautiful day!
    

    查找

    Rain
    Sun
    Cloud
    Beautiful
    Is
    

    脚本

    grep -i -o -n -f lookup patterns > tmp1
    grep -i -n -f lookup patterns > tmp2
    join -t ':' -o 1.2,2.2 tmp1 tmp2 | sed -e 's/:/,/'
    

    产生以下结果

    sun,The sun is shining!
    is,The sun is shining!
    is,It is a beautiful day!
    beautiful,It is a beautiful day!
    

    如果您想要查找匹配和模式逗号分隔的输出,这里有一个可以工作的小型 python 2.x 脚本。它将查找读入一个缓冲区,然后遍历这些模式。

    script.py

    import sys, re
    
    lookups = [re.compile(l.strip(),re.I) for l in open(sys.argv[1])]
    for line in open(sys.argv[2]):
        for lookup in lookups:
            if lookup.search(line):
                print "{0},{1}".format(lookup.pattern, line),
    

    运行python script.py lookup patterns 产生:

    Sun,The sun is shining!
    Is,The sun is shining!
    Beautiful,It is a beautiful day!
    Is,It is a beautiful day!
    

    【讨论】:

      【解决方案9】:

      这可能不会更快,但你可以试试:

      for i in `cat lookup_file`; 
        do  
          tmpv=`grep -i ${i} pattern_file | xargs echo ${i},`; 
          echo ${tmpv} | sed '/^$/d'; 
      done
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-06-27
        • 1970-01-01
        • 2021-01-09
        • 2010-11-27
        • 1970-01-01
        • 1970-01-01
        • 2014-07-31
        相关资源
        最近更新 更多