CoderCastrov logo
CoderCastrov
Парсер

Как парсить государственные данные с помощью JavaScript

Как парсить государственные данные с помощью JavaScript
просмотров
11 мин чтение
#Парсер

Используя инспектор сети, jQuery, querySelector и async/await для получения структурированных данных с запутанных веб-сайтов

Когда вы пытаетесь проанализировать политические данные - будь то создание списка избирателей в вашем районе или отслеживание избирательных тенденций - вы часто должны получать государственные данные из интернета.

Проблема в том, что государственные веб-сайты редко позволяют вам скачать файл Excel или CSV со всеми базовыми данными. Чаще всего вам придется обратиться к проверенной временем технике парсинга структурированных данных с запутанного веб-сайта.

Если вы хотите стать хорошим в захвате и анализе государственных данных, вам придется добавить множество инструментов для парсинга в свой арсенал, от парсинга грязных PDF до становления GreaseMonkey. Я пришлось освоить немало из них за свою жизнь, поэтому я хотел использовать этот пост, чтобы познакомить вас с некоторыми из них.

В этом посте мы сосредоточимся на более простых методах: получение структурированных данных с более современных веб-сайтов с помощью JavaScript. В качестве примера мы будем использовать мой недавний проект по созданию списка всех "ящиков для голосования по почте", которые были предложены на выборах 2020 года.

(Техники, которые я вам покажу, должны работать для любого вида данных, не только для государственных данных, хотя я обнаружил, что вам они понадобятся больше всего на государственных веб-сайтах!)

Простой способ: нахождение базовых данных в консоли

Многие современные веб-сайты хранят структурированные данные в CSV- или JSON-файле, а затем имеют красивый интерфейс, который загружает эти данные и представляет их в интерактивном пользовательском интерфейсе. Это самые простые сайты для парсинга: вам не нужно разбираться с пользовательским интерфейсом, если вы можете найти структурированные данные.

Однако поиск этого файла может быть немного сложным.

Например, в Мичигане вы можете найти пункты сдачи через MichiganDropbox.com. К сожалению, для того чтобы получить список пунктов сдачи, нужно сделать много кликов, что сделает парсинг очень трудоемким:

Наш первый шаг - узнать, загружает ли этот веб-сайт данные из CSV- или JSON-файла. Для этого мы открываем консоль разработчика (которую можно открыть, используя "Инструменты разработчика") и переходим на вкладку "Сеть". Эта вкладка показывает все файлы, загруженные веб-страницей. Большинство из них - это JavaScript-скрипты или CSS-стили... но посмотрите, что еще мы находим!

Примечание: читатели сообщили мне, что Medium уменьшает некоторые изображения в этой статье, из-за чего мелкий текст может выглядеть размытым. Чтобы помочь, я загрузил все изображения из этой статьи в репозиторий GitHub в оригинальном качестве.

Страница загружает файл под названием dropboxLocations.json, который, кажется, и есть то, что нам нужно! Когда мы переходим на вкладку "Ответ", чтобы посмотреть, что находится внутри этого файла, мы видим данные о пунктах сдачи, скрытые на нескольких уровнях в структуре объекта.

Бум! Мы нашли золотую жилу. Как только у нас будет эта JSON-структура, мы сможем получить список всех пунктов сдачи без необходимости парсить данные. Я скачал файл, щелкнув правой кнопкой мыши по названию файла и выбрав "Скопировать ответ"; процесс может отличаться в других браузерах. Посмотрите на JSON-файл сами:

Теперь нам просто нужно передать эту JSON-структуру в скрипт или инструмент, который мы используем для анализа. В нашем случае мы хотели создать огромный CSV-файл со всеми пунктами сдачи, поэтому я сначала использовал этот отличный конвертер JSON в CSV.

Трудный путь: манипуляции в консоли

Однако вы не всегда сможете найти базовый файл CSV или JSON. Рассмотрим поиск Dropbox в Джорджии на GaBallotDropbox.org. Как и в Мичигане, на этом веб-сайте вы можете просматривать Dropbox в каждом округе, но я, по крайней мере, не смог найти исходный источник данных. (Я думаю, что они "минимизировали" набор данных, чтобы уменьшить его размер файла; это имеет побочный эффект, делая его гораздо сложнее извлечь.)

