working code for task INDX-0003
This commit is contained in:
parent
8425b6a500
commit
c37a368ae4
@ -7,18 +7,20 @@ import lombok.Setter;
|
|||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Entity
|
@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 {
|
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(cascade = CascadeType.ALL)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "site_id", nullable = false)
|
@JoinColumn(name = "site_id", nullable = false)
|
||||||
private SiteEntity site;
|
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;
|
private String path;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
@ -3,7 +3,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,5 @@ import searchengine.model.SiteEntity;
|
|||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface SiteRepository extends CrudRepository<SiteEntity, Integer> {
|
public interface SiteRepository extends CrudRepository<SiteEntity, Integer> {
|
||||||
|
SiteEntity findByUrl(String url);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
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.util.Random;
|
import java.time.LocalDateTime;
|
||||||
|
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;
|
||||||
@ -19,79 +32,164 @@ 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 Random random = new Random();
|
private final SiteRepository siteRepository;
|
||||||
|
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) {
|
||||||
System.out.println("Индексация уже запущена");
|
logger.info("Индексация уже запущена");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
indexingIsRunning = true;
|
indexingIsRunning = true;
|
||||||
|
|
||||||
// Выводим список сайтов для индексации
|
// Выводим список сайтов для индексации
|
||||||
System.out.println("Список сайтов для индексации:");
|
logger.info("Список сайтов для индексации:");
|
||||||
sitesList.getSites().forEach(site ->
|
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 startUrl = sitesList.getSites().get(1).getUrl();
|
||||||
String siteName = sitesList.getSites().get(0).getName();
|
String siteName = sitesList.getSites().get(1).getName();
|
||||||
|
|
||||||
System.out.println("Начинаем парсинг сайта " + siteName + " (" + startUrl + ")");
|
logger.info("Начинаем парсинг сайта {} ({})", 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);
|
||||||
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
|
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) {
|
} catch (URISyntaxException e) {
|
||||||
System.out.println("Некорректный URL: " + startUrl);
|
logger.error("Некорректный URL: {}", startUrl, e);
|
||||||
|
handleIndexingError(startUrl, "Некорректный URL: " + startUrl);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
System.out.println("Индексация была прервана: " + e.getMessage());
|
logger.error("Индексация была прервана: {}", e.getMessage(), e);
|
||||||
Thread.currentThread().interrupt();
|
handleIndexingError(startUrl, "Индексация была прервана: " + e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Произошла ошибка при индексации: {}", e.getMessage(), e);
|
||||||
|
handleIndexingError(startUrl, "Произошла ошибка при индексации: " + e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
indexingIsRunning = false;
|
indexingIsRunning = false;
|
||||||
System.out.println("\n=== Индексация завершена ===");
|
logger.info("=== Индексация завершена ===");
|
||||||
System.out.println("Всего страниц проиндексировано: " + visitedLinks.size());
|
logger.info("Всего страниц проиндексировано: {}", visitedLinks.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parse(Link link, Set<URI> visitedLinks) throws InterruptedException {
|
private void parse(Link link, SiteEntity site, Set<URI> visitedLinks, Set<Link> allLinks) 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
|
||||||
Thread.sleep(50 + random.nextInt(150));
|
try {
|
||||||
System.out.println("Парсим страницу: " + link.uri());
|
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 -> {
|
for (Link child : new HashSet<>(allLinks)) { // Создаем копию множества для итерации
|
||||||
|
if (!visitedLinks.contains(child.uri())) {
|
||||||
try {
|
try {
|
||||||
parse(child, visitedLinks);
|
parse(child, site, visitedLinks, allLinks);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
logger.error("Парсинг был прерван: {}", e.getMessage(), e);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new RuntimeException("Парсинг был прерван: " + e.getMessage());
|
throw new RuntimeException("Парсинг был прерван: " + e.getMessage(), e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Произошла ошибка при парсинге {}: {}", child.uri(), e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addChildLinks(Link link, Set<URI> visitedLinks) {
|
private void addChildLinks(Link link, SiteEntity site, Set<URI> visitedLinks, Set<Link> allLinks) {
|
||||||
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();
|
||||||
@ -109,34 +207,46 @@ public class IndexingServiceImpl implements IndexingService {
|
|||||||
|
|
||||||
// Проверяем, что ссылка корректна
|
// Проверяем, что ссылка корректна
|
||||||
if (isCorrectUrl(link.uri(), href, visitedLinks)) {
|
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) {
|
} 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) {
|
} 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) {
|
if (path == null || path.isEmpty()) {
|
||||||
path = ""; // Если путь равен null, заменяем на пустую строку
|
path = "/"; // Устанавливаем корневой путь, если он пустой
|
||||||
} 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) {
|
||||||
return href; // В случае ошибки возвращаем исходный URI
|
logger.warn("Некорректный URL: {}", href, e);
|
||||||
|
return href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,11 +254,13 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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 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);
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user