Apache Nutch es un Web crawler open source altamente extensible y escalable. Es un proyecto proveniente de Apache Lucene que se ha diversificado y mantiene dos bases de código fuente: la rama 1.x, madura y lista para entornos de producción, posee una configuración de grano fino y se basa en estructuras de datos de Apache Hadoop, lo cual es ideal para procesamiento por lotes; y la rama 2.x, la cual es una alternativa que emerge directamente de la 1.x, pero se diferencia en que el almacenamiento de datos se abstrae de cualquier implementación específica mediante el uso de Apache Gora. Esto significa que la rama 2.x permite implementar una pila o modelo flexible para almacenara todo (tiempo de fetch, estado, contenido, texto recuperado, enlaces salientes, enlaces entrantes, etc.) en cualquier tipo de solución NoSQL.

Al permitir el uso de plugins y ser modular, Apache Nutch tiene el beneficio de proveer interfaces extensibles a implementaciones personalizadas. Adicionalmente se posibilita el uso de plugins para el indexado, y actualmente existen implementaciones para Apache Solr, Elasticsearch y otros.

Apache Nutch puede correr en un nodo simple o de forma distribuida en un cluster Hadoop y está implementado en Java. Esto implica que se debe contar con Java instalado.

Para quienes no estén familiarizados con el término, un Web crawler, también conocido como spider, spiderbot o simplemente "robot", es un programa que se encarga de navegar páginas Web de forma automática para recolectar datos y enlaces a otras páginas. Típicamente se utilizan para descubrir e indexar enlaces a páginas Web. De esta forma son capaces de recolectar todos los enlaces en uno o múltiples sitios Web y descubrir la estructura y relación entre os mismos. Esta tarea es la que típicamente realizan los buscadores para descubrir el contenido que luego indexarán para ofrecer en los resultados de las búsquedas (claro está que para ofrecer un resultado, previamente se necesita conocerlo, conocer su existencia y contenido).

¿Para qué necesitaría o para qué puede ser útil un crawler? Supongamos que quiero obtener todos los productos en venta en Mercado Libre Argentina y guardar el título junto con el precio para buscar oportunidades de reselling. Ahí está, simplemente basta con poner un Web crawler apuntando a "mercadolibre.com.ar" para luego indexar los datos obtenidos con Elasticsearch. Sólo por citar un ejemplo donde no es posible implementar una solución manual ni es adecuado implementar una propia.

Veamos entonces cómo compilar Apache Nutch 2.x con MongoDB como datastore y Elasticsearch como indexador.

SPOILER ALERT: No es posible indexar en versiones posteriores a la 2.3.3 de Elasticsearch desde Apache Nutch 2.x. Sólo es posible hacerlo utilizando el plugin REST "indexer-elastic-rest" disponible en la última versión estable de Apache Nutch 1.x (ver alternativas al final del artículo).

Compilar Apache Nutch 2.x en Devuan

Abrir una sesión como root e instalar las dependencias necesarias para compilar Nutch:

root@devuan:/home/emi# apt-get install openjdk-8-jdk-headless ant mongodb

Es necesario crear una base de datos y usuario para Nutch en MongoDB.

Descargar y verificar el paquete (desde uno de los mirrors en la sección de descargas):

root@devuan:/home/emi# wget http://apache.dattatec.com/nutch/2.3.1/apache-nutch-2.3.1-src.tar.gz
root@devuan:/home/emi# wget https://apache.org/dist/nutch/2.3.1/apache-nutch-2.3.1-src.tar.gz.sha512

Para verificar la integridad del paquete ejecutar:

root@devuan:/home/emi# sha512sum -c apache-nutch-2.3.1-src.tar.gz.sha512 
apache-nutch-2.3.1-src.tar.gz: OK

Si es está todo bien, extraer el paquete:

root@devuan:/home/emi# tar xzf apache-nutch-2.3.1-src.tar.gz

La configuración de Nutch se encuentra dentro del directorio conf/:

root@devuan:/home/emi# cd apache-nutch-2.3.1/conf/

El primer paso en la configuración es definir el acceso a la base MongoDB:

root@devuan:/home/emi/apache-nutch-2.3.1/conf# nano gora.properties

Dentro del archivo gora.properties se debe definir el datastore MongoDB:

############################
# MongoDBStore properties  #
############################
gora.datastore.default=org.apache.gora.mongodb.store.MongoStore
gora.mongodb.override_hadoop_configuration=false
gora.mongodb.mapping.file=/gora-mongodb-mapping.xml
gora.mongodb.servers=localhost:27017
gora.mongodb.db=nutch
gora.mongodb.login=nutch
gora.mongodb.secret=12345

El archivo gora-mongodb-mapping.xml define el mapeo de estructuras. No es necesario modificarlo.

root@devuan:/home/emi/apache-nutch-2.3.1/conf# ls -l gora-mongodb-mapping.xml 
-rw-rw-r-- 1 root root 3194 ene 10  2016 gora-mongodb-mapping.xml

En el archivo de configuración de Nutch (nutch-site.xml), establecer MongoDB como datastore y Elasticsearch como indexer:

root@devuan:/home/emi/apache-nutch-2.3.1/conf# nano nutch-site.xml

La siguiente es una configuración básica que sirve para comenzar a trabajar con esta configuración:

<configuration>
 <property>
    <name>http.agent.name</name>
    <value>Spider de Linuxito</value>
  </property>
  <property>
    <name>storage.data.store.class</name>
    <value>org.apache.gora.mongodb.store.MongoStore</value>
    <description>Clase para almacenar datos en MongoDB</description>
  </property>
  <property>
    <name>plugin.includes</name>
    <value>protocol-httpclient|urlfilter-regex|parse-(text|tika|js)|index-(basic|anchor)|query-(basic|site|url)|response-(json|xml)|summary-basic|scoring-opic|urlnormalizer-(pass|regex|basic)|indexer-elastic</value>
  </property>
  <property>
    <name>db.ignore.internal.links</name>
    <value>false</value>
  </property>
  <property>
    <name>db.ignore.external.links</name>
    <value>false</value>
  </property>
  <property>
    <name>elastic.host</name>
    <value>localhost</value>
  </property>
  <property>
    <name>elastic.port</name>
    <value>9300</value>
  </property>
  <property>
    <name>elastic.cluster</name>
    <value>elasticsearch</value>
  </property>
  <property>
    <name>elastic.index</name>
    <value>nutchindex</value>
  </property>
  <property>
    <name>parser.character.encoding.default</name>
    <value>utf-8</value>
  </property>
  <property>
    <name>http.content.limit</name>
    <value>6553600</value>
  </property>
  <property>
  <name>elastic.max.bulk.docs</name>
  <value>250</value>
  <description>Maximum size of the bulk in number of documents.</description>
</property>
<property>
  <name>elastic.max.bulk.size</name>
  <value>2500500</value>
  <description>Maximum size of the bulk in bytes.</description>
</property>
</configuration>

Todas las propiedades de configuración se obtienen del archivo de ejemplo nutch-default.xml.

Por supuesto, la configuración del sitio en el archivo nutch-site.xml se puede modificar luego de compilar Nutch.

Es importante utilizar una configuración correcta en las propiedades db.ignore.internal.links y db.ignore.external.links, ya que si se ignoran enlaces internos y externos (ambas propiedades seteadas en true), Nutch no recuperará más que las URLs en el archivo de semillas. Tener en cuenta que una URL perteneciente a otro subdominio se considera externa, ya que se trata de otro host.

El último paso en la configuración consiste en habilitar el módulo "gora-mongodb" en el archivo ivy/ivy.xml:

root@devuan:/home/emi/apache-nutch-2.3.1/conf# cd ..
root@devuan:/home/emi/apache-nutch-2.3.1# nano ivy/ivy.xml

Descomentar las siguientes líneas para habilitar MongoDB:

    <!-- Uncomment this to use MongoDB as Gora backend. -->
    <!--
    <dependency org="org.apache.gora" name="gora-mongodb" rev="0.6.1" conf="*->default" />
    -->

Debe quedar así:

    <!-- Uncomment this to use MongoDB as Gora backend. -->
    <dependency org="org.apache.gora" name="gora-mongodb" rev="0.6.1" conf="*->default" />

Finalmente es posible compilar Nutch ejecutando ant runtime en el directorio base de los fuentes:

root@devuan:/home/emi/apache-nutch-2.3.1# ant clean
root@devuan:/home/emi/apache-nutch-2.3.1# ant runtime

Esto llevará un cierto tiempo dependiendo de los recursos disponibles:

BUILD SUCCESSFUL
Total time: 20 minutes 45 seconds

Los binarios compilados se encuentran dentro del directorio runtime/local/bin/:

root@devuan:/home/emi/apache-nutch-2.3.1# cd runtime/local/
root@devuan:/home/emi/apache-nutch-2.3.1/runtime/local# ls -l
total 28
drwxr-xr-x  2 root root  4096 oct  4 10:11 bin
drwxr-xr-x  2 root root  4096 oct  4 10:11 conf
drwxr-xr-x  3 root root 12288 oct  4 10:11 lib
drwxr-xr-x 39 root root  4096 oct  4 10:11 plugins
drwxr-xr-x  3 root root  4096 oct  4 10:11 test

Cambiar el ownership de la instalación al usuario con el que correrá Nutch y cerrar la sesión de root:

root@devuan:/home/emi# chown -R emi:emi apache-nutch-2.3.1*
root@devuan:/home/emi# exit
emi@devuan:~$ cd apache-nutch-2.3.1/runtime/local/

Primeros pasos con Apache Nutch 2.x

Nutch necesita una o más URLs donde a partir de las cuales comenzar a recuperar páginas. Se denominan semillas y necesitan apuntar a páginas válidas. Con sólo una URL basta para comenzar a recuperar páginas:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ mkdir seed
emi@devuan:~/apache-nutch-2.3.1/runtime/local$ echo "https://www.mercadolibre.com.ar" > seed/urls.txt

Para limitar la búsqueda a un dominio específico (por ejemplo "mercadolibre.com.ar") incluyendo todos sus subdominios, utilizar la siguiente expresión regular:

# accept anything else
#+.
+mercadolibre.com.ar

Para comenzar a trabajar con Nutch, el primer paso consiste en inyectar las URLs semilla. Si al ejecutar bin/nutch se obtiene el error "JAVA_HOME is not set", significa que se debe configurar dicha variable de entorno correctamente:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch inject seed/urls.txt
Error: JAVA_HOME is not set.

Buscar el directorio base de la máquina virtual de Java (JVM):

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ find / -type d -name jvm 2>/dev/null
/usr/lib/jvm
/usr/lib/debug/usr/lib/jvm
/usr/share/gdb/auto-load/usr/lib/jvm

En los sistemas Devuan y derivados se encuentra dentro de /usr/lib/jvm/:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ ll /usr/lib/jvm/
total 20
drwxr-xr-x  4 root root 4096 sep  4 12:41 .
drwxr-xr-x 84 root root 4096 oct  3 11:09 ..
lrwxrwxrwx  1 root root   24 ene  6  2017 default-java -> java-1.8.0-openjdk-amd64
drwxr-xr-x  4 root root 4096 sep  4 12:05 java-1.5.0-gcj-6-amd64
lrwxrwxrwx  1 root root   20 nov  1  2017 java-1.8.0-openjdk-amd64 -> java-8-openjdk-amd64
-rw-r--r--  1 root root 2600 ago  9 19:11 .java-1.8.0-openjdk-amd64.jinfo
drwxr-xr-x  7 root root 4096 oct  3 11:09 java-8-openjdk-amd64

