【问题标题】:Best approach to continuously scan for a string in a streaming buffer连续扫描流缓冲区中字符串的最佳方法
【发布时间】:2013-01-28 15:31:45
【问题描述】:

我有这种情况,我的函数连续接收各种长度的数据。数据可以是任何东西。我想找到在此数据中寻找特定字符串的最佳方法。该解决方案需要以某种方式缓冲以前的数据,但我无法解决这个问题。

这是一个问题的例子:

数据输入 -> [\x00\x00\x01\x23B][][LABLABLABLABLA\x01TO][KEN][BLA\x01]...

如果每个 [...] 代表一个数据块,而 [] 代表一个没有项目的数据块,那么扫描字符串 TOKEN 的最佳方法是什么?

更新: 我意识到这个问题有点复杂。 [] 不是分隔符。我只是用它们来描述上面例子中块的结构。此外,TOKEN 本身也不是静态字符串。它是可变长度的。我认为逐行读取的最佳方法是如何将可变长度的流缓冲区读取到行中。

【问题讨论】:

  • 找到字符串后要做什么?你需要一些之前的数据吗?如果没有,您可以使用例如扫描它。 KMP,当你到达一个块的末尾时,获取下一个继续扫描(直到找到或结束流)。
  • 为什么这个问题被标记为“回调”?
  • @junix:由于“块”中的函数“接收数据”,我猜有问题的函数是一个回调,每个块调用一次。

标签: c callback stream buffer


【解决方案1】:

搜索TOKEN最简单的方法是:

  • 尝试从流中的位置 0 开始匹配“TOKEN”
  • 尝试从流中的位置 1 开始匹配“TOKEN”

因此,您需要缓冲的只是流中等于“TOKEN”长度的字节数(5 个字节,或者实际上是 4 个字节即可)。在每个位置尝试匹配“TOKEN”,这可能需要等到您至少有 5 个字节读入缓冲区。如果匹配失败,倒回到你开始匹配的地方,加一。由于您不会倒带超过您正在搜索的字符串的长度(减一),这就是您真正需要的所有缓冲区。

接下来的技术问题是,当您从流中连续读取数据时,如何保持 5 字节的缓冲数据。一种方式是所谓的“循环缓冲区”。另一种方法,尤其是在令牌很小的情况下,是使用更大的缓冲区,并且每当您离结尾太近时,将所需的字节复制到开头并重新开始。

如果您的函数是一个回调,为每个新数据块调用一次,那么您将需要从一次调用到下一次调用保持某种状态,以允许跨越两个数据块的匹配。如果幸运的话,您的回调 API 包含一个“用户数据指针”,您可以将其设置为指向您喜欢的任何包含缓冲区的结构。如果没有,您将需要全局或线程局部变量。

如果流的数据速率很高,那么您可能需要考虑使用 KMP 算法或其他方式来加快速度。

【讨论】:

  • “如果匹配失败,倒退到你开始匹配的地方,加一”有一个弱点。关于匹配成功时该怎么做的答案尚不清楚。候选暗示是,如果匹配成功,则不需要倒带。如果匹配成功与否,则需要倒回到您开始的位置,因为针/令牌可以部分匹配自身。或者简单地说,“在每个位置尝试匹配“TOKEN”......进入你的缓冲区。如果匹配失败,倒回......你真的需要。”
  • @chux:我可能误解了问题的上下文,但我想我假设如果匹配成功(即你找到了整个令牌)你会返回 true 这就是结束它。取决于代码是否只需要测试令牌的存在,或者做更多的事情。即使您应该计算令牌,无论您是否应该在成功时倒带,它仍然是开放的:字符串 aaaa 是否包含令牌 aa 两次(在这种情况下不倒带)或三次(在这种情况下倒带)?需要更多规范。
  • 同意你的 cmets,特别是关于需要更多规范的问题。
  • +1 来自我;这是一个很好的答案。这确实应该是公认的答案。如果它有一个例子,我想它本来会是......而且我不会觉得有必要自己写。
【解决方案2】:

抱歉,由于我对问题的理解不正确,我投票删除了我之前的答案。我没有仔细阅读,并认为 [] 是令牌分隔符。

对于您的问题,我建议您构建一个基于简单计数器的小型状态机: 对于每个字符,您都执行以下伪代码:

if (received_character == token[pos]) {
    ++pos;
    if (pos >= token_length) {
        token_received = 1;
    }
}
else {
    pos = 0; // Startover
}

这需要最少的处理器周期和最少的内存,因此您不需要缓冲除了刚刚收到的块之外的任何内容。

