@ -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 ( 0 ) . getUrl ( ) ;
String siteName = sitesList . getSites ( ) . get ( 0 ) . getName ( ) ;
String siteName = sitesList . getSites ( ) . get ( 0 ) . 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 , S iteEntity site , S et< URI > visited Links, Set < Link > all Links) 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 , visited Links, all Links) ;
} 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 , S et< URI > visited Links) {
private void addChildLinks ( Link link , S iteEntity site , S et< URI > visited Links, Set < Link > all Links) {
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 ) ;
}
}
}