Es conveniente utilizar el enlace simbólico default-java. Exportar la variable de entorno JAVA_HOME apuntando al mismo:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ export JAVA_HOME="/usr/lib/jvm/default-java"

Inyectar en la base de Nutch las URLs a recuperar:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch inject seed/urls.txt
InjectorJob: starting at 2018-10-05 09:59:44
InjectorJob: Injecting urlDir: seed/urls.txt
InjectorJob: Using class org.apache.gora.mongodb.store.MongoStore as the Gora storage class.
InjectorJob: total number of urls rejected by filters: 0
InjectorJob: total number of urls injected after normalization and filtering: 1
Injector: finished at 2018-10-05 09:59:59, elapsed: 00:00:15

Crawling con Nutch

El proceso de crawling con Nutch se divide en 4 tareas: 1- generar urls a recuperar (bin/nutch generate); 2- recuperar páginas a partir de las URLs en la base (bin/nutch fetch); 3- parsear las páginas descargadas para obtener nuevas URLs a generar en el paso 1 (bin/nutch parse); y 4- actualizar la base de datos (bin/nutch updatedb). Este proceso se repite hasta el infinito, o hasta que no se descubran nuevas URLs (en este caso significaría que hemos descargado todo el sitio Web).

Este proceso se puede realizar manualmente, aunque Nutch provee un script Bash que lo hace de manera automática (bin/crawl). Sin embargo este script está pensado para indexar en Apache Solr, con lo cual el paso de indexado no será realizado.

Para ejecutar el proceso de forma manual, seguir los siguientes pasos:

1- Generar las URLs:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch generate -topN 100
GeneratorJob: starting at 2018-10-05 10:01:25
GeneratorJob: Selecting best-scoring urls due for fetch.
GeneratorJob: starting
GeneratorJob: filtering: true
GeneratorJob: normalizing: true
GeneratorJob: topN: 100
GeneratorJob: finished at 2018-10-05 10:01:29, time elapsed: 00:00:04
GeneratorJob: generated batch id: 1538744485-497126277 containing 1 URLs

2- Descargar las páginas a partir de las URLs en la base:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch fetch -all
FetcherJob: starting at 2018-10-05 10:27:51
FetcherJob: fetching all
FetcherJob: threads: 10
FetcherJob: parsing: false
FetcherJob: resuming: false
FetcherJob : timelimit set for : -1
Using queue mode : byHost
Fetcher: threads: 10
QueueFeeder finished: total 1 records. Hit by time limit :0
fetching https://www.mercadolibre.com.ar/ (queue crawl delay=5000ms)

[...]

3- A continuación, parsear las páginas recuperadas en busca de nuevas URLs:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch parse -all
ParserJob: starting at 2018-10-05 10:29:25
ParserJob: resuming:    false
ParserJob: forced reparse:      false
ParserJob: parsing all
Parsing https://www.mercadolibre.com.ar/

[...]

4- Finalmente, actualizar la base de datos:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch updatedb -all
DbUpdaterJob: starting at 2018-10-05 11:05:53
DbUpdaterJob: updatinging all
DbUpdaterJob: finished at 2018-10-05 11:05:58, time elapsed: 00:00:04

Volver al paso 1.

