Câu hỏi Đọc các tệp văn bản lớn với các luồng trong C #


Tôi đã có một nhiệm vụ đáng yêu để tìm ra cách xử lý các tệp lớn được tải vào trình soạn thảo tập lệnh của ứng dụng của chúng ta (nó giống như VBA cho sản phẩm nội bộ của chúng tôi cho các macro nhanh). Hầu hết các tập tin có dung lượng khoảng 300-400 KB. Nhưng khi họ vượt quá 100 MB, quy trình có một thời gian khó khăn (như bạn mong đợi).

Điều gì xảy ra là tập tin được đọc và đẩy vào một RichTextBox mà sau đó được điều hướng - đừng lo lắng quá nhiều về phần này.

Nhà phát triển đã viết mã ban đầu chỉ đơn giản là sử dụng StreamReader và đang thực hiện

[Reader].ReadToEnd()

có thể mất nhiều thời gian để hoàn thành.

Nhiệm vụ của tôi là chia nhỏ mã này, đọc nó trong các đoạn thành một bộ đệm và hiển thị một thanh tiến trình với một tùy chọn để hủy bỏ nó.

Một số giả định:

  • Hầu hết các tệp sẽ từ 30-40 MB
  • Nội dung của tập tin là văn bản (không phải nhị phân), một số là định dạng Unix, một số là DOS.
  • Một khi các nội dung được lấy ra, chúng tôi tìm ra những terminator được sử dụng.
  • Không ai quan tâm một khi nó được nạp thời gian cần để hiển thị trong richtextbox. Nó chỉ là tải ban đầu của văn bản.

Bây giờ cho các câu hỏi:

  • Tôi có thể chỉ đơn giản là sử dụng StreamReader, sau đó kiểm tra thuộc tính Length (để ProgressMax) và phát hành một Read cho một kích thước bộ đệm và lặp qua một vòng lặp while TRONG KHI bên trong một nhân viên nền, do đó, nó không chặn các chủ đề giao diện người dùng chính? Sau đó quay trở lại trình xây dựng chuỗi thành chuỗi chính sau khi hoàn tất.
  • Nội dung sẽ đi đến một StringBuilder. thế nào tôi có thể khởi tạo StringBuilder với kích thước của luồng nếu độ dài có sẵn?

Đây có phải là những ý tưởng tốt trong ý kiến ​​chuyên môn của bạn không? Tôi đã có một vài vấn đề trong quá khứ với việc đọc nội dung từ các luồng, bởi vì nó sẽ luôn luôn bỏ lỡ vài byte cuối cùng hoặc một cái gì đó, nhưng tôi sẽ hỏi một câu hỏi nếu đây là trường hợp.


76
2018-01-29 12:36


gốc


30-40MB tập lệnh? Thánh cá thu! Tôi ghét phải viết mã xem xét ... - dthorpe
Nó chỉ là vài dòng mã. Xem thư viện này tôi đang sử dụng để đọc các tập tin lớn hơn 25GB và nhiều hơn nữa. github.com/Agenty/FileReader - Vicky


Các câu trả lời:


Bạn có thể cải thiện tốc độ đọc bằng cách sử dụng BufferedStream, như sau:

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

Tháng 3 năm 2013 CẬP NHẬT

Gần đây tôi đã viết mã để đọc và xử lý (tìm kiếm văn bản trong) 1 tệp văn bản GB-ish (lớn hơn nhiều so với tệp có liên quan ở đây) và đạt được hiệu suất đáng kể bằng cách sử dụng mẫu nhà sản xuất / người tiêu dùng. Tác vụ của nhà sản xuất đọc trong các dòng văn bản bằng cách sử dụng BufferedStream và đưa cho họ một nhiệm vụ tiêu dùng riêng biệt đã thực hiện tìm kiếm.

Tôi đã sử dụng điều này như một cơ hội để tìm hiểu TPL Dataflow, rất phù hợp để nhanh chóng viết mã mẫu này.

Tại sao BufferedStream lại nhanh hơn

Bộ đệm là một khối byte trong bộ nhớ được sử dụng để lưu trữ dữ liệu, do đó giảm số lượng cuộc gọi đến hệ điều hành. Bộ đệm cải thiện hiệu năng đọc và ghi. Một bộ đệm có thể được sử dụng để đọc hoặc viết, nhưng không bao giờ đồng thời cả hai. Các phương thức Read và Write của BufferedStream tự động duy trì bộ đệm.

Cập nhật tháng 12 năm 2014: Mileage của bạn có thể thay đổi

