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