处理大型文本文件

5
问题: 我有一个巨大的原始文本文件(假设为3G),我需要遍历文件中的每个单词并找出该单词在文件中出现了多少次。 我的解决方案: 将巨型文件分割成多个文件,每个分割文件都按排序方式存储单词。例如,以“a”开头的所有单词将存储在“_a.dic”文件中。因此,在任何时候,我们不会超过26个文件。
这种方法的问题是,
我可以使用流读取文件,但想使用线程读取文件的某些部分。例如,使用单独的线程读取0-1024字节(至少有4-8个线程,根据盒子中存在的处理器数量而定)。这可行吗?还是我在做梦?
是否有更好的方法?
注意:它应该是纯C++或C语言解决方案。不允许使用数据库等。

1
你能更具体地说明一下如何搜索文本文件吗?这个文件相对静态,需要在静态文件上运行多次搜索吗?你需要为许多不同的单词进行搜索,还是关键是尽快完成单个单词的搜索?你搜索的单词通常会有模式吗 - 即少数单词占大多数搜索量。 - jthg
您希望避免一次性加载到内存中,流正是为您的情况而创建的。 - Matthieu M.
3
使用线程读取文件的目的是什么?假设您的文件存储在传统硬盘上,直接流式读取文件是最快的方法。如果您有多个线程同时请求文件的不同部分,您的硬盘头会反复跳动,这将抵消任何通过多线程获取的优势。 - StriplingWarrior
R文本挖掘包(tm),生成语料库。 - Nicholas Hamilton
10个回答

15
你需要查看Kernighan和Pike的'编程实践',特别是第三章。
在C++中,使用基于字符串和计数的映射(std::map<string,size_t>,如果我没记错)。读取文件(只需一次-它太大了,不能多次读取),并将其分割为单词(对于某个“单词”的定义),并递增找到的每个单词的映射条目中的计数。
在C中,你必须自己创建地图。(或者找到David Hanson的“C接口和实现”)。
或者你可以使用Perl、Python或Awk(它们都有关联数组,相当于一个映射)。

根据3GB文件的内容和您拥有的内存量,将其全部读入映射表中可能会因映射表的内存开销而导致内存不足。 - jthg
5
英语中大约有10万个单词。假设“单词”的定义不区分大小写,且包含标点符号,因此每个单词有5个变体。假设平均每个单词有10个字符(过多),并且映射开销为22字节。那么我们就有了5 * 100,000 * 32 = 16 MB的存储需求。哪种计算机会受到这个问题的困扰呢? - Jonathan Leffler
1
我不确定它是否会这样。如果只有几千个单词,地图大小就不会那么大。因此,这取决于不同单词的数量,而不是文件大小。 - Matthieu M.
我手头有一个英语单词列表(我认为是Moby列表,但不确定),大约显示了三十万个单词,但这仍然不是特别难处理的。我不会考虑标点符号 - 在计算单词数之前,我会将其剥离(除了缩略词的情况)。大多数人不希望xx,被视为两个不同的单词。 - Jerry Coffin
1
我做过非常类似的事情,只有一个细节不同。我的初始代码确实使用了 std::map<std::string, T>,但是一旦我解决了错误,我将其更改为 std::map<StringShim, T>StringShim 是一个简单的 4 字节类,包装了一个 char*;实际的字符串由单个 StringPool 管理。这样效率显著提高了。 - MSalters
显示剩余5条评论

6

我认为使用多个线程并行读取文件的部分内容并不会有太大帮助。我预计这个应用程序受限于硬盘的带宽和延迟,而不是实际的单词计数。这样一个多线程版本可能表现得更差,因为“准随机”文件访问通常比“线性文件”访问慢。

如果CPU在单线程版本中确实很忙,可能会有潜在的加速效果。一个线程可以读取大块数据并将它们放入有限容量的队列中。一堆其他工作线程可以操作各自的块并计算单词。在计数工作线程完成后,您需要合并单词计数器。


2
我会称之为近乎确定的事情。CPU 应该比磁盘更快地处理字节,因此没有什么可以并行化的东西。 - jprete
1
我同意。我甚至可以更进一步地说,即使整个文件都在内存中,CPU 仍然会比从内存中读取单词更快地处理它们。 - jthg
不同意上一句话。从内存中读取文本会触发CPU的预取器,速度非常快。瓶颈将是O(log N)的随机访问搜索单词计数器。它们不太可能全部适合L2缓存中。 - MSalters
是的,预取器确实帮了很大的忙,但我已经假设了完美的预取。只为了看看我说的话是否荒谬,我进行了一个粗略的数量级计算:内存带宽约为每秒10GB。假设CPU是一个Core 2,可以在每个周期内执行一次比较(对于单核心来说),那就等于大约(每个周期8字节)*(每秒20亿个周期)=每秒16GB。这个数量级大致相同。有人愿意提供更准确的数字吗? - jthg