Dựa trên các ý kiến, FileStream nên sử dụng BufferedStream trong nội bộ. Vào thời điểm câu trả lời này được cung cấp lần đầu tiên, tôi đã đo được hiệu suất đáng kể bằng cách thêm BufferedStream. Lúc đó tôi đang nhắm mục tiêu .NET 3.x trên nền tảng 32 bit. Hôm nay, nhắm mục tiêu .NET 4.5 trên nền tảng 64 bit, tôi không thấy bất kỳ cải tiến nào.

Liên quan

Tôi đã xem xét một trường hợp mà streaming một tập tin CSV lớn, tạo ra cho dòng phản ứng từ một hành động ASP.Net MVC là rất chậm. Thêm một BufferedStream cải thiện hiệu suất bằng 100x trong trường hợp này. Để xem thêm Unbuffered Output Rất chậm


151
2018-03-10 01:22



Dude, BufferedStream tạo ra tất cả sự khác biệt. +1 :) - Marcus
Có một chi phí để yêu cầu dữ liệu từ một hệ thống con IO. Trong trường hợp quay đĩa, bạn có thể phải đợi đĩa quay vào vị trí để đọc đoạn dữ liệu tiếp theo hoặc tệ hơn, đợi đầu đĩa di chuyển. Trong khi SSD không có bộ phận cơ khí để làm chậm mọi thứ, vẫn có chi phí hoạt động cho mỗi IO để truy cập chúng. Luồng đệm được đọc nhiều hơn những gì mà StreamReader yêu cầu, giảm số lượng cuộc gọi đến hệ điều hành và cuối cùng là số lượng yêu cầu IO riêng biệt. - Eric J.
Có thật không? Điều này làm cho không có sự khác biệt trong kịch bản thử nghiệm của tôi. Theo Brad Abrams không có lợi ích khi sử dụng BufferedStream qua FileStream. - Nick Cox
@NickCox: Kết quả của bạn có thể thay đổi dựa trên hệ thống con IO cơ bản của bạn. Trên một đĩa quay và một bộ điều khiển đĩa không có dữ liệu trong bộ nhớ đệm của nó (và cả dữ liệu không được lưu trong Windows), tốc độ tăng lên rất lớn. Cột của Brad được viết vào năm 2004. Tôi đã đo được những cải tiến thực sự, quyết liệt gần đây. - Eric J.
Điều này là vô ích theo: stackoverflow.com/questions/492283/… FileStream đã sử dụng bộ đệm trong nội bộ. - Erwin Mayer


Bạn nói rằng bạn đã được yêu cầu hiển thị thanh tiến trình trong khi tệp lớn đang tải. Có phải vì người dùng thực sự muốn xem chính xác% tải tệp hay chỉ vì họ muốn có phản hồi trực quan rằng có điều gì đó đang xảy ra?

Nếu sau này là đúng, thì giải pháp trở nên đơn giản hơn nhiều. Cứ làm đi reader.ReadToEnd() trên một chuỗi nền và hiển thị thanh tiến trình loại marquee thay vì một thanh tiến trình thích hợp.

Tôi nâng cao điểm này bởi vì trong kinh nghiệm của tôi, điều này thường xảy ra. Khi bạn đang viết một chương trình xử lý dữ liệu, thì người dùng chắc chắn sẽ quan tâm đến một con số hoàn chỉnh%, nhưng đối với bản cập nhật giao diện người dùng đơn giản nhưng chậm, họ có nhiều khả năng chỉ muốn biết rằng máy tính không bị hỏng. :-)


14
2018-01-29 13:03



Nhưng người dùng có thể hủy cuộc gọi ReadToEnd không? - Tim Scarborough
@Tim, cũng phát hiện. Trong trường hợp đó, chúng ta quay lại StreamReader vòng lặp. Tuy nhiên, nó sẽ vẫn đơn giản hơn vì không cần đọc trước để tính toán chỉ báo tiến trình. - Christian Hayter


Nếu bạn đọc thống kê hiệu suất và điểm chuẩn trên trang web này, bạn sẽ thấy rằng cách nhanh nhất để đọc (vì đọc, viết và xử lý khác nhau) một tệp văn bản là đoạn mã sau đây:

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

Tất cả khoảng 9 phương pháp khác nhau đều được đánh dấu bằng băng ghế dự bị, nhưng có vẻ như nó xuất hiện trước phần lớn thời gian, thậm chí ra ngoài thực hiện bộ đọc đệm như những độc giả khác đã đề cập.


13
2017-09-19 14:21



