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


96

Tôi có một nhiệm vụ đáng yêu là tìm ra cách xử lý các tệp lớn đang được tải vào trình chỉnh sửa tập lệnh của ứng dụng của chúng tôi (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 có dung lượng khoảng 300-400 KB tải tốt. Nhưng khi chúng vượt quá 100 MB, quá trình này sẽ gặp khó khăn (như bạn mong đợi).

Điều xảy ra là tệp được đọc và chuyển vào RichTextBox, 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à làm

[Reader].ReadToEnd()

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

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

Một số giả định:

  • Hầu hết các tệp sẽ có dung lượng 30-40 MB
  • Nội dung của tệp 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.
  • Sau khi nội dung được truy xuất, chúng tôi sẽ tìm ra dấu chấm hết được sử dụng.
  • Không ai lo lắng khi nó được tải, thời gian cần thiết để 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ỉ cần sử dụng StreamReader, sau đó kiểm tra thuộc tính Độ dài (vì vậy ProgressMax) và đưa ra lệnh Đọc cho một kích thước bộ đệm đã đặt và lặp lại trong vòng lặp trong khi WHILST bên trong trình làm việc nền, vì vậy nó không chặn luồng giao diện người dùng chính? Sau đó, trả lại trình xây dựng chuỗi về chuỗi chính sau khi hoàn thành.
  • Nội dung sẽ được chuyển đến một StringBuilder. 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 không?

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


29
Tệp script 30-40MB? Cá thu thần thánh! Tôi ghét phải xem xét mã mà ...
dthorpe

Tôi biết câu hỏi này khá cũ nhưng tôi đã tìm thấy nó vào ngày hôm trước và đã thử nghiệm đề xuất cho MemoryMappedFile và đây là phương pháp nhanh nhất. Một phép so sánh đang đọc tệp 7.616.939 dòng 345MB thông qua phương pháp dòng đọc mất hơn 12 giờ trên máy của tôi trong khi thực hiện cùng một lần tải và đọc qua MemoryMappedFile mất 3 giây.
csonon

Nó chỉ là một vài dòng mã. Xem thư viện này mà tôi đang sử dụng để đọc các tệp 25gb và nhiều hơn nữa. github.com/Agenty/FileReader
Vikash Rathee

Câu trả lời:


175

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)
    {

    }
}

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

Gần đây tôi đã viết mã để đọc và xử lý (tìm kiếm văn bản trong) các tệp văn bản 1 GB-ish (lớn hơn nhiều so với các tệp 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. Nhiệm vụ nhà sản xuất đọc các dòng văn bản bằng cách sử dụng BufferedStreamvà giao chúng cho một nhiệm vụ người tiêu dùng riêng biệt thực hiện tìm kiếm.

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

Tại sao BufferedStream nhanh hơn

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

CẬP NHẬT tháng 12 năm 2014: Số dặm của bạn có thể thay đổi

Dựa trên các nhận xét, FileStream nên sử dụng BufferedStream nội bộ. Tại thời điểm câu trả lời này được cung cấp lần đầu tiên, tôi đã đo lường mức tăng hiệu suất đáng kể bằng cách thêm Dòng đệm. Vào thời điểm đó, 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 thiện nào.

Có liên quan

Tôi đã gặp trường hợp phát trực tuyến tệp CSV lớn, được tạo tới luồng Phản hồi từ hành động ASP.Net MVC rất chậm. Trong trường hợp này, việc thêm BufferedStream đã cải thiện hiệu suất gấp 100 lần. Để biết thêm, hãy xem Đầu ra không có bộ đệm rất chậm


12
Dude, BufferedStream tạo ra tất cả sự khác biệt. +1 :)
Marcus

2
Yêu cầu dữ liệu từ hệ thống con IO phải trả phí. Trong trường hợp đĩa xoay, bạn có thể phải đợi đĩa quay vào vị trí để đọc phần dữ liệu tiếp theo, hoặc tệ hơn, đợi đầu đĩa di chuyển. Mặc dù SSD không có các bộ phận cơ học để làm chậm mọi thứ, nhưng vẫn có chi phí cho mỗi lần hoạt động IO để truy cập chúng. Luồng được đệm đọc nhiều hơn những gì 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.

4
Có thật không? Điều này không tạo ra 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 gì khi sử dụng BufferedStream qua FileStream.
Nick Cox

2
@NickCox: Kết quả của bạn có thể khác nhau dựa trên hệ thống con IO cơ bản của bạn. Trên đĩa quay và 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 Windows lưu trong bộ đệm), tốc độ tăng là rất lớn. Chuyên mục của Brad được viết vào năm 2004. Tôi đã đo lường những cải tiến thực tế, mạnh mẽ gần đây.
Eric J.

3
Điều này là vô ích theo: stackoverflow.com/questions/492283/… FileStream đã sử dụng bộ đệm bên trong.
Erwin Mayer

21

Nếu bạn đọc số liệu thống kê về 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ý đều khác nhau) tệp văn bản là đoạn mã sau:

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

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


2
Điều này hoạt động tốt khi tách một tệp postgres 19GB ra để dịch nó thành cú pháp sql trong nhiều tệp. Cảm ơn anh chàng postgres đã không bao giờ thực thi 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ư được đền đáp cho các tệp thực sự lớn, chẳng hạn như lớn hơn 150MB (bạn cũng thực sự nên sử dụng a 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ự)
Joshua G