Sin embargo, para la primera vez es más simple utilizar el script Bash:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/crawl seed/urls.txt inicio 3
No SOLRURL specified. Skipping indexing.
Injecting seed URLs
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch inject seed/urls.txt -crawlId inicio
InjectorJob: starting at 2018-10-08 10:16:23
InjectorJob: Injecting urlDir: seed/urls.txt
InjectorJob: Using class org.apache.gora.mongodb.store.MongoStore as the Gora storage class.
InjectorJob: total number of urls rejected by filters: 0
InjectorJob: total number of urls injected after normalization and filtering: 1
Injector: finished at 2018-10-08 10:16:26, elapsed: 00:00:03
lun oct 8 10:16:26 -03 2018 : Iteration 1 of 3
Generating batchId
Generating a new fetchlist
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch generate -D mapred.reduce.tasks=2 -D mapred.child.java.opts=-Xmx1000m -D mapred.reduce.tasks.speculative.execution=false -D mapred.map.tasks.speculative.execution=false -D mapred.compress.map.output=true -topN 50000 -noNorm -noFilter -adddays 0 -crawlId inicio -batchId 1539004586-18854
GeneratorJob: starting at 2018-10-08 10:16:28
GeneratorJob: Selecting best-scoring urls due for fetch.
GeneratorJob: starting
GeneratorJob: filtering: false
GeneratorJob: normalizing: false
GeneratorJob: topN: 50000
GeneratorJob: finished at 2018-10-08 10:16:32, time elapsed: 00:00:04
GeneratorJob: generated batch id: 1539004586-18854 containing 1 URLs
Fetching : 
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch fetch -D mapred.reduce.tasks=2 -D mapred.child.java.opts=-Xmx1000m -D mapred.reduce.tasks.speculative.execution=false -D mapred.map.tasks.speculative.execution=false -D mapred.compress.map.output=true -D fetcher.timelimit.mins=180 1539004586-18854 -crawlId inicio -threads 50
FetcherJob: starting at 2018-10-08 10:16:34
FetcherJob: batchId: 1539004586-18854
FetcherJob: threads: 50
FetcherJob: parsing: false
FetcherJob: resuming: false
FetcherJob : timelimit set for : 1539015394398
Using queue mode : byHost
Fetcher: threads: 50
QueueFeeder finished: total 0 records. Hit by time limit :0
-finishing thread FetcherThread0, activeThreads=0
Fetcher: throughput threshold: -1
Fetcher: throughput threshold sequence: 5
-finishing thread FetcherThread2, activeThreads=47
-finishing thread FetcherThread3, activeThreads=46
-finishing thread FetcherThread4, activeThreads=45
-finishing thread FetcherThread5, activeThreads=44
-finishing thread FetcherThread6, activeThreads=43
-finishing thread FetcherThread7, activeThreads=42
-finishing thread FetcherThread8, activeThreads=41
-finishing thread FetcherThread9, activeThreads=40
-finishing thread FetcherThread10, activeThreads=39
-finishing thread FetcherThread11, activeThreads=38
-finishing thread FetcherThread12, activeThreads=37
-finishing thread FetcherThread13, activeThreads=36
-finishing thread FetcherThread14, activeThreads=35
-finishing thread FetcherThread15, activeThreads=34
-finishing thread FetcherThread16, activeThreads=33
-finishing thread FetcherThread17, activeThreads=32
-finishing thread FetcherThread18, activeThreads=31
-finishing thread FetcherThread19, activeThreads=30
-finishing thread FetcherThread20, activeThreads=29
-finishing thread FetcherThread21, activeThreads=28
-finishing thread FetcherThread22, activeThreads=27
-finishing thread FetcherThread23, activeThreads=26
-finishing thread FetcherThread24, activeThreads=25
-finishing thread FetcherThread25, activeThreads=24
-finishing thread FetcherThread26, activeThreads=23
-finishing thread FetcherThread27, activeThreads=22
-finishing thread FetcherThread28, activeThreads=21
-finishing thread FetcherThread29, activeThreads=20
-finishing thread FetcherThread30, activeThreads=19
-finishing thread FetcherThread31, activeThreads=18
-finishing thread FetcherThread32, activeThreads=17
-finishing thread FetcherThread33, activeThreads=16
-finishing thread FetcherThread34, activeThreads=15
-finishing thread FetcherThread35, activeThreads=14
-finishing thread FetcherThread36, activeThreads=13
-finishing thread FetcherThread37, activeThreads=12
-finishing thread FetcherThread38, activeThreads=11
-finishing thread FetcherThread39, activeThreads=10
-finishing thread FetcherThread40, activeThreads=9
-finishing thread FetcherThread41, activeThreads=8
-finishing thread FetcherThread42, activeThreads=7
-finishing thread FetcherThread43, activeThreads=6
-finishing thread FetcherThread44, activeThreads=5
-finishing thread FetcherThread45, activeThreads=4
-finishing thread FetcherThread46, activeThreads=3
-finishing thread FetcherThread47, activeThreads=2
-finishing thread FetcherThread1, activeThreads=1
-finishing thread FetcherThread48, activeThreads=0
-finishing thread FetcherThread49, activeThreads=0
0/0 spinwaiting/active, 0 pages, 0 errors, 0.0 0 pages/s, 0 0 kb/s, 0 URLs in 0 queues
-activeThreads=0
Using queue mode : byHost
Fetcher: threads: 50
QueueFeeder finished: total 1 records. Hit by time limit :0
fetching https://ofertas.mercadolibre.com.ar/ofertas-de-la-semana (queue crawl delay=5000ms)
Fetcher: throughput threshold: -1
Fetcher: throughput threshold sequence: 5
-finishing thread FetcherThread23, activeThreads=48
-finishing thread FetcherThread22, activeThreads=47
-finishing thread FetcherThread21, activeThreads=46
-finishing thread FetcherThread20, activeThreads=45
-finishing thread FetcherThread19, activeThreads=44
-finishing thread FetcherThread18, activeThreads=43
-finishing thread FetcherThread17, activeThreads=42
-finishing thread FetcherThread16, activeThreads=41
-finishing thread FetcherThread15, activeThreads=40
-finishing thread FetcherThread14, activeThreads=39
-finishing thread FetcherThread13, activeThreads=38
-finishing thread FetcherThread12, activeThreads=37
-finishing thread FetcherThread11, activeThreads=36
-finishing thread FetcherThread10, activeThreads=35
-finishing thread FetcherThread9, activeThreads=34
-finishing thread FetcherThread8, activeThreads=33
-finishing thread FetcherThread7, activeThreads=32
-finishing thread FetcherThread6, activeThreads=31
-finishing thread FetcherThread5, activeThreads=30
-finishing thread FetcherThread4, activeThreads=29
-finishing thread FetcherThread3, activeThreads=28
-finishing thread FetcherThread2, activeThreads=27
-finishing thread FetcherThread1, activeThreads=26
-finishing thread FetcherThread30, activeThreads=25
-finishing thread FetcherThread47, activeThreads=24
-finishing thread FetcherThread46, activeThreads=23
-finishing thread FetcherThread45, activeThreads=22
-finishing thread FetcherThread44, activeThreads=21
-finishing thread FetcherThread43, activeThreads=20
-finishing thread FetcherThread42, activeThreads=19
-finishing thread FetcherThread41, activeThreads=18
-finishing thread FetcherThread40, activeThreads=17
-finishing thread FetcherThread39, activeThreads=16
-finishing thread FetcherThread38, activeThreads=15
-finishing thread FetcherThread37, activeThreads=14
-finishing thread FetcherThread36, activeThreads=13
-finishing thread FetcherThread35, activeThreads=12
-finishing thread FetcherThread34, activeThreads=11
-finishing thread FetcherThread33, activeThreads=10
-finishing thread FetcherThread32, activeThreads=9
-finishing thread FetcherThread31, activeThreads=8
-finishing thread FetcherThread48, activeThreads=7
-finishing thread FetcherThread49, activeThreads=7
-finishing thread FetcherThread29, activeThreads=6
-finishing thread FetcherThread28, activeThreads=5
-finishing thread FetcherThread27, activeThreads=4
-finishing thread FetcherThread26, activeThreads=3
-finishing thread FetcherThread25, activeThreads=2
-finishing thread FetcherThread24, activeThreads=1
-finishing thread FetcherThread0, activeThreads=0
0/0 spinwaiting/active, 1 pages, 0 errors, 0.2 0 pages/s, 492 492 kb/s, 0 URLs in 0 queues
-activeThreads=0
FetcherJob: finished at 2018-10-08 10:16:49, time elapsed: 00:00:15
Parsing : 
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch parse -D mapred.reduce.tasks=2 -D mapred.child.java.opts=-Xmx1000m -D mapred.reduce.tasks.speculative.execution=false -D mapred.map.tasks.speculative.execution=false -D mapred.compress.map.output=true -D mapred.skip.attempts.to.start.skipping=2 -D mapred.skip.map.max.skip.records=1 1539004586-18854 -crawlId inicio
ParserJob: starting at 2018-10-08 10:16:51
ParserJob: resuming:    false
ParserJob: forced reparse:      false
ParserJob: batchId:     1539004586-18854
Parsing https://ofertas.mercadolibre.com.ar/ofertas-de-la-semana
ParserJob: success
ParserJob: finished at 2018-10-08 10:16:56, time elapsed: 00:00:05
CrawlDB update for inicio
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch updatedb -D mapred.reduce.tasks=2 -D mapred.child.java.opts=-Xmx1000m -D mapred.reduce.tasks.speculative.execution=false -D mapred.map.tasks.speculative.execution=false -D mapred.compress.map.output=true 1539004586-18854 -crawlId inicio
DbUpdaterJob: starting at 2018-10-08 10:16:58
DbUpdaterJob: batchId: 1539004586-18854
DbUpdaterJob: finished at 2018-10-08 10:17:03, time elapsed: 00:00:04
Skipping indexing tasks: no SOLR url provided.
lun oct 8 10:17:03 -03 2018 : Iteration 2 of 3
Generating batchId
Generating a new fetchlist
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch generate -D mapred.reduce.tasks=2 -D mapred.child.java.opts=-Xmx1000m -D mapred.reduce.tasks.speculative.execution=false -D mapred.map.tasks.speculative.execution=false -D mapred.compress.map.output=true -topN 50000 -noNorm -noFilter -adddays 0 -crawlId inicio -batchId 1539004623-21385
GeneratorJob: starting at 2018-10-08 10:17:04
GeneratorJob: Selecting best-scoring urls due for fetch.
GeneratorJob: starting
GeneratorJob: filtering: false
GeneratorJob: normalizing: false
GeneratorJob: topN: 50000
GeneratorJob: finished at 2018-10-08 10:17:09, time elapsed: 00:00:04
GeneratorJob: generated batch id: 1539004623-21385 containing 99 URLs
Fetching : 
/home/emi/apache-nutch-2.3.1/runtime/local/bin/nutch fetch -D mapred.reduce.tasks=2 -D mapred.child.java.opts=-Xmx1000m -D mapred.reduce.tasks.speculative.execution=false -D mapred.map.tasks.speculative.execution=false -D mapred.compress.map.output=true -D fetcher.timelimit.mins=180 1539004623-21385 -crawlId inicio -threads 50
FetcherJob: starting at 2018-10-08 10:17:11
FetcherJob: batchId: 1539004623-21385
FetcherJob: threads: 50
FetcherJob: parsing: false
FetcherJob: resuming: false
FetcherJob : timelimit set for : 1539015431036
Using queue mode : byHost
Fetcher: threads: 50
QueueFeeder finished: total 15 records. Hit by time limit :0
fetching https://www.mercadolibre.com.ar/registration (queue crawl delay=5000ms)
Fetcher: throughput threshold: -1
Fetcher: throughput threshold sequence: 5
fetching https://home.mercadolibre.com.ar/categories (queue crawl delay=5000ms)
fetching https://instrumentos.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://camaras-digitales.mercadolibre.com.ar/camaras-accesorios/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://electronica.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://hogar.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://myaccount.mercadolibre.com.ar/purchases/list (queue crawl delay=5000ms)
fetching https://telefonia.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 6 pages, 0 errors, 1.2 1 pages/s, 1312 1312 kb/s, 7 URLs in 1 queues
50/50 spinwaiting/active, 7 pages, 0 errors, 0.7 0 pages/s, 755 198 kb/s, 7 URLs in 1 queues
fetching https://www.mercadolibre.com.ar/tiendas-oficiales (queue crawl delay=5000ms)
49/50 spinwaiting/active, 7 pages, 0 errors, 0.5 0 pages/s, 503 0 kb/s, 6 URLs in 1 queues
49/50 spinwaiting/active, 7 pages, 0 errors, 0.4 0 pages/s, 377 0 kb/s, 6 URLs in 1 queues
50/50 spinwaiting/active, 8 pages, 0 errors, 0.3 0 pages/s, 550 1242 kb/s, 6 URLs in 1 queues
fetching https://www.mercadolibre.com.ar/gz/app (queue crawl delay=5000ms)
50/50 spinwaiting/active, 9 pages, 0 errors, 0.3 0 pages/s, 470 68 kb/s, 5 URLs in 1 queues
fetching https://www.mercadolibre.com.ar/gz/home/navigation (queue crawl delay=5000ms)
49/50 spinwaiting/active, 9 pages, 0 errors, 0.3 0 pages/s, 403 0 kb/s, 4 URLs in 1 queues
* queue: https://www.mercadolibre.com.ar
  maxThreads    = 1
  inProgress    = 1
  crawlDelay    = 5000
  minCrawlDelay = 0
  nextFetchTime = 1539004668731
  now           = 1539004671093
  0. https://www.mercadolibre.com.ar/mercadopuntos/descuentosexclusivos
  1. https://www.mercadolibre.com.ar/gz/cart
  2. https://www.mercadolibre.com.ar/ayuda
  3. https://www.mercadolibre.com.ar/
