【问题标题】:Fastest way to split a huge text into smaller chunks将大文本拆分成小块的最快方法
【发布时间】:2016-03-29 13:44:25
【问题描述】:

我已经使用下面的代码来拆分字符串,但是需要很多时间。

using (StreamReader srSegmentData = new StreamReader(fileNamePath))
{
    string strSegmentData = "";
    string line = srSegmentData.ReadToEnd();
    int startPos = 0;

    ArrayList alSegments = new ArrayList();
    while (startPos < line.Length && (line.Length - startPos) >= segmentSize)
    {
        strSegmentData = strSegmentData + line.Substring(startPos, segmentSize) + Environment.NewLine;
        alSegments.Add(line.Substring(startPos, segmentSize) + Environment.NewLine);
        startPos = startPos + segmentSize;
    }
}

请建议我另一种方法将字符串拆分成固定大小的小块

【问题讨论】:

  • String.Split 可能是一种选择
  • 这可能会有所帮助:*.com/questions/568968/…
  • 我们没有任何特定的字符可以使用Split,只需要根据大小(字符数)分隔字符串
  • 哪位需要很长时间? srSegmentData.ReadToEnd();while 循环?你真的测量过吗?
  • 你为什么使用ArrayList? 10 年前的事了。

标签: c# .net substring


【解决方案1】:

首先,您应该定义 块大小 的含义。如果您的意思是 具有固定数量的 代码单元 的块,那么您的实际算法可能很慢,但它可以工作。如果这不是您想要的,而您实际上是指 具有固定数量 字符 的块,那么它就坏了。我在这篇 Code Review 帖子中讨论了一个类似的问题:Split a string into chunks of the same length 然后我将在这里仅重复相关部分。

  • 您正在对 Char 进行分区,但 String 是 UTF-16 编码的,那么您可能会在至少三种情况下产生损坏的字符串:

    1. 一个字符由多个代码单元编码。该字符的 Unicode 代码点被编码为两个 UTF-16 代码单元,每个代码单元最终可能分为两个不同的片段(并且两个字符串都将是无效)。
    2. 一个字符由多个代码点组成。您正在处理由两个单独的 Unicode 代码点组成的字符(例如汉字符?)。
    3. 一个字符具有组合字符或修饰符。这比您想象的更常见:例如 Unicode 组合字符,如 U+0300 COMBINING GRAVE ACCENT 用于 build à 和 Unicode 修饰符,例如U+02BC MODIFIER LETTER APOSTROPHE
  • character 对编程语言和人类的定义非常不同,例如在斯洛伐克语中, 是单个字符,但它是由 2/3 Unicode 制成的在这种情况下,代码点也是 2/3 UTF-16 代码单元,然后是 "dž".Length &gt; 1。有关此问题和其他文化问题的更多信息,请访问How can I perform a Unicode aware character by character comparison?
  • 存在连字。假设一个连字是一个代码点(并且还假设它被编码为一个代码单元),那么您会将其视为单个字形,但它代表两个字符。在这种情况下该怎么办? character 的一般定义可能非常模糊,因为根据使用该词的学科,它具有不同的含义。您无法(可能)正确处理所有事情,但您应该设置一些约束并记录代码行为。

一个提议的(未经测试的)实现可能是这样的:

public static IEnumerable<string> Split(this string value, int desiredLength)
{
    var characters = StringInfo.GetTextElementEnumerator(value);
    while (characters.MoveNext())
        yield return String.Concat(Take(characters, desiredLength));
}

private static IEnumerable<string> Take(TextElementEnumerator enumerator, int count)
{
    for (int i = 0; i < count; ++i)
    {
        yield return (string)enumerator.Current;

        if (!enumerator.MoveNext())
            yield break;
    }
}

它没有针对速度进行优化(如您所见,我尝试使用枚举使代码保持简短和清晰),但是对于大文件,它仍然比您的实现更好(请参阅下一段了解原因)。

关于您的代码请注意:

  • 您正在构建一个巨大的ArrayList (?!) 来保存结果。另请注意,通过这种方式,您可以多次调整 ArrayList 的大小(即使给定输入大小和块大小,则其最终大小是已知的)。
  • strSegmentData 被多次重建,如果你需要积累字符,你必须使用StringBuilder 否则每个操作都会分配一个新的字符串并复制旧的值(它很慢,也给垃圾收集器增加了压力)。

有更快的实现(请参阅链接的代码审查帖子,尤其是 Heslacher's implementation 以获得更快的版本)并且如果您不需要正确处理 Unicode(您确定您只管理美国ASCII 字符)然后还有一个漂亮的readable implementation from Jon Skeet(请注意,在分析您的代码之后,您可能仍会提高其对大文件预分配正确大小的输出列表的性能)。我这里不再重复他们的代码,请参考链接的帖子。

在你的具体你不需要在内存中读取整个大文件,你可以一次读取/解析n个字符(不用太担心磁盘访问,I/O 被缓冲)。它会稍微降低性能,但会大大提高内存使用率。或者,您可以逐行阅读(管理处理跨行块)。

【讨论】:

    【解决方案2】:

    以下是我对您的问题和代码的分析(阅读 cmets)

    using (StreamReader srSegmentData = new StreamReader(fileNamePath))
    {
        string strSegmentData = "";
        string line = srSegmentData.ReadToEnd(); // Why are you reading this till the end if it is such a long string?
        int startPos = 0;
    
        ArrayList alSegments = new ArrayList(); // Better choice would be to use List<string>
        while (startPos < line.Length && (line.Length - startPos) >= segmentSize)
        {
            strSegmentData = strSegmentData + line.Substring(startPos, segmentSize) + Environment.NewLine; // Seem like you are inserting linebreaks at specified interval in your original string. Is that what you want?
            alSegments.Add(line.Substring(startPos, segmentSize) + Environment.NewLine); // Why are you recalculating the Substring? Why are you appending the newline if the aim is to just "split"
            startPos = startPos + segmentSize;
        }
    }
    

    做出各种假设,下面是我推荐的用于拆分长字符串的代码。这只是一种干净的方式来完成您在示例中所做的事情。您可以对此进行优化,但不确定您正在寻找多快。

    static void Main(string[] args) {
        string fileNamePath = "ConsoleApplication1.pdb";
        var segmentSize = 32;
    
        var op = ReadSplit(fileNamePath, segmentSize);
        var joinedSTring = string.Join(Environment.NewLine, op);
    }
    
    static List<string> ReadSplit(string filePath, int segmentSize) {
        var splitOutput = new List<string>();
        using (var file = new StreamReader(filePath, Encoding.UTF8, true, 8 * 1024 )) {
            char []buffer = new char[segmentSize];
            while (!file.EndOfStream) {
                int n = file.ReadBlock(buffer, 0, segmentSize);
                splitOutput.Add(new string(buffer, 0, n));
            }
        }
    
        return splitOutput;
    }
    

    我没有对我的版本进行任何性能测试,但我猜它比你的版本快。

    另外,我不确定您打算如何使用输出,但是在执行 I/O 时一个很好的优化是使用异步调用。在处理大型 string 时,一个好的优化(以可读性和复杂性为代价)是坚持使用 char[]

    注意

    • 您在读取文件时可能需要处理字符编码问题
    • 如果您的内存中已经有长字符串并且文件读取只是包含在演示中,那么您应该使用StringReader 类而不是StreamReader

    【讨论】: