【问题标题】:Compare many text files that contain duplicate "stubs" from the previous and next file and remove duplicate text automatically比较上一个和下一个文件中包含重复“存根”的许多文本文件,并自动删除重复文本
【发布时间】:2010-10-17 22:15:33
【问题描述】:

我有大量文本文件(1000 多个),每个文件都包含来自学术期刊的文章。不幸的是,每篇文章的文件还包含上一篇文章结尾(开头)和下一篇文章开头(结尾)的“存根”。

我需要删除这些存根以准备对文章进行频率分析,因为存根构成重复数据。

在所有情况下都没有简单的字段来标记每篇文章的开头和结尾。但是,在这两种情况下,重复文本的格式似乎相同且位于同一行。

将每个文件与下一个文件进行比较然后删除重复文本的 1 个副本的脚本将是完美的。这似乎是编程时非常常见的问题,所以我很惊讶我找不到任何可以做到这一点的东西。

文件名按顺序排序,因此将每个文件依次与下一个文件进行比较的脚本应该可以工作。例如

bul_9_5_181.txt bul_9_5_186.txt

是两篇文章,一篇从第 181 页开始,另一篇从第 186 页开始。这两篇文章都包含在下面。

有两卷测试数据位于 [http://drop.io/fdsayre][1]

注意:我是一名学者,正在为心理学史上的一个项目对旧期刊文章进行内容分析。我不是程序员,但我确实有 10 年以上的 linux 经验,并且通常可以在我进行的过程中解决问题。

感谢您的帮助

文件名:bul_9_5_181.txt

通感

ISI

表示黑色物体或与黑色有关的想法的大多数葡萄牙语单词。诚然,这种关联并不是真正的联觉,但作者认为,这些逻辑和自发的关联与真实的有色试镜案例之间只是程度问题。 参考文献

DOWNEY, JUNE E. 一个有色味觉的案例。阿米尔。 J. of Psycho!., 1911, 22, S28-539MEDEIROS-E-ALBUQUERQUE。 Sur un phenomene de synopsie presente par des Millions de sujets。 / 。德心理。规范等路径,1911, 8, 147-151。 MYERS, C. S. 通感症案例。英国人。 J. of Psychol., 1911, 4, 228-238.

情感现象——实验 约翰·F·谢泼德教授 密歇根大学

在这一年里,莱比锡实验室发表了三篇文章。 Drozynski (2) 反对使用味觉和嗅觉刺激来研究有感觉的器质反应,因为可能涉及呼吸障碍。他使用有节奏的听觉刺激,并发现当以不同的速率和不同的分组给予时,它们伴随着每个受试者的特征感受。他用脉搏计和水体积描记器记录胸部呼吸和曲线。每个实验都以正常记录开始,然后给予刺激,然后是对比刺激;最后,取了另一个正常值。测量呼吸的长度和深度(没有记录时间线),并确定吸气长度与呼气长度的关系。还测量了脉搏的长度和高度。表格总结了作者在每种感觉的反应期间发现每个数量增加或减少的次数。伴随给定节奏的感觉状态总是复杂的,但结果是指那个似乎占主导地位的维度。仅从记录中复制了一些与正常和反应期无关的摘录。作者指出,兴奋会增加呼吸的频率和深度、吸气-呼气比以及脉搏的频率和大小。手臂体积有起伏。只要效果是安静的,它会导致速度和深度的降低

182

约翰·F·谢泼德

呼吸、吸气-呼气比、脉率和大小。手臂体积显示出随着呼吸波增加的趋势。随和的表现

【问题讨论】:

  • 每个文件中实际文章的开头和结尾是否没有以某种方式标记?
  • 没有。最接近的是每篇文章的标题和作者姓名。它们具有以下格式: NAME OF ARTICLE BY FIRSTNAME LASTNAME 但还有其他全大写片段(运行头),尽管不是连续行上的标题和作者姓名的组合。
  • @fdsayre — 我做了一些小的格式更改,以便您的示例(希望)能更好地脱颖而出。希望你不要介意。 :-)
  • @ben-blank 看起来确实更好,谢谢。
  • @fdsayre -- 添加文件名保留。

