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>
 | 
			
		||||
            <version>4.0.0</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.jsoup</groupId>
 | 
			
		||||
            <artifactId>jsoup</artifactId>
 | 
			
		||||
            <version>1.18.3</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
    <build>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@ import org.springframework.http.ResponseEntity;
 | 
			
		||||
import org.springframework.web.bind.annotation.GetMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.RequestMapping;
 | 
			
		||||
import org.springframework.web.bind.annotation.RestController;
 | 
			
		||||
import searchengine.config.SitesList;
 | 
			
		||||
import searchengine.dto.statistics.StatisticsResponse;
 | 
			
		||||
import searchengine.services.IndexingService;
 | 
			
		||||
import searchengine.services.StatisticsService;
 | 
			
		||||
 | 
			
		||||
@RestController
 | 
			
		||||
@@ -13,9 +13,11 @@ import searchengine.services.StatisticsService;
 | 
			
		||||
public class ApiController {
 | 
			
		||||
 | 
			
		||||
    private final StatisticsService statisticsService;
 | 
			
		||||
    private final IndexingService indexingService;
 | 
			
		||||
 | 
			
		||||
    public ApiController(StatisticsService statisticsService) {
 | 
			
		||||
    public ApiController(StatisticsService statisticsService, IndexingService indexingService) {
 | 
			
		||||
        this.statisticsService = statisticsService;
 | 
			
		||||
        this.indexingService = indexingService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/statistics")
 | 
			
		||||
@@ -25,7 +27,7 @@ public class ApiController {
 | 
			
		||||
 | 
			
		||||
    @GetMapping("/startIndexing")
 | 
			
		||||
    public ResponseEntity<StatisticsResponse> startIndexing() {
 | 
			
		||||
        System.out.println(new SitesList().getSites());
 | 
			
		||||
        indexingService.startIndexing();
 | 
			
		||||
        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)
 | 
			
		||||
    private SiteEntity site;
 | 
			
		||||
 | 
			
		||||
    @Column(columnDefinition = "TEXT NOT NULL, UNIQUE KEY INDEX_PATH (path(255))") // Unique index version
 | 
			
		||||
//    @Column(columnDefinition = "TEXT NOT NULL, INDEX PATH_INDEX USING BTREE(path(255))") // Non-unique index version
 | 
			
		||||
    @Column(columnDefinition = "VARCHAR(255) NOT NULL", unique = true)
 | 
			
		||||
    private String path;
 | 
			
		||||
 | 
			
		||||
    @Column(nullable = false)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
package searchengine.services;
 | 
			
		||||
 | 
			
		||||
public interface IndexingService {
 | 
			
		||||
    void startIndexing();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,163 @@
 | 
			
		||||
package searchengine.services;
 | 
			
		||||
 | 
			
		||||
public class IndexingServiceImpl implements IndexingService {
 | 
			
		||||
    boolean indexingIsRunning = false;
 | 
			
		||||
import lombok.RequiredArgsConstructor;
 | 
			
		||||
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() {
 | 
			
		||||
        if (indexingIsRunning) {
 | 
			
		||||
            System.out.println("Индексация уже запущена");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Логика для запуска индексации или перенидексации сайтов
 | 
			
		||||
        System.out.println("Запуск индексации");
 | 
			
		||||
        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:
 | 
			
		||||
  sites:
 | 
			
		||||
    - url: https://www.lenta.ru
 | 
			
		||||
      name: Лента.ру
 | 
			
		||||
    - url: https://wiki.kuksa.dev
 | 
			
		||||
      name: Wiki Kuksa.Dev
 | 
			
		||||
    - url: https://www.autelrobotics.com
 | 
			
		||||
      name: Autel Robotics
 | 
			
		||||
    - url: https://www.skillbox.ru
 | 
			
		||||
      name: Skillbox
 | 
			
		||||
    - url: https://www.playback.ru
 | 
			
		||||
      name: PlayBack.Ru
 | 
			
		||||
#    - url: https://www.playback.ru
 | 
			
		||||
#      name: PlayBack.Ru
 | 
			
		||||
		Reference in New Issue
	
	Block a user