50/50 spinwaiting/active, 10 pages, 0 errors, 0.3 0 pages/s, 363 85 kb/s, 4 URLs in 1 queues
* queue: https://www.mercadolibre.com.ar
  maxThreads    = 1
  inProgress    = 0
  crawlDelay    = 5000
  minCrawlDelay = 0
  nextFetchTime = 1539004678058
  now           = 1539004676094
  0. https://www.mercadolibre.com.ar/mercadopuntos/descuentosexclusivos
  1. https://www.mercadolibre.com.ar/gz/cart
  2. https://www.mercadolibre.com.ar/ayuda
  3. https://www.mercadolibre.com.ar/
fetching https://www.mercadolibre.com.ar/mercadopuntos/descuentosexclusivos (queue crawl delay=5000ms)
50/50 spinwaiting/active, 11 pages, 0 errors, 0.2 0 pages/s, 323 0 kb/s, 3 URLs in 1 queues
* queue: https://www.mercadolibre.com.ar
  maxThreads    = 1
  inProgress    = 0
  crawlDelay    = 5000
  minCrawlDelay = 0
  nextFetchTime = 1539004684008
  now           = 1539004681094
  0. https://www.mercadolibre.com.ar/gz/cart
  1. https://www.mercadolibre.com.ar/ayuda
  2. https://www.mercadolibre.com.ar/