Điều này làm việc tốt cho việc tách rời một tệp postgres 19GB để dịch nó thành cú pháp sql trong nhiều tệp. Cảm ơn anh chàng postgres người không bao giờ thực hiện các thông số của tôi một cách chính xác. /thở dài - Damon Drake
Sự khác biệt về hiệu suất ở đây dường như trả cho các tệp thực sự lớn, như lớn hơn 150MB (bạn cũng nên sử dụng StringBuilder để tải chúng vào bộ nhớ, tải nhanh hơn vì nó không tạo chuỗi mới mỗi khi bạn thêm ký tự) - b729sefc


Đối với các tệp nhị phân, cách nhanh nhất để đọc chúng tôi đã tìm thấy là đây.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

Trong các bài kiểm tra của tôi nhanh hơn hàng trăm lần.


7
2017-09-30 12:38



Bạn có bằng chứng nào về điều này không? Tại sao nên sử dụng OP này trên bất kỳ câu trả lời nào khác? Hãy đào sâu hơn một chút và cung cấp chi tiết hơn một chút - Dylan Corriveau


Sử dụng nhân viên nền và chỉ đọc một số dòng giới hạn. Đọc thêm chỉ khi người dùng cuộn.

Và cố gắng không bao giờ sử dụng ReadToEnd (). Đó là một trong những chức năng mà bạn nghĩ "tại sao họ lại làm nó?"; nó là một kịch bản kiddies ' helper tốt với những thứ nhỏ, nhưng như bạn thấy, nó hút cho các tệp lớn ...

Những kẻ nói với bạn để sử dụng StringBuilder cần phải đọc MSDN thường xuyên hơn:

Xem xét hiệu suất
Các phương thức Concat và AppendFormat vừa ghép nối dữ liệu mới với đối tượng String hoặc StringBuilder hiện có. Một hoạt động nối đối tượng String luôn tạo một đối tượng mới từ chuỗi hiện có và dữ liệu mới. Một đối tượng StringBuilder duy trì một bộ đệm để thích ứng với dữ liệu mới. Dữ liệu mới được nối vào cuối bộ đệm nếu phòng có sẵn; nếu không, một bộ đệm mới, lớn hơn được cấp phát, dữ liệu từ bộ đệm ban đầu được sao chép vào bộ đệm mới, sau đó dữ liệu mới được nối vào bộ đệm mới. Hiệu suất của một hoạt động nối cho một đối tượng String hoặc StringBuilder phụ thuộc vào tần suất phân bổ bộ nhớ xảy ra.
Một hoạt động nối chuỗi luôn phân bổ bộ nhớ, trong khi hoạt động nối StringBuilder chỉ cấp phát bộ nhớ nếu bộ đệm đối tượng StringBuilder quá nhỏ để chứa dữ liệu mới. Do đó, lớp String là thích hợp hơn cho một hoạt động nối nếu một số lượng cố định của các đối tượng String được nối. Trong trường hợp đó, các phép nối nối riêng lẻ thậm chí có thể được kết hợp thành một thao tác đơn lẻ bởi trình biên dịch. Một đối tượng StringBuilder thích hợp hơn cho một phép nối nối nếu một số chuỗi tùy ý được nối; ví dụ, nếu một vòng lặp nối một chuỗi ngẫu nhiên các chuỗi đầu vào của người dùng.

Điêu đo co nghia la khổng lồ cấp phát bộ nhớ, việc sử dụng hệ thống tệp hoán đổi lớn, mô phỏng các phần của ổ đĩa cứng của bạn hoạt động như bộ nhớ RAM, nhưng ổ đĩa cứng rất chậm.

Tùy chọn StringBuilder có vẻ tốt cho những người sử dụng hệ thống như một người dùng đơn, nhưng khi bạn có hai hoặc nhiều người dùng đọc các tệp lớn cùng một lúc, bạn có một vấn đề.


6
2018-01-29 12:42



xa ra các bạn siêu nhanh! không may vì cách hoạt động của macro mà toàn bộ luồng cần được tải. Như tôi đã đề cập, đừng lo lắng về phần richtext. Tải ban đầu của nó, chúng tôi đang muốn cải thiện. - Nicole Lee
để bạn có thể làm việc trong các phần, đọc dòng X đầu tiên, áp dụng macro, đọc dòng X thứ hai, áp dụng macro, v.v ... nếu bạn giải thích macro này làm gì, chúng tôi có thể giúp bạn chính xác hơn - Tufo


Điều này là đủ để bạn bắt đầu.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}

5
2018-01-29 12:56



