From 8425b6a5009076156941d4c910f51441a1526339 Mon Sep 17 00:00:00 2001 From: Eduard Kuksa Date: Sun, 16 Mar 2025 13:19:21 +0700 Subject: [PATCH] complete task INDX-0002. parse one site from list --- pom.xml | 5 + src/main/java/searchengine/model/Link.java | 42 +++++ .../services/IndexingServiceImpl.java | 154 +++++++++++++++++- src/main/resources/application.yaml | 10 +- 4 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 src/main/java/searchengine/model/Link.java diff --git a/pom.xml b/pom.xml index 041b6a4..891d7db 100644 --- a/pom.xml +++ b/pom.xml @@ -50,6 +50,11 @@ spring-dotenv 4.0.0 + + org.jsoup + jsoup + 1.18.3 + diff --git a/src/main/java/searchengine/model/Link.java b/src/main/java/searchengine/model/Link.java new file mode 100644 index 0000000..0a69365 --- /dev/null +++ b/src/main/java/searchengine/model/Link.java @@ -0,0 +1,42 @@ +package searchengine.model; + +import java.net.URI; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public record Link(URI uri, int depth, Set children, Link parent) { + public Link { + children = new HashSet<>(); + } + + public Link(URI uri, Link parent) { + this(uri, (parent == null) ? 0 : parent.depth() + 1, new HashSet<>(), parent); + } + + public void addChild(URI childUri) { + children.add(new Link(childUri, this)); + } + + // Переопределяем equals и hashCode, исключая поля children и parent + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Link link = (Link) o; + return depth == link.depth && Objects.equals(uri, link.uri); + } + + @Override + public int hashCode() { + return Objects.hash(uri, depth); // Исключаем children и parent + } + + @Override + public String toString() { // А он нужен? + return "Link{" + + "uri=" + uri + + ", depth=" + depth + + '}'; // Исключаем children и parent + } +} \ No newline at end of file diff --git a/src/main/java/searchengine/services/IndexingServiceImpl.java b/src/main/java/searchengine/services/IndexingServiceImpl.java index ba79883..e62534a 100644 --- a/src/main/java/searchengine/services/IndexingServiceImpl.java +++ b/src/main/java/searchengine/services/IndexingServiceImpl.java @@ -1,14 +1,27 @@ package searchengine.services; import lombok.RequiredArgsConstructor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; import org.springframework.stereotype.Service; import searchengine.config.SitesList; +import searchengine.model.Link; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; @Service @RequiredArgsConstructor public class IndexingServiceImpl implements IndexingService { - boolean indexingIsRunning = false; + private boolean indexingIsRunning = false; private final SitesList sitesList; + private final Random random = new Random(); @Override public void startIndexing() { @@ -16,12 +29,137 @@ public class IndexingServiceImpl implements IndexingService { System.out.println("Индексация уже запущена"); return; } - - System.out.println("Список сайтов для индексации:"); - sitesList.getSites().forEach(site -> System.out.println("URL: " + site.getUrl() + ", Название: " + site.getName())); - - System.out.println("Запуск индексации"); indexingIsRunning = true; - System.out.println("Индексация запущена"); + + // Выводим список сайтов для индексации + System.out.println("Список сайтов для индексации:"); + sitesList.getSites().forEach(site -> + System.out.println("URL: " + site.getUrl() + ", Название: " + site.getName()) + ); + + // Начинаем парсинг первого сайта + String startUrl = sitesList.getSites().get(0).getUrl(); + String siteName = sitesList.getSites().get(0).getName(); + + System.out.println("Начинаем парсинг сайта " + siteName + " (" + startUrl + ")"); + + Set visitedLinks = ConcurrentHashMap.newKeySet(); // Набор для отслеживания посещенных URL + + try { + URI startUri = new URI(startUrl); + System.out.printf("\n=== Начало индексации %s (%s) ===\n", siteName, startUri); + Link rootLink = new Link(startUri, null); // Создаем корневой Link + parse(rootLink, visitedLinks); // Запуск парсинга + } catch (URISyntaxException e) { + System.out.println("Некорректный URL: " + startUrl); + } catch (InterruptedException e) { + System.out.println("Индексация была прервана: " + e.getMessage()); + Thread.currentThread().interrupt(); + } finally { + indexingIsRunning = false; + System.out.println("\n=== Индексация завершена ==="); + System.out.println("Всего страниц проиндексировано: " + visitedLinks.size()); + } } -} + + private void parse(Link link, Set visitedLinks) throws InterruptedException { + // Добавляем текущий URL в visitedLinks до начала обработки + if (!visitedLinks.add(link.uri())) { + return; // Если URL уже был обработан, выходим + } + + // Задержка для соблюдения правил robots.txt + Thread.sleep(50 + random.nextInt(150)); + System.out.println("Парсим страницу: " + link.uri()); + + // Добавляем дочерние ссылки + addChildLinks(link, visitedLinks); + + // Рекурсивно обрабатываем дочерние ссылки + link.children().forEach(child -> { + try { + parse(child, visitedLinks); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Парсинг был прерван: " + e.getMessage()); + } + }); + } + + private void addChildLinks(Link link, Set visitedLinks) { + try { + Document document = Jsoup.connect(link.uri().toString()) + .userAgent("Mozilla/5.0") + .referrer("https://www.google.com") + .get(); + + document.select("a[href]").forEach(element -> { + String hrefStr = element.attr("href").strip(); + try { + // Кодируем URL перед созданием объекта URI + String encodedHrefStr = URLEncoder.encode(hrefStr, StandardCharsets.UTF_8) + .replaceAll("%3A", ":") + .replaceAll("%2F", "/"); + URI href = URI.create(encodedHrefStr); + + if (!href.isAbsolute()) { + href = link.uri().resolve(href); + } + href = normalizeUri(href); + + // Проверяем, что ссылка корректна + if (isCorrectUrl(link.uri(), href, visitedLinks)) { + link.addChild(href); // Используем метод addChild из класса Link + } + } catch (Exception e) { + // Игнорируем некорректные URL без вывода сообщений + } + }); + } catch (Exception e) { + // Игнорируем ошибки без вывода сообщений + } + } + + private URI normalizeUri(URI href) { + try { + String path = href.getPath(); // Получаем путь + if (path == null) { + path = ""; // Если путь равен null, заменяем на пустую строку + } else { + path = path.replaceAll("/+$", ""); // Убираем завершающие слэши + } + return new URI( + href.getScheme(), + href.getAuthority(), + path, // Используем обработанный путь + null, + href.getFragment() + ); + } catch (URISyntaxException e) { + return href; // В случае ошибки возвращаем исходный URI + } + } + + private boolean isCorrectUrl(URI baseUri, URI href, Set visitedLinks) { + // Проверка на некорректные протоколы + String hrefStr = href.toString(); + if (hrefStr.startsWith("javascript:") || hrefStr.startsWith("mailto:") || hrefStr.startsWith("tel:")) { + return false; + } + + // Проверка на принадлежность тому же домену + if (href.getHost() == null || !href.getHost().equals(baseUri.getHost())) { + return false; + } + + // Проверка на файлы и якоря + Pattern patternNotFile = Pattern.compile("(\\S+(\\.(?i)(jpg|png|gif|bmp|pdf|php|doc|docx|rar))$)"); + Pattern patternNotAnchor = Pattern.compile("#([\\w\\-]+)?$"); + if (href.isOpaque() || patternNotFile.matcher(hrefStr).find() || patternNotAnchor.matcher(hrefStr).find()) { + return false; + } + + // Проверка на уже посещенные ссылки + return !visitedLinks.contains(href); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 57ecf45..5dfa0a1 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -16,9 +16,11 @@ spring: indexing-settings: sites: - - url: https://www.lenta.ru - name: Лента.ру + - url: https://wiki.kuksa.dev + name: Wiki Kuksa.Dev + - url: https://www.autelrobotics.com + name: Autel Robotics - url: https://www.skillbox.ru name: Skillbox - - url: https://www.playback.ru - name: PlayBack.Ru \ No newline at end of file +# - url: https://www.playback.ru +# name: PlayBack.Ru \ No newline at end of file