标签: text scripting nlp duplicate-data


【解决方案1】:

看起来一个更简单的解决方案实际上可以工作。

似乎没有人使用文件名提供的信息。如果您确实使用了此信息,则可能无需在文件之间进行任何比较来识别重叠区域。编写 OCR 的人可能对这个问题有所思考。

文件名中的最后一个数字告诉您该文件的起始页码是多少。此页码也单独出现在文件中的一行上。看起来这一行之前和之后都是空行。因此,对于给定的文件,您应该能够查看序列中下一个文件的名称,并确定您应该开始删除文本的页码。由于此页码出现在您的文件中,只需查找仅包含此页码的行(前后为空行)并删除该行和之后的所有内容。序列中的最后一个文件可以保留。

这是一个算法的大纲

  1. 选择一个文件;称之为:file1
  2. 查看下一个文件的文件名;称之为:file2
  3. 从file2的文件名中提取页码;叫它:pageNumber
  4. 扫描 file1 的内容,直到找到只包含 pageNumber 的行
  5. 确保此行前后各有一个空行。
  6. 删除此行以及之后的所有内容
  7. 转到序列中的下一个文件

【讨论】:

  • 嗯。昨晚我意识到文件名在这种情况下很有用,但我没有想到文件中的页码。这很聪明。
  • 对 OP 的评论:“是的......我刚刚意识到这一点。具有讽刺意味的是,我已经提取了所有页码以使用元数据,而且它们只是坐在文本文件和一个与文件名和内容链接的数据库。”
【解决方案2】:

您可能应该尝试这样的事情(我现在已经根据您提供的示例数据对其进行了测试):

#!/usr/bin/ruby

class A_splitter
    Title   = /^[A-Z]+[^a-z]*$/
    Byline  = /^BY /
    Number = /^\d*$/
    Blank_line = /^ *$/
    attr_accessor :recent_lines,:in_references,:source_glob,:destination_path,:seen_in_last_file
    def initialize(src_glob,dst_path=nil)
        @recent_lines = []
        @seen_in_last_file = {}
        @in_references = false
        @source_glob = src_glob
        @destination_path = dst_path
        @destination = STDOUT
        @buffer = []
        split_em
        end
    def split_here
        if destination_path
            @destination.close if @destination
            @destination = nil
          else
            print "------------SPLIT HERE------------\n" 
          end
        print recent_lines.shift
        @in_references = false
        end
    def at_page_break
        ((recent_lines[0] =~ Title  and recent_lines[1] =~ Blank_line and recent_lines[2] =~ Number) or
         (recent_lines[0] =~ Number and recent_lines[1] =~ Blank_line and recent_lines[2] =~ Title))
        end
    def print(*args)
        (@destination || @buffer) << args
        end
    def split_em
        Dir.glob(source_glob).sort.each { |filename|
            if destination_path
                @destination.close if @destination
                @destination = File.open(File.join(@destination_path,filename),'w')
                print @buffer
                @buffer.clear
              end
            in_header = true
            File.foreach(filename) { |line|
                line.gsub!(/\f/,'')
                if in_header and seen_in_last_file[line]
                    #skip it
                  else 
                    seen_in_last_file.clear if in_header
                    in_header = false
                    recent_lines << line
                    seen_in_last_file[line] = true
                  end
                3.times {recent_lines.shift} if at_page_break
                if recent_lines[0] =~ Title and recent_lines[1] =~ Byline
                    split_here
                  elsif in_references and recent_lines[0] =~ Title and recent_lines[0] !~ /\d/
                    split_here
                  elsif recent_lines.length > 4
                    @in_references ||= recent_lines[0] =~ /^REFERENCES *$/
                    print recent_lines.shift
                  end
                }
            } 
        print recent_lines
        @destination.close if @destination
        end
    end

A_splitter.new('bul_*_*_*.txt','test_dir')

基本上,按顺序运行文件,并在每个文件中按顺序运行行,从每个文件中省略前一个文件中存在的行并将其余行打印到 STDOUT(可以从中通过管道传输)除非指定了目标目录(在示例中称为“test_dir”,请参见最后一行),在这种情况下,将在指定目录中创建与包含大部分内容的文件同名的文件。