fetching https://www.mercadolibre.com.ar/gz/cart (queue crawl delay=5000ms)
50/50 spinwaiting/active, 12 pages, 0 errors, 0.2 0 pages/s, 290 0 kb/s, 2 URLs in 1 queues
* queue: https://www.mercadolibre.com.ar
  maxThreads    = 1
  inProgress    = 0
  crawlDelay    = 5000
  minCrawlDelay = 0
  nextFetchTime = 1539004690271
  now           = 1539004686095
  0. https://www.mercadolibre.com.ar/ayuda
  1. https://www.mercadolibre.com.ar/
fetching https://www.mercadolibre.com.ar/ayuda (queue crawl delay=5000ms)
49/50 spinwaiting/active, 12 pages, 0 errors, 0.2 0 pages/s, 264 0 kb/s, 1 URLs in 1 queues
* queue: https://www.mercadolibre.com.ar
  maxThreads    = 1
  inProgress    = 1
  crawlDelay    = 5000
  minCrawlDelay = 0
  nextFetchTime = 1539004690271
  now           = 1539004691096
  0. https://www.mercadolibre.com.ar/
50/50 spinwaiting/active, 13 pages, 0 errors, 0.2 0 pages/s, 253 132 kb/s, 1 URLs in 1 queues
* queue: https://www.mercadolibre.com.ar
  maxThreads    = 1
  inProgress    = 0
  crawlDelay    = 5000
  minCrawlDelay = 0
  nextFetchTime = 1539004696563
  now           = 1539004696097
  0. https://www.mercadolibre.com.ar/
