Статьи

Как использовать Nutch из Java, а не из командной строки

Apache Nutch — это платформа с открытым исходным кодом, написанная на Java. Его цель — помочь нам сканировать набор веб-сайтов (или весь Интернет), извлекать контент и подготавливать его для индексации, скажем, Solr. Довольно полезная структура, если вы спросите меня, однако она предназначена для использования только в основном из командной строки. Вы скачиваете архив, распаковываете его и запускаете бинарный файл. Он ползет, и вы получаете данные. Однако у меня есть проект, в котором это сканирование нужно было встроить в мое собственное приложение Java. Я понял, что для этого нет полной документации. Отсюда и этот блог. Он объясняет, как вы можете использовать Nutch из Java, а не из командной строки.

Я буду говорить о Nutch 1.15. Есть более поздняя версия 2+, но мне не удалось заставить ее работать. Если вы знаете как, оставьте свой комментарий ниже.

Я бы рекомендовал вам сначала прочитать этот урок , чтобы понять, как Nutch работает из командной строки. Ну, это все равно помогло мне.

Теперь посмотрим, как мы можем использовать Nutch без командной строки. Во-первых, вам нужны эти зависимости в вашем pom.xml (Nutch использует Apache Hadoop , поэтому нам нужна вторая зависимость):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<project>
  <dependencies>
    <dependency>
      <groupId>org.apache.nutch</groupId>
      <artifactId>nutch</artifactId>
      <version>1.15</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-common</artifactId>
      <version>2.7.2</version>
    </dependency>
    [...]
  </dependencies>
  [...]
</project>

Далее, это ваш код Java, который выполняет всю работу:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.nutch.crawl.CrawlDb;
import org.apache.nutch.crawl.Generator;
import org.apache.nutch.crawl.Injector;
import org.apache.nutch.fetcher.Fetcher;
import org.apache.nutch.parse.ParseSegment;
import org.apache.nutch.tools.FileDumper;
public class Main {
  public static void main(String... args) throws Exception {
    // Create a default configuration object, which will read
    // the content of nutch-default.xml file from the classpath,
    // parse it and take its entire content as the default
    // configuration. Funny, but this interface is from Hadoop:
    Configuration conf = NutchConfiguration.create();
    // Now it's possible to reset some configuration parameters
    // by using this set() method. This one is mandatory, if you
    // don't set it the crawling won't work. The value is used
    // as User-Agent HTTP header.
    conf.set("http.agent.name", "me, myself, and I");
    // This one is also mandatory and we will discuss its
    // value below. You need to point Nutch to the directory
    // with compiled plugins and this collection is NOT in
    // its default JAR package, unfortunately.
    conf.set("plugin.folders", System.getProperty("nutch.plugins.dir"));
    // First, we need to have a directory where everything will
    // happen. I assume you are familiar with Maven, so let's use
    // its default temporary directory "target":
    Path home = new Path("target");
    // Next, we have to create a file with a list of URLs Nutch will
    // start crawling from:
    String[] urls = { "http://www.zerocracy.com" };
    final Path targets = new Path(home, "urls");
    Files.createDirectory(Paths.get(targets.toString()));
    Files.write(
      Paths.get(targets.toString(), "list-of-urls.txt"),
      String.join("\n", urls).getBytes()
    );
    // Next, we have to point Nutch to the directory with the
    // text file and let it "inject" our URLs into its database:
    new Injector(conf).inject(
      new Path(home, "crawldb"), // the directory with its database
      new Path(home, "urls"), // the directory with text files with URLs
      true, true // no idea what this is
    );
    // Now, it's time to do a few cycles of fetching, parsing, and
    // updating. This is how Nutch works, in increments. Each increment
    // will bring new web pages to the database. The more increments
    // you run, the deeper Nutch will go into the Internet. Five here
    // is a very small number. If you really want to crawl deeper,
    // you will need hundreds of increments. I guess, anyway. I haven't tried it.
    for (int idx = 0; idx < 5; ++idx) {
      this.cycle(home, conf);
    }
    // Now it's time to dump what is fetched to a new directory,
    // which will contain HTML pages and all other files when
    // finished.
    Files.createDirectory(Paths.get(new Path(home, "dump").toString()));
    new FileDumper().dump(
      new File(new Path(home, "dump").toString()), // where to dump
      new File(new Path(home, "segments").toString()),
      null, true, false, true
    );
  }
  private void cycle(Path home, Configuration conf) {
    // This is the directory with "segments". Each fetching cycle
    // will produce its own collection of files. Each collection
    // is called a segment.
    final Path segments = new Path(home, "segments");
    // First, we generate a list of target URLs to fetch from:
    new Generator(conf).generate(
      new Path(home, "crawldb"),
      new Path(home, "segments"),
      1, 1000L, System.currentTimeMillis()
    );
    // Then, we get the path of the current segment:
    final Path sgmt = Batch.segment(segments);
    // Then, we fetch, parse and update:
    new Fetcher(conf).fetch(sgmt, 10);
    new ParseSegment(conf).parse(sgmt);
    new CrawlDb(conf).update(
      new Path(home, "crawldb"),
      Files.list(Paths.get(segments.toString()))
          .map(p -> new Path(p.toString()))
          .toArray(Path[]::new),
      true, true
    );
  }
  private static Path segment(final Path dir) throws IOException {
    // Get the path of the most recent segment in the list,
    // sorted by the date/time of their creation.
    final List<Path> list = Files.list(Paths.get(dir.toString()))
      .map(p -> new Path(p.toString()))
      .sorted(Comparator.comparing(Path::toString))
      .collect(Collectors.toList());
    return list.get(list.size() - 1);
  }
}