【讨论】:

  • 这个非回溯答案对不包含重复字母的针“TOKEN”的效果要好于其他针。例如,它没有在大海捞针“AAAB”中找到针“AAB”,因为当它击中第三个 A 时,它会将 pos 设置为零(因为它需要一个 B)并且没有意识到它已经看到了针的初始子串。当然,可以为任何给定的针构建一个有限状态机(因此,如果您现在知道针,这可能是一个不错的选择)。但是在运行时构建该机器的代码可能过大了。
  • @SteveJessop 这不只是开头字母的问题吗?无论如何+1指出这个问题。实际上,您可以在运行您的单词时构建一个具有多个版本的计数器的非确定性机器。 (当然会使实现更加复杂)
  • 是的,我认为它必须涉及第一个字母。失败是指针的某些初始段出现在其他地方(因此被重置丢弃),这意味着第一个字母必须作为该初始段的一部分出现在其他地方。并且同意,如果您缓冲它,您可以使用数量等于您必须缓冲的字节数的计数器轻松地做到这一点。如果提问者想做些聪明的事,那么有大量关于这个主题的文献:-)
  • 这确实是容易出错的逻辑,特别是考虑到OP已经澄清,“...... TOKEN不是静态字符串......”。尽管如此,我很惊讶它被接受了,但这并不能阻止我投反对票。 :)
  • @Seb 嗯,是的。也许您没有注意到,更新是在我发布答案后插入的。但是,我既没有看到您的方法有什么优势,也没有看到我的方法容易出错?它很简单,完全符合 OP 的要求。
【解决方案3】:

如果指针包含在内存中,则可以假设您可以分配一个相同大小的对象来读取(例如char input_array[needle_size];)。

要开始搜索过程,请用文件中的字节填充该对象(例如size_t sz = fread(input_array, 1, input_size, input_file);)并尝试匹配(例如if (sz == needle_size && memcmp(input_array, needle, needle_size) == 0) { /* matched */ }

如果匹配失败,或者您想在匹配成功后继续搜索,请将位置前移一个字节(例如memmove(input_array, input_array + 1, input_size - 1); input_array[input_size - 1] = fgetc(input_file);,然后重试。

有人担心这个想法在 cmets 中复制了太多字节。虽然我不认为这种担忧有很大的好处(因为没有证据表明有重要价值),但可以通过使用循环数组来避免复制;我们在pos % needle_size 处插入新字符,并比较该边界之前和之后的区域,就好像它们分别是尾部和头部一样。例如:

void find_match(FILE *input_file, char const *needle, size_t needle_size) {
    char input_array[needle_size];
    size_t sz = fread(input_array, 1, needle_size, input_file);
    if (sz != needle_size) {
        // No matches possible
        return;
    }

    setvbuf(input_file, NULL, _IOFBF, BUFSIZ);
    unsigned long long pos = 0;
    for (;;) {
        size_t cursor = pos % needle_size;
        int tail_compare = memcmp(input_array, needle + needle_size - cursor, cursor),
            head_compare = memcmp(input_array + cursor, needle, needle_size - cursor);
        if (head_compare == 0 && tail_compare == 0) {
            printf("Match found at offset %llu\n", pos);
        }
        int c = fgetc(input_file);
        if (c == EOF) {
            break;
        }
        input_array[cursor] = c;
        pos++;
    }
}

【讨论】:

  • 抱歉,这个解决方案不仅对于代码应该执行的操作而言过于复杂,而且还浪费了 CPU 功率和内存。例如。一直复制整个样本是非常低效的。
  • @junix 首先,在考虑速度之前,我们需要考虑正确性。如果它不能 100% 地工作,那就是有问题,需要修复。一旦它在 100% 的时间内工作,我们就可以对其进行分析,以确定它是否是整个软件中最重要的瓶颈。您是否有任何证据表明这是最重要的瓶颈?该代码通常会使用哪些参数显着减慢速度?我不相信你明白你在说什么......
  • @junix 正如我所说,我认为你不明白你在说什么。否则你会明白分析器会识别出这些你可能还不了解的东西,称为缓存未命中;正是那些缓存未命中(以及其他类型的未命中)可能会导致严重的瓶颈。除此之外,这里的主要瓶颈是 IO,不是因为getchar (test it with setvbuf, I dare you),而是因为底层文件系统很慢。你也有同样的问题。
  • @junix P.S.不要求第一个块令牌的大小;我不知道你是从哪里误解的......这只会加剧我的担忧,即你不知道你在说什么。此外,当needle 是像"ololo" 这样的重复字符串并且流包含像"olololo" 这样的字符串的重叠实例(那里有两个匹配项)时,您的算法将失败(它只识别一个匹配项)。当你吃过一些不起眼的馅饼时,请随时联系我。我相信我们可以一起学到很多东西......
  • 请不要谈论你不明白的事情。 您是否有任何证据表明我的代码将成为最严重的瓶颈? 如果没有,那么花宝贵的时间考虑其他部分进行优化也许是个好主意。如果needle 包含 10 次 a 并且流包含 9 次 a 后跟 b,则流将读取 9 次后跟 b 的 a 到 input_array,与 needle 比较,并与一次调用 memcmp 不匹配将10个字符比较后识别一次。您的代码还需要比较相同的 10 个字符一次。
猜你喜欢
  • 2014-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-25
  • 2016-12-11
  • 1970-01-01
  • 2020-05-07
相关资源
最近更新 更多