它还删除了分页部分(期刊标题、作者和页码)。

它做了两个拆分测试:

  • 对标题/署名对的测试
  • 在参考部分之后的第一个标题行上进行测试

(应该很明显如何为额外的分割点添加测试)。

留作后人:

如果您不指定目标目录,它只会在输出流中的拆分点处放置一个 split-here 行。这应该使测试更容易(您可以只使用less 输出),当您希望它们在单个文件中时,只需将其通过管道传输到csplit(例如,使用

csplit -f abstracts - '---SPLIT HERE---' '{*}'

什么的)把它剪掉。

【讨论】:

  • 这看起来很有趣。如果“拆分”是指将文件分开,那么我确实需要将它们拆分。我做了一个快速测试,它似乎有效,但如果不保持每篇文章完整,就很难比较。谢谢。
  • 所以这使用标题和署名来确定每篇文章的正确开始(以及结束)?如果是这样,不幸的是它不会起作用,因为没有唯一标识每篇文章的起点的特定字段。有些使用标题/作者,但其他(评论/等)没有作者字段。
  • 因此我认为脚本需要将每个文件的开头/结尾与下一个文件进行比较,并删除一组重复项。存根应该只在大约。两边各 1/2 页,仅包含上一个/下一个文件。抱歉,这太复杂了,可能无法解决。
  • 聪明地忽略重复行的顺序。很有可能有效。
  • @fdsayre -- 可以是任意数量的测试;标题和署名只是示例。我将添加另一个我注意到的模式作为示例。
【解决方案3】:

这是 Perl 中另一个可能的解决方案的开始(它按原样工作,但如果需要,可能会变得更复杂)。听起来好像您所关心的只是在整个语料库中删除重复项,并且并不真正关心一篇文章的最后一部分是否在下一篇的文件中,只要它没有在任何地方重复。如果是这样,此解决方案将删除重复的行,在整个文件集中只留下任何给定行的一个副本。

您可以只在包含文本文件的目录中运行文件,不带参数,或者指定一个文件名,该文件名包含您要处理的文件列表,按照您希望它们处理的顺序。我推荐后者,因为在命令行上使用 lsglob等简单命令时,您的文件名(至少在您提供的示例文件中)不会自然地按顺序列出> 在 Perl 脚本中。因此,它不一定会相互比较正确的文件,因为它只是顺着列表运行(由 glob 命令输入或生成)。如果您指定列表,则可以保证它们将按正确的顺序进行处理,并且不会花费那么长时间来正确设置。

脚本只是打开两个文件并记下第二个文件的前三行。然后它为第一个文件打开一个新的输出文件(原始文件名 + '.new'),并将第一个文件中的所有行写到新的输出文件中,直到找到第二个文件的前三行。很有可能最后一个文件中的第二个文件没有三行,但在我抽查的所有文件中,由于期刊名称标题和页码,这似乎是这种情况。一行肯定是不够的,因为期刊标题通常是第一行,这会提前结束。

我还应注意,您输入的文件列表中的最后一个文件将不会被处理(即基于它创建一个新文件),因为此过程不会更改它。

这是脚本:

#!/usr/bin/perl
use strict;

my @files;
my $count = @ARGV;
if ($count>0){
    open (IN, "$ARGV[0]");
    @files = <IN>;
    close (IN);
} else {
    @files = glob "bul_*.txt";
}
$count = @files;
print "Processing $count files.\n";

my $lastFile="";
foreach(@files){
    if ($lastFile ne ""){
        print "Processing $_\n";
        open (FILEB,"$_");
        my @fileBLines = <FILEB>;
        close (FILEB);
        my $line0 = $fileBLines[0];
            if ($line0 =~ /\(/ || $line0 =~ /\)/){
                    $line0 =~ s/\(/\\\(/;
                    $line0 =~ s/\)/\\\)/;
            }
        my $line1 = $fileBLines[1];
        my $line2 = $fileBLines[2];
        open (FILEA,"$lastFile");
        my @fileALines = <FILEA>;
        close (FILEA);
        my $newName = "$lastFile.new";
        open (OUT, ">$newName");
        my $i=0;
        my $done = 0;
        while ($done != 1 and $i < @fileALines){
            if ($fileALines[$i] =~ /$line0/ 
                && $fileALines[$i+1] == $line1
                && $fileALines[$i+2] == $line2) {
                $done=1;
            } else {
                print OUT $fileALines[$i];
                $i++;
            }
        }
        close (OUT);
    }
    $lastFile = $_;
}