Поскольку мы не можем найти блоб, нам придется скрести его старомодным способом. К счастью, веб-сайт довольно прост: просто выберите округ из выпадающего меню, и все Dropbox будут немедленно отображены ниже.

В Джорджии более 100 округов, поэтому щелкать по каждому округу в выпадающем меню было бы большой проблемой. Таким образом, наш подход будет примерно таким:

Приближенное изображение данных Dropbox, которые выводятся для каждого округа.

Открытие выпадающего списка

Давайте попробуем использовать "Проверить элемент" для того, чтобы выяснить, как программно открыть выпадающий список. Кажется, что стрелка вверх должна это сделать, поэтому давайте щелкнем по ней, чтобы найти соответствующий HTML-код:

Использование 'Проверить элемент' для поиска кнопки, открывающей выпадающий список.

Теперь нам нужно найти уникальный способ идентификации кнопки, чтобы мы могли вызвать ее с помощью JavaScript. К сожалению, у этого элемента нет уникального id, но у него есть class, который кажется необычным: MuiAutocomplete-popupIndicator.

Итак, давайте перейдем на вкладку "Консоль" и введем некоторый JavaScript-код, который будет щелкать по этой кнопке. Кажется, что на странице уже используется что-то вроде jQuery с помощью $, поэтому мы можем использовать типичный синтаксис $("селектор") из jQuery.

// Нажать на выпадающий список
$('.MuiAutocomplete-popupIndicator').click();

Похоже, что это работает: запуск этого кода открывает меню.

Программное открытие выпадающего списка.

Клик по округу

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

The tooltip on the “Clinch County” item tells us that its ID is `countySelector-option-31`.

Еще лучше, идентификаторы довольно систематичны: это countySelector-option-###, где числа увеличиваются от 0 для первого округа в алфавитном порядке (Appling) до 159 для последнего округа в алфавитном порядке (Worth). (Интересный факт: Джорджия имеет больше округов чем любой другой штат, кроме Техаса.) Мы будем использовать это удобное свойство позже.

Но сначала нам нужно написать код для щелчка по элементу списка. Это довольно просто, но чтобы сделать это интереснее, мы введем шаблонные строки JavaScript:

// Клик по элементу выпадающего списка
// В качестве примера выберем округ Clinch (#31)
let i = 31;
$(`#countySelector-option-${i}`).click();

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

Programmatically opening the dropdown, and then clicking on a list item, lets us pull up any county we desire. We just need to set `i` accordingly.

Получение списка dropbox'ов

Теперь нам нужно изучить HTML и получить список dropbox'ов. К сожалению, элементы, содержащие информацию о dropbox'ах, не имеют уникальных имен классов, поэтому нам придется найти некоторый обходной путь для доступа к ним.

Inspect Element показывает, как информация о dropbox'ах вложена в HTML.

Путем проб и ошибок я обнаружил, что информация о dropbox'ах хранится в элементах MuiBox-root, вложенных глубоко в элементе jss6. Этот код позволяет нам получить список контейнеров dropbox'ов:

// Что бы ни было `$`, оно не является обычным jQuery.
// Поэтому я создаю обертку вокруг альтернативы на основе нативных средств.
let $$ = x => document.querySelectorAll(x); // получить NodeList div'ов, содержащих данные о dropbox'ах
let dropboxDivs = $$(".jss6 div.MuiBox-root div.MuiBox-root");

Как я намекал, то, что ���, не является обычным jQuery. Поэтому он не может выполнить мой сложный строковый селектор. Вместо этого я создал обертку вокруг document.querySelectorAll, который является новой нативной копией jQuery в JavaScript. Большинство селекторов, которые работают с jQuery, работают и с querySelectorAll, но новая функция имеет настолько неприятное название, что я просто не мог не придумать замечательное сокращение. (Я использовал новый синтаксис стрелочной функции, чтобы создать функцию с минимальным кодом.)

В любом случае, этот код работает по назначению, позволяя нам получить данные о всех dropbox'ах в выбранном округе.

Получение `<div>`, содержащих информацию о dropbox'ах. Это хранится в массиве объектов 'Node'.

