Compare commits
	
		
			4 Commits
		
	
	
		
			ee4bb2175f
			...
			a60533aa1a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a60533aa1a | ||
| 
						 | 
					8425b6a500 | ||
| 
						 | 
					1209b34ceb | ||
| 
						 | 
					443ea20d55 | 
							
								
								
									
										5
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								pom.xml
									
									
									
									
									
								
							@@ -50,6 +50,11 @@
 | 
				
			|||||||
            <artifactId>spring-dotenv</artifactId>
 | 
					            <artifactId>spring-dotenv</artifactId>
 | 
				
			||||||
            <version>4.0.0</version>
 | 
					            <version>4.0.0</version>
 | 
				
			||||||
        </dependency>
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.jsoup</groupId>
 | 
				
			||||||
 | 
					            <artifactId>jsoup</artifactId>
 | 
				
			||||||
 | 
					            <version>1.18.3</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
    </dependencies>
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <build>
 | 
					    <build>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,8 +4,8 @@ import org.springframework.http.ResponseEntity;
 | 
				
			|||||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
					import org.springframework.web.bind.annotation.GetMapping;
 | 
				
			||||||
import org.springframework.web.bind.annotation.RequestMapping;
 | 
					import org.springframework.web.bind.annotation.RequestMapping;
 | 
				
			||||||
import org.springframework.web.bind.annotation.RestController;
 | 
					import org.springframework.web.bind.annotation.RestController;
 | 
				
			||||||
import searchengine.config.SitesList;
 | 
					 | 
				
			||||||
import searchengine.dto.statistics.StatisticsResponse;
 | 
					import searchengine.dto.statistics.StatisticsResponse;
 | 
				
			||||||
 | 
					import searchengine.services.IndexingService;
 | 
				
			||||||
import searchengine.services.StatisticsService;
 | 
					import searchengine.services.StatisticsService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@RestController
 | 
					@RestController
 | 
				
			||||||
@@ -13,9 +13,11 @@ import searchengine.services.StatisticsService;
 | 
				
			|||||||
public class ApiController {
 | 
					public class ApiController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private final StatisticsService statisticsService;
 | 
					    private final StatisticsService statisticsService;
 | 
				
			||||||
 | 
					    private final IndexingService indexingService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ApiController(StatisticsService statisticsService) {
 | 
					    public ApiController(StatisticsService statisticsService, IndexingService indexingService) {
 | 
				
			||||||
        this.statisticsService = statisticsService;
 | 
					        this.statisticsService = statisticsService;
 | 
				
			||||||
 | 
					        this.indexingService = indexingService;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @GetMapping("/statistics")
 | 
					    @GetMapping("/statistics")
 | 
				
			||||||
@@ -25,7 +27,7 @@ public class ApiController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @GetMapping("/startIndexing")
 | 
					    @GetMapping("/startIndexing")
 | 
				
			||||||
    public ResponseEntity<StatisticsResponse> startIndexing() {
 | 
					    public ResponseEntity<StatisticsResponse> startIndexing() {
 | 
				
			||||||
        System.out.println(new SitesList().getSites());
 | 
					        indexingService.startIndexing();
 | 
				
			||||||
        return ResponseEntity.ok(statisticsService.getStatistics());
 | 
					        return ResponseEntity.ok(statisticsService.getStatistics());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										42
									
								
								src/main/java/searchengine/model/Link.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/main/java/searchengine/model/Link.java
									
									
									
									
									
										Normal file
									
								
							@@ -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<Link> 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
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -18,8 +18,7 @@ public class PageEntity {
 | 
				
			|||||||
    @JoinColumn(name = "site_id", nullable = false)
 | 
					    @JoinColumn(name = "site_id", nullable = false)
 | 
				
			||||||
    private SiteEntity site;
 | 
					    private SiteEntity site;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Column(columnDefinition = "TEXT NOT NULL, UNIQUE KEY INDEX_PATH (path(255))") // Unique index version
 | 
					    @Column(columnDefinition = "VARCHAR(255) NOT NULL", unique = true)
 | 
				
			||||||
//    @Column(columnDefinition = "TEXT NOT NULL, INDEX PATH_INDEX USING BTREE(path(255))") // Non-unique index version
 | 
					 | 
				
			||||||
    private String path;
 | 
					    private String path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Column(nullable = false)
 | 
					    @Column(nullable = false)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
package searchengine.services;
 | 
					package searchengine.services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public interface IndexingService {
 | 
					public interface IndexingService {
 | 
				
			||||||
 | 
					    void startIndexing();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,163 @@
 | 
				
			|||||||
package searchengine.services;
 | 
					package searchengine.services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class IndexingServiceImpl implements IndexingService {
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
    boolean indexingIsRunning = false;
 | 
					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.Set;
 | 
				
			||||||
 | 
					import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
 | 
					import java.util.regex.Pattern;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class IndexingServiceImpl implements IndexingService {
 | 
				
			||||||
 | 
					    private boolean indexingIsRunning = false;
 | 
				
			||||||
 | 
					    private final SitesList sitesList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
    public void startIndexing() {
 | 
					    public void startIndexing() {
 | 
				
			||||||
        if (indexingIsRunning) {
 | 
					        if (indexingIsRunning) {
 | 
				
			||||||
            System.out.println("Индексация уже запущена");
 | 
					            System.out.println("Индексация уже запущена");
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        // Логика для запуска индексации или перенидексации сайтов
 | 
					 | 
				
			||||||
        System.out.println("Запуск индексации");
 | 
					 | 
				
			||||||
        indexingIsRunning = true;
 | 
					        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<URI> 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<URI> visitedLinks) throws InterruptedException {
 | 
				
			||||||
 | 
					        // Добавляем текущий URL в visitedLinks до начала обработки
 | 
				
			||||||
 | 
					        if (!visitedLinks.add(link.uri())) {
 | 
				
			||||||
 | 
					            return; // Если URL уже был обработан, выходим
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Задержка для защиты от блокировки
 | 
				
			||||||
 | 
					        Thread.sleep((long) (50 + (Math.random() * 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<URI> 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<URI> 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -16,9 +16,11 @@ spring:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
indexing-settings:
 | 
					indexing-settings:
 | 
				
			||||||
  sites:
 | 
					  sites:
 | 
				
			||||||
    - url: https://www.lenta.ru
 | 
					    - url: https://wiki.kuksa.dev
 | 
				
			||||||
      name: Лента.ру
 | 
					      name: Wiki Kuksa.Dev
 | 
				
			||||||
 | 
					    - url: https://www.autelrobotics.com
 | 
				
			||||||
 | 
					      name: Autel Robotics
 | 
				
			||||||
    - url: https://www.skillbox.ru
 | 
					    - url: https://www.skillbox.ru
 | 
				
			||||||
      name: Skillbox
 | 
					      name: Skillbox
 | 
				
			||||||
    - url: https://www.playback.ru
 | 
					#    - url: https://www.playback.ru
 | 
				
			||||||
      name: PlayBack.Ru
 | 
					#      name: PlayBack.Ru
 | 
				
			||||||
		Reference in New Issue
	
	Block a user