Читання великих текстових файлів із потоками на C #


96

У мене прекрасне завдання - розробити, як обробляти великі файли, що завантажуються в редактор сценаріїв нашої програми (це як VBA для нашого внутрішнього продукту для швидких макросів). Більшість файлів складає близько 300-400 КБ, що є прекрасним завантаженням. Але коли вони перевищують 100 МБ, процес має важкі часи (як і слід було очікувати).

Що трапляється, так це те, що файл читається і переноситься в RichTextBox, який потім здійснюється в навігації - не турбуйтеся надто про цю частину.

Розробник, який написав початковий код, просто використовує StreamReader і робить це

[Reader].ReadToEnd()

що може зайняти досить тривалий час.

Моє завдання - розбити цей біт коду, прочитати його шматками в буфер і показати індикатор виконання з можливістю скасувати його.

Деякі припущення:

  • Більшість файлів становитимуть 30-40 МБ
  • Вміст файлу - текстовий (не двійковий), деякі мають формат Unix, інші - DOS.
  • Після отримання вмісту ми з’ясовуємо, який термінатор використовується.
  • Нікого це не турбує, як тільки завантажується час, необхідний для візуалізації в текстовому полі. Це лише початкове завантаження тексту.

Тепер щодо питань:

  • Чи можу я просто використати StreamReader, а потім перевірити властивість Length (так ProgressMax) і видати Read для встановленого розміру буфера і перебирати за деякий час цикл WHILST всередині фонового працівника, щоб він не блокував основний потік UI? Потім поверніть конструктор рядків до основного потоку після його завершення.
  • Вміст буде спрямовано до StringBuilder. чи можу я ініціалізувати StringBuilder розміром потоку, якщо довжина доступна?

Це (на вашу думку професіонали) хороші ідеї? Раніше у мене було кілька проблем із читанням вмісту з Streams, оскільки він завжди буде пропускати останні кілька байтів або щось інше, але я поставлю інше питання, якщо це так.


29
30-40 МБ файлів скриптів? Скумбрія свята! Не хотілося б переглядати код ...
dthorpe

Я знаю, що ці запитання досить старі, але днями я знайшов їх і перевірив рекомендації щодо MemoryMappedFile, і це найшвидший метод. Порівняння - читання файлу розміром 7616939 рядків розміром 345 МБ методом readline займає 12+ годин на моїй машині, виконуючи те саме завантаження, а читання через MemoryMappedFile - 3 секунди.
csonon

Це лише кілька рядків коду. Дивіться цю бібліотеку, яку я використовую для читання 25 ГБ та більше великих файлів. github.com/Agenty/FileReader
Vikash Rathee

Відповіді:


175

Ви можете покращити швидкість читання за допомогою BufferedStream, наприклад:

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

    }
}

Березень 2013 ОНОВЛЕННЯ

Нещодавно я написав код для читання та обробки (пошук тексту в) 1 ГБ текстових файлів (набагато більший, ніж задіяні тут файли) і досяг значного приросту продуктивності, використовуючи шаблон виробника / споживача. Завдання продюсера читали в рядках тексту за допомогою BufferedStreamі передавали їх окремому споживчому завданню, яке виконувало пошук.

Я використав це як можливість вивчити TPL Data Flow, який дуже добре підходить для швидкого кодування цього шаблону.

Чому BufferedStream швидше

Буфер - це блок байтів в пам'яті, який використовується для кешування даних, зменшуючи тим самим кількість викликів до операційної системи. Буфери покращують ефективність читання та запису. Буфер можна використовувати як для читання, так і для письма, але ніколи одночасно для обох. Методи читання та запису BufferedStream автоматично підтримують буфер.

Грудень 2014 ОНОВЛЕННЯ: Ваш пробіг може змінюватися

Виходячи з коментарів, FileStream повинен використовувати BufferedStream всередині. На момент першого надання цієї відповіді я виміряв значний приріст продуктивності, додавши BufferedStream. У той час я націлювався на .NET 3.x на 32-розрядної платформі. Сьогодні, орієнтуючись на .NET 4.5 на 64-розрядної платформі, я не бачу жодних покращень.

Пов'язані

Я натрапив на випадок, коли потокове передавання великого згенерованого файлу CSV у потік відповіді від дії ASP.Net MVC було дуже повільним. У цьому випадку додавання BufferedStream покращило продуктивність у 100 разів. Докладніше див. Небуферований вихід Дуже повільний


12
Чувак, BufferedStream робить все різне. +1 :)
Маркус,

