From c37a368ae488529d8247157fab55d1bbbfc4c157 Mon Sep 17 00:00:00 2001 From: Eduard Kuksa Date: Thu, 3 Apr 2025 20:13:17 +0700 Subject: [PATCH] working code for task INDX-0003 --- .../java/searchengine/model/PageEntity.java | 8 +- .../repository/PageRepository.java | 4 + .../repository/SiteRepository.java | 1 + .../services/IndexingServiceImpl.java | 199 ++++++++++++++---- 4 files changed, 173 insertions(+), 39 deletions(-) diff --git a/src/main/java/searchengine/model/PageEntity.java b/src/main/java/searchengine/model/PageEntity.java index 534889a..36c83a3 100644 --- a/src/main/java/searchengine/model/PageEntity.java +++ b/src/main/java/searchengine/model/PageEntity.java @@ -7,18 +7,20 @@ import lombok.Setter; @Getter @Setter @Entity -@Table(name = "page", indexes = @Index(columnList = "path")) +//@Table(name = "page", indexes = @Index(columnList = "site_id, path", unique = true)) // Или используем составной индекс, если нужно сохранить уникальность +@Table(name = "page", indexes = @Index(columnList = "path"), uniqueConstraints = @UniqueConstraint(columnNames = {"site_id", "path"})) public class PageEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(nullable = false) private int id; - @ManyToOne(cascade = CascadeType.ALL) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "site_id", nullable = false) private SiteEntity site; - @Column(columnDefinition = "VARCHAR(255) NOT NULL", unique = true) + @Column(columnDefinition = "VARCHAR(255) NOT NULL") +// @Column(columnDefinition = "VARCHAR(255) NOT NULL", unique = true) // Или убираем unique = true private String path; @Column(nullable = false) diff --git a/src/main/java/searchengine/repository/PageRepository.java b/src/main/java/searchengine/repository/PageRepository.java index a727f35..0fc9427 100644 --- a/src/main/java/searchengine/repository/PageRepository.java +++ b/src/main/java/searchengine/repository/PageRepository.java @@ -3,7 +3,11 @@ package searchengine.repository; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import searchengine.model.PageEntity; +import searchengine.model.SiteEntity; @Repository public interface PageRepository extends CrudRepository { + void deleteBySite(SiteEntity site); + + PageEntity findBySiteAndPath(SiteEntity site, String path); } diff --git a/src/main/java/searchengine/repository/SiteRepository.java b/src/main/java/searchengine/repository/SiteRepository.java index 60dd735..f985e7c 100644 --- a/src/main/java/searchengine/repository/SiteRepository.java +++ b/src/main/java/searchengine/repository/SiteRepository.java @@ -6,4 +6,5 @@ import searchengine.model.SiteEntity; @Repository public interface SiteRepository extends CrudRepository { + SiteEntity findByUrl(String url); } diff --git a/src/main/java/searchengine/services/IndexingServiceImpl.java b/src/main/java/searchengine/services/IndexingServiceImpl.java index e62534a..fe1f18a 100644 --- a/src/main/java/searchengine/services/IndexingServiceImpl.java +++ b/src/main/java/searchengine/services/IndexingServiceImpl.java @@ -1,17 +1,30 @@ package searchengine.services; import lombok.RequiredArgsConstructor; +import org.jsoup.HttpStatusException; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import searchengine.config.SitesList; import searchengine.model.Link; +import searchengine.model.SiteEntity; +import searchengine.model.PageEntity; +import searchengine.model.StatusType; +import searchengine.repository.PageRepository; +import searchengine.repository.SiteRepository; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.Random; +import java.time.LocalDateTime; +import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; @@ -19,79 +32,164 @@ import java.util.regex.Pattern; @Service @RequiredArgsConstructor public class IndexingServiceImpl implements IndexingService { + private static final Logger logger = LoggerFactory.getLogger(IndexingServiceImpl.class); private boolean indexingIsRunning = false; private final SitesList sitesList; - private final Random random = new Random(); + private final SiteRepository siteRepository; + private final PageRepository pageRepository; @Override + @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, noRollbackFor = Exception.class) public void startIndexing() { if (indexingIsRunning) { - System.out.println("Индексация уже запущена"); + logger.info("Индексация уже запущена"); return; } indexingIsRunning = true; // Выводим список сайтов для индексации - System.out.println("Список сайтов для индексации:"); + logger.info("Список сайтов для индексации:"); sitesList.getSites().forEach(site -> - System.out.println("URL: " + site.getUrl() + ", Название: " + site.getName()) + logger.info("URL: {}, Название: {}", site.getUrl(), site.getName()) ); // Начинаем парсинг первого сайта - String startUrl = sitesList.getSites().get(0).getUrl(); - String siteName = sitesList.getSites().get(0).getName(); + String startUrl = sitesList.getSites().get(1).getUrl(); + String siteName = sitesList.getSites().get(1).getName(); - System.out.println("Начинаем парсинг сайта " + siteName + " (" + startUrl + ")"); + logger.info("Начинаем парсинг сайта {} ({})", siteName, startUrl); Set visitedLinks = ConcurrentHashMap.newKeySet(); // Набор для отслеживания посещенных URL + Set allLinks = ConcurrentHashMap.newKeySet(); // Набор для хранения всех ссылок try { URI startUri = new URI(startUrl); - System.out.printf("\n=== Начало индексации %s (%s) ===\n", siteName, startUri); + logger.info("=== Начало индексации {} ({}) ===", siteName, startUri); + + // Удаляем существующие данные по этому сайту + SiteEntity site = siteRepository.findByUrl(startUrl); + if (site != null) { + logger.info("Найден существующий SiteEntity с ID: {}", site.getId()); + pageRepository.deleteBySite(site); + logger.info("Удалены все PsgeEntity для SiteEntity с ID: {}", site.getId()); + siteRepository.delete(site); + logger.info("Удален SiteEntity с ID: {}", site.getId()); + } + + // Создаем новую запись в таблице site со статусом INDEXING + site = new SiteEntity(); + site.setName(siteName); + site.setUrl(startUrl); + site.setStatus(StatusType.INDEXING); + site.setStatus_time(LocalDateTime.now()); + site = siteRepository.save(site); // Сохраняем и обновляем объект + + logger.info("Создана новая запись в таблице site с ID: {}", site.getId()); + Link rootLink = new Link(startUri, null); // Создаем корневой Link - parse(rootLink, visitedLinks); // Запуск парсинга + allLinks.add(rootLink); // Добавляем корневую ссылку во множество всех ссылок + parse(rootLink, site, visitedLinks, allLinks); // Запуск парсинга + + // Обновляем статус сайта на INDEXED + site.setStatus(StatusType.INDEXED); + site.setStatus_time(LocalDateTime.now()); + siteRepository.save(site); + + logger.info("Статус сайта обновлен на INDEXED с ID: {}", site.getId()); } catch (URISyntaxException e) { - System.out.println("Некорректный URL: " + startUrl); + logger.error("Некорректный URL: {}", startUrl, e); + handleIndexingError(startUrl, "Некорректный URL: " + startUrl); } catch (InterruptedException e) { - System.out.println("Индексация была прервана: " + e.getMessage()); - Thread.currentThread().interrupt(); + logger.error("Индексация была прервана: {}", e.getMessage(), e); + handleIndexingError(startUrl, "Индексация была прервана: " + e.getMessage()); + } catch (Exception e) { + logger.error("Произошла ошибка при индексации: {}", e.getMessage(), e); + handleIndexingError(startUrl, "Произошла ошибка при индексации: " + e.getMessage()); } finally { indexingIsRunning = false; - System.out.println("\n=== Индексация завершена ==="); - System.out.println("Всего страниц проиндексировано: " + visitedLinks.size()); + logger.info("=== Индексация завершена ==="); + logger.info("Всего страниц проиндексировано: {}", visitedLinks.size()); } } - private void parse(Link link, Set visitedLinks) throws InterruptedException { + private void parse(Link link, SiteEntity site, Set visitedLinks, Set allLinks) throws InterruptedException { // Добавляем текущий URL в visitedLinks до начала обработки if (!visitedLinks.add(link.uri())) { + logger.debug("URL уже был обработан: {}", link.uri()); return; // Если URL уже был обработан, выходим } + // Обновляем время статуса + site.setStatus_time(LocalDateTime.now()); + siteRepository.save(site); + // Задержка для соблюдения правил robots.txt - Thread.sleep(50 + random.nextInt(150)); - System.out.println("Парсим страницу: " + link.uri()); + try { + Thread.sleep((long) (50 + (Math.random() * 150))); + } catch (InterruptedException e) { + logger.error("Парсинг был прерван: {}", e.getMessage(), e); + Thread.currentThread().interrupt(); + throw new RuntimeException("Парсинг был прерван: " + e.getMessage(), e); + } + + logger.info("Парсим страницу: {}", link.uri()); // Добавляем дочерние ссылки - addChildLinks(link, visitedLinks); + try { + addChildLinks(link, site, visitedLinks, allLinks); + } catch (Exception e) { + logger.error("Ошибка при добавлении дочерних ссылок для {}: {}", link.uri(), e.getMessage(), e); + throw e; // Пробрасываем исключение дальше + } // Рекурсивно обрабатываем дочерние ссылки - link.children().forEach(child -> { - try { - parse(child, visitedLinks); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Парсинг был прерван: " + e.getMessage()); + for (Link child : new HashSet<>(allLinks)) { // Создаем копию множества для итерации + if (!visitedLinks.contains(child.uri())) { + try { + parse(child, site, visitedLinks, allLinks); + } catch (InterruptedException e) { + logger.error("Парсинг был прерван: {}", e.getMessage(), e); + Thread.currentThread().interrupt(); + throw new RuntimeException("Парсинг был прерван: " + e.getMessage(), e); + } catch (Exception e) { + logger.error("Произошла ошибка при парсинге {}: {}", child.uri(), e.getMessage(), e); + } } - }); + } } - private void addChildLinks(Link link, Set visitedLinks) { + private void addChildLinks(Link link, SiteEntity site, Set visitedLinks, Set allLinks) { try { + logger.debug("Пытаемся получить страницу: {}", link.uri()); Document document = Jsoup.connect(link.uri().toString()) .userAgent("Mozilla/5.0") .referrer("https://www.google.com") .get(); + logger.debug("Страница успешно получена: {}", link.uri()); + + // Проверяем, что site не равен null + if (site == null) { + throw new IllegalStateException("SiteEntity не может быть null"); + } + + // Получаем путь + String path = link.uri().getPath(); + if (path == null || path.isEmpty()) { + path = "/"; // Устанавливаем корневой путь, если он пустой + } + PageEntity existingPage = pageRepository.findBySiteAndPath(site, path); + if (existingPage == null) { + // Сохраняем страницу в базу данных + PageEntity page = new PageEntity(); + page.setSite(site); // Устанавливаем связь с SiteEntity + page.setPath(path); + page.setCode(document.connection().response().statusCode()); + page.setContent(document.outerHtml()); + pageRepository.save(page); + logger.info("Сохранена страница: {}", link.uri()); + } else { + logger.warn("Страница уже существует: {}", link.uri()); + } document.select("a[href]").forEach(element -> { String hrefStr = element.attr("href").strip(); @@ -109,34 +207,46 @@ public class IndexingServiceImpl implements IndexingService { // Проверяем, что ссылка корректна if (isCorrectUrl(link.uri(), href, visitedLinks)) { - link.addChild(href); // Используем метод addChild из класса Link + Link childLink = new Link(href, link); + allLinks.add(childLink); // Добавляем дочернюю ссылку в список всех ссылок + logger.debug("Добавлена дочерняя ссылка: {}", childLink.uri()); } } catch (Exception e) { - // Игнорируем некорректные URL без вывода сообщений + logger.warn("Некорректная ссылка: {}", hrefStr, e); } }); + } catch (HttpStatusException e) { + logger.warn("HTTP ошибка при добавлении дочерних ссылок: Status={}, URL={}", e.getStatusCode(), e.getUrl()); + } catch (DataIntegrityViolationException e) { + logger.error("Ошибка целостности данных при сохранении страницы: {}", e.getMessage()); + throw e; // Пробрасываем исключение дальше } catch (Exception e) { - // Игнорируем ошибки без вывода сообщений + logger.error("Ошибка при добавлении дочерних ссылок: {}", e.getMessage(), e); + throw new RuntimeException("Ошибка при добавлении дочерних ссылок", e); } } private URI normalizeUri(URI href) { try { - String path = href.getPath(); // Получаем путь - if (path == null) { - path = ""; // Если путь равен null, заменяем на пустую строку + String path = href.getPath(); + if (path == null || path.isEmpty()) { + path = "/"; // Устанавливаем корневой путь, если он пустой } else { - path = path.replaceAll("/+$", ""); // Убираем завершающие слэши + path = path.replaceAll("/+$", ""); + if (path.isEmpty()) { + path = "/"; + } } return new URI( href.getScheme(), href.getAuthority(), - path, // Используем обработанный путь + path, null, href.getFragment() ); } catch (URISyntaxException e) { - return href; // В случае ошибки возвращаем исходный URI + logger.warn("Некорректный URL: {}", href, e); + return href; } } @@ -144,11 +254,13 @@ public class IndexingServiceImpl implements IndexingService { // Проверка на некорректные протоколы String hrefStr = href.toString(); if (hrefStr.startsWith("javascript:") || hrefStr.startsWith("mailto:") || hrefStr.startsWith("tel:")) { + logger.debug("Некорректный протокол: {}", hrefStr); return false; } // Проверка на принадлежность тому же домену if (href.getHost() == null || !href.getHost().equals(baseUri.getHost())) { + logger.debug("Ссылка не принадлежит тому же домену: {}", hrefStr); return false; } @@ -156,10 +268,25 @@ public class IndexingServiceImpl implements IndexingService { 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()) { + logger.debug("Ссылка на файл или якорь: {}", hrefStr); return false; } // Проверка на уже посещенные ссылки return !visitedLinks.contains(href); } + + private void handleIndexingError(String startUrl, String errorMessage) { + SiteEntity site = siteRepository.findByUrl(startUrl); + if (site != null) { + site.setStatus(StatusType.FAILED); + site.setLast_error(errorMessage); + site.setStatus_time(LocalDateTime.now()); + siteRepository.save(site); + logger.error("Ошибка индексации для сайта {}: {}", startUrl, errorMessage); + } else { + logger.error("Не удалось найти SiteEntity для URL: {}", startUrl); + } + logger.error(errorMessage); + } } \ No newline at end of file