fetching https://www.mercadolibre.com.ar/ (queue crawl delay=5000ms)
-finishing thread FetcherThread29, activeThreads=49
-finishing thread FetcherThread31, activeThreads=48
-finishing thread FetcherThread41, activeThreads=47
-finishing thread FetcherThread34, activeThreads=46
-finishing thread FetcherThread37, activeThreads=45
-finishing thread FetcherThread40, activeThreads=44
-finishing thread FetcherThread38, activeThreads=43
-finishing thread FetcherThread21, activeThreads=42
-finishing thread FetcherThread25, activeThreads=41
-finishing thread FetcherThread20, activeThreads=40
-finishing thread FetcherThread33, activeThreads=39
-finishing thread FetcherThread27, activeThreads=38
-finishing thread FetcherThread26, activeThreads=37
-finishing thread FetcherThread30, activeThreads=36
-finishing thread FetcherThread35, activeThreads=35
-finishing thread FetcherThread22, activeThreads=34
-finishing thread FetcherThread36, activeThreads=33
-finishing thread FetcherThread19, activeThreads=32
-finishing thread FetcherThread18, activeThreads=31
-finishing thread FetcherThread17, activeThreads=30
-finishing thread FetcherThread39, activeThreads=29
-finishing thread FetcherThread44, activeThreads=28
-finishing thread FetcherThread24, activeThreads=27
-finishing thread FetcherThread28, activeThreads=26
-finishing thread FetcherThread23, activeThreads=25
-finishing thread FetcherThread42, activeThreads=24
-finishing thread FetcherThread32, activeThreads=23
-finishing thread FetcherThread15, activeThreads=22
-finishing thread FetcherThread16, activeThreads=21
-finishing thread FetcherThread0, activeThreads=20
-finishing thread FetcherThread2, activeThreads=19
-finishing thread FetcherThread7, activeThreads=18
-finishing thread FetcherThread4, activeThreads=17
-finishing thread FetcherThread6, activeThreads=16
-finishing thread FetcherThread8, activeThreads=15
-finishing thread FetcherThread13, activeThreads=14
-finishing thread FetcherThread49, activeThreads=13
-finishing thread FetcherThread11, activeThreads=12
-finishing thread FetcherThread12, activeThreads=11
-finishing thread FetcherThread10, activeThreads=10
-finishing thread FetcherThread5, activeThreads=9
-finishing thread FetcherThread3, activeThreads=8
-finishing thread FetcherThread14, activeThreads=7
-finishing thread FetcherThread9, activeThreads=6
-finishing thread FetcherThread48, activeThreads=5
-finishing thread FetcherThread43, activeThreads=4
-finishing thread FetcherThread47, activeThreads=3
-finishing thread FetcherThread1, activeThreads=2
-finishing thread FetcherThread46, activeThreads=1
-finishing thread FetcherThread45, activeThreads=0
0/0 spinwaiting/active, 14 pages, 0 errors, 0.2 0 pages/s, 271 482 kb/s, 0 URLs in 0 queues
-activeThreads=0
Using queue mode : byHost
Fetcher: threads: 50
fetching https://deportes.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
Fetcher: throughput threshold: -1
Fetcher: throughput threshold sequence: 5
fetching https://ropa.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://listado.mercadolibre.com.ar/_Tienda_hp-store_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://computacion.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
QueueFeeder finished: total 84 records. Hit by time limit :0
fetching https://vender.mercadolibre.com.ar/ (queue crawl delay=5000ms)
fetching https://juegos-juguetes.mercadolibre.com.ar/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://www.mercadolibre.com/jms/mla/lgz/login?platform_id=ml&go=https%3A%2F%2Fofertas.mercadolibre.com.ar%2Fofertas-de-la-semana&loginType=explicit (queue crawl delay=5000ms)
fetching https://www.mercadopago.com.ar/tarjeta-mercadopago?utm_medium=referral&utm_source=mercadolibre.com.ar&utm_campaign=cobranded_acquisition_banner_exhibitor (queue crawl delay=5000ms)
fetching https://vehiculos.mercadolibre.com.ar/accesorios/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://analytics.mercadolibre.com.ar/ (queue crawl delay=5000ms)
49/50 spinwaiting/active, 7 pages, 0 errors, 1.4 1 pages/s, 1287 1287 kb/s, 74 URLs in 2 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_garden-life_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
50/50 spinwaiting/active, 9 pages, 0 errors, 0.9 0 pages/s, 885 482 kb/s, 73 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/corrientes/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 9 pages, 0 errors, 0.6 0 pages/s, 590 0 kb/s, 72 URLs in 1 queues
50/50 spinwaiting/active, 10 pages, 0 errors, 0.5 0 pages/s, 494 208 kb/s, 72 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_casa-del-audio_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
50/50 spinwaiting/active, 11 pages, 0 errors, 0.4 0 pages/s, 437 209 kb/s, 71 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_babynet_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 11 pages, 0 errors, 0.4 0 pages/s, 364 0 kb/s, 70 URLs in 1 queues
50/50 spinwaiting/active, 12 pages, 0 errors, 0.3 0 pages/s, 344 224 kb/s, 70 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_PriceRange_10000-0_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://listado.mercadolibre.com.ar/industrias-oficinas/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 12 pages, 0 errors, 0.3 0 pages/s, 301 0 kb/s, 68 URLs in 1 queues
50/50 spinwaiting/active, 13 pages, 0 errors, 0.3 0 pages/s, 292 221 kb/s, 68 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_prestigio_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 13 pages, 0 errors, 0.3 0 pages/s, 263 0 kb/s, 67 URLs in 1 queues
49/50 spinwaiting/active, 13 pages, 0 errors, 0.2 0 pages/s, 239 0 kb/s, 67 URLs in 1 queues
50/50 spinwaiting/active, 14 pages, 0 errors, 0.2 0 pages/s, 236 207 kb/s, 67 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_PriceRange_3000-10000_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
fetching https://listado.mercadolibre.com.ar/_Tienda_fravega_Deal_ofertas-de-la-semana_af_to (queue crawl delay=5000ms)
50/50 spinwaiting/active, 15 pages, 0 errors, 0.2 0 pages/s, 236 234 kb/s, 65 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/chaco/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
50/50 spinwaiting/active, 16 pages, 0 errors, 0.2 0 pages/s, 234 213 kb/s, 64 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_adidas-performance_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 16 pages, 0 errors, 0.2 0 pages/s, 219 0 kb/s, 63 URLs in 1 queues
50/50 spinwaiting/active, 17 pages, 0 errors, 0.2 0 pages/s, 218 211 kb/s, 63 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/la-rioja/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
50/50 spinwaiting/active, 18 pages, 0 errors, 0.2 0 pages/s, 218 213 kb/s, 62 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Deal_ofertas-de-la-semana_Discount_20-100 (queue crawl delay=5000ms)
49/50 spinwaiting/active, 18 pages, 0 errors, 0.2 0 pages/s, 206 0 kb/s, 61 URLs in 1 queues
50/50 spinwaiting/active, 19 pages, 0 errors, 0.2 0 pages/s, 221 488 kb/s, 61 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_adidas-originals_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 19 pages, 0 errors, 0.2 0 pages/s, 210 0 kb/s, 60 URLs in 1 queues
50/50 spinwaiting/active, 20 pages, 0 errors, 0.2 0 pages/s, 210 212 kb/s, 60 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_reflejar_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 20 pages, 0 errors, 0.2 0 pages/s, 200 0 kb/s, 59 URLs in 1 queues
50/50 spinwaiting/active, 21 pages, 0 errors, 0.2 0 pages/s, 201 206 kb/s, 59 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Deal_ofertas-de-la-semana_Installments_NoInterest (queue crawl delay=5000ms)
fetching https://listado.mercadolibre.com.ar/neuquen/_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
50/50 spinwaiting/active, 22 pages, 0 errors, 0.2 0 pages/s, 201 213 kb/s, 57 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Deal_ofertas-de-la-semana_Discount_40-100 (queue crawl delay=5000ms)
49/50 spinwaiting/active, 22 pages, 0 errors, 0.2 0 pages/s, 193 0 kb/s, 56 URLs in 1 queues
50/50 spinwaiting/active, 23 pages, 0 errors, 0.2 0 pages/s, 197 291 kb/s, 56 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Tienda_educando_Deal_ofertas-de-la-semana (queue crawl delay=5000ms)
49/50 spinwaiting/active, 23 pages, 0 errors, 0.2 0 pages/s, 189 0 kb/s, 55 URLs in 1 queues
50/50 spinwaiting/active, 24 pages, 0 errors, 0.2 0 pages/s, 190 207 kb/s, 55 URLs in 1 queues
fetching https://listado.mercadolibre.com.ar/_Deal_ofertas-de-la-semana_Discount_30-100 (queue crawl delay=5000ms)