2
Запит даних із підсистеми введення-виведення коштує. У разі обертання дисків, можливо, вам доведеться зачекати, поки диск повернеться в потрібне положення, щоб прочитати наступний шматок даних, або ще гірше - почекати, поки головка диска переміститься. Незважаючи на те, що твердотільні накопичувачі не мають механічних деталей, щоб уповільнити ситуацію, доступ до них все одно оплачується за операцію вводу-виводу. Буферовані потоки читають більше, ніж просто запити StreamReader, зменшуючи кількість викликів до ОС і, зрештою, кількість окремих запитів вводу-виводу.
Ерік Дж.,

4
Дійсно? Це не має різниці в моєму тестовому сценарії. За словами Бреда Абрамса, використання BufferedStream над FileStream не має ніякої вигоди.
Nick Cox

2
@NickCox: Результати можуть відрізнятися залежно від вашої підсистеми введення-виведення. На обертовому диску та дисковому контролері, який не має даних у кеші (а також даних, не кешованих Windows), прискорення величезне. Колонка Бреда була написана в 2004 році. Я нещодавно виміряв фактичні, кардинальні покращення.
Eric J.

3
Це марно в відповідно до: stackoverflow.com/questions/492283 / ... FileStream вже використовує буфер всередині.
Erwin Mayer

21

Якщо ви прочитаєте статистику продуктивності та тестів на цьому веб-сайті , ви побачите, що найшвидший спосіб читання (оскільки читання, запис і обробка різні) текстовий файл - це такий фрагмент коду:

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

Всі приблизно 9 різних методів були позначені, але цей, здається, виходить вперед більшу частину часу, навіть виконуючи буферизований читач, як згадували інші читачі.


2
Це добре працювало для видалення файлу postgres розміром 19 Гб, щоб перевести його в синтаксис sql у декілька файлів. Дякую хлопцеві postgres, який ніколи не виконував мої параметри правильно. / зітхання
Деймон Дрейк

Здається, різниця в продуктивності окупається для справді великих файлів, таких як більші за 150 МБ (також вам дійсно слід використовувати a StringBuilderдля завантаження їх в пам’ять, завантажується швидше, оскільки він не створює новий рядок кожного разу, коли ви додаєте символи)
Joshua G

15

Ви говорите, що вас попросили показати індикатор виконання під час завантаження великого файлу. Це тому, що користувачі щиро хочуть бачити точний відсоток завантаження файлів, або просто тому, що вони хочуть візуального зворотного зв’язку про те, що щось відбувається?

Якщо останнє відповідає дійсності, то рішення стає набагато простішим. Просто виконайте reader.ReadToEnd()фоновий потік і виведіть індикатор прогресу типу шатру замість відповідного.

Я піднімаю це питання, оскільки на моєму досвіді це часто буває. Коли ви пишете програму обробки даних, тоді користувачів точно зацікавить% повна цифра, але для простих, але повільних оновлень інтерфейсу вони, швидше за все, просто хочуть знати, що комп’ютер не вийшов з ладу. :-)


2
Але чи може користувач скасувати дзвінок ReadToEnd?
Тім Скарборо

@ Тим, добре помічений. У цьому випадку ми повертаємось до StreamReaderциклу. Однак це все одно буде простіше, оскільки для обчислення показника прогресу не потрібно читати заздалегідь.
Крістіан Хейтер,

8

Щодо бінарних файлів, я знайшов найшвидший спосіб їх читання.

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

У моїх тестах це в сотні разів швидше.


2
Чи є у вас якісь вагомі докази цього? Чому OP повинен використовувати це над будь-якою іншою відповіддю? Будь ласка, копайте трохи глибше і розкажіть трохи більше деталей
Ділан Корріво,

7

Використовуйте фоновий працівник і читайте лише обмежену кількість рядків. Читайте більше лише тоді, коли користувач прокручує.

І намагайтеся ніколи не використовувати ReadToEnd (). Ви вважаєте однією з функцій, "чому вони це зробили?"; це скрипт- помічник для дітей, який чудово поєднується з дрібницями, але, як бачите, це відстій для великих файлів ...

Тим хлопцям, які кажуть вам використовувати StringBuilder, потрібно частіше читати MSDN:

Міркування щодо продуктивності
Методи Concat та AppendFormat об'єднують нові дані до існуючого об'єкта String або StringBuilder. Операція об'єднання рядкових об'єктів завжди створює новий об'єкт із існуючого рядка та нових даних. Об'єкт StringBuilder підтримує буфер для конкатенації нових даних. Нові дані додаються до кінця буфера, якщо є вільне місце; в іншому випадку виділяється новий, більший буфер, дані з вихідного буфера копіюються в новий буфер, а потім нові дані додаються до нового буфера. Виконання операції конкатенації для об'єкта String або StringBuilder залежить від того, як часто відбувається виділення пам'яті.
Операція конкатенації рядків завжди виділяє пам'ять, тоді як операція конкатенації StringBuilder виділяє пам'ять лише в тому випадку, якщо буфер об'єкта StringBuilder занадто малий для розміщення нових даних. Отже, клас String є кращим для операції конкатенації, якщо об'єднано фіксовану кількість об'єктів String. У цьому випадку окремі операції конкатенації можуть навіть об'єднати компілятор в одну операцію. Об'єкт StringBuilder є кращим для операції конкатенації, якщо об'єднано довільну кількість рядків; наприклад, якщо цикл об'єднує випадкову кількість рядків вводу користувача.

Це означає величезний розподіл пам'яті, що стає великим використанням системи обміну файлами, що імітує розділи жорсткого диска, щоб діяти як пам'ять оперативної пам'яті, але жорсткий диск дуже повільний.

Параметр StringBuilder добре виглядає для тих, хто використовує систему як монокористувач, але коли у вас є два або більше користувачів, які одночасно читають великі файли, у вас виникає проблема.


далеко ви, хлопці, супершвидкі! на жаль, через те, як працює макрос, потрібно завантажити весь потік. Як я вже згадував, не хвилюйтеся щодо частини Richtext. Його початкове завантаження ми хочемо покращити.
Ніколь Лі

щоб ви могли працювати по частинах, читати перші X рядки, застосовувати макрос, читати другі X рядки, застосовувати макрос тощо ... якщо ви поясните, що робить цей макрос, ми можемо допомогти вам з більшою точністю
Tufo

5

Цього має бути достатньо для початку.

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
Я б перемістив "var buffer = new char [1024]" з циклу: необов'язково створювати новий буфер кожного разу. Просто поставте його перед "while (count> 0)".
Tommy Carlier

4

Погляньте на наступний фрагмент коду. Ви вже згадували Most files will be 30-40 MB. Це вимагає читання 180 МБ за 1,4 секунди на чотирьохядерному процесорі Intel:

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);
        }
    }
}

Оригінальна стаття


3
Такі випробування, як відомо, ненадійні. Ви будете читати дані з кешу файлової системи, коли будете повторювати тест. Це принаймні на порядок швидше, ніж реальний тест, який зчитує дані з диска. Файл 180 МБ не може зайняти менше 3 секунд. Перезавантажте машину, запустіть тест один раз для дійсного числа.
Ганс Пасант,

7
рядок stringBuilder.Append є потенційно небезпечним, вам потрібно замінити його на stringBuilder.Append (fileContents, 0, charsRead); щоб переконатись, що ви не додаєте 1024 символів, навіть коли потік закінчився раніше.
Йоганнес Рудольф

@JohannesRudolph, твій коментар щойно вирішив мені помилку. Як ви придумали номер 1024?
OfirD

3

Ви могли б бути краще використовувати пам'ять відображених файли обробки тут .. пам'ять відображається файл підтримка буде приблизно в .NET 4 (я думаю ... Я чув , що через кого - то ще про це говорити), отже , ця оболонку , яка використовує р / закликає виконати ту саму роботу ..

Редагувати: Дивіться тут на MSDN про те, як це працює, ось запис у блозі, який вказує, як це робиться в майбутньому .NET 4, коли він вийде як реліз. Посилання, яке я давав раніше, - це обгортка навколо шпигуна для досягнення цього. Ви можете перенести весь файл в пам’ять і переглядати його як розсувне вікно під час прокрутки файлу.


2

Всі чудові відповіді! однак для тих, хто шукає відповідь, вони видаються дещо неповними.

Як стандартний рядок може бути лише розміру X, від 2 Гб до 4 Гб, залежно від вашої конфігурації, ці відповіді насправді не відповідають запиту OP. Одним із методів є робота зі списком рядків:

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);
}
}

Деякі можуть захотіти Tokenise та розділити лінію під час обробки. Список рядків тепер може містити дуже великі обсяги тексту.


1

Ітератор може бути ідеальним для такого типу робіт:

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);
            }
        }
    }
}

Ви можете зателефонувати за допомогою наступного:

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();

По мірі завантаження файлу ітератор повертає номер прогресу від 0 до 100, який ви можете використовувати для оновлення панелі прогресу. Після завершення циклу StringBuilder буде містити вміст текстового файлу.

Крім того, оскільки вам потрібен текст, ми можемо просто використовувати BinaryReader для читання символів, що забезпечить правильне вирівнювання буферів при читанні будь-яких багатобайтових символів ( UTF-8 , UTF-16 тощо).

Це все робиться без використання фонових завдань, потоків або складних спеціальних автоматів.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.