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

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

Вот 4 заданных вопроса и ссылка на набор данных, который программа должна была проанализировать.

  • Напишите программу, которая распечатает общее количество строк в файле.
  • Обратите внимание, что восьмой столбец содержит имя человека. Напишите программу, которая загружает эти данные и создает массив со всеми строками имен. Распечатайте 432-е и 43243-е имена.
  • Обратите внимание, что 5-й столбец содержит дату. Подсчитайте, сколько пожертвований было сделано за каждый месяц, и распечатайте результаты.
  • Обратите внимание, что восьмой столбец содержит имя человека. Создайте массив с каждым именем. Определите, какое имя наиболее часто встречается в данных и сколько раз оно встречается.

Ссылка на данные: https://www.fec.gov/files/bulk-downloads/2018/indiv18.zip

При распаковке папки вы должны увидеть один главный .txt файл размером 2,55 ГБ и папку, содержащую меньшие части этого основного файла (это то, что я использовал при тестировании своих решений перед переходом к основному файлу).

Не так уж и страшно, правда? Кажется выполнимым. Итак, давайте поговорим о том, как я подошел к этому.

Два оригинальных решения Node.js, которые я придумал

Обработка больших файлов не является чем-то новым для JavaScript, на самом деле, в основной функциональности Node.js есть ряд стандартных решений для чтения и записи в файлы и из файлов.

Самым простым является fs.readFile(), в котором весь файл считывается в память, а затем обрабатываются после того, как Node его прочитает, а второй вариант - fs.createReadStream(), который передает данные в поток (и из него) аналогично другим языкам, таким как Python и Java. .

Решение, которое я выбрал для работы, и почему

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

Мне это показалось проще, чем разбивать весь файл на части после того, как он был прочитан, и таким образом пропускать строки.

Реализация кода Node.js CreateReadStream () и ReadFile ()

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

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

Я также добавил несколько переменных (и комментариев) для хранения различных битов данных: массив lineCount, names, массив и объект donation, а также массив firstNames и объект dupeNames. Вы увидите, где это применимо, чуть позже.

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

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

lineCount и names на 432-м и 43243-м индексах не потребовали дополнительных манипуляций. Найти наиболее распространенное имя и количество пожертвований на каждый месяц было немного сложнее.

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

Пожертвования также потребовали от меня создать аналогичный объект из пар ключ-значение, создать функцию logDateElements(), где я мог бы красиво использовать интерполяцию строк ES6 для отображения ключей и значений для каждого месяца пожертвования. Затем создайте new Map(), преобразуя объект dateDonations в массив массивов и перебирая каждый массив, вызывая на нем функцию logDateElements(). Ух! Не все так просто, как я сначала подумал.

Но это сработало. По крайней мере, с меньшим файлом размером 400 МБ, который я использовал для тестирования…

Сделав это с fs.createReadStream(), я вернулся и реализовал свои решения с fs.readFile(), чтобы увидеть различия. Вот код для этого, но я не буду вдаваться в подробности здесь - он очень похож на первый фрагмент, только выглядит более синхронно (однако, если вы не используете функцию fs.readFileSync(), JavaScript будет запускать этот код так же асинхронно, как и все остальные). его другой код, не беспокойтесь.

Если вы хотите увидеть мое полное репо со всем моим кодом, вы можете увидеть его здесь.

Первоначальные результаты от Node.js

С помощью своего рабочего решения я добавил путь к файлу readFileStream.js для файла монстра размером 2,55 ГБ и наблюдал, как мой сервер Node аварийно завершил работу с ошибкой JavaScript heap out of memory.

Оказывается, хотя Node.js передает поток ввода и вывода файла, в промежутках между ними он все еще пытается удерживать все содержимое файла в памяти, что не может сделать с файлом такого размера. Узел может одновременно хранить до 1,5 ГБ памяти, но не более.

Так что ни одно из моих текущих решений не соответствовало полной задаче.

Мне нужно было новое решение. Решение для еще больших наборов данных, передаваемых через Node.

Новое решение для потоковой передачи данных

Я нашел свое решение в виде EventStream, популярного модуля NPM с более чем 2 миллионами загрузок в неделю и обещанием «упростить создание потоков и работу с ними».

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

Реализация кода EventStream

Вот мой новый код с использованием модуля NPM EventStream.

Самым большим изменением стали команды конвейера в начале файла - весь этот синтаксис - это способ, которым документация EventStream рекомендует разбивать поток на фрагменты, разделенные символом \n в конце каждой строки файла .txt.

Единственное, что мне пришлось изменить, это ответ names. Мне пришлось немного обмануть это, поскольку, если я попытался добавить все имена 13MM в массив, я снова столкнулся с проблемой нехватки памяти. Я обошел это, просто собрав 432-е и 43 243-е имена и добавив их в свой собственный массив. Не совсем то, о чем спрашивали, но эй, мне нужно было проявить немного творчества.

Результаты от Node.js и EventStream: раунд 2

Хорошо, с внедрением нового решения я снова запустил Node.js с моим файлом размером 2,55 ГБ, и мои пальцы скрестили пальцы, это сработает. Проверьте результаты.

Успех!

Заключение

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

Следите за обновлениями для второй части этой серии, где я сравниваю свои три разных способа чтения данных в Node.js с тестированием производительности, чтобы увидеть, какой из них действительно превосходит другие. Результаты открывают глаза - особенно по мере того, как объем данных становится больше ...

Спасибо за чтение. Надеюсь, это дает вам представление о том, как обрабатывать большие объемы данных с помощью Node.js. Мы очень ценим аплодисменты и акции!

Если вам понравилось это читать, возможно, вам понравятся и другие мои блоги:

Ссылки и дополнительные ресурсы: