Compare commits

..

No commits in common. "614f97c5d5eaab8b55b02637d3b90dbc7ae44b99" and "8425b6a5009076156941d4c910f51441a1526339" have entirely different histories.

4 changed files with 37 additions and 171 deletions

View File

@ -7,20 +7,18 @@ import lombok.Setter;
@Getter @Getter
@Setter @Setter
@Entity @Entity
//@Table(name = "page", indexes = @Index(columnList = "site_id, path", unique = true)) // Или используем составной индекс, если нужно сохранить уникальность @Table(name = "page", indexes = @Index(columnList = "path"))
@Table(name = "page", indexes = @Index(columnList = "path"), uniqueConstraints = @UniqueConstraint(columnNames = {"site_id", "path"}))
public class PageEntity { public class PageEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false) @Column(nullable = false)
private int id; private int id;
@ManyToOne(fetch = FetchType.LAZY) @ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "site_id", nullable = false) @JoinColumn(name = "site_id", nullable = false)
private SiteEntity site; private SiteEntity site;
@Column(columnDefinition = "VARCHAR(255) NOT NULL") @Column(columnDefinition = "VARCHAR(255) NOT NULL", unique = true)
// @Column(columnDefinition = "VARCHAR(255) NOT NULL", unique = true) // Или убираем unique = true
private String path; private String path;
@Column(nullable = false) @Column(nullable = false)

View File

@ -3,11 +3,7 @@ package searchengine.repository;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import searchengine.model.PageEntity; import searchengine.model.PageEntity;
import searchengine.model.SiteEntity;
@Repository @Repository
public interface PageRepository extends CrudRepository<PageEntity, Integer> { public interface PageRepository extends CrudRepository<PageEntity, Integer> {
void deleteBySite(SiteEntity site);
PageEntity findBySiteAndPath(SiteEntity site, String path);
} }

View File

@ -6,5 +6,4 @@ import searchengine.model.SiteEntity;
@Repository @Repository
public interface SiteRepository extends CrudRepository<SiteEntity, Integer> { public interface SiteRepository extends CrudRepository<SiteEntity, Integer> {
SiteEntity findByUrl(String url);
} }

View File