Обратите внимание, что Path здесь не Path из JDK. Это Path от Hadoop. Не спрашивай меня почему.

Это кажется довольно простым алгоритмом, однако есть одна сложная часть. Для работы Nutch требуется несколько плагинов, представляющих собой отдельные пакеты JAR, которые он не включает в свой JAR по умолчанию. Они существуют в двоичном дистрибутиве и довольно тяжелые (более 250 МБ в Nutch 1.15). Nutch ожидает, что вы загрузите весь дистрибутив, nutch и запустите бинарный nutch , который они предоставляют, который будет работать с предоставленными плагинами.

Что мы можем сделать, теперь, когда мы находимся на Java, а не в командной строке? Вот что я предлагаю:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<project>
  <build>
    <plugins>
      <plugin>
        <groupId>com.googlecode.maven-download-plugin</groupId>
        <artifactId>download-maven-plugin</artifactId>
        <version>1.4.1</version>
        <executions>
          <execution>
            <id>download-nutch</id>
            <phase>generate-resources</phase>
            <goals>
              <goal>wget</goal>
            </goals>
            <configuration>
              <unpack>true</unpack>
              <outputDirectory>${project.build.directory}</outputDirectory>
              <overwrite>false</overwrite>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  [...]
</project>

Этот плагин Maven загрузит весь двоичный дистрибутив Nutch и распакует его в target/apache-nutch-1.15 . Плагины будут в target/apache-nutch-1.15/plugins . Единственное, что нам еще нужно сделать, это установить системное свойство для модульного теста:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<project>
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <configuration>
            <systemPropertyVariables>
              <nutch.plugins.dir>${project.build.directory}/apache-nutch-1.15/plugins</nutch.plugins.dir>
            </systemPropertyVariables>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
    [...]
  </build>
  [...]
</project>

На самом деле, еще одна вещь, которую мы должны сделать: скопировать содержимое каталога conf из их двоичного дистрибутива в наш каталог src/main/resources . Есть много файлов, в том числе самый важный nutch-default.xml . Все они должны быть доступны на classpath, иначе Nutch будет жаловаться во многих местах и ​​не сможет загрузить Configuration .

Вы можете увидеть, как все это работает вместе в этом репозитории GitHub, который я создал для иллюстрации примера: yegor256 / nutch-in-java .

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

Опубликовано на Java Code Geeks с разрешения Егора Бугаенко, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Как использовать Nutch из Java, а не из командной строки

Мнения, высказанные участниками Java Code Geeks, являются их собственными.