Tôi sẽ di chuyển "var buffer = new char [1024]" ra khỏi vòng lặp: nó không cần thiết để tạo ra một bộ đệm mới mỗi lần. Chỉ cần đặt nó trước khi "trong khi (đếm> 0)". - Tommy Carlier


Hãy xem đoạn mã sau. Bạn đã đề cập Most files will be 30-40 MB. Điều này tuyên bố đọc 180 MB trong 1,4 giây trên Intel Quad Core:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

Bài báo gốc


4
2018-01-29 12:52



Những loại xét nghiệm này nổi tiếng không đáng tin cậy. Bạn sẽ đọc dữ liệu từ bộ nhớ cache của tệp hệ thống khi bạn lặp lại kiểm tra. Đó là ít nhất một đơn đặt hàng của cường độ nhanh hơn so với một thử nghiệm thực tế mà đọc dữ liệu ra khỏi đĩa. Tệp 180 MB không thể mất ít hơn 3 giây. Khởi động lại máy của bạn, chạy thử nghiệm một lần cho số thực. - Hans Passant
dòng stringBuilder.Append có khả năng nguy hiểm, bạn cần thay thế nó bằng stringBuilder.Append (fileContents, 0, charsRead); để đảm bảo bạn không thêm đầy đủ 1024 ký tự ngay cả khi luồng đã kết thúc trước đó. - Johannes Rudolph


Bạn có thể sử dụng các tập tin ánh xạ bộ nhớ được tốt hơn đây.. Sự hỗ trợ tập tin ánh xạ bộ nhớ sẽ được xung quanh trong .NET 4 (Tôi nghĩ rằng ... Tôi nghe nói rằng thông qua một người nào khác nói về nó), do đó wrapper này trong đó sử dụng p / invokes để làm cùng một công việc ..

Chỉnh sửa: Xem ở đây trên MSDN cho nó hoạt động như thế nào, đây là Blog mục nhập chỉ ra cách nó được thực hiện trong .NET sắp tới 4 khi nó ra mắt như là bản phát hành. Các liên kết tôi đã đưa ra trước đó là một wrapper xung quanh pinvoke để đạt được điều này. Bạn có thể ánh xạ toàn bộ tệp vào bộ nhớ và xem nó như một cửa sổ trượt khi cuộn qua tệp.


3
2018-01-29 12:52





Trình lặp có thể là hoàn hảo cho loại công việc này:

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

Bạn có thể gọi nó bằng cách sử dụng như sau:

string filename = "C:\\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

Khi tệp được tải, trình lặp sẽ trả về số tiến trình từ 0 đến 100, mà bạn có thể sử dụng để cập nhật thanh tiến trình của mình. Khi vòng lặp kết thúc, StringBuilder sẽ chứa nội dung của tệp văn bản.

Ngoài ra, bởi vì bạn muốn văn bản, chúng tôi chỉ có thể sử dụng BinaryReader để đọc trong các ký tự, sẽ đảm bảo rằng bộ đệm của bạn xếp hàng chính xác khi đọc bất kỳ ký tự nhiều byte nào (UTF-8, UTF-16, v.v.)

Điều này được thực hiện hoàn toàn mà không cần sử dụng các tác vụ nền, chủ đề hoặc các máy trạng thái tùy chỉnh phức tạp.


1
2017-07-09 18:35





Tất cả các câu trả lời xuất sắc! tuy nhiên, đối với ai đó đang tìm kiếm câu trả lời, những câu trả lời này có vẻ hơi không hoàn chỉnh.

Như một chuỗi tiêu chuẩn chỉ có thể kích thước X, 2Gb đến 4Gb tùy thuộc vào cấu hình của bạn, những câu trả lời này không thực sự hoàn thành câu hỏi của OP. Một phương pháp là làm việc với Danh sách các chuỗi:

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

Một số có thể muốn Tokenise và chia dòng khi xử lý. Danh sách chuỗi bây giờ có thể chứa khối lượng văn bản rất lớn.


0
2018-01-22 05:58





Tôi biết câu hỏi này là khá cũ nhưng tôi tìm thấy nó vào ngày khác và đã thử nghiệm các khuyến nghị cho MemoryMappedFile và đây là bàn tay xuống phương pháp nhanh nhất. Một so sánh là đọc một tập tin 34516.939 dòng 345MB thông qua một phương pháp readline mất 12+ giờ trên máy tính của tôi trong khi thực hiện tải cùng và đọc qua MemoryMappedFile mất 3 giây.


0
2018-03-13 11:20