Бачу не один я зустрічався з такою проблемою, коли прив’язуєш текстовий файл до звичайного списка в програмі ZennoPoster, і бачиш, що все працює нормально в багатопотоковому режимі. А потім в один день файл виростає, тому що наприклад є багато даних які по черзі потрібно опрацювати, чи банально звідкись завантажили великий файл, з якого треба видалити мусорні рядки – і результатом такого звичайного підходу буде повне пожирання оперативної пам’яті програмою ZennoPoster.
До мене також звертався користувач програми ZennoPoster, у якого робота з файлом в багатопотоковому режимі з використанням звичайних списків використовувала майже 64 гігабайти оперативної пам’яті. І з цією проблемою потрібно було щось робити.
Значить на форумі звичайно зразу порекомендують використовувати базу даних, навіть тоді, коли завдання виглядає дуже простим – взяти перший рядок і видалити його. І навіть коли я сформував рішення, яке по-моєму в цьому випадку можна вважати хорошим, то сам користувач подивився на нього, і написав що ось так мені порадив ChatGPT, і це працює, проте потрібно адаптувати для роботи в програмі ZennoPoster.
// підкорегований чуть-чуть код, який видав ChatGPT який не годиться для роботи з великими файлами string path = @"C:\file.txt"; string[] lines1 = File.ReadAllLines(path); string line = lines1[0]; string[] lines2 = lines1.Skip(1).ToArray(); File.WriteAllLines(path, lines2); Console.WriteLine(line);
Власне в цьому фрагменті коду, який нібито працює зразу є декілька проблем.
ReadAllLines зразу поміщає всі рядки з файлу в оперативну пам’ять компьютера.
.ToArray() знову виділить оперативну пам’ять компьютера.
В результаті у lines1 і lines2 буде виділена пам’ять на одині і тіж дані.
Адаптувати цей код для роботи в ZennoPoster не проблема, просто видалити рядок з Console.WriteLine. І хоча цей код працює з маленькими файлами, всерівно щоб він працював у багатопотоковому режимі потрібно синхронізувати потоки:
// підкорегований чуть-чуть код, який видав ChatGPT який не годиться для роботи з великими файлами string path = @"C:\file.txt"; string line = ""; lock(SyncObjects.ListSyncer) { string[] lines1 = File.ReadAllLines(path); line = lines1[0]; string[] lines2 = lines1.Skip(1).ToArray(); File.WriteAllLines(path, lines2); }
Але, як я вже говорив вище – це не вирішує проблему з оперативною пам’ятью, яку буде використовувати проєкт, якщо файли будуть великого розміру. Також треба розуміти, що такий код буде працювати помало в багатопотоковому режимі, тому що основний час буде тратитись на виділення пам’яті і на перезапис даних на диску.
Так от що потрібно робити, щоб можна було працювати, і проблем не було при роботі з великими текстовими файлами?
В першу чергу потрібно зчитувати не сам файл, а посилання на рядки в текстовому файлі, тобто використовувати так зване ліниве зчитування, а також відмовитись від ідеї брати рядок з видаленням з великого файла.
var lines = File.ReadLines(path);
В цей момент ми в оперативну пам’ять не будемо зчитувати весь файл, це ніби інструкція прочитати файл, коли це буде необхідно.
Після чого, ми можемо прочитати будь-який рядок:
line = lines.First(); // Прочитати перший рядок line = lines.Skip(5).First(); // Прочитати 6-й рядок line = lines.Last(); // Прочитати останній рядок
Важливо, що у такому випадку ми використаєм оперативну пам’ять тільки для одного рядка з даними, тобто мізерну кількість.
Код буде виглядати вже приблизно так (і як бачимо, ми не записуємо дані в файл):
string path = @"C:\file.txt"; string line = ""; lock(SyncObjects.ListSyncer) { // синхронізація потоків var lines = File.ReadLines(path); line = lines.Skip(5).First(); // Прочитати 6-й рядок } return line;
Проблема може бути тоді, коли наприклад файл не існує, або в ньому немає необхідної кількості рядків. Тому, попередньо можливо потрібно перевірити саму наявність файлу і перевірити чи є достатньо в ньому рядків, або одягнути в конструкцію try/catch щоб вивести повідомлення в лог, хоча я там також генерую помилку, тому що якщо файл не прочитаний, чи немає достатньої кількості рядків хочеться вийти по червоному виходу з блока:
string path = @"C:\file.txt"; string line = ""; lock(SyncObjects.ListSyncer) { // синхронізація потоків try { var lines = File.ReadLines(path); line = lines.Skip(5).First(); // Прочитати 6-й рядок } catch (Exception e) { project.SendWarningToLog(e.Message, true); throw new Exception(e.Message); } } return line;
Залишається вишенька на торті – добавити можливість вказувати який рядок потрібно прочитати, і добавити всі необхідні перевірки, щоб код не вилітав по помилці коли без цього можна обійтись. За умову завдання поставимо таке – що в текстовому файлі зберігаються кошельки bitcoin чи будь-якої іншої криптовалюти. І наше завдання перевіряти баланси цих кошельків раз в якийсь період часу (саме тому і потрібно було брати перший рядок і добавляти його в кінець файлу).
Тоді, щоб не придумувати логіку з глобальними змінними, ми можемо номер рядка, з яким ми працюємо зберігати у окремому текстовому файлі. Так як ми не будемо змінювати свій основний великий файл – то ми можемо один раз порахувати кількість рядків в ньому, і зберегти в цьому ж текстовому файлі. Щоб не думати як там з нього брати дані – можна наприклад зберігати у вигляді json, тоді просто буде його десеріалізувати.
А логіку зробимо таку – синхронізуємо потоки, і першим ділом перевіримо наявність двух файлів – великого, з якого будемо читати рядки, і тимчасового, в якому буде зберігатись у вигляді json рядка кількість рядків і рядок який був взятий останнім.
Якщо немає великого файла – просто вийдемо по помилці.
Якщо немає маленького файла – тоді наше завдання створити його, і записати в нього 0 як номер рядка, і порахувати кількість рядків у великому файлі.
Думаю, сам код, скаже більше, ніж опис логіки:
string path_counter = Path.Combine(project.Directory, "counter.txt"); string path = Path.Combine(project.Directory, "file.txt"); string line = string.Empty; lock(SyncObjects.ListSyncer) { // синхронізація потоків bool check_f1 = File.Exists(path_counter); bool check_f2 = File.Exists(path); if(!check_f2) throw new Exception("Немає великого файла"); // виходимо - немає звідки читати дані var dic = new Dictionary<string, int>(); if(!check_f1) { // файл counter ще не створений - заповнюємо за змовчуванням dic["row"] = 0; dic["max"] = File.ReadLines(path).Count(); } else { try { // пробуємо прочитати дані dic = Global.ZennoLab.Json.JsonConvert.DeserializeObject<Dictionary<string, int>>(File.ReadAllText(path_counter).Trim()); } catch { // не змогли прочитати дані - створюємо за змовчуванням dic["row"] = 0; dic["max"] = File.ReadLines(path).Count(); } } if(!(dic["max"] > dic["row"])) { // якщо дійшли до кінця файла - створємо за змовчуванням dic["row"] = 0; dic["max"] = File.ReadLines(path).Count(); } try { // Просто читаємо необхідний рядок line = File.ReadLines(path).Skip(dic["row"]).First(); // вказуємо який наступний рядок читати будемо dic["row"]++; // зберігаємо налаштування в файл File.WriteAllText(path_counter, Global.ZennoLab.Json.JsonConvert.SerializeObject(dic, Global.ZennoLab.Json.Formatting.Indented)); } catch (Exception e) { project.SendWarningToLog(e.Message, true); // якщо виявилось що рядка з таким номером нема - перерахуємо кількість рядків dic["max"] = File.ReadLines(path).Count(); // зберігаємо налаштування в файл File.WriteAllText(path_counter, Global.ZennoLab.Json.JsonConvert.SerializeObject(dic, Global.ZennoLab.Json.Formatting.Indented)); throw new Exception(e.Message); } } return line;
Власне такий варіант підходить для роботи з файлами, кількість рядків у яких не перевищує int.MaxValue, тобто 2 147 483 647. Якщо кількість рядків в файлі буде більшою – то думаю методи Skip і Count не зможуть працювати корректно. В такому випадку можливо варто порізати великий файл на менші файли і працювати вже з ними.
Ще можна звичайно використовувати StreamReader щоб читати файл по рядкам. Проте, в цьому випадку я не побачив ніякої переваги, тому File.ReadLines мені більш до душі підійшов для вирішення подібних завданнь.
А на рахунок ChatGPT – то звичайно він може без проблем написати хороший код, якщо йому детально описати весь алгоритм того, що саме хочеться одержати і які саме методи використовувати, і які саме помилки і яким чином їх потрібно обробляти. Проте, коли я знаю як такі завдання вирішуються, то самостійно написати код мені набагато простіше, ніж тратити час на те, щоб об’яснити ChatGPT що саме він повинен зібрати, щоб це могло корректно працювати.