编辑:在第一行添加了括号检查,稍后进入正则表达式检查重复性,如果发现则转义它们,以免弄乱重复性检查。 p>

【讨论】:

  • 这看起来非常好,并且在小样本上测试时有效。对快速生成列表有什么建议吗?我正在玩排序和查找,但他们似乎真的不喜欢这些字段,尤其是第二个字段中的 11 和 1。
  • 知道了:sort -t "_" -n -k2,2 -k3,3 -k4,4
  • 错误:在正则表达式中处理 bul_2_6_200.txt Unmatched );标记为
  • 第 34 行 = "如果 ($fileALines[$i] =~ /$line0/"
  • 我去看看。该文件 (bul_2_6_200.txt) 是否是您提供的文件集中的后续文件?
【解决方案4】:

你有一个不平凡的问题。在文件 1 的末尾和文件 2 的开头找到重复的文本很容易编写代码。但是你不想删除重复的文本——你想拆分它第二条开始的地方。正确拆分可能很棘手——一个标记是全部大写,另一个是下一行开头的BY

从连续文件中获取示例会有所帮助,但下面的脚本适用于一个测试用例。 在尝试此代码之前,请备份所有文件。代码覆盖现有文件。

实现在Lua。 算法大致是:

  1. 忽略文件 1 末尾和文件 2 开头的空行。
  2. 查找文件 1 结尾和文件 2 开头共有的一长串行。
    • 这是通过尝试一系列 40 行,然后是 39 行,依此类推
  3. 从两个文件中删除序列并将其命名为overlap
  4. 在标题处分割重叠
  5. 将重叠的第一部分附加到文件 1;将第二部分添加到 file2。
  6. 用行列表覆盖文件内容。

代码如下:

#!/usr/bin/env lua

local ext = arg[1] == '-xxx' and '.xxx' or ''
if #ext > 0 then table.remove(arg, 1) end  

