Python:数据流中查找特定子串的简单算法
目录
问题背景
HTTP的服务器响应包括响应头部(headers)和body。两者的分割标记是连续的2个\r\n,即\r\n\r\n 4字节的序列。现在的问题是,编写一个简单的算法,找到这个headers和body的分界。
定义:什么是数据流?
一个输入Stream提供一个int read(char[] buff, int len)接口的方法。那么,什么是数据流?
介于read API的定义,我们可以把数据流看作潜在无限多个buff的list。其中,每个buff对象至少包含一个字节的数据(但是TCP/IP所约束的read API的规范可能不是这么说的?),也就是说,len(buff)>=1。
C++中有rope的概念,用于描述list<char[]>这一概念。它可以认为是标准string对象的内存存储优化的实现。
原始方法
把list<char[]>拼接为一个string对象,然后调用find/index/search方法。
缺点:每次拼接操作会导致内存分配和memcpy,导致性能开销太大,并有可能引入动态内存分配触发的大延迟,影响实际系统中的可用性。
学术上的标准方法
把要查找的子串模式编译为‘AC自动机’,或者简单一点,挖掘子串内在的冗余重复,使用‘KMP’,或者可以考虑像‘BM’那样从后往前搜索。
缺点:实现复杂性太高,并且在数据流(list<char[]>)这种特殊的数据结构上还要做修改适配。
实用的简单算法
考虑下面的几点:
- 对数据流而言,存在‘滑动窗口’的概念,也就是说,子串“\r\n\r\n”只有4个字节,理论上,在上次搜索操作没有找到的情况下,理论上,总是查找最近没有搜索的‘滑动窗口’长度的输入即可。
- 问题是,“\r\n\r\n”可能在2个(或更多)buff边界处分割
- 那也就是说,我们需要把原始的数据流list<char[]>对象拆分为2+1类:已经被扫描但没有找到的;待扫描的(这一概念相当于一个滑动窗口);以及待处理的。
- 待扫描的最多只需要保存4个buff对象(!),我们可以把这部分buff对象简单地拼接为一个string对象,如果找到,直接返回
- 如果没有找到,则丢弃第1个buff对象(append到‘已经被扫描但没有找到的’list的最后),算法循环继续
这一算法本质上还是原始的string.find,关键是它为‘数据流’这一限制作了必要的修改,以避免大内存的动态分配开销。