Кажется, что каждому dropbox'у предоставляется несколько строк текста: есть название местоположения, затем адрес синим цветом, затем часы работы черным цветом, а затем необязательный раздел "Примечание" оранжевым цветом. Похоже, что адрес находится внутри тега <a> внутри блока, в то время как остальные строки находятся в теге <p>.

HTML внутри каждого из `<div>`, содержащих информацию о dropbox'ах.

Чтобы собрать эти данные, мы просто смотрим на innerHTML каждого тега <p> или <a> здесь. Затем мы сохраняем это как объект внутри массива с названием data.

dropboxDivs.forEach((div) => {
  data.push({
    location: arrayGet(div.children, 0, "innerHTML"),
    address: arrayGet(div.children, 1, "innerHTML"),
    hours: arrayGet(div.children, 2, "innerHTML"),
    notes: arrayGet(div.children, 3, "innerHTML"),
  });
});

arrayGet - это просто удобная функция, которую я создал, которая проверяет, существует ли нужный индекс в массиве, и только затем получает innerHTML, если этот индекс существует. Это простой способ защиты от ошибок "выхода за границы индекса". (Не волнуйтесь, я предоставлю весь использованный мной код в конце этого примера.)

Цикл по всем округам

Наконец, нам просто нужно пройти через каждый округ (от #0 до #159). Общий ход алгоритма выглядит так:

let data = [];for (let i = 0; i `< 160; i++) {
  // Нажать на выпадающий список
  // Нажать на элемент выпадающего списка #i
  // Получить <div>`, содержащие каждый выпадающий список
  // Извлечь данные из каждого `<div>` и добавить в `data`
}

Исправление проблем синхронизации с помощью async/await

Одна проблема, которую я заметил в своем алгоритме, заключалась в том, что код для нажатия на элемент выпадающего списка иногда не работал, а также код для извлечения данных из <div> иногда не мог найти нужные элементы. Это было странно, так как каждый фрагмент работал в отдельности.

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

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

// Это позволяет нам синхронно ждать X миллисекунд
const wait = ms => new Promise(res => setTimeout(res, ms));

После определения этой функции мы вставляем ее между строками кода, чтобы заставить скрипт подождать:

// Что бы ни было `$`, оно не является обычным jQuery.
// Поэтому я создаю обертку вокруг альтернативы на основе нативных средств.
let $$ = x => document.querySelectorAll(x); // получить NodeList div'ов, содержащих данные о dropbox'ах
let dropboxDivs = $$(".jss6 div.MuiBox-root div.MuiBox-root");
await``await``async``async``async/await`���

Мы помещаем этот код сразу после выполнения любого действия, которое изменяет веб-страницу, включая нажатие кнопок. Подождав, мы гарантируем, что следующая строка кода выполнится после завершения анимации. Я выбрал подождать полсекунды (500 мс), чтобы быть уверенным, но можно было бы обойтись и меньшим значением.

Кстати, ключевое слово data� делает эту операцию ожидания "синхронной": оно блокирует выполнение будущего кода до его завершения, а не запускает его в отдельный "поток" как таковой.

Но data� работает только внутри функции �������, согласно правилам конструкции async/await. Поэтому нам просто нужно обернуть этот код в функцию ������� и вызвать ее, например, так:

async function go() {
  for (let i = 0; i < 160; i++) {
    // Обычный код
  }
}go();

Сборка всего вместе

Когда мы объединяем код для изменения страницы, код для парсинга, вспомогательные функции, циклы и волшебство �������������, мы получаем удобный скрипт, который получает все ящики для голосования в Джорджии и сохраняет их в массиве ������. С несколькими дополнительными настройками кода для решения краевых случаев, мы готовы.

Вот полный скрипт для парсинга Джорджии!** **Я надеюсь, что он все еще работает к моменту вашего прочтения, но в любом случае, он должен быть полезным учебным инструментом.

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

После выполнения скрипта, информация о всех 360 ящиках находится в переменной ������.

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

Вот наш окончательный файл JSON:

Вот и все! Продвинутые темы?

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

Есть еще несколько продвинутых тем, о которых мы можем поговорить позже: разбор сырого HTML по-старинке, парсинг PDF-файлов, использование OCR для цифрового преобразования отсканированных документов и т. д. Дайте мне знать в комментариях, если вы хотите узнать больше об этом!