Используем Node.js для работы с большими файлами и наборами raw-данных



    Этот пост — перевод оригинальной статьи Пейдж Нидринхауз, full-stack software engineer. Ее основная специальность — JavaScript, но Пейдж изучает и другие языки и фреймворки. А полученным опытом делится со своими читателями. К слову, статья будет интересна начинающим разработчикам.

    Недавно я столкнулась с задачей, которая меня заинтересовала, — нужно было извлечь определенные данные из огромного объема неструктурированных файлов Федеральной избирательной комиссии США. Я не слишком много работала с raw-данными, поэтому решила принять вызов и взяться за эту задачу. В качестве инструмента для ее решения я выбрала Node.js.

    Skillbox рекомендует: Онлайн-курс «Профессия Frontend-разработчик».

    Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

    Задача была описана четырьмя пунктами:
    • Программа должна просчитывать общее число строк в файле.
    • Каждая восьмая колонка содержит имя человека. Нужно загрузить эти данные и создать массив со всеми содержащимися в файле именами. Необходимо отобразить 432-е и 43 243-е имя.
    • Каждая пятая колонка содержит дату внесения пожертвований добровольцами. Посчитайте, сколько всего пожертвований вносится каждый месяц, и выведите общий результат.
    • Каждая восьмая колонка содержит имя человека. Создайте массив, выбрав лишь имя, без фамилии. Узнайте, какое имя встречается чаще всего и сколько раз?

    (Оригинальную задачу можно просмотреть вот по этой ссылке.)

    Файл, с которым необходимо работать, — обычный .txt объемом 2,55 ГБ. Есть также папка, которая содержит части главного файла (на них можно отлаживать работу программы, не занимаясь анализом всего огромного массива).

    Два возможных решения на Node.js


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

    Привычное — fs.readFile(). Оно позволяет прочитать весь файл, занеся его в память, а затем использовать Node.

    Альтернатива — fs.createReadStream(), функция, передающая данные подобно тому, как это организовано в других языках — например, в Python или Java.

    Решение которое я выбрала


    Поскольку мне нужно было просчитывать общее число строк и парсить данные для разбора имен и дат, я решила остановиться на втором варианте. Здесь я могла использовать функцию rl.on(‘line’,...) для получения необходимых данных из строк.

    Node.js CreateReadStream() & ReadFile() Code

    Ниже — код, который я написала при помощи Node.js и функции fs.createReadStream().



    Изначально мне было нужно все настроить, понимая, что импорт данных требует таких функций Node.js, как fs (file system), readline и stream. Далее я смогла создать instream и outstream вместе с readLine.createInterface(). Полученный код дал возможность разбирать файл построчно, забирая необходимые данные.

    Кроме того, я добавила несколько переменных и комментариев для работы с конкретными данными. Это lineCount, dupeNames и массивы names, donation и firstNames.

    В функции rl.on('line',…) я смогла задать разбор файла построчно. Так, я ввела переменную lineCount для каждой строки. Я использовала метод JavaScript split () для парсинга имен, добавляя их в мой массив names. Далее я отделила лишь имена, без фамилий, одновременно выделяя исключения, вроде наличия двойных имен, инициалов в середине имени и т.п. Далее я отделила год и дату от колонки данных, преобразовав все это в формат YYYY-MM и добавив в массив dateDonationCount.

    В функции rl.on('close',...) я выполнила все преобразования данных, добавленных в массивы, с внесением полученной информации в console.log.

    lineCount и names необходимы для определения 432-го и 43 243-го имен, преобразований тут никаких не требуется. А вот выявление наиболее часто встречающегося в массиве имени и определение количества пожертвований — задачи более сложные.

    Для того чтобы выявить самое частое имя, мне пришлось создать объект пар значений для каждого имени (ключа) и количества упоминаний Object.entries(). (значение), а затем преобразовать все это в массив массивов, используя функцию ES6. После этого задачи сортировки имен и выявления наиболее повторяющегося уже не представляли сложности.

    С пожертвованиями я проделала примерно тот же фокус: создала объект пар значений и функцию logDateElements(), которая позволила мне, используя интерполяцию ES6, отобразить ключи и значения для каждого месяца. Затем я создала new Map(), преобразовав объект dateDonations в метамассив, и циклично обработала каждый массив при помощи logDateElements(). (Вышло не так и просто, как казалось в начале.)

    Но это сработало, я смогла прочитать относительно небольшой файл объемом в 400 МБ, выделив нужную информацию.

    После этого я опробовала fs.createReadStream() — я реализовала задачу на fs.readFile(), для того чтобы увидеть разницу. Вот код:



    Все решение вы можете увидеть вот здесь.

    Результаты работы с Node.js


    Решение оказалось рабочим. Я добавила путь к файлу readFileStream.js и… наблюдала, как сервер Node упал с ошибкой JavaScript heap out of memory.



    Оказалось, что, хотя все и работало, но это решение пыталось передавать все содержимое файла в память, что было невозможно с объемом в 2,55 ГБ. Node может одновременно работать с 1,5 ГБ в памяти, не больше.

    Поэтому ни одно из моих решений не подошло. Понадобилось новое, которое смогло работать даже с такими объемными файлами.

    Новое решение


    Как оказалось, нужно было использовать популярный NPM-модуль EventStream.

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



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

    В принципе, единственное, что мне пришлось изменить, — это ответ names. У меня не получилось поместить 130 млн имен в массив — снова проявилась ошибка нехватки памяти. Я решила проблему, просчитав 432-е и 43 243-е имя и внеся их в собственный массив. Немного не то, о чем просили в условиях, но кто сказал, что нельзя быть креативным?

    Раунд 2. Пробуем программу в работе


    Да, все тот же файл объемом в 2,55 ГБ, скрещиваем пальцы и следим за результатом.



    Успех!

    Как оказалось, просто Node.js для решения подобных задач не подходит, его возможности несколько ограничены. А вот расширив их при помощи модулей, можно работать и с такими крупными файлами.

    Skillbox рекомендует:

    Skillbox
    Онлайн-университет профессий будущего

    Comments 17

      +13
      Если я правильно понял статью — то это пример достаточно плохого js разработчика.

      Мало того что она не разобралась почему stream вдруг считал файл в память(он не должен github.com/nodejs/help/issues/194) и просто использовала npm библиотеку, так еще и было предположение считать файл с данными в память целиком(ненуаче такого ?).
        0
        Мода на diversity вот к такому и ведет.
        «Ну и что, что код на уровне плохого джуниора, зато автор — тётка!»
        +13
        Великолепная статья, если рассматривать её как юмористическое произведение. «Full-stack software engineer» на серьёзных щщах рассказывает, как дошла до очевидных вещей, преподнося это как эврику. «Для чтения двухгигового файла нужно использовать потоки!» (над головой автора загорается лампочка). Прелестно, просто прелестно. Для забивания гвоздя в доску нужно использовать молоток.
          +4
          И на медиуме как всегда комментарии из разряда «Great job», «Cool article!».
            0

            Последнее время токсичность не в моде, поэтому фразы вроде Great job говорят чисто из вежливости, что-бы не показаться токсичным. Даже тогда, когда хотят сказать что-то другое. Это что-то вроде GG в игре. Когда всё плохо, и хочется наговорить кучу гадостей всем в своей команде за то, что вместо командной игры они показали нечто противоположное, но вы собираете волю в кулак, стискиваете зубы и хвалите команду лузеров(и себя такого же, заодно) и пишете что игра была великолепной, хотя всё ровно наоборот. Как по мне правда лучше всего этого политеса, но мы живём в безумное время, когда правда не в моде.

          0
          > Этот пост — перевод оригинальной статьи Пейдж Найдринхаус, full-stack software engineer.

          <зануда>Paige Niedringhaus произносится как Пейдж Нидринхауз (Нидринхоз)</зануда>
            0
            Спасибо, поправили :)
            +5
            Задачу она могла решить проще и быстрее.
            // количество строк
            wc -l src.txt
            // вывести имена из номеров строк
            head -n 423 src.txt | tail -n 1 | cut -f 8 -t '|'
            head -n 43243 src.txt | tail -n 1 | cut -f 8 -t '|'
            // количество пожертвований по месяцам
            cat src.txt | cut -f nY,nM -t '|' | sort | uniq -c
            // выборка самого частого имени аналогично, только добавить ещё cut и после uniq добавить сортировку и head -n 1
            

            Имхо будет быстрее ноды, менее требовательно к ресурсам и каждой задаче свой инструмент.
              +2
              Так не интересно — статью писать не про что будет :)
            +8
            Код в картинках — отличное решение. Реклама школы программирования неграмотной статьей — ещё более отличное решение. Назвать 2,5GB очень, очень большим это настолько печально насколько вообще может быть. Я понимаю если бы речь шла о файле размером хотя бы пару сотен ГБ или лучше терабайты. В общем я разочарован. И понятно откуда берутся статьи habr.com/post/423889 И вообще, имхо разработчик
            для решения подобных задач не подходит, его возможности несколько ограничены
              0
              Ну так, все ингредиенты в гармоничном сочетании :3
                0
                Там, на medium.com, до сих пор не завезли нормальную подсветку кода (только через embed с github), поэтому большинство публикаций там извращается со скриншотами. И за это там еще просят $5 в месяц за безлимит (бесплатно только 3 статьи в месяц).

                Хабра на них нет!
                0
                Переводчик то молодец, перевел. Но ставить плюс рука не поднимается.
                  0
                  А могла бы загуглить «почему нода вылетает с ошибкой по памяти», и после ближайшего совета на stackoverflow подкрутить --max-old-space-size и остановиться на первом варианте: вот и ладненько.
                    0

                    И тут еще при использовании модуля readline событие 'close' совсем не говорит, что строк больше не будет. Как раз нашел эту статью в поисках решения данной проблемы (тут решения нет).

                    Only users with full accounts can post comments. Log in, please.