3

首先 - 决定用于保存单词的数据结构。

显而易见的选择是map。但也许Trie会更好地为您服务。在每个节点中,您保存单词的计数。 0意味着它只是一个单词的一部分。您可以使用流并基于字符读取文件将其插入到trie中。

其次 - 多线程是好还是不好? 这个问题不容易回答。根据数据结构增长的大小以及您如何并行化,答案可能会有所不同。

  1. 单线程-简单直接易于实现。
  2. 多个读取器线程和一个数据结构的多线程。然后,您必须同步访问数据结构。在Trie中,您只需要锁定实际所在的节点,因此多个读取器可以在没有太多干扰的情况下访问数据结构。自平衡树可能会有所不同,特别是在重新平衡时。
  3. 多个读取器线程,每个线程都有自己的数据结构。每个线程在读取文件的一部分时建立自己的数据结构。完成后,必须组合结果(这应该很容易)。

你必须考虑一件事-你必须为每个线程找到一个单词边界来开始,但这不应该是一个大问题(例如,每个线程在其起点行走直到第一个单词边界并从那里开始,在结束时每个线程完成它正在处理的单词)。


很好的总结,加1赞成提到trie作为一个不明显的解决方案。 - Konrad Rudolph

1

虽然您可以使用第二个线程在读取数据后分析数据,但这样做可能不会带来太大的收益。尝试使用多个线程读取数据几乎肯定会降低速度而不是提高速度。使用多个线程处理数据是没有意义的--处理速度将比读取速度快得多,因此即使只有一个额外的线程,限制也将是磁盘速度。

获得显着速度的一种(可能的)方法是绕过通常的iostreams--虽然有些iostreams几乎与使用C FILE*一样快,但我不知道有什么东西真正更快,有些则慢得多。如果您在运行具有明显不同于C的I/O模型的系统(例如Windows),则可以通过小心处理获得更多的收益。

问题很简单:您正在阅读的文件(可能)比您可用的缓存空间大,但如果您不打算再次读取文件的块(至少如果您做得明智),则不会从缓存中获得任何好处。因此,您想告诉系统绕过任何缓存,并尽可能直接从磁盘驱动器传输数据到您可以处理它的内存中。在类Unix系统中,这可能是通过使用open()read()实现的(并不会带来太大好处)。在Windows中,这是通过CreateFileReadFile实现的,向CreateFile传递FILE_FLAG_NO_BUFFERING标志-如果正确使用,则速度可能增加约一倍。
您还得到了一些答案,提倡使用各种并行构造进行处理。我认为这些基本上是错误的。除非您做了一些极其愚蠢的事情,否则统计文件中的单词所需的时间将仅比简单读取文件多几毫秒。
我会使用两个缓冲区,每个缓冲区大约一兆大小。将数据读入一个缓冲区,将该缓冲区交给计数线程来计算其中的单词数。在此期间,将数据读入第二个缓冲区。当这些完成后,基本上交换缓冲区并继续。在交换缓冲区时,您需要进行一些额外的处理,以处理可能跨越从一个缓冲区到下一个缓冲区的单词,但这相当简单(基本上,如果缓冲区不以空格结尾,则在开始操作下一个数据缓冲区时仍处于单词中)。
只要您确定它只会在多处理器(多核心)机器上使用,使用真正的线程就可以了。如果有可能在单核心机器上执行此操作,最好使用具有重叠I/O的单个线程。

1

正如其他人所指出的,瓶颈将是磁盘 I/O。因此,我建议您使用重叠 I/O。这基本上倒转了程序逻辑。您不必在代码中决定何时进行 I/O,而是告诉操作系统在完成一些I/O时调用您的代码。如果您使用I/O完成端口,甚至可以告诉操作系统为处理文件块使用多个线程。


0

首先,我相信C/C++不是处理这个问题的最佳方法。 理想情况下,您可以使用一些map/reduce来进行并行处理。

但是,假设您的约束条件,以下是我的建议。

1)将文本文件拆分为较小的块。 您不必按单词的第一个字母进行拆分。 只需将它们分成5000字的块即可。 在伪代码中,您可以执行以下操作:

index = 0

numwords = 0

mysplitfile = openfile(index-split.txt)

while (bigfile >> word)

mysplitfile << word

numwords ++

if (numwords > 5000)

    mysplitfile.close()

    index++

    mysplitfile = openfile(index-split.txt)

2) 使用共享的映射数据结构和pthread来生成新线程以读取每个子文件。再次提供伪代码:

maplock = create_pthread_lock()

sharedmap = std::map()

对于每个index-split.txt文件:

spawn-new-thread(myfunction, filename, sharedmap, lock)

转换为中文:

dump_map(sharedmap)

void myfunction(filename, sharedmap) {

localmap = std::map<string, size_t>();

file = openfile(filename)

while (file >> word)

    if !localmap.contains(word)
         localmap[word] = 0

    localmap[word]++

acquire(lock)
for key,value in localmap
    if !sharedmap.contains(key)
         sharedmap[key] = 0

    sharedmap[key] += value
release(lock)

}

很抱歉语法错误,最近一直在写Python。


使用锁肯定不是一个好主意。你会破坏并行性。如果你想要进行MT,更简单的方法是让每个线程玩自己的地图,最后再合并它们。 - Matthieu M.
嗨,Spitzanator,你读过《Python自然语言处理》这本书吗? - zeroin23
有人能解释一下为什么这个被踩了吗?这是一个合适的回答吗,还是像之前提到的使用多线程读取光盘不太有效?还是因为Pythonic伪代码的原因? - asyncwait

0

不是C语言,看起来有点丑陋,但只需要2分钟就能完成:

perl -lane '$h{$_}++ for @F; END{for $w (sort {$h{$b}<=>$h{$a} || $a cmp $b} keys %h) {print "$h{$w}\t$w"}}' file > freq

使用-n循环遍历每一行
使用-a将每一行分割成@F单词
每个$_单词都会增加哈希表%h
一旦到达fileEND
按频率$h{$b}<=>$h{$a}对哈希表进行sort
如果两个频率相同,则按字母顺序排序$a cmp $b
打印频率$h{$w}和单词$w
将结果重定向到文件'freq'

我在一个大小为3.3GB,包含580,000,000个单词的文本文件上运行了这段代码。
Perl 5.22在173秒内完成。

我的输入文件已经去除了标点符号,并将大写字母转换为小写字母,使用了以下代码:
perl -pe "s/[^a-zA-Z \t\n']/ /g; tr/A-Z/a-z/" file_raw > file
(运行时间为144秒)


字数统计脚本可以用awk编写:
awk '{for (i=1; i<=NF; i++){h[$i]++}} END{for (w in h){printf("%s\t%s\n", h[w], w)}}' file | sort -rn > freq

0

C语言的解决方案?

我认为Perl就是为了这个目的而生的。


我同意。在Perl中处理文本文件是相对自然的。 - Martin York
再说,用C++编写这个解决方案是直截了当和容易的(尽管多线程可能会在C++和Perl中提出相同的问题)。 - Konrad Rudolph
我觉得需要使用C++来计算文件中单词的实例数量,无论文件大小如何,这个想法对我来说很奇怪。我的意思并不是冒犯。我相信这里提出的解决方案对某些人来说完全可行,但我比较传统。只需要10行Perl代码就能搞定。 - Oren Mazor
我不知道为什么这篇帖子被踩了。只是因为它不是C语言解决方案吗?正如Oren所说,如果一开始使用perl(或awk,...),原始问题就已经解决了。速度可能不像精细的C版本那样快,但它们基本上处于相同的复杂性水平。 - Codism

0

流只有一个光标。如果您同时使用多个线程访问流,则无法确定要读取的位置。读取是从光标位置完成的。

我会做的是只有一个线程(可能是主线程)读取流并将读取的字节分派给其他线程。

例如:

  • 线程#i已准备好,并要求主线程提供下一部分,
  • 主线程读取下一个1Mb并将其提供给线程1,
  • 线程#i读取1Mb并按您想要的方式计算单词数,
  • 线程#i完成其工作并再次请求下一个1Mb。

通过这种方式,您可以将流的读取与流的分析分开。


我认为在处理线程方面没有任何价值。这种任务绝对是I/O受限的。你的硬盘无法以足够快的速度提供数据,甚至无法加载一个核心。 - divegeek

0

3
我甚至无法想象使用正则表达式搜索一个3GB的文件所带来的恐惧。 - jthg
除非......正则表达式引擎优化为流处理。 - jthg
我有一个程序,定期使用正则表达式处理大量数据,而且速度非常快。 - ryber

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接