@ -1,30 +1,17 @@
package searchengine.services; package searchengine.services;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; 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.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.config.SitesList;
import searchengine.model.Link; 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.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.util.Random;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -32,164 +19,79 @@ import java.util.regex.Pattern;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class IndexingServiceImpl implements IndexingService { public class IndexingServiceImpl implements IndexingService {
private static final Logger logger = LoggerFactory.getLogger(IndexingServiceImpl.class);
private boolean indexingIsRunning = false; private boolean indexingIsRunning = false;
private final SitesList sitesList; private final SitesList sitesList;
private final SiteRepository siteRepository; private final Random random = new Random();
private final PageRepository pageRepository;
@Override @Override
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, noRollbackFor = Exception.class)
public void startIndexing() { public void startIndexing() {
if (indexingIsRunning) { if (indexingIsRunning) {
logger.info("Индексация уже запущена"); System.out.println("Индексация уже запущена");
return; return;
} }
indexingIsRunning = true; indexingIsRunning = true;
// Выводим список сайтов для индексации // Выводим список сайтов для индексации
logger.info("Список сайтов для индексации:"); System.out.println("Список сайтов для индексации:");
sitesList.getSites().forEach(site -> sitesList.getSites().forEach(site ->
logger.info("URL: {}, Название: {}", site.getUrl(), site.getName()) System.out.println("URL: " + site.getUrl() + ", Название: " + site.getName())
); );
// Начинаем парсинг первого сайта // Начинаем парсинг первого сайта
String startUrl = sitesList.getSites().get(0).getUrl(); String startUrl = sitesList.getSites().get(0).getUrl();
String siteName = sitesList.getSites().get(0).getName(); String siteName = sitesList.getSites().get(0).getName();
logger.info("Начинаем парсинг сайта {} ({})", siteName, startUrl); System.out.println("Начинаем парсинг сайта " + siteName + " (" + startUrl + ")");
Set<URI> visitedLinks = ConcurrentHashMap.newKeySet(); // Набор для отслеживания посещенных URL Set<URI> visitedLinks = ConcurrentHashMap.newKeySet(); // Набор для отслеживания посещенных URL
Set<Link> allLinks = ConcurrentHashMap.newKeySet(); // Набор для хранения всех ссылок
try { try {
URI startUri = new URI(startUrl); URI startUri = new URI(startUrl);
logger.info("=== Начало индексации {} ({}) ===", siteName, startUri); System.out.printf("\n=== Начало индексации %s (%s) ===\n", 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 Link rootLink = new Link(startUri, null); // Создаем корневой Link
allLinks.add(rootLink); // Добавляем корневую ссылку во множество всех ссылок parse(rootLink, visitedLinks); // Запуск парсинга
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) { } catch (URISyntaxException e) {
logger.error("Некорректный URL: {}", startUrl, e); System.out.println("Некорректный URL: " + startUrl);
handleIndexingError(startUrl, "Некорректный URL: " + startUrl);
} catch (InterruptedException e) { } catch (InterruptedException e) {
logger.error("Индексация была прервана: {}", e.getMessage(), e); System.out.println("Индексация была прервана: " + e.getMessage());
handleIndexingError(startUrl, "Индексация была прервана: " + e.getMessage()); Thread.currentThread().interrupt();
} catch (Exception e) {
logger.error("Произошла ошибка при индексации: {}", e.getMessage(), e);
handleIndexingError(startUrl, "Произошла ошибка при индексации: " + e.getMessage());
} finally { } finally {
indexingIsRunning = false; indexingIsRunning = false;
logger.info("=== Индексация завершена ==="); System.out.println("\n=== Индексация завершена ===");
logger.info("Всего страниц проиндексировано: {}", visitedLinks.size()); System.out.println("Всего страниц проиндексировано: " + visitedLinks.size());
} }
} }
private void parse(Link link, SiteEntity site, Set<URI> visitedLinks, Set<Link> allLinks) throws InterruptedException { private void parse(Link link, Set<URI> visitedLinks) throws InterruptedException {
// Добавляем текущий URL в visitedLinks до начала обработки // Добавляем текущий URL в visitedLinks до начала обработки
if (!visitedLinks.add(link.uri())) { if (!visitedLinks.add(link.uri())) {
logger.debug("URL уже был обработан: {}", link.uri());
return; // Если URL уже был обработан, выходим return; // Если URL уже был обработан, выходим
} }
// Обновляем время статуса
site.setStatus_time(LocalDateTime.now());
siteRepository.save(site);
// Задержка для соблюдения правил robots.txt // Задержка для соблюдения правил robots.txt
try { Thread.sleep(50 + random.nextInt(150));
Thread.sleep((long) (50 + (Math.random() * 150))); System.out.println("Парсим страницу: " + link.uri());
} catch (InterruptedException e) {
logger.error("Парсинг был прерван: {}", e.getMessage(), e);
Thread.currentThread().interrupt();
throw new RuntimeException("Парсинг был прерван: " + e.getMessage(), e);
}
logger.info("Парсим страницу: {}", link.uri());
// Добавляем дочерние ссылки // Добавляем дочерние ссылки
try { addChildLinks(link, visitedLinks);
addChildLinks(link, site, visitedLinks, allLinks);
} catch (Exception e) {
logger.error("Ошибка при добавлении дочерних ссылок для {}: {}", link.uri(), e.getMessage(), e);
throw e; // Пробрасываем исключение дальше
}
// Рекурсивно обрабатываем дочерние ссылки // Рекурсивно обрабатываем дочерние ссылки
for (Link child : new HashSet<>(allLinks)) { // Создаем копию множества для итерации link.children().forEach(child -> {
if (!visitedLinks.contains(child.uri())) {
try { try {
parse(child, site, visitedLinks, allLinks); parse(child, visitedLinks);
} catch (InterruptedException e) { } catch (InterruptedException e) {
logger.error("Парсинг был прерван: {}", e.getMessage(), e);
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new RuntimeException("Парсинг был прерван: " + e.getMessage(), e); throw new RuntimeException("Парсинг был прерван: " + e.getMessage());
} catch (Exception e) {
logger.error("Произошла ошибка при парсинге {}: {}", child.uri(), e.getMessage(), e);
}
}
} }
});
} }
private void addChildLinks(Link link, SiteEntity site, Set<URI> visitedLinks, Set<Link> allLinks) { private void addChildLinks(Link link, Set<URI> visitedLinks) {
try { try {
logger.debug("Пытаемся получить страницу: {}", link.uri());
Document document = Jsoup.connect(link.uri().toString()) Document document = Jsoup.connect(link.uri().toString())
.userAgent("Mozilla/5.0") .userAgent("Mozilla/5.0")
.referrer("https://www.google.com") .referrer("https://www.google.com")
.get(); .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 -> { document.select("a[href]").forEach(element -> {
String hrefStr = element.attr("href").strip(); String hrefStr = element.attr("href").strip();
@ -207,46 +109,34 @@ public class IndexingServiceImpl implements IndexingService {
// Проверяем, что ссылка корректна // Проверяем, что ссылка корректна
if (isCorrectUrl(link.uri(), href, visitedLinks)) { if (isCorrectUrl(link.uri(), href, visitedLinks)) {
Link childLink = new Link(href, link); link.addChild(href); // Используем метод addChild из класса Link
allLinks.add(childLink); // Добавляем дочернюю ссылку в список всех ссылок
logger.debug("Добавлена дочерняя ссылка: {}", childLink.uri());
} }
} catch (Exception e) { } catch (Exception e) {
logger.warn("Некорректная ссылка: {}", hrefStr, e); // Игнорируем некорректные URL без вывода сообщений
} }
}); });
} catch (HttpStatusException e) {
logger.warn("HTTP ошибка при добавлении дочерних ссылок: Status={}, URL={}", e.getStatusCode(), e.getUrl());
} catch (DataIntegrityViolationException e) {
logger.error("Ошибка целостности данных при сохранении страницы: {}", e.getMessage());
throw e; // Пробрасываем исключение дальше
} catch (Exception e) { } catch (Exception e) {
logger.error("Ошибка при добавлении дочерних ссылок: {}", e.getMessage(), e); // Игнорируем ошибки без вывода сообщений
throw new RuntimeException("Ошибка при добавлении дочерних ссылок", e);
} }
} }
private URI normalizeUri(URI href) { private URI normalizeUri(URI href) {
try { try {
String path = href.getPath(); String path = href.getPath(); // Получаем путь
if (path == null || path.isEmpty()) { if (path == null) {
path = "/"; // Устанавливаем корневой путь, если он пустой path = ""; // Если путь равен null, заменяем на пустую строку
} else { } else {
path = path.replaceAll("/+$", ""); path = path.replaceAll("/+$", ""); // Убираем завершающие слэши
if (path.isEmpty()) {
path = "/";
}
} }
return new URI( return new URI(
href.getScheme(), href.getScheme(),
href.getAuthority(), href.getAuthority(),
path, path, // Используем обработанный путь
null, null,
href.getFragment() href.getFragment()
); );
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
logger.warn("Некорректный URL: {}", href, e); return href; // В случае ошибки возвращаем исходный URI
return href;
} }
} }
@ -254,13 +144,11 @@ public class IndexingServiceImpl implements IndexingService {
// Проверка на некорректные протоколы // Проверка на некорректные протоколы
String hrefStr = href.toString(); String hrefStr = href.toString();
if (hrefStr.startsWith("javascript:") || hrefStr.startsWith("mailto:") || hrefStr.startsWith("tel:")) { if (hrefStr.startsWith("javascript:") || hrefStr.startsWith("mailto:") || hrefStr.startsWith("tel:")) {
logger.debug("Некорректный протокол: {}", hrefStr);
return false; return false;
} }
// Проверка на принадлежность тому же домену // Проверка на принадлежность тому же домену
if (href.getHost() == null || !href.getHost().equals(baseUri.getHost())) { if (href.getHost() == null || !href.getHost().equals(baseUri.getHost())) {
logger.debug("Ссылка не принадлежит тому же домену: {}", hrefStr);
return false; return false;
} }
@ -268,25 +156,10 @@ public class IndexingServiceImpl implements IndexingService {
Pattern patternNotFile = Pattern.compile("(\\S+(\\.(?i)(jpg|png|gif|bmp|pdf|php|doc|docx|rar))$)"); Pattern patternNotFile = Pattern.compile("(\\S+(\\.(?i)(jpg|png|gif|bmp|pdf|php|doc|docx|rar))$)");
Pattern patternNotAnchor = Pattern.compile("#([\\w\\-]+)?$"); Pattern patternNotAnchor = Pattern.compile("#([\\w\\-]+)?$");
if (href.isOpaque() || patternNotFile.matcher(hrefStr).find() || patternNotAnchor.matcher(hrefStr).find()) { if (href.isOpaque() || patternNotFile.matcher(hrefStr).find() || patternNotAnchor.matcher(hrefStr).find()) {
logger.debug("Ссылка на файл или якорь: {}", hrefStr);
return false; return false;
} }
// Проверка на уже посещенные ссылки // Проверка на уже посещенные ссылки
return !visitedLinks.contains(href); 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);
}
} }