local function lines(filename)
  local l = { }
  for line in io.lines(filename) do table.insert(l, (line:gsub('', ''))) end
  assert(#l > 0, "No lines in file " .. filename)
  return l
end

local function write_lines(filename, lines)
  local f = assert(io.open(filename .. ext, 'w'))
  for i = 1, #lines do
    f:write(lines[i], '\n')
  end
  f:close()
end

local function lines_match(line1, line2)
  io.stderr:write(string.format("%q ==? %q\n", line1, line2))
  return line1 == line2 -- could do an approximate match here
end

local function lines_overlap(l1, l2, k)
  if k > #l2 or k > #l1 then return false end
  io.stderr:write('*** k = ', k, '\n')
  for i = 1, k do
    if not lines_match(l2[i], l1[#l1 - k + i]) then
      if i > 1 then
        io.stderr:write('After ', i-1, ' matches: FAILED <====\n')
      end
      return false
    end
  end
  return true
end

function find_overlaps(fname1, fname2)
  local l1, l2 = lines(fname1), lines(fname2)
  -- strip trailing and leading blank lines
  while l1[#l1]:find '^[%s]*$' do table.remove(l1)    end
  while l2[1]  :find '^[%s]*$' do table.remove(l2, 1) end
  local matchsize  -- # of lines at end of file 1 that are equal to the same 
                   -- # at the start of file 2
  for k = math.min(40, #l1, #l2), 1, -1 do
    if lines_overlap(l1, l2, k) then
      matchsize = k
      io.stderr:write('Found match of ', k, ' lines\n')
      break
    end
  end

  if matchsize == nil then
    return false -- failed to find an overlap
  else
    local overlap = { }
    for j = 1, matchsize do
      table.remove(l1) -- remove line from first set
      table.insert(overlap, table.remove(l2, 1))
    end
    return l1, overlap, l2
  end
end

local function split_overlap(l)
  for i = 1, #l-1 do
    if l[i]:match '%u' and not l[i]:match '%l' then -- has caps but no lowers
      -- io.stderr:write('Looking for byline following ', l[i], '\n')
      if l[i+1]:match '^%s*BY%s' then
        local first = {}
        for j = 1, i-1 do
          table.insert(first, table.remove(l, 1))
        end
        -- io.stderr:write('Split with first line at ', l[1], '\n')
        return first, l
      end
    end
  end
end

local function strip_overlaps(filename1, filename2)
  local l1, overlap, l2 = find_overlaps(filename1, filename2)
  if not l1 then
    io.stderr:write('No overlap in ', filename1, ' an

【讨论】:

  • 我很高兴我没有错过一些明显的答案,但“不平凡”听起来并不好。这个脚本看起来不错。不幸的是,它现在以“无重叠”的形式退出。我已将文件样本上传至:dl.getdropbox.com/u/239647/bul_9_5_181.txtdl.getdropbox.com/u/239647/bul_9_5_186.txt
  • 两个问题:您的文件使用 ^L 的方式不一致,我的重叠检测器需要改进。文件需要多长时间?
  • 最大文件250KB左右,不正常。绝大多数都在 100KB 以下。平均值可能是 30KB。这些都是学术文章,所以虽然少数是大型报告,但大多数是几页。谢谢。
  • 好的,我已经改进了一些东西,使它可以在你的两个测试文件上运行。它最多发现 40 行重叠。让我知道进展如何......
  • 这看起来很不错。刚刚对几个文件进行了测试,但今晚/明天早上将对其进行锻炼。谢谢。
【解决方案5】:

存根是否与前一个文件的末尾相同?还是不同的行尾/OCR 错误?

有没有办法辨别文章的开头?也许是缩进的摘要?然后,您可以浏览每个文件并丢弃第一个标题之前和(包括)第二个标题之后的所有内容。

【讨论】:

  • OCR 很好,所以它们在所有实际用途上都是相同的。请参阅对 OP 的评论,了解关于每篇文章开头的正确标记的评论,我使用了所有 300 个字符。那里。 :)
【解决方案6】:

标题和作者总是在一行吗?并且该行是否总是包含大写的“BY”一词?如果是这样,您可能可以使用 awk 完成一项公平的工作,使用这些标准作为开始/结束标记。

编辑:我真的不认为使用 diff 会起作用,因为它是一种用于比较大致相似文件的工具。您的文件(从差异的角度来看)实际上完全不同 - 我认为它会立即不同步。但是,我不是 diff 大师 :-)

【讨论】:

  • 标题和作者的名字通常是分开的,后面的行全部大写。不幸的是,这些并不总是标记文章的开头,例如,某些文章(评论)没有标题/作者姓名,因此使用 DIFF 可能效果最好。
【解决方案7】:

假设存根在两个文件中完全相同:

#!/usr/bin/perl

use strict;

use List::MoreUtils qw/ indexes all pairwise /;

my @files = @ARGV;

my @previous_text;

for my $filename ( @files ) {
    open my $in_fh,  '<', $filename          or die;
    open my $out_fh, '>', $filename.'.clean' or die;

    my @lines = <$in_fh>;
    print $out_fh destub( \@previous_text, @lines );
    @previous_text = @lines;
}


sub destub {
    my @previous = @{ shift() };
    my @lines = @_;

    my @potential_stubs = indexes { $_ eq $lines[0] } @previous;

    for my $i ( @potential_stubs ) {
        # check if the two documents overlap for that index
        my @p = @previous[ $i.. $#previous ];
        my @l = @lines[ 0..$#previous-$i ];

        return @lines[ $#previous-$i + 1 .. $#lines ]
                if all { $_ } pairwise { $a eq $b } @p, @l;

    }

    # no stub detected
    return @lines;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-28
    • 1970-01-01
    • 2012-05-14
    相关资源
    最近更新 更多