【问题标题】:Read from StreamReader in batches分批从 StreamReader 中读取
【发布时间】:2010-09-28 20:29:32
【问题描述】:

我在尝试通过 StreamReader 将 800MB 文本文件加载到 DataTable 时遇到了 OutOfMemory 异常。我想知道是否有办法从内存流中批量加载 DataTable,即从 StreamReader 读取文本文件的前 10,000 行,创建 DataTable,使用 DataTable 执行某些操作,然后将接下来的 10,000 行加载到 StreamReader 和很快。

我的谷歌在这里不是很有帮助,但似乎应该有一个简单的方法来做到这一点。最终,我将使用 SqlBulkCopy 将 DataTables 写入 MS SQL 数据库,因此如果有比我所描述的更简单的方法,我将感谢您提供正确方向的快速指针。

编辑 - 这是我正在运行的代码:

public static DataTable PopulateDataTableFromText(DataTable dt, string txtSource)
{

    StreamReader sr = new StreamReader(txtSource);
    DataRow dr;
    int dtCount = dt.Columns.Count;
    string input;
    int i = 0;

    while ((input = sr.ReadLine()) != null)
    {

        try
        {
            string[] stringRows = input.Split(new char[] { '\t' });
            dr = dt.NewRow();
            for (int a = 0; a < dtCount; a++)
            {
                string dataType = dt.Columns[a].DataType.ToString();
                if (stringRows[a] == "" && (dataType == "System.Int32" || dataType == "System.Int64"))
                {
                    stringRows[a] = "0";
                }
                dr[a] = Convert.ChangeType(stringRows[a], dt.Columns[a].DataType);

            }
            dt.Rows.Add(dr);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
        i++;
    }
    return dt;
}

这是返回的错误:

“System.OutOfMemoryException:引发了“System.OutOfMemoryException”类型的异常。
在 System.String.Split(Char[] 分隔符,Int32 计数,StringSplitOptions 选项)
在 System.String.Split(Char[] 分隔符}
在 Harvester.Config.PopulateDataTableFromText(DataTable dt, String txtSource) in C:...."

关于将数据直接加载到 SQL 中的建议 - 我对 C# 有点陌生,但我认为这基本上就是我正在做的事情? SqlBulkCopy.WriteToServer 获取我从文本文件创建的 DataTable 并将其导入 sql。有没有我想念的更简单的方法来做到这一点?

编辑:哦,我忘了提 - 此代码不会与 SQL Server 在同一台服务器上运行。数据文本文件在服务器 B 上,需要写入服务器 A 中的表。这是否排除了使用 bcp?

【问题讨论】:

  • 你真的需要一次在内存中保存全部内容吗?
  • 我不太确定问题出在哪里。 Streamreader 不会将整个流内容加载到内存中。应该没有什么可以阻止您从 StreamReader 读取流的一部分,进行处理然后加载下一部分等等。您能否展示代码的相关部分,以便我们提供更多帮助?

标签: c# streamreader


【解决方案1】:

您是否考虑过将数据直接加载到 SQL Server 中,然后在数据库中对其进行操作? 数据库引擎已经被设计为以有效的方式执行大量数据的操作。这可能会产生更好的整体结果,并允许您利用数据库和 SQL 语言的功能来完成繁重的工作。这是旧的“更聪明地工作而不是更努力地工作”原则。

有许多different methods to load data into SQL Server,因此您可能需要检查这些以查看是否合适。如果您使用的是 SQLServer 2005 或更高版本,并且确实需要对 C# 中的数据进行一些操作,则始终可以使用managed stored procedure

这里要意识到OutOfMemoryException 有点误导。 Memory is more than just the amount of physical RAM you have。您可能用完的是可寻址内存。这是一个非常不同的事情。

当您将一个大文件加载到内存中并将其转换为DataTable 时,它可能需要远不止 800Mb 来表示相同的数据。因为 32 位 .NET 进程仅限于在 2Gb 的可寻址内存下,您可能永远无法在一个批次中处理这么多的数据。

您可能需要做的是以流方式处理数据。 换句话说,不要尝试将其全部加载到 DataTable 中,然后批量插入到 SQLServer .而是分块处理文件,在完成后清除之前的行集。

现在,如果您可以访问具有大量内存的 64 位计算机(以避免虚拟机抖动)并且 作为 64 位 .NET 运行时的副本,您可能会在不改变运行代码的情况下摆脱困境。但我还是建议进行必要的更改,因为即使在那种环境下,它也可能会提高性能。

【讨论】:

  • “您可能需要做的是以流式方式处理数据”太好了 - 这是我想我想做的,但我很难找到一个如何做这个。这听起来与下面的其他建议相似,并且是我在 SO 上找到的其他问题的类似答案,但到目前为止我找到的答案都没有一个例子..
  • 800Mb 文本是 1.6Gb(+ 开销)作为 UTF-16,这是 C# 字符串使用的,所以所有的地址空间都没有了。
  • @tt2:请参阅Thomas Levesque's answer,了解流式解决方案的示例。
  • 可寻址内存上的链接读得很好,谢谢分享。
【解决方案2】:

你真的需要分批处理数据吗?或者你可以逐行处理它吗?在后一种情况下,我认为 Linq 在这里可能会很有帮助,因为它可以很容易地通过方法的“管道”传输数据。这样你就不需要一次加载很多数据,一次只加载一行

首先,您需要使您的StreamReader 可枚举。这可以通过扩展方法轻松完成:

public static class TextReaderExtensions
{
    public static IEnumerable<string> Lines(this TextReader reader)
    {
        string line;
        while((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

这样您就可以使用StreamReader 作为 Linq 查询的来源。

那么你需要一个方法来接受一个字符串并将其转换为DataRow

DataRow ParseDataRow(string input)
{
    // Your parsing logic here
    ...
}

使用这些元素,您可以轻松地将文件中的每一行投影到 DataRow,并使用它做任何您需要的事情:

using (var reader = new StreamReader(fileName))
{
    var rows = reader.Lines().Select(ParseDataRow);
    foreach(DataRow row in rows)
    {
        // Do something with the DataRow
    }
}

(请注意,您可以使用简单的循环来执行类似的操作,而无需使用 Linq,但我认为 Linq 使代码更具可读性...)

【讨论】:

  • +1。很好的例子。我可能会注意到您要确保不要将行添加到创建它的 DataTable 中,否则您将失去流解决方案的内存优势。
  • 太棒了 - 谢谢你的例子,我会试试,告诉你进展如何
【解决方案3】:

SqlBulkCopy.WriteToServer 具有接受 IDataReader 的重载。您可以将自己的 IDataReader 实现为 StreamReader 的包装器,其中 Read() 方法将使用 StreamReader 中的一行。这样,数据将“流式传输”到数据库中,而不是首先尝试在内存中将其构建为 DataTable。 希望对您有所帮助。

【讨论】:

  • 好主意,但是实现 IDataReader 需要做很多工作,只是因为方法的数量......
  • 我承认这是很多工作。我已经实现了一个 EnumeratorDataReader,它基本上使 Enumerator 适应于 IDataReader。这样,我将永远不需要再次实现 IDataReader,我只需实现一个迭代器块来解析一行(类似于您的示例 Thomas),然后从中创建一个 EnumeratorDataReader。我看不到通过 StackOverflow 共享此代码的实用方法,否则我会。
【解决方案4】:

作为此处其他答案的更新,我也在研究这个问题,并遇到了this page which provides a great C# example on reading a text file by chunks, processing in parallel, and then bulk inserting into a database

代码的症结在于这个循环:

//Of note: it's faster to read all the lines we are going to act on and 
            //then process them in parallel instead of reading and processing line by line.
            //Code source: http://cc.davelozinski.com/code/c-sharp-code/read-lines-in-batches-process-in-parallel
            while (blnFileHasMoreLines)
            {
                batchStartTime = DateTime.Now;  //Reset the timer

                //Read in all the lines up to the BatchCopy size or
                //until there's no more lines in the file
                while (intLineReadCounter < BatchSize && !tfp.EndOfData)
                {
                    CurrentLines[intLineReadCounter] = tfp.ReadFields();
                    intLineReadCounter += 1;
                    BatchCount += 1;
                    RecordCount += 1;
                }

                batchEndTime = DateTime.Now;    //record the end time of the current batch
                batchTimeSpan = batchEndTime - batchStartTime;  //get the timespan for stats

                //Now process each line in parallel.
                Parallel.For(0, intLineReadCounter, x =>
                //for (int x=0; x < intLineReadCounter; x++)    //Or the slower single threaded version for debugging
                {
                    List<object> values = null; //so each thread gets its own copy. 

                    if (tfp.TextFieldType == FieldType.Delimited)
                    {
                        if (CurrentLines[x].Length != CurrentRecords.Columns.Count)
                        {
                            //Do what you need to if the number of columns in the current line
                            //don't match the number of expected columns
                            return; //stop now and don't add this record to the current collection of valid records.
                        }

                        //Number of columns match so copy over the values into the datatable
                        //for later upload into a database
                        values = new List<object>(CurrentRecords.Columns.Count);
                        for (int i = 0; i < CurrentLines[x].Length; i++)
                            values.Add(CurrentLines[x][i].ToString());

                        //OR do your own custom processing here if not using a database.
                    }
                    else if (tfp.TextFieldType == FieldType.FixedWidth)
                    {
                        //Implement your own processing if the file columns are fixed width.
                    }

                    //Now lock the data table before saving the results so there's no thread bashing on the datatable
                    lock (oSyncLock)
                    {
                        CurrentRecords.LoadDataRow(values.ToArray(), true);
                    }

                    values.Clear();

                }
                ); //Parallel.For   

                //If you're not using a database, you obviously won't need this next piece of code.
                if (BatchCount >= BatchSize)
                {   //Do the SQL bulk copy and save the info into the database
                    sbc.BatchSize = CurrentRecords.Rows.Count;
                    sbc.WriteToServer(CurrentRecords);

                    BatchCount = 0;         //Reset these values
                    CurrentRecords.Clear(); //  "
                }

                if (CurrentLines[intLineReadCounter] == null)
                    blnFileHasMoreLines = false;    //we're all done, so signal while loop to stop

                intLineReadCounter = 0; //reset for next pass
                Array.Clear(CurrentLines, 0, CurrentLines.Length);

            } //while blnhasmorelines

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多