Обработка файлов журналов с помощью D (часть II)

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

Определим папку, в которой будут размещены текстовые файлы с логами, переменную-накопитель с базой пользовательской информации, счетчик-идентификатор, а также подготовим переменную под файл подозрительных действий:

// путь к папке с логами
enum string logFileDirectory = `D:\Logs\`;
// счетчик по генерации идентификаторов
int counter;
// общая база пользователей
UserInfo[string] usersBase;
// файл подозрительных действий
File suspiciousActionFile;

Создадим базу, заполнив переменную usersBase с помощью описанных ранее процедур:

// создать базу на основе всех файлов из папки
dirEntries(logFileDirectory, SpanMode.shallow)
	.filter!(a => a.isFile)
	.each!(a => processFile(a, usersBase, counter));

То как работает, эта конструкция было рассказано в одном из наших рецептов.

Теперь, когда у нас есть совокупная информация по каждому пользователю, то можно пройти по базе данных с помощью простого цикла:

foreach (userName; usersBase.keys) // работаем со сгенерированной базой
{
     // формирование нужных структур
}

Итак, каждый ключ – это фамилия-имя пользователя, согласно некоторым соображениям, нужно сформировать два файла, которые необходимо заполнить несколько отличающейся информацией (а заодно подготовим структуры данных):

// файлы сводки и статистики
File summaryFile, statisticFile;
// сводка по пользователю
Summary summary;
// статистика на пользователя
Statistic statistic;

Выполним самые простые действия: откроем на запись файл подозрительных действий (он общий для всех пользователей), извлечем из базы пользователя по фамилии-имени (не забывайте, сейчас мы рассматриваем внутренности цикла!), список его действий, выделим только подозрительные и посчитаем общее количество действий:

suspiciousActionFile.open(logFileDirectory ~ `suspicious_action.txt`, `a`);
auto user = usersBase[userName]; // конкретный пользователь
auto actions = user.actions;  // действия пользователя
auto suspiciousActions = actions
                        .filter!(a => a.suspicioned); // подозрительные действия
auto numberOfAction = actions.length; // общее количество действий

Количество подозрительных действий можно вычислить так:

auto numberOfSuspicious = suspiciousActions
                            .filter!(a => a.suspicioned)
                            .array
                            .length; // количество подозрительных действий

А время последнего действия можно получить, если отсортировать все действия по времени, взять первый элемент и получить значение свойства time:

auto lastTime = actions
                    .sort!((a,b) => (a.time > b.time))
                    .front
                    .time; // время последнего действия

Осуществим наполнение структур, представляющих собой файлы с данными статистики и сводки:

// заполнить краткую сводку
summary.username = userName;
summary.numberOfSuspicious = numberOfSuspicious;
summary.timeOfLastAction = lastTime;
// заполнить статистику
statistic.id = user.id;
statistic.numberOfActions = numberOfAction;
statistic.numberOfSuspicious = numberOfSuspicious;
statistic.timeOfLastAction = lastTime;

А теперь запишем каждую из этих структур в свой файл (в этом нам поможет заблаговременно перегруженные методы toString):

// Запись в файл сводки
summaryFile.open(logFileDirectory ~ userName ~ `_summary.txt`, `w`);
summaryFile.write(summary);
summaryFile.close;

// Запись в файл статистики
statisticFile.open(logFileDirectory ~ userName ~ `_statistic.txt`, `w`);
statisticFile.write(statistic);
statisticFile.close;

Можно, наконец, записать все подозрительные действия:

// Запись в файл подозрительных действий
suspiciousActions.each!(a => suspiciousActionFile.writeln(a.time, " ", a.description));

На этом основной цикл окончен, и почти все задачи выполнены – остается только закрыть файл подозрительных действий:

suspiciousActionFile.close;

На этом описание программы закончено, можно свести код из обоих статей в один единственный файл, и после запуска генератора, указания пути к папке с логами, можно запускать файл с помощью rdmd. Результатом работы программы будет несколько файлов вида фамилия_имя_statistic.txt, фамилия_имя_summary.txt и suspicious_action.txt, которые будут содержать всю нужную информацию.

К примеру, результат работы программы для вышеприведенных данных (спойлер)

Файл Vasiliyi Shandybin_statistic.txt

Identifier : 2
Number of action: 11400
Number of suspicious: 6859
Time of last action : 2021-Nov-28 09:15:00

Файл Vasiliyi Shandybin_summary.txt

Name : Vasiliyi Shandybin
Number of suspicious: 6859
Time of last action : 2021-Nov-28 09:15:00

Файл suspicious_action.txt

2021-Nov-28 09:56:54 Leave building
2021-Nov-26 05:08:10 Exploites vulnerability
2021-Nov-24 17:15:39 Exploites vulnerability
2021-Nov-24 06:04:27 Catch the fire
2021-Nov-24 00:47:26 Destroy PC
2021-Nov-23 06:41:44 Destroy PC
2021-Nov-23 02:16:26 Destroy PC
2021-Nov-21 09:11:21 Exploites vulnerability
2021-Nov-21 06:58:02 Leave building

Полный код примера (конечно же под спойлером).

import std.algorithm;
import std.array;
import std.conv;
import std.datetime;
import std.file;
import std.format;
import std.range;
import std.stdio;
import std.string;

// Информация по текущему пользователю
struct UserInfo
{
    int id;
    string name;
    Action[] actions;
}

// Информация по конкретным записям из файла
struct UserEntry
{
	string name;
    Action action;
}

// Информация о действии
struct Action
{
    DateTime time;
    string description;
    bool suspicioned;
}

// Короткая сводка на юзера
struct Summary
{
    string username;
    int numberOfSuspicious;
    DateTime timeOfLastAction;

    string toString()
    {
        return format("Name : %s\nNumber of suspicious: %s\nTime of last action : %s",
            username,
            numberOfSuspicious,
            timeOfLastAction
        );
    }
}

// Статистика по пользователю
struct Statistic
{
    int id;
    int numberOfActions;
    int numberOfSuspicious;
    DateTime timeOfLastAction;

    string toString()
    {
        return format("Identifier : %s\nNumber of action: %s\nNumber of suspicious: %s\nTime of last action : %s",
            id,
            numberOfActions,
            numberOfSuspicious,
            timeOfLastAction
        );
    }
}

// Извлечь дату из строки
final DateTime parseDateFrom(string line)
{
    auto dateTimeBlocks = line[1..$-1].split;

    auto date = dateTimeBlocks[0].split("/");
    
    auto day = to!int(date[0]);
    auto month = to!int(date[1]);
    auto year = to!int(date[2]);

    auto time = dateTimeBlocks[1].split(":");
    auto hour = to!int(time[0]);
    auto minute = to!int(time[1]);
    auto second = to!int(time[2]);

    return DateTime(year, month, day, hour, minute, second);
}

// Извлечь текущую информацию
final UserEntry parseLine(string line)
{
    UserEntry userEntry;

    auto lineBlocks = line.split(" "); // значащие единицы строки
    auto date = parseDateFrom(lineBlocks[0] ~ " " ~ lineBlocks[1]); // дата и время 
    userEntry.name = lineBlocks[2] ~ " " ~ lineBlocks[3]; // имя пользователя
    auto description = to!string(lineBlocks[5..$].join(" ")); // описание действия

    // определение степени легальности действия
    switch (description)
    {
        case "Enter in system", 
             "Create documents",
             "Exit from system",
             "Copy documents",
             "Remove documents":
             userEntry.action = Action(date, description, false);
             break;
        default:
            userEntry.action = Action(date, description, true);
            break;
    }
    return userEntry;
}

// Обработка одного файла
final void processFile(string fileName, ref UserInfo[string] usersBase, ref int counter)
{
    final void addInDictionary(UserEntry userEntry, ref UserInfo[string] usersBase, ref int counter)
    {
        if (userEntry.name in usersBase) // если пользователь есть в базе, 
                                        // то обновить информацию, иначе создать новую запись в базе
        {
            UserInfo userInfo;

            userInfo = usersBase[userEntry.name];
            userInfo.actions ~= userEntry.action;

            usersBase[userEntry.name] = userInfo;
        }
        else
        {
            UserInfo userInfo;
            
            userInfo.id = counter;
            userInfo.name = userEntry.name;
            userInfo.actions ~= userEntry.action;
            
            usersBase[userEntry.name] = userInfo;

            counter++;
        }
    }

    // создать базу на основе записей из файла
    (cast(string) std.file.read(fileName))
                                .splitLines
                                .map!(a => parseLine(a))
                                .each!(a => addInDictionary(a, usersBase, counter));
}


void main()
{
    // путь к папке с логами
    enum string logFileDirectory = `D:\Logs\`;
    // счетчик по генерацию идентификаторов
    int counter;
    // общая база пользователей
    UserInfo[string] usersBase;
    // файл подозрительных действий
    File suspiciousActionFile;

    // создать базу на основе всех файлов из папки
    dirEntries(logFileDirectory, SpanMode.shallow)
                            .filter!(a => a.isFile)
                            .each!(a => processFile(a, usersBase, counter));

   
    foreach (userName; usersBase.keys) // работаем со сгенерированной базой
    {
        // файлы сводки и статистики
        File summaryFile, statisticFile;
        // сводка по пользователю
        Summary summary;
        // статистика на пользователя
        Statistic statistic;
        
        suspiciousActionFile.open(logFileDirectory ~ `suspicious_action.txt`, `w`);

        auto user = usersBase[userName]; // конкретный юзер
        
        auto actions = user.actions;  // действия юзера

        auto suspiciousActions = actions
                                    .filter!(a => a.suspicioned); // подозрительные действия

        auto numberOfAction = actions.length; // общее количество действий  

        auto numberOfSuspicious = suspiciousActions
                                    .filter!(a => a.suspicioned)
                                    .array
                                    .length; // количество подозрительных действий

        auto lastTime = actions
                                .sort!((a,b) => (a.time > b.time))
                                .front
                                .time; // время последнего действия
        
        // заполнить краткую сводку
        summary.username = userName;
        summary.numberOfSuspicious = numberOfSuspicious;
        summary.timeOfLastAction = lastTime;

        // заполнить статистику
        statistic.id = user.id;
        statistic.numberOfActions = numberOfAction;
        statistic.numberOfSuspicious = numberOfSuspicious;
        statistic.timeOfLastAction = lastTime;

        // Запись в файл сводки
        summaryFile.open(logFileDirectory ~ userName ~ `_summary.txt`, `w`);
        summaryFile.write(summary);
        summaryFile.close;

        // Запись в файл статистики
        statisticFile.open(logFileDirectory ~ userName ~ `_statistic.txt`, `w`);
        statisticFile.write(statistic);
        statisticFile.close;

        // Запись в файл подозрительных действий
        suspiciousActions.each!(a => suspiciousActionFile.writeln(a.time, " ", a.description));
    }
    suspiciousActionFile.close;
}

Таким образом, D очень удобен не только для системного и прикладного программирования, но и для скриптового тоже. Ряд возможностей D позволяет с комфортом решать повседневные и рутинные задачи, не отвлекаясь на другие языки программирования. Сочетание функционального стиля с императивным порой жутко выглядит и вполне может испортить самый качественный код, но иногда это очень действенный способ решения какой-либо мелкой внезапно возникшей задачи. В общем, используйте D для автоматизации своей работы, и старайтесь писать код аккуратно, а не так как порой на работе пишем его мы.

Добавить комментарий