Я хочу реализовать алгоритм поиска и замены в потоке, где я бы создал класс потока, который будет принимать исходный поток в качестве входных данных, а метод чтения будет читать из исходного потока по мере необходимости и выполнять поиск/замену как поток читается. Это предотвратит одновременную загрузку всего набора данных в память и позволит выполнять поиск и замену в очень больших наборах данных.
Чтобы сделать это, я подумал, что начну с существующего алгоритма поиска, адаптирую его к потоковому подходу, а затем адаптирую функцию замены.
Ниже приведена реализация Кнута Морриса Пратта, которую я адаптировал из онлайн-примера. Кто-нибудь адаптировал что-то подобное к потоковому подходу? Мне нужно будет учитывать поиск через границы чтения, что я пока не знаю, как бы я это сделал.
/// <summary>
/// The KMP matching algorithm uses degenerating property (SearchPattern having same sub-SearchPatterns appearing more than once in the SearchPattern)
/// of the SearchPattern and improves the worst case complexity to O(n). The basic idea behind KMP’s algorithm is: whenever we
/// detect a mismatch (after some matches), we already know some of the characters in the text of the next window. We take
/// advantage of this information to avoid matching the characters that we know will anyway match.
/// </summary>
/// <seealso cref="https://www.geeksforgeeks.org/kmp-algorithm-for-pattern-searching/"/>
public class KMPSearch
{
/// <summary>
/// Pattern we are looking for
/// </summary>
readonly string sSearchPattern_m;
/// <summary>
/// Text we are looking in
/// </summary>
readonly string sData_m;
/// <summary>
/// Index for text
/// </summary>
private int iDataPosition_m;
private int iDataLength_m;
/// <summary>
/// Index for search pattern
/// </summary>
private int iSearchPatternPosition_m;
/// <summary>
/// A proper prefix is prefix with whole string not allowed. For example, prefixes of “ABC” are “”, “A”, “AB” and “ABC”.
/// Proper prefixes are “”, “A” and “AB”. Suffixes of the string are “”, “C”, “BC” and “ABC”.
/// </summary>
readonly int[] lstLongestProperPrefix_m;
public KMPSearch(string sSearchPattern, string sData)
{
this.sSearchPattern_m = sSearchPattern;
this.sData_m = sData;
this.iDataLength_m = sData.Length;
// create lps that will hold the longest prefix suffix values for SearchPattern
this.lstLongestProperPrefix_m = new int[sSearchPattern.Length];
// Pre-process the SearchPattern (calculate lps array)
this.ComputeLPSArray();
this.iDataPosition_m = 0; // index for txt
this.iSearchPatternPosition_m = 0; // index for pat
}
/// <summary>
/// Find next match
/// </summary>
/// <returns></returns>
public int Next()
{
int iMatchIndex = -1;
//We start comparison of pat[iSearchPatternPosition_m] with iSearchPatternPosition_m = 0 with characters of current window of text.
//We keep matching characters txt[iDataPosition_m] and pat[iSearchPatternPosition_m] and keep incrementing iDataPosition_m and iSearchPatternPosition_m while
//pat[iSearchPatternPosition_m] and txt[iDataPosition_m] keep matching.
while (iDataPosition_m < this.iDataLength_m)
{
if (this.sSearchPattern_m[iSearchPatternPosition_m] == this.sData_m[iDataPosition_m])
{
iSearchPatternPosition_m++;
iDataPosition_m++;
}
if (iSearchPatternPosition_m == sSearchPattern_m.Length)
{
//Console.WriteLine("Found SearchPattern at index %d ", iDataPosition_m - iSearchPatternPosition_m);
iMatchIndex = iDataPosition_m - iSearchPatternPosition_m;
iSearchPatternPosition_m = this.lstLongestProperPrefix_m[iSearchPatternPosition_m - 1];
}
// mismatch after j matches
else if (iDataPosition_m < this.iDataLength_m && this.sSearchPattern_m[iSearchPatternPosition_m] != this.sData_m[iDataPosition_m])
{
//When we see a mismatch
//* We know that characters pat[0..iSearchPatternPosition_m - 1] match with txt[iDataPosition_m-iSearchPatternPosition_m..iDataPosition_m - 1]
// (Note that iSearchPatternPosition_m starts with 0 and increment it only when there is a match).
//* We also know (from above definition) that lps[iSearchPatternPosition_m - 1] is count of characters of pat[0…iSearchPatternPosition_m - 1]
// that are both proper prefix and suffix.
//* From above two points, we can conclude that we do not need to match these lps[iSearchPatternPosition_m - 1] characters with
// txt[iDataPosition_m -iSearchPatternPosition_m..iDataPosition_m - 1] because we know that
// these characters will anyway match. Let us consider above example to understand this.
// Do not match lps[0..lps[iSearchPatternPosition_m - 1]] characters,
// they will match anyway
if (iSearchPatternPosition_m != 0)
{
iSearchPatternPosition_m = this.lstLongestProperPrefix_m[iSearchPatternPosition_m - 1];
}
else
{
iDataPosition_m = iDataPosition_m + 1;
}
}
if (iMatchIndex > -1)
{
return iMatchIndex;
}
}
return iMatchIndex;
}
/// <summary>
/// A proper prefix is prefix with whole string not allowed. For example, prefixes of “ABC” are “”, “A”, “AB” and “ABC”.
/// Proper prefixes are “”, “A” and “AB”. Suffixes of the string are “”, “C”, “BC” and “ABC”.
/// Fills lps for given pattern pat[0..M-1]
/// lps[i] = the longest proper prefix of pat[0..i] which is also a suffix of pat[0..i].
/// </summary>
private void ComputeLPSArray()
{
// length of the previous longest prefix suffix
int len = 0;
this.lstLongestProperPrefix_m[0] = 0; // lps[0] is always 0
// the loop calculates lps[i] for i = 1 to M-1
int i = 1;
while (i < this.sSearchPattern_m.Length)
{
if (this.sSearchPattern_m[i] == this.sSearchPattern_m[len])
{
len++;
this.lstLongestProperPrefix_m[i] = len;
i++;
}
else // (pat[i] != pat[len])
{
// This is tricky. Consider the example.
// AAACAAAA and i = 7. The idea is similar
// to search step.
if (len != 0)
{
len = this.lstLongestProperPrefix_m[len - 1];
// Also, note that we do not increment
// i here
}
else // if (len == 0)
{
this.lstLongestProperPrefix_m[i] = 0;
i++;
}
}
}
}
}
Затем я взял описанный выше алгоритм и модифицировал его в Stream. В качестве доказательства концепции поток будет вызывать событие во время своего метода чтения каждый раз, когда находит шаблон поиска. В настоящее время у него есть ограничение, заключающееся в невозможности поиска за границами чтения. Таким образом, если за раз считывается 1024 байта, а длина исходного потока составляет 2048 байт, для чтения всего потока выполняется два чтения. Проблема в том, что если шаблон поиска начинается с индекса 1000 и имеет длину 40 байт, он не будет найден. Я думаю, что как только эта проблема будет решена, реальная функциональность замены не будет такой сложной. Я ищу предложения о том, как реализовать поиск через границы чтения. Вероятно, это связано с кэшированием части предыдущего чтения. Кто-нибудь знает о потоковой реализации, подобной этой, или предложениях?
public class KMPSearchStream : Stream
{
public override bool CanRead { get { return true; } }
public override bool CanSeek => throw new NotImplementedException();
public override bool CanWrite => throw new NotSupportedException();
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public class PatternFoundEventArgs
{
public int Index { get; internal set; }
}
public delegate void PatternFoundEvent(PatternFoundEventArgs e);
private Stream strSource_m;
/// <summary>
/// Pattern we are looking for
/// </summary>
private byte[] searchPattern_m;
/// <summary>
/// Text we are looking in
/// </summary>
private byte[] data_m;
/// <summary>
/// Index for text
/// </summary>
private int iDataPosition_m;
private int iDataLength_m;
/// <summary>
/// Index for search pattern
/// </summary>
private int iSearchPatternPosition_m;
/// <summary>
/// A proper prefix is prefix with whole string not allowed. For example, prefixes of “ABC” are “”, “A”, “AB” and “ABC”.
/// Proper prefixes are “”, “A” and “AB”. Suffixes of the string are “”, “C”, “BC” and “ABC”.
/// </summary>
readonly int[] lstLongestProperPrefix_m;
public KMPSearchStream(Stream strSource, byte[] searchPattern)
{
if (strSource == null)
{
throw new ArgumentNullException(nameof(strSource), "Source stream is null.");
}
if (searchPattern == null || searchPattern.Length == 0)
{
throw new ArgumentNullException(nameof(searchPattern), "Pattern to find is null or empty.");
}
this.strSource_m = strSource;
this.searchPattern_m = searchPattern;
// create lps that will hold the longest prefix suffix values for SearchPattern
this.lstLongestProperPrefix_m = new int[searchPattern.Length];
// Pre-process the SearchPattern (calculate lps array)
this.ComputeLPSArray();
this.iDataPosition_m = 0; // index for txt
this.iSearchPatternPosition_m = 0; // index for pat
}
public event PatternFoundEvent OnPatternFound;
public override void Flush()
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
int iRead = this.strSource_m.Read(buffer, offset, count);
this.iDataPosition_m = 0; // index for txt
this.iSearchPatternPosition_m = 0; // index for pat
this.data_m = buffer;
this.iDataPosition_m = offset;
this.iDataLength_m = iRead;
int iIndex;
while ((iIndex = this.Next()) > -1)
{
this.OnPatternFound(new PatternFoundEventArgs()
{
Index = iIndex
});
}
return iRead;
}
/// <summary>
/// A proper prefix is prefix with whole string not allowed. For example, prefixes of “ABC” are “”, “A”, “AB” and “ABC”.
/// Proper prefixes are “”, “A” and “AB”. Suffixes of the string are “”, “C”, “BC” and “ABC”.
/// Fills lps for given pattern pat[0..M-1]
/// lps[i] = the longest proper prefix of pat[0..i] which is also a suffix of pat[0..i].
/// </summary>
private void ComputeLPSArray()
{
// length of the previous longest prefix suffix
int len = 0;
this.lstLongestProperPrefix_m[0] = 0; // lps[0] is always 0
// the loop calculates lps[i] for i = 1 to M-1
int i = 1;
while (i < this.searchPattern_m.Length)
{
if (this.searchPattern_m[i] == this.searchPattern_m[len])
{
len++;
this.lstLongestProperPrefix_m[i] = len;
i++;
}
else // (pat[i] != pat[len])
{
// This is tricky. Consider the example.
// AAACAAAA and i = 7. The idea is similar
// to search step.
if (len != 0)
{
len = this.lstLongestProperPrefix_m[len - 1];
// Also, note that we do not increment
// i here
}
else // if (len == 0)
{
this.lstLongestProperPrefix_m[i] = 0;
i++;
}
}
}
}
/// <summary>
/// Find next match
/// </summary>
/// <returns></returns>
public int Next()
{
int iMatchIndex = -1;
//We start comparison of pat[iSearchPatternPosition_m] with iSearchPatternPosition_m = 0 with characters of current window of text.
//We keep matching characters txt[iDataPosition_m] and pat[iSearchPatternPosition_m] and keep incrementing iDataPosition_m and iSearchPatternPosition_m while
//pat[iSearchPatternPosition_m] and txt[iDataPosition_m] keep matching.
while (iDataPosition_m < this.iDataLength_m)
{
if (this.searchPattern_m[iSearchPatternPosition_m] == this.data_m[iDataPosition_m])
{
iSearchPatternPosition_m++;
iDataPosition_m++;
}
if (iSearchPatternPosition_m == searchPattern_m.Length)
{
//Console.WriteLine("Found SearchPattern at index %d ", iDataPosition_m - iSearchPatternPosition_m);
iMatchIndex = iDataPosition_m - iSearchPatternPosition_m;
iSearchPatternPosition_m = this.lstLongestProperPrefix_m[iSearchPatternPosition_m - 1];
}
// mismatch after j matches
else if (iDataPosition_m < this.iDataLength_m && this.searchPattern_m[iSearchPatternPosition_m] != this.data_m[iDataPosition_m])
{
//When we see a mismatch
//* We know that characters pat[0..iSearchPatternPosition_m - 1] match with txt[iDataPosition_m-iSearchPatternPosition_m..iDataPosition_m - 1]
// (Note that iSearchPatternPosition_m starts with 0 and increment it only when there is a match).
//* We also know (from above definition) that lps[iSearchPatternPosition_m - 1] is count of characters of pat[0…iSearchPatternPosition_m - 1]
// that are both proper prefix and suffix.
//* From above two points, we can conclude that we do not need to match these lps[iSearchPatternPosition_m - 1] characters with
// txt[iDataPosition_m -iSearchPatternPosition_m..iDataPosition_m - 1] because we know that
// these characters will anyway match. Let us consider above example to understand this.
// Do not match lps[0..lps[iSearchPatternPosition_m - 1]] characters,
// they will match anyway
if (iSearchPatternPosition_m != 0)
{
iSearchPatternPosition_m = this.lstLongestProperPrefix_m[iSearchPatternPosition_m - 1];
}
else
{
iDataPosition_m = iDataPosition_m + 1;
}
}
if (iMatchIndex > -1)
{
return iMatchIndex;
}
}
return iMatchIndex;
}
}