[...]

Este script requiere pasar como parámetros la ruta al archivo de semillas, un ID para la tarea, y la cantidad de rondas.

Para este ejemplo utilicé tres rondas, y luego de algo más de dos horas y media, recuperó 24.312 páginas:

emi@devuan:~/nutch$ mongo
MongoDB shell version: 3.2.11
connecting to: test
> show dbs
admin  0.000GB
local  0.000GB
nutch  0.120GB
> use nutch
switched to db nutch
> show collections
inicio_webpage
> db.inicio_webpage.count()
24312
> db.stats()
{
        "db" : "nutch",
        "collections" : 1,
        "objects" : 24312,
        "avgObjSize" : 9015.767028627839,
        "dataSize" : 219191328,
        "storageSize" : 128016384,
        "numExtents" : 0,
        "indexes" : 1,
        "indexSize" : 1294336,
        "ok" : 1
}

Tal vez para comenzar sea más adecuado utilizar una o a lo sumo dos pasadas (1 o 2 como último parámetro al script).

Enviando una consulta a la base de datos Mongo es posible obtener todas las URLs recuperadas a un archivo de texto plano:

emi@devuan:~$ mongo localhost/nutch --eval "DBQuery.shellBatchSize=50; db.inicio_webpage.find({}, {'_id': 0, 'baseUrl': 1})" > cmdout.mongo
emi@devuan:~$ head cmdout.mongo 
MongoDB shell version: 3.2.11
connecting to: localhost/nutch
{ "baseUrl" : "https://ofertas.mercadolibre.com.ar/ofertas-de-la-semana" }
{  }
{ "baseUrl" : "https://camaras-digitales.mercadolibre.com.ar/camaras-accesorios/_Deal_ofertas-de-la-semana" }
{ "baseUrl" : "https://computacion.mercadolibre.com.ar/_Deal_ofertas-de-la-semana" }
{ "baseUrl" : "https://deportes.mercadolibre.com.ar/_Deal_ofertas-de-la-semana" }
{ "baseUrl" : "https://juegos-juguetes.mercadolibre.com.ar/_Deal_ofertas-de-la-semana" }
{ "baseUrl" : "https://listado.mercadolibre.com.ar/_Deal_ofertas-de-la-semana" }
{ "baseUrl" : "https://listado.mercadolibre.com.ar/_Deal_ofertas-de-la-semana_Discount_10-100" }

Indexando con Elasticsearch

Ya sea habiendo realizado el proceso de crawling de forma manual, o utilizando el script bin/crawl, será necesario indexar toda la información obtenida (contenido, metadatos, links, anchor text, etc.) en Elasticsearch.

Para ello simplemente ejecutar:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ bin/nutch index elasticsearch -all
IndexingJob: starting
Active IndexWriters :
ElasticIndexWriter
        elastic.cluster : elastic prefix cluster
        elastic.host : hostname
        elastic.port : port  (default 9300)
        elastic.index : elastic index command 
        elastic.max.bulk.docs : elastic bulk index doc counts. (default 250) 
        elastic.max.bulk.size : elastic bulk index length. (default 2500500 ~2.5MB)


IndexingJob: done.

Sin embargo, en el log se encuentran los siguientes errores:

emi@devuan:~/apache-nutch-2.3.1/runtime/local$ tail logs/hadoop.log 
        elastic.port : port  (default 9300)
        elastic.index : elastic index command 
        elastic.max.bulk.docs : elastic bulk index doc counts. (default 250) 
        elastic.max.bulk.size : elastic bulk index length. (default 2500500 ~2.5MB)


2018-10-09 11:11:56,286 INFO  elasticsearch.plugins - [Orator] loaded [], sites []
2018-10-09 11:11:56,405 INFO  client.transport - [Orator] failed to get node info for [#transport#-1][devuan][inet[localhost/127.0.0.1:9300]], disconnecting...
org.elasticsearch.transport.NodeDisconnectedException: [][inet[localhost/127.0.0.1:9300]][cluster:monitor/nodes/info] disconnected
2018-10-09 11:11:56,405 INFO  indexer.IndexingJob - IndexingJob: done.

Aquí es donde empiezan los problemas...

Existe un plugin desarrollado por un tercero que es compatible con Elasticsearch 5. Sin embargo estoy utilizando la versión 6.4, así que no me sirve. Por ende la mejor alternativa consiste en utilizar el plugin que utiliza la interfaz REST (puerto 9200) en lugar del protocolo binario (puerto 9300). Sin embargo, este plugin sólo está disponible para Apache Nutch 1.x. (no es posible compilarlo para la versión 2.x porque depende de la base de código de 1.x).

Las alternativas que quedan para indexar en Elasticsearch desde Apache Nutch son:

  • Utilizar una versión anterior a Elasticsearch 2.3.3 con Apache Nutch 2.x.
  • Utilizar el plugin REST "indexer-elastic-rest" con apache Nutch 1.x.
  • Conectar directamente MongoDB con Elasticsearch utilizando mongo-connector.

"Tengo hasta ahí".

En próximos artículos veremos cómo sigue la cuestión.

Referencias


Tal vez pueda interesarte


Compartí este artículo