15

Bạn nói rằng bạn đã được yêu cầu hiển thị thanh tiến trình trong khi một tệp lớn đang tải. Đó là 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 phản hồi trực quan rằng có điều gì đó đang xảy ra?

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

Tôi nêu ra quan điểm này bởi vì theo 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, 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 các bản cập nhật giao diện người dùng đơn giản nhưng chậm, nhiều khả năng họ chỉ muốn biết rằng máy tính không bị lỗi. :-)


2
Nhưng người dùng có thể hủy cuộc gọi ReadToEnd không?
Tim Scarborough

@Tim, được phát hiện tốt. Trong trường hợp đó, chúng ta quay lại StreamReadervòng lặp. Tuy nhiên, nó vẫn sẽ đơn giản hơn vì không cần đọc trước để tính toán chỉ số tiến độ.
Christian Hayter

8

Đối với các tệp nhị phân, cách đọc chúng nhanh nhất mà 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 thử nghiệm của tôi, nó nhanh hơn hàng trăm lần.


2
Bạn có bằng chứng khó khăn nào về điều này không? Tại sao OP nên sử dụng câu trả lời này thay cho bất kỳ câu trả lời nào khác? Hãy tìm hiểu sâu hơn và đưa ra một chút chi tiết hơn
Dylan Corriveau

7

Sử dụng công cụ nền và chỉ đọc một số dòng giới hạn. Chỉ đọc thêm 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ọ tạo ra nó?"; nó là một trình trợ giúp của những đứa trẻ tập lệnh hoạt động tốt với những thứ nhỏ, nhưng như bạn thấy, nó rất tệ đối với các tệp lớn ...

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

Cân nhắc về hiệu suất
Phương thức Concat và AppendFormat đều nối dữ liệu mới với một đối tượng String hoặc StringBuilder hiện có. Thao tác nối đối tượng chuỗi 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 để phù hợp với việc nối các dữ liệu mới. Dữ liệu mới được nối vào cuối bộ đệm nếu còn chỗ; 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 cấp phát bộ nhớ xảy ra.
Hoạt động nối chuỗi luôn cấp phát 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 thích hợp hơn cho thao tác nối nếu một số lượng cố định đối tượng String được nối. Trong trường hợp đó, các hoạt động nối riêng lẻ thậm chí có thể được kết hợp thành một hoạt động duy nhất bởi trình biên dịch. Một đối tượng StringBuilder thích hợp hơn cho thao tác 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 số chuỗi ngẫu nhiên của người dùng nhập.

Điều đó có nghĩa là phân bổ bộ nhớ rất lớn , những gì trở thành sử dụng lớn của hệ thống tệp hoán đổi, mô phỏng các phần của ổ đĩa cứng của bạn để hoạt động giống như bộ nhớ RAM, nhưng ổ đĩa cứng rất chậm.

Tùy chọn StringBuilder có vẻ ổn đối với những người sử dụng hệ thống với tư cách một người dùng đơn lẻ, 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 gặp sự cố.


xa các bạn là siêu nhanh! không may là do 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. Đó là lần tải ban đầu mà chúng tôi muốn cải thiện.
Nicole Lee,

để bạn có thể làm việc theo từng phần, đọc X dòng đầu tiên, áp dụng macro, đọc X dòng 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

5

Điều này sẽ đủ để 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();
    }
}

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

4

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


3
Những loại thử nghiệm này nổi tiếng là không đáng tin cậy. Bạn sẽ đọc dữ liệu từ bộ nhớ cache của hệ thống tệp khi bạn lặp lại kiểm tra. Đó là nhanh hơn ít nhất một bậc của cường độ so với thử nghiệm thực đọc dữ liệu khỏi đĩa. Một tệp 180 MB không thể mất ít hơn 3 giây. Khởi động lại máy, chạy test 1 lần cho số thực.
Hans Passant

7
dòng stringBuilder.Append tiềm ẩn nhiều nguy cơ, 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

@JohannesRudolph, bình luận của bạn vừa giải quyết được lỗi cho tôi. Làm thế nào bạn nghĩ ra con số 1024?
HeyJude

3

Bạn có thể tốt hơn nên sử dụng xử lý tệp được ánh xạ bộ nhớ ở đây .. Hỗ trợ tệp được ánh xạ bộ nhớ sẽ có trong .NET 4 (Tôi nghĩ ... Tôi đã nghe điều đó thông qua một người khác nói về nó), do đó trình bao bọc này sử dụng p / kêu gọi thực hiện công việc tương tự ..

Chỉnh sửa: Xem tại đây trên MSDN để biết cách hoạt động của nó, đây là mục blog cho biết nó được thực hiện như thế nào trong .NET 4 sắp tới khi nó được phát hành. Liên kết mà tôi đã đưa ra trước đó là một trình bao bọc 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.


2

Tất cả các câu trả lời xuất sắc! tuy nhiên, đối với một người đang tìm kiếm câu trả lời, những điều này dường như không đầy đủ.

Là một Chuỗi tiêu chuẩn chỉ có thể có 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ự đáp ứng 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à tách 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.


1

Một trình lặp có thể 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, bạn có thể sử dụng số này để 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, vì bạn muốn văn bản, chúng tôi chỉ có thể sử dụng BinaryReader để đọc theo ký tự, điều này 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.).

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


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.