Фільтрування рядків у різні файли

Інколи бувають завдання, коли потрібно взяти текстовий файл і розділити його на декілька враховуючи якусь умову. Коли в цьому файлі мало рядків і умова тільки одна – то звичайно це можна швидко зробити у звичайному блокноті. Складність появляється тоді, коли рядків декілька сотень тисяч, і умов яким повинен відповідати рядок також. У таких випадках мені зручно вирішувати завдання в програмі ZennoPoster.

Для прикладу можна поставити таку задачу – є текстовий файл, у якому є 1 000 000 рядків. У деяких рядках є символ @. Таких рядків приблизно 30%. Замовнику потрібно витягнути з нього всі рядки у яких є символ @ в інший файл, і видалити ці рядки з вхідного файла.

Замовник спочатку пробував вирішити це завдання самостійно. Він прив’язав текстовий файл до списку ZennoPoster. Потім перебирав його в циклі до того часу, поки не знаходив рядок який містить символ @. Знайшовши необхідний рядок він зберігав його в інший список, і видаляв рядок по номеру з вхідного списку. Після чого повторював ці дії знову і знову, до того часу, поки не пройде весь вхідний список.
Проте на практиці виявилось що час виконання цього завдання дуже великий. Замовник бачить проблему в тому, що файл містить багато рядків і вирішив пришвидшити виконання збільшивши кількість потоків потоків.

Проте, він скоріш за все нічого не знає про алгоритмічну складність. Замість того, щоб проводити перевірку умови 1 раз на 1 рядок, він перевіряв кожний рядок стільки раз скільки є рядків у вхідному списку за виключенням кількості рядків, які були видалені. Тобто 1 000 000 x (1 000 000 – 30%) = приблизно 660 000 000 000 разів. Тому не дивно, що час виконання був надзвичайно великим. Скоріш за все збільшення кількості потоків для вирішення цієї задачі не пришвидшило б її виконання, тому що крім самої проблеми з алгоритмом потоки завжди вставали б у чергу на зчитування і запис даних, а також була б проблема синхронізації контексту. Наприклад один потік провірив до 10 000 рядка, інший до 100 000 рядка. Потім коли було знайдено співпадіння – потік видалив би рядок – і в цей час інший потік продовжив би роботу зі списком, у якому уже зовсім інші дані (рядки змістились би у позиціях на видалений елемент).

Власне що потрібно знати, це те, що мінімально можлива кількість перевірок рядків така, як кількість рядків у списку. Тому потрібно прагнути сформулювати таку логіку, при якій 1 рядок береться на перевірку умови тільки один раз. Власне самий швидкий і правильний варіант, який підійде у більшості випадків – це просто у звичайному циклі брати рядки по черзі, перевіряти умову, після чого добавляти цей рядок у нові умовні списки good і bad. При цьому, список з вхідними рядками бажано не змінювати. Приклад коду, який вирішує цю задачу фільтрування вхідного списку на два інші по умові наявності символа @ в рядку може виглядати так:

var input = project.Lists["input"];
var good = project.Lists["good"];
var bad = project.Lists["bad"];

string pattern = "@";
string line = string.Empty;

for(int i=0;i<input.Count;i++) {
	if(input[i].Contains(pattern)) good.Add(input[i]);
	else bad.Add(input[i]);
}

Можна цю саму роботу виконати використовуючи Linq, при цьому можна перезаписувати файл з вхідними даними, наприклад рядками, у яких немає символа @. А список з результатами попередньо можна прив’язати до файла у якому ми хочемо бачити результат:

var input = project.Lists["input"].ToList();
var good = project.Lists["good"];

string pattern = "@";

var temp_good = input.Where(x=>x.Contains(pattern));
var temp_bad = input.Where(x=>!x.Contains(pattern));

project.Lists["input"].Clear();
project.Lists["input"].AddRange(temp_bad.ToList());

good.AddRange(temp_good.ToList());

Використання Linq може пришвидшити виконання, наприклад коли ми маємо справу не з однією провіркою, а з декількома. Діло в тому, що Linq працює з лінивими колекціями, які виконуються не зразу, а в останній момент, у моєму коді це відбувається при визові temp_good.ToList() і temp_bad.ToList(). Тобто якщо наступні рядки повинні виконатись тільки один раз при завершенні коду то це вже суттєво пришвидшить роботу (тому що не потрібно буде записувати файл декілька разів, і вся обробка буде виконуватись в оперативній пам’яті компьютера):

project.Lists["input"].Clear();
project.Lists["input"].AddRange(temp_bad.ToList());

Приклад коду, який виконує роботу фільтрації рядків по декількох файлах:

var input = project.Lists["input"].ToList();
var good = project.Lists["good"];

string[] patterns = new[]{"@1", "@2", "@3"};
var temp_input = input.Where(x=>!string.IsNullOrEmpty(x));

foreach(string pattern in patterns) {
string path = Path.Combine(project.Directory, string.Format("{0}.txt", pattern));
good.Bind(path);
good.Clear();

var temp_good = temp_input.Where(x=>x.Contains(pattern));
good.AddRange(temp_good.ToList());
temp_input = temp_input.Where(x=>!x.Contains(pattern));

}

project.Lists["input"].Clear();
project.Lists["input"].AddRange(temp_input.ToList());

Хочу зазначити, що способів вирішення задач може бути декілька, і їх приходиться змінювати в залежності від того, які дані ми подаємо на вхід, які саме дані ми хочемо одержати на виході і як саме ми хочемо користуватись рішенням (якщо завдання працювати тільки з одним паттерном – ми будуємо одну логіку роботи, якщо потрібно опрацювати декілька умов – логіка у нас також зміниться, якщо потрібно зберігати результат в файли, або коли зберігати немає необхідності). Тому коли є складнощі і важко самостійно розібратись – потрібно звертатись до людей які вже мали справу з вирішенням подібних задач. Але, якщо є достатньо вільного часу – то краще ознайомитись з поняттям алгоритмічної складності і приймати її до уваги при проектуванні автоматизації своїми сценаріями.

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *