如何使用正则表达式(在Perl或* nix终端中)匹配巨大语料库中列表中的单词?

时间:2013-09-19 00:02:46

标签: regex perl grep nlp corpus

来自.txt文件中的给定名词列表,其中名词由新行分隔,例如:

hooligan
football
brother
bollocks

...和一个单独的.txt文件,其中包含一系列由新行分隔的正则表达式,如下所示:

[a-z]+\tNN(S)?
[a-z]+\tJJ(S)?

...我想通过语料库的每个句子运行正则表达式,每次正则表达式匹配模式时,如果该模式包含名词列表中的一个名词,我想打印出来输出中的名词和(通过制表符分隔)与之匹配的正则表达式。以下是结果输出的示例:

football    [a-z]+NN(S)?\'s POS[a-z]+NN(S)?
hooligan    [a-z]+NN(S)?,,[a-z]+JJ[a-z]+NN(S)?
hooligan    [a-z]+NN(S)?,,[a-z]+JJ[a-z]+NN(S)?
football    [a-z]+NN(S)?[a-z]+NN(S)?
brother [a-z]+PP$[a-z]+NN(S)?
bollocks    [a-z]+DT[a-z]+NN(S)?
football    [a-z]+NN(s)?(be)VBZnotRB

我将使用的语料库很大(数十GB)并具有以下格式(每个句子都包含在标签<s>中):

<s>
Hooligans   hooligan    NNS 1   4   NMOD
,   ,   ,   2   4   P
unbridled   unbridled   JJ  3   4   NMOD
passion passion NN  4   0   ROOT
-   -   :   5   4   P
and and CC  6   4   CC
no  no  DT  7   9   NMOD
executive   executive   JJ  8   9   NMOD
boxes   box NNS 9   4   COORD
.   .   SENT    10  0   ROOT
</s>
<s>
Hooligans   hooligan    NNS 1   4   NMOD
,   ,   ,   2   4   P
unbridled   unbridled   JJ  3   4   NMOD
passion passion NN  4   0   ROOT
-   -   :   5   4   P
and and CC  6   4   CC
no  no  DT  7   9   NMOD
executive   executive   JJ  8   9   NMOD
boxes   box NNS 9   4   COORD
.   .   SENT    10  0   ROOT
</s>
<s>
Portsmouth  Portsmouth  NP  1   2   SBJ
bring   bring   VVP 2   0   ROOT
something   something   NN  3   2   OBJ
entirely    entirely    RB  4   5   AMOD
different   different   JJ  5   3   NMOD
to  to  TO  6   5   AMOD
the the DT  7   12  NMOD
Premiership Premiership NP  8   12  NMOD
:   :   :   9   12  P
football    football    NN  10  12  NMOD
's  's  POS 11  10  NMOD
past    past    NN  12  6   PMOD
.   .   SENT    13  2   P
</s>
<s>
This    this    DT  1   2   SBJ
is  be  VBZ 2   0   ROOT
one one CD  3   2   PRD
of  of  IN  4   3   NMOD
Britain Britain NP  5   10  NMOD
's  's  POS 6   5   NMOD
most    most    RBS 7   8   AMOD
ardent  ardent  JJ  8   10  NMOD
football    football    NN  9   10  NMOD
cities  city    NNS 10  4   PMOD
:   :   :   11  2   P
think   think   VVP 12  2   COORD
Liverpool   Liverpool   NP  13  0   ROOT
or  or  CC  14  13  CC
Newcastle   Newcastle   NP  15  19  SBJ
in  in  IN  16  15  ADV
miniature   miniature   NN  17  16  PMOD
,   ,   ,   18  15  P
wound   wind    VVD 19  13  COORD
back    back    RB  20  19  ADV
three   three   CD  21  22  NMOD
decades decade  NNS 22  19  OBJ
.   .   SENT    23  2   P
</s>

我开始使用PERL中的脚本来实现我的目标,并且为了不使用如此庞大的数据集耗尽内存,我使用模块Tie::File以便我的脚本将读取一行时间(而不是试图在内存中打开整个语料库文件)。这将与语料库完美配合,其中每个句子对应于一行,但在当前情况下,句子在更多行上分布并由标签分隔。

有没有办法使用组合unix终端命令(例如cat和grep)来实现我想要的?或者,哪个是这个问题的最佳解决方案? (一些代码示例会很棒)。

2 个答案:

答案 0 :(得分:3)

简单的正则表达式替换足以从名词列表中提取匹配数据,Regexp::Assemble可以处理识别来自其​​他文件匹配的模式的要求。而且,正如Jonathan Leffler在评论中提到的那样,设置输入记录分隔符允许您一次读取一条记录,即使每条记录跨越多行也是如此。

将所有内容合并到一个正在运行的示例中,我们得到:

#!/usr/bin/env perl    

use strict;
use warnings;
use 5.010;

use Regexp::Assemble;

my @nouns = qw( hooligan football brother bollocks );
my @patterns = ('[a-z]+\s+NN(S)?', '[a-z]+\s+JJ(S)?');

my $name_re = '(' . join('|', @nouns) . ')'; # Assumes no regex metacharacters

my $ra = Regexp::Assemble->new(track => 1);
$ra->add(@patterns);

local $/ = '<s>';

while (my $line = <DATA>) {
  my $match = $ra->match($line);
  next unless defined $match;

  while ($line =~ /$name_re/g) {
    say "$1\t\t$match";
  }
}


__DATA__
...

... __DATA__部分的内容是原始问题中提供的样本语料库。为了保持答案紧凑,我没有把它包含在这里。另请注意,在这两种模式中,我将\t更改为\s+;这是因为复制并粘贴样本语料库时未保留选项卡。

运行该代码,我得到输出:

hooligan        [a-z]+\s+NN(S)?
hooligan        [a-z]+\s+NN(S)?
football        [a-z]+\s+NN(S)?
football        [a-z]+\s+NN(S)?
football        [a-z]+\s+JJ(S)?
football        [a-z]+\s+JJ(S)?

编辑:更正了正则表达式。我最初将\t替换为\s,只有在前面只有一个空格时才会匹配NNJJ。它现在还匹配多个空格,可以更好地模拟原始\t

答案 1 :(得分:1)

我最终编写了一个解决我问题的快速代码。我使用Tie :: File来处理大量的文本数据集并指定</s>作为记录分隔符,正如Jonathan Leffler所建议的那样(Dave Sherohman提出的解决方案似乎非常优雅,但我无法尝试)。 在句子分离后,我隔离了我需要的列(第2和第3),然后运行正则表达式。在打印输出之前,我检查匹配的单词是否出现在我的单词列表中:如果没有,则从输出中排除。

我在这里分享我的代码(包括评论)以防其他人需要类似的东西。

它有点脏,它肯定可以优化,但它适用于我,它支持非常大的语料库(我用10GB的语料库测试它:它在几个小时内成功完成)。

use strict;
use Tie::File; #This module makes a file look like a Perl array, each array element corresponds to a line of the file.

if ($#ARGV < 0 ) {  print "Usage: perl albzcount.pl corpusfile\n"; exit; }

#read nouns list (.txt file with one word per line - line breaks LF)
my $nouns_list = "nouns.txt";
open(DAT, $nouns_list) || die("Could not open the config file $nouns_list or file doesn't exist!"); 
my @nouns_contained_in_list=<DAT>;
close(DAT);

# Reading regexp list (.txt file with one regexp per line - line breaks LF)
my $regex_list = "regexp.txt";
open(DAT, $regex_list) || die("Could not open the config file $regex_list or file doesn't exist!");
my @regexps_contained_in_list=<DAT>;
close(DAT);

# Reading Corpus File (each sentence is spread on more lines and separated by tag <s>)
my $corpusfile = $ARGV[0]; #Corpus filename (passed as an argument through the command)

# With TIE I don't load the entire file in an array. Perl thinks it's an array but the file is actually read line by line
# This is the key to manipulate huge text files without running out of memory
tie my @raw_corpus_data, 'Tie::File', $corpusfile,  recsep => '</s>' or die "Can't read file: $!\n";

#START go throught the sentences of the corpus (spread on multiple lines and separated by <s>), one by one
foreach my $corpus_line (@raw_corpus_data){

#take a single sentence (that is spread along different lines).
#NB each line contains "columns" separated by tab
my @corpus_sublines = split('\n', $corpus_line); 

#declare variable. Later values will be appended to it
my $corpus_line; 

    #for each line that composes a sentence
    foreach my $sentence_newline(@corpus_sublines){ a

    #explode by tab (column separator)
    my @corpus_columns = split('\t', $sentence_newline); 

    #put together new sentences using just column 2 and 3 (noun and tag) for each original sentence
    $corpus_line .= "@corpus_columns[1]\t@corpus_columns[2]\n";

    #... Now the corpus has the format I want and can be processed
    }

    #foreach regex
    foreach my $single_regexp(@regexps_contained_in_list){ 

        # Remove the new lines (both \n and \r - depending on the OS) from the regexp present in the file. 
        # Without this, the regular expressions read from the file don't always work.
        $single_regexp =~ s/\r|\n//g; 

            #if the corpus line analyzed in this cycle matches the regexp
            if($corpus_line =~ m/$single_regexp/) { 

            # explode by tab the matched results so the first word $onematch[0] can be isolated
            # $& is the entire matched string
            my @onematch = split('\t', $&);

                # OUTPUT RESULTS
                #if the matched noun is not empty and it is part of the word list
                if ($onematch[0] ne "" && grep( /^$onematch[0]$/, @nouns_contained_in_list )) { 
                print "$onematch[0]\t$single_regexp\n";
                } # END OUTPUT RESULTS
            } #END if the corpus line analyzed in this cycle matches the regexp
    } #END foreach regex
} #END go throught the lines of the corpus, one by one

# Untie the source corpus file
untie @raw_corpus_data;