«1. Введение

Мы видим использование поисковых роботов каждый раз, когда используем нашу любимую поисковую систему. Они также обычно используются для очистки и анализа данных с веб-сайтов.

В этом уроке мы узнаем, как использовать Crawler4j для настройки и запуска наших собственных поисковых роботов. Crawler4j — это проект Java с открытым исходным кодом, который позволяет нам легко это делать.

2. Настройка

Давайте воспользуемся Maven Central, чтобы найти самую последнюю версию и установить зависимость Maven:

<dependency>
    <groupId>edu.uci.ics</groupId>
    <artifactId>crawler4j</artifactId>
    <version>4.4.0</version>
</dependency>

3. Создание сканеров

3.1. Простой сканер HTML

Мы начнем с создания простого сканера, который просматривает HTML-страницы на https://baeldung.com.

Давайте создадим наш сканер, расширив WebCrawler в нашем классе сканера и определив шаблон для исключения определенных типов файлов:

public class HtmlCrawler extends WebCrawler {

    private final static Pattern EXCLUSIONS
      = Pattern.compile(".*(\\.(css|js|xml|gif|jpg|png|mp3|mp4|zip|gz|pdf))$");

    // more code
}

В каждом классе сканера мы должны переопределить и реализовать два метода: shouldVisit и visit.

Теперь давайте создадим наш метод shouldVisit, используя созданный нами шаблон EXCLUSIONS:

@Override
public boolean shouldVisit(Page referringPage, WebURL url) {
    String urlString = url.getURL().toLowerCase();
    return !EXCLUSIONS.matcher(urlString).matches() 
      && urlString.startsWith("https://www.baeldung.com/");
}

Затем мы можем выполнить обработку посещенных страниц в методе visit:

@Override
public void visit(Page page) {
    String url = page.getWebURL().getURL();

    if (page.getParseData() instanceof HtmlParseData) {
        HtmlParseData htmlParseData = (HtmlParseData) page.getParseData();
        String title = htmlParseData.getTitle();
        String text = htmlParseData.getText();
        String html = htmlParseData.getHtml();
        Set<WebURL> links = htmlParseData.getOutgoingUrls();

        // do something with the collected data
    }
}

После того, как наш сканер будет написан, нам нужно настроить и запустить его:

File crawlStorage = new File("src/test/resources/crawler4j");
CrawlConfig config = new CrawlConfig();
config.setCrawlStorageFolder(crawlStorage.getAbsolutePath());

int numCrawlers = 12;

PageFetcher pageFetcher = new PageFetcher(config);
RobotstxtConfig robotstxtConfig = new RobotstxtConfig();
RobotstxtServer robotstxtServer= new RobotstxtServer(robotstxtConfig, pageFetcher);
CrawlController controller = new CrawlController(config, pageFetcher, robotstxtServer);

controller.addSeed("https://www.baeldung.com/");

CrawlController.WebCrawlerFactory<HtmlCrawler> factory = HtmlCrawler::new;

controller.start(factory, numCrawlers);

Мы настроили каталог временного хранилища, указали количество потоков сканирования и задали краулеру начальный URL.

Также следует отметить, что метод CrawlController.start() является блокирующей операцией. Любой код после этого вызова будет выполняться только после завершения работы сканера.

3.2. ImageCrawler

По умолчанию crawler4j не сканирует двоичные данные. В следующем примере мы включим эту функцию и просканируем все файлы JPEG в Baeldung.

Давайте начнем с определения класса ImageCrawler с конструктором, который принимает каталог для сохранения изображений:

public class ImageCrawler extends WebCrawler {
    private final static Pattern EXCLUSIONS
      = Pattern.compile(".*(\\.(css|js|xml|gif|png|mp3|mp4|zip|gz|pdf))$");
    
    private static final Pattern IMG_PATTERNS = Pattern.compile(".*(\\.(jpg|jpeg))$");
    
    private File saveDir;
    
    public ImageCrawler(File saveDir) {
        this.saveDir = saveDir;
    }

    // more code

}

Затем давайте реализуем метод shouldVisit:

@Override
public boolean shouldVisit(Page referringPage, WebURL url) {
    String urlString = url.getURL().toLowerCase();
    if (EXCLUSIONS.matcher(urlString).matches()) {
        return false;
    }

    if (IMG_PATTERNS.matcher(urlString).matches() 
        || urlString.startsWith("https://www.baeldung.com/")) {
        return true;
    }

    return false;
}

Теперь мы готовы реализовать метод посещения:

@Override
public void visit(Page page) {
    String url = page.getWebURL().getURL();
    if (IMG_PATTERNS.matcher(url).matches() 
        && page.getParseData() instanceof BinaryParseData) {
        String extension = url.substring(url.lastIndexOf("."));
        int contentLength = page.getContentData().length;

        // write the content data to a file in the save directory
    }
}

Запуск нашего ImageCrawler аналогичен запуску HttpCrawler, но нам нужно настроить его для включения двоичного содержимого:

CrawlConfig config = new CrawlConfig();
config.setIncludeBinaryContentInCrawling(true);

// ... same as before
        
CrawlController.WebCrawlerFactory<ImageCrawler> factory = () -> new ImageCrawler(saveDir);
        
controller.start(factory, numCrawlers);

3.3. Сбор данных

Теперь, когда мы рассмотрели пару основных примеров, давайте расширим возможности нашего HtmlCrawler, чтобы собирать базовую статистику во время сканирования.

Во-первых, давайте определим простой класс для хранения нескольких статистических данных:

public class CrawlerStatistics {
    private int processedPageCount = 0;
    private int totalLinksCount = 0;
    
    public void incrementProcessedPageCount() {
        processedPageCount++;
    }
    
    public void incrementTotalLinksCount(int linksCount) {
        totalLinksCount += linksCount;
    }
    
    // standard getters
}

Затем давайте изменим наш HtmlCrawler, чтобы он принимал экземпляр CrawlerStatistics через конструктор:

private CrawlerStatistics stats;
    
public HtmlCrawler(CrawlerStatistics stats) {
    this.stats = stats;
}

С нашим новым объектом CrawlerStatistics, давайте изменим метод посещения, чтобы собирать то, что нам нужно:

@Override
public void visit(Page page) {
    String url = page.getWebURL().getURL();
    stats.incrementProcessedPageCount();

    if (page.getParseData() instanceof HtmlParseData) {
        HtmlParseData htmlParseData = (HtmlParseData) page.getParseData();
        String title = htmlParseData.getTitle();
        String text = htmlParseData.getText();
        String html = htmlParseData.getHtml();
        Set<WebURL> links = htmlParseData.getOutgoingUrls();
        stats.incrementTotalLinksCount(links.size());

        // do something with collected data
    }
}

Теперь вернемся к нашему контроллеру и предоставим HtmlCrawler экземпляр CrawlerStatistics:

CrawlerStatistics stats = new CrawlerStatistics();
CrawlController.WebCrawlerFactory<HtmlCrawler> factory = () -> new HtmlCrawler(stats);

3.4. Несколько сканеров

Опираясь на наши предыдущие примеры, давайте теперь посмотрим, как мы можем запустить несколько сканеров с одного и того же контроллера.

Рекомендуется, чтобы каждый сканер использовал свой собственный каталог временного хранения, поэтому нам нужно создать отдельные конфигурации для каждого из них, которые мы будем запускать.

CrawlControllers могут совместно использовать один RobotstxtServer, но в остальном нам в основном нужна копия всего.

До сих пор мы использовали метод CrawlController.start для запуска наших поисковых роботов и отметили, что это метод блокировки. Для запуска нескольких мы будем использовать CrawlerControlller.startNonBlocking в сочетании с CrawlController.waitUntilFinish.

Теперь давайте создадим контроллер для одновременного запуска HtmlCrawler и ImageCrawler:

File crawlStorageBase = new File("src/test/resources/crawler4j");
CrawlConfig htmlConfig = new CrawlConfig();
CrawlConfig imageConfig = new CrawlConfig();
        
// Configure storage folders and other configurations
        
PageFetcher pageFetcherHtml = new PageFetcher(htmlConfig);
PageFetcher pageFetcherImage = new PageFetcher(imageConfig);
        
RobotstxtConfig robotstxtConfig = new RobotstxtConfig();
RobotstxtServer robotstxtServer = new RobotstxtServer(robotstxtConfig, pageFetcherHtml);

CrawlController htmlController
  = new CrawlController(htmlConfig, pageFetcherHtml, robotstxtServer);
CrawlController imageController
  = new CrawlController(imageConfig, pageFetcherImage, robotstxtServer);
        
// add seed URLs
        
CrawlerStatistics stats = new CrawlerStatistics();
CrawlController.WebCrawlerFactory<HtmlCrawler> htmlFactory = () -> new HtmlCrawler(stats);
        
File saveDir = new File("src/test/resources/crawler4j");
CrawlController.WebCrawlerFactory<ImageCrawler> imageFactory
  = () -> new ImageCrawler(saveDir);
        
imageController.startNonBlocking(imageFactory, 7);
htmlController.startNonBlocking(htmlFactory, 10);

htmlController.waitUntilFinish();
imageController.waitUntilFinish();

4. Конфигурация

Мы уже видели кое-что из того, что можно настроить. Теперь давайте рассмотрим некоторые другие общие настройки.

Настройки применяются к экземпляру CrawlConfig, который мы указываем в нашем контроллере.

4.1. Ограничение глубины сканирования

По умолчанию наши сканеры будут сканировать как можно глубже. Чтобы ограничить их глубину, мы можем установить глубину сканирования:

crawlConfig.setMaxDepthOfCrawling(2);

Исходные URL-адреса считаются на глубине 0, поэтому глубина сканирования, равная 2, будет проходить на два уровня дальше исходного URL-адреса.

4.2. Максимальное количество страниц для выборки

Еще один способ ограничить количество страниц, которые будут охватывать наши поисковые роботы, — установить максимальное количество страниц для сканирования:

crawlConfig.setMaxPagesToFetch(500);

4.3. Максимальное количество исходящих ссылок

Мы также можем ограничить количество исходящих ссылок с каждой страницы:

crawlConfig.setMaxOutgoingLinksToFollow(2000);

4.4. Задержка вежливости

«Поскольку очень эффективные поисковые роботы могут легко создавать нагрузку на веб-серверы, у crawler4j есть то, что он называет задержкой из-за вежливости. По умолчанию установлено значение 200 миллисекунд. Мы можем изменить это значение, если нам нужно:

crawlConfig.setPolitenessDelay(300);

4.5. Включить бинарный контент

Мы уже использовали опцию включения бинарного контента в наш ImageCrawler:

crawlConfig.setIncludeBinaryContentInCrawling(true);

4.6. Включить HTTPS

По умолчанию сканеры будут включать HTTPS-страницы, но мы можем отключить это:

crawlConfig.setIncludeHttpsPages(false);

4.7. Возобновляемое сканирование

Если у нас есть продолжительный поисковый робот, и мы хотим, чтобы он возобновлялся автоматически, мы можем настроить возобновляемое сканирование. Включение может замедлить работу:

crawlConfig.setResumableCrawling(true);

4.8. Строка пользовательского агента

Строка пользовательского агента по умолчанию для Crawler4j — Crawler4j. Давайте настроим это:

crawlConfig.setUserAgentString("baeldung demo (https://github.com/yasserg/crawler4j/)");

Мы только что рассмотрели некоторые из основных конфигураций. Мы можем взглянуть на класс CrawConfig, если нас интересуют некоторые из более сложных или малоизвестных параметров конфигурации.

5. Заключение

В этой статье мы использовали Crawler4j для создания собственных поисковых роботов. Мы начали с двух простых примеров сканирования HTML и изображений. Затем мы использовали эти примеры, чтобы увидеть, как мы можем собирать статистику и одновременно запускать несколько поисковых роботов.

Полные примеры кода доступны на GitHub.