Jsoup Java: примеры парсинга таблиц и списков

Когда на странице встречается таблица с данными или структурированный список, обычного извлечения текста недостаточно: нужно пройтись по строкам, ячейкам, пунктам, сохранив взаимосвязи. Библиотека 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 и сразу получать результат в удобном виде.