Что такое jsoup? Парсер HTML для Java
Парсинг веб-сайтов, созданных для современных браузеров, намного сложнее, чем было десять лет назад. Jsoup - удобный API, который делает парсинг веб-сайтов тривиальным с помощью обхода DOM, CSS-селекторов, методов, подобных JQuery, и многого другого. Но это не без своих ограничений. Каждый парсер - это тикающая бомба.
Реальный HTML ненадежен. Он меняется без предупреждения, так как это не задокументированный API. Когда наша Java-программа не справляется с парсингом, мы внезапно оказываемся перед тикающей бомбой. В некоторых случаях это простая проблема, которую мы можем воспроизвести локально и развернуть. Но нюансированные изменения в дереве DOM могут быть сложнее заметить в локальном тестовом случае. В таких случаях нам нужно понять проблему в дереве разбора перед развертыванием обновления. В противном случае у нас может быть сломанный продукт в производстве.
Прежде чем мы перейдем к деталям отладки jsoup, давайте сначала ответим на вопрос выше и обсудим основные концепции, лежащие в основе jsoup.
Сайт jsoup определяет его следующим образом:
jsoup - это библиотека Java для работы с реальным HTML. Она предоставляет очень удобный API для получения URL-адресов, извлечения и манипулирования данными с использованием лучших методов DOM HTML5 и селекторов CSS.
jsoup реализует спецификацию WHATWG HTML5 и разбирает HTML в тот же DOM, что и современные браузеры.
С этим в виду, давайте перейдем непосредственно к простому примеру, также взятому с того же сайта:
Document doc = Jsoup.connect("https://en.wikipedia.org/").get();
log(doc.title());
Elements newsHeadlines = doc.select("#mp-itn b a");
for (Element headline : newsHeadlines) {
log("%s\n\t%s",
headline.attr("title"), headline.absUrl("href"));
}
Этот фрагмент кода получает заголовки новостей с Википедии. В приведенном выше коде вы можете увидеть несколько интересных особенностей:
- Подключение к URL практически без проблем - просто передайте строку URL в метод connect
- Есть особые случаи для некоторых дочерних элементов. Например, заголовок представлен в виде простого метода, который возвращает строку без выборки из дерева DOM
- Однако мы можем выбрать запись, используя довольно сложный синтаксис селектора
Если вы смотрите на это и думаете "это выглядит хрупко". Да, это так.
Простой тест Jsoup
Для демонстрации отладки я создал простой демо-проект, который вы можете скачать здесь.
Вы можете использовать следующую зависимость Maven для установки Jsoup в любую программу на Java. Maven автоматически загрузит jar-файл Jsoup:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.14.3</version>
</dependency>
Этот демо-проект представляет собой простое Java-приложение, которое возвращает полный список внешних ссылок и элементов с атрибутами src на странице. Он основан на коде отсюда, преобразованном в программу на Java с использованием Spring Boot. Код Jsoup относительно короткий:
public Set<String> listLinks(String url, boolean includeMedia) throws IOException {
Document doc = Jsoup.connect(url).get();
Elements links = doc.select("a[href]");
Elements imports = doc.select("link[href]");
Set<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
if(includeMedia) {
Elements media = doc.select("[src]");
for (Element src : media) {
result.add(src.absUrl("src"));
//result.add(src.attr("abs:src"));
}
}
for (Element link : imports) {
result.add(link.absUrl("abs:href"));
}
for (Element link : links) {
result.add(link.absUrl("abs:href"));
}
return result;
}
Как видите, мы получаем входную строку URL. Мы также можем использовать потоки ввода, но это делает парсинг относительных URL-адресов немного сложнее (нам все равно нужен базовый URL). Затем мы ищем ссылки и объекты с атрибутом src. Затем код добавляет их все во множество, чтобы сохранить записи отсортированными и уникальными.
Мы предоставляем это в виде веб-сервиса с использованием следующего кода:
@RestController
public class ParseLinksWS {
private final ParseLinks parseLinks;
public ParseLinksWS(ParseLinks parseLinks) {
this.parseLinks = parseLinks;
}
@GetMapping("/parseLinks")
public Set<String> listLinks(@RequestParam String url, @RequestParam(required = false) Boolean includeMedia) throws IOException {
return parseLinks.listLinks(url, includeMedia == null ? true : includeMedia);
}
}
После запуска приложения мы можем использовать его с помощью простой команды curl:
curl -H "Content-Type: application/json" "http://localhost:8080/parseLinks?url=https%3A%2F%2Flightrun.com"
Это выводит список URL-адресов, на которые ссылаются на домашней странице Lightrun.
Отладка ошибок парсинга
Типичные проблемы с парсингом строк возникают, когда изменяется объект элемента. Например, структура страницы Википедии может измениться, и метод select выше может внезапно перестать работать. Это часто является тонкой ошибкой, например, отсутствие элемента DOM в иерархии объектов Java, что может вызвать сбой метода select.
К сожалению, это может быть тонкая ошибка. Особенно при работе с вложенными узлами и зависимостями между документами. Большинство разработчиков решают эту проблему, регистрируя большое количество данных. Это может быть проблемой по двум основным причинам:
- Большие журналы - они сложны для чтения и очень дорогостоящие для обработки
- Нарушение конфиденциальности/GDPR - скрапинг сайта может включать конфиденциальную информацию, относящуюся к конкретному пользователю. Что еще хуже!
- Скрапинг сайта может измениться, чтобы включить конфиденциальную информацию после того, как был внедрен парсинг. Регистрация этой конфиденциальной информации может нарушать различные законы.
Если мы не регистрируем достаточно данных и не можем воспроизвести проблему локально, возникают сложности. Мы застреваем в цикле "добавить журналы, собрать, протестировать, развернуть, воспроизвести - повторить".
Lightrun предлагает лучший способ. Просто отслеживайте конкретную ошибку непосредственно в продакшене, проверьте проблему и создайте исправление, которое будет работать с одним развертыванием.
ПРИМЕЧАНИЕ: В этом руководстве предполагается, что вы установили Lightrun и понимаете основные концепции его использования. Если нет, пожалуйста, ознакомьтесь с документацией.
Навигация в браузерной модели DOM
Предположим, что вы не знаете, где искать, хорошим местом для начала является изучение API jsoup. Это может привести вас обратно к пользовательскому коду. Здорово в том, что это работает независимо от вашего кода. Мы можем найти правильную строку/файл для снимка, изучая вызов API.
Я нажал Ctrl+Click (на Mac используйте Meta+Click) на вызов метода select здесь:
Elements links = doc.select("a[href]");
И это привело меня к классу Element. Внутри него я нажал Ctrl+Click на метод Selector "select" и попал в "интересное" место.
Здесь я могу разместить условный снимок, чтобы увидеть каждый случай, когда выполняется запрос "a[href]":
Это может показать мне методы/строки, выполняющие этот запрос:
Это может сильно помочь в сужении общей проблемной области в иерархии объектов документа.
Иногда снимка может быть недостаточно. Нам может понадобиться использовать журнал. Преимущество журналирования заключается в том, что мы можем получить много информации, но только для конкретного случая и по требованию.
Ценность журналов заключается в том, что они могут отслеживать проблему так же, как и шаги в коде. Место, где мы разместили снимок, проблематично для журналов. Мы знаем, какой запрос был отправлен, но у нас еще нет значения, которое возвращается. Мы можем легко решить эту проблему с помощью журналов. Сначала мы добавляем журнал с следующим текстом:
"Выполняется запрос {query}"
Затем, чтобы узнать, сколько записей мы получили, мы просто переходим к вызывающему коду (который мы знаем благодаря стеку в снимке) и добавляем следующий журнал там:
Запрос ссылок вернул {links.size()}
Это создает следующий журнал, который позволяет нам увидеть, что у нас есть 147 ссылок a[href]
. Преимущество этого заключается в том, что дополнительные журналы вплетаются в существующие журналы в контексте:
Feb 02, 2022 11:25:27 AM org.jsoup.select.Selector select
INFO: LOGPOINT: Выполняется запрос a[href]
Feb 02, 2022 11:25:27 AM com.lightrun.demo.jsoupdemo.service.ParseLinks listLinks
INFO: LOGPOINT: Запрос ссылок вернул 147
Feb 02, 2022 11:25:27 AM org.jsoup.select.Selector select
INFO: LOGPOINT: Выполняется запрос link[href]
Feb 02, 2022 11:25:27 AM org.jsoup.select.Selector select
INFO: LOGPOINT: Выполняется запрос [src]
Избегайте проблем с безопасностью и GDPR
GDPR и проблемы безопасности могут стать проблемой утечки информации о пользователе в журналы. Это может быть серьезной проблемой, и Lightrun помогает существенно снизить этот риск.
Lightrun предлагает два потенциальных решения, которые могут использоваться вместе, когда это применимо.
Передача журналов
Большая проблема с GDPR - это прием журналов. Если вы регистрируете личные данные пользователей, а затем отправляете их в облако, они остаются там на долгое время. Их трудно найти после факта, и их очень сложно исправить.
Lightrun предоставляет возможность перенаправлять все внедренные журналы Lightrun непосредственно в среду разработки. Это имеет преимущество удаления шума от других разработчиков, которые могут работать с журналами. Он также может пропустить прием (по выбору).
Чтобы отправлять журналы только в плагин, выберите режим перенаправления как "плагин".
Сокращение/Блоклисты Персональной Идентифицирующей Информации (ПИИ)
Персонально Идентифицирующая Информация (ПИИ) является основой GDPR и также представляет собой значительный риск безопасности. Злонамеренный разработчик в вашей организации может попытаться использовать Lightrun для сбора информации о пользователях. Блоклисты предотвращают разработчиков от размещения действий в определенных файлах.
Сокращение ПИИ позволяет скрывать информацию, соответствующую определенным шаблонам, из журналов (например, формат номера кредитной карты и т. д.). Это можно определить в веб-интерфейсе Lightrun с помощью роли менеджера.
TL;DR
С использованием Java для парсинга контента, jsoup является очевидным лидером. Разработка с использованием jsoup представляет собой не только операции со строками или обработку аспектов подключения. Помимо получения объекта документа, он также обрабатывает сложные аспекты, необходимые для работы с элементами DOM и скриптинга.
Парсинг - это рискованное занятие. Он может сломаться в мгновение ока, когда веб-сайт незначительно изменяется.
Еще хуже, он может сломаться у некоторых пользователей в странных способах, которые невозможно воспроизвести локально.
Благодаря Lightrun мы можем отлаживать такие сбои непосредственно в производственной среде и быстро публиковать рабочую версию. Вы можете использовать Lightrun бесплатно, зарегистрировавшись здесь.