Когда на странице встречается таблица с данными или структурированный список, обычного извлечения текста недостаточно: нужно пройтись по строкам, ячейкам, пунктам, сохранив взаимосвязи. Библиотека jsoup даёт удобные методы для работы с таблицами и списками прямо из коробки, а несколько строчек Java кода позволяют превратить HTML-разметку в двумерный массив строк или список объектов. В этой статье я собрал практические примеры парсинга таблиц и списков с помощью Jsoup.
Базовая загрузка страницы и выделение таблицы
Для начала получим документ и найдём первую таблицу по селектору. Метод select() возвращает коллекцию элементов, поэтому даже если таблица одна, мы извлекаем её через first().
Document doc = Jsoup.connect("https://example.com/data").get();
Element table = doc.select("table").first();
Если на странице несколько таблиц, лучше использовать более точный селектор — класс или id: doc.select("table.prices"), doc.select("#report").
Извлечение заголовков таблицы
Часто первая строка таблицы содержит названия колонок внутри тегов <th>. Jsoup позволяет вытащить их отдельно от данных.
Elements headers = table.select("th");
List<String> headerList = headers.eachText();
System.out.println(headerList); // [Название, Цена, Количество]
Если заголовки находятся внутри <thead>, селектор можно уточнить: table.select("thead th").
Обход строк и ячеек: превращаем таблицу в список списков
Самый универсальный способ — пройти по всем строкам <tr> и внутри каждой строки извлечь все ячейки <td>. Так мы получим двумерную структуру данных, пригодную для дальнейшего анализа.
Elements rows = table.select("tr");
List<List<String>> tableData = new ArrayList<>();
for (Element row : rows) {
List<String> rowData = new ArrayList<>();
Elements cols = row.select("td");
for (Element col : cols) {
rowData.add(col.text().trim());
}
if (!rowData.isEmpty()) {
tableData.add(rowData);
}
}
// Вывод результата
for (List<String> row : tableData) {
System.out.println(row);
}
Обратите внимание на проверку !rowData.isEmpty(): она отсеивает пустые строки, которые могут появиться из-за тегов <tr> внутри <thead> или при наличии заголовков. Так мы получаем чистые данные, готовые к записи в CSV, базу или отображению в UI.
Парсинг таблицы с объединёнными ячейками
Иногда таблицы содержат атрибуты colspan и rowspan. Jsoup не обрабатывает их автоматически, но мы можем учесть сдвиги вручную. Вот пример, как восстановить логическую сетку для простой таблицы с colspan.
Elements rows = table.select("tr");
int columnCount = 0;
for (Element row : rows) {
Elements cells = row.select("td, th");
int currentCol = 0;
for (Element cell : cells) {
int colspan = Integer.parseInt(cell.attr("colspan").isEmpty() ? "1" : cell.attr("colspan"));
int rowspan = Integer.parseInt(cell.attr("rowspan").isEmpty() ? "1" : cell.attr("rowspan"));
// заполняем матрицу с учётом colspan и rowspan
currentCol += colspan;
}
columnCount = Math.max(columnCount, currentCol);
}
Полноценная обработка rowspan сложнее и требует хранения информации о занятых ячейках в предыдущих строках, но этот пример даёт направление для адаптации под конкретные нужды.
Извлечение списков: ul и ol
Ненумерованные и нумерованные списки парсятся ещё проще. Все пункты <li> извлекаются одним селектором.
Element list = doc.select("ul.menu").first();
Elements items = list.select("li");
for (Element item : items) {
String text = item.text();
String link = item.select("a").attr("abs:href");
System.out.println(text + " -> " + link);
}
Для вложенных списков структура сохраняется автоматически, но если нужно сохранить иерархию, потребуется рекурсивный обход дочерних <ul> и <ol>.
Пример рекурсивного парсинга вложенных списков
Предположим, у нас есть многоуровневое меню, и мы хотим сохранить древовидную структуру. Следующий пример обходит <ul> рекурсивно и строит карту «родитель → дочерние элементы».
public static void parseNestedList(Element ul, Map<String, List<String>> result, String parentKey) {
Elements items = ul.select("> li");
for (Element li : items) {
String text = li.ownText(); // текст самого пункта, без дочерних элементов
Element childUl = li.select("> ul").first();
List<String> children = new ArrayList<>();
if (childUl != null) {
// рекурсивно собираем дочерние пункты
for (Element childLi : childUl.select("> li")) {
children.add(childLi.ownText());
}
parseNestedList(childUl, result, text);
}
result.computeIfAbsent(parentKey, k -> new ArrayList<>()).add(text);
}
}
Метод ownText() возвращает только непосредственный текст элемента, игнорируя содержимое вложенных тегов, что полезно при парсинге многоуровневых структур.
Парсинг определений (dl/dt/dd)
Списки определений часто используются для отображения пар «термин — описание». Jsoup позволяет извлечь их так же легко.
Element dl = doc.select("dl.glossary").first();
Elements terms = dl.select("dt");
Elements definitions = dl.select("dd");
for (int i = 0; i < terms.size(); i++) {
String term = terms.get(i).text();
String definition = i < definitions.size() ? definitions.get(i).text() : "";
System.out.println(term + " — " + definition);
}
Объединение данных из нескольких таблиц
На странице может быть несколько однотипных таблиц. Соберём данные со всех в один плоский список.
List<String> allValues = new ArrayList<>();
Elements tables = doc.select("table.data");
for (Element table : tables) {
Elements cells = table.select("td");
for (Element cell : cells) {
allValues.add(cell.text().trim());
}
}
System.out.println("Всего ячеек: " + allValues.size());
Практический пример: парсинг курса валют
Завершим реальным сценарием: возьмём страницу с таблицей валют и превратим её в список объектов Rate.
class Rate {
String currency;
double buy;
double sell;
Rate(String c, double b, double s) { currency = c; buy = b; sell = s; }
}
Document doc = Jsoup.connect("https://example.com/rates").get();
Element table = doc.select("table.rates").first();
List<Rate> rates = new ArrayList<>();
for (Element row : table.select("tr")) {
Elements cols = row.select("td");
if (cols.size() >= 3) {
String currency = cols.get(0).text();
double buy = Double.parseDouble(cols.get(1).text().replace(",", "."));
double sell = Double.parseDouble(cols.get(2).text().replace(",", "."));
rates.add(new Rate(currency, buy, sell));
}
}
rates.forEach(r -> System.out.println(r.currency + ": " + r.buy + "/" + r.sell));
Этот пример демонстрирует полный цикл: загрузка, поиск, извлечение и преобразование данных в типизированные объекты Java. В реальном проекте стоит добавить обработку исключений и проверку на null.
Коротко о главном
- Таблицы парсятся комбинацией
select("tr")иselect("td"). - Для заголовков используйте
select("th")или уточняйте черезthead. - Вложенные списки обрабатываются рекурсивным вызовом с селектором
> li. - Объединённые ячейки (
colspan,rowspan) требуют дополнительной логики для восстановления сетки. - Всегда добавляйте проверки на пустоту и размер коллекции перед извлечением, чтобы избежать
IndexOutOfBoundsException.
Теперь у вас есть готовые jsoup java примеры для парсинга таблиц и списков. Это снимет большую часть рутины при извлечении данных из веб-страниц. Вы можете брать любой из приведённых фрагментов, адаптировать под свою структуру HTML и сразу получать результат в удобном виде.