¿Cómo creo una Web Spider de CLI que usa palabras clave y filtra el contenido?

10

Quiero encontrar mis artículos en el foro de literatura obsoleto (obsoleto) e-bane.net . Algunos de los módulos del foro están deshabilitados, y no puedo obtener una lista de artículos de su autor. Además, el sitio no está indexado por los motores de búsqueda como Google, Yndex, etc.

La única forma de encontrar todos mis artículos es abrir la página de archivo del sitio (fig.1). Luego debo seleccionar cierto año y mes, por ejemplo, enero de 2013 (fig.1). Y luego debo inspeccionar cada artículo (fig.2) si al principio está escrito mi apodo - pa4080 (fig.3). Pero hay unos pocos miles de artículos.

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

ingrese la descripción de la imagen aquí

He leído algunos temas de la siguiente manera, pero ninguna de las soluciones se ajusta a mis necesidades:

Publicaré mi propia solución . Pero para mí es interesante: ¿hay alguna forma más elegante de resolver esta tarea?

pa4080
fuente

Respuestas:

3

script.py:

#!/usr/bin/python3
from urllib.parse import urljoin
import json

import bs4
import click
import aiohttp
import asyncio
import async_timeout


BASE_URL = 'http://e-bane.net'


async def fetch(session, url):
    try:
        with async_timeout.timeout(20):
            async with session.get(url) as response:
                return await response.text()
    except asyncio.TimeoutError as e:
        print('[{}]{}'.format('timeout error', url))
        with async_timeout.timeout(20):
            async with session.get(url) as response:
                return await response.text()


async def get_result(user):
    target_url = 'http://e-bane.net/modules.php?name=Stories_Archive'
    res = []
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, target_url)
        html_soup = bs4.BeautifulSoup(html, 'html.parser')
        date_module_links = parse_date_module_links(html_soup)
        for dm_link in date_module_links:
            html = await fetch(session, dm_link)
            html_soup = bs4.BeautifulSoup(html, 'html.parser')
            thread_links = parse_thread_links(html_soup)
            print('[{}]{}'.format(len(thread_links), dm_link))
            for t_link in thread_links:
                thread_html = await fetch(session, t_link)
                t_html_soup = bs4.BeautifulSoup(thread_html, 'html.parser')
                if is_article_match(t_html_soup, user):
                    print('[v]{}'.format(t_link))
                    # to get main article, uncomment below code
                    # res.append(get_main_article(t_html_soup))
                    # code below is used to get thread link
                    res.append(t_link)
                else:
                    print('[x]{}'.format(t_link))

        return res


def parse_date_module_links(page):
    a_tags = page.select('ul li a')
    hrefs = a_tags = [x.get('href') for x in a_tags]
    return [urljoin(BASE_URL, x) for x in hrefs]


def parse_thread_links(page):
    a_tags = page.select('table table  tr  td > a')
    hrefs = a_tags = [x.get('href') for x in a_tags]
    # filter href with 'file=article'
    valid_hrefs = [x for x in hrefs if 'file=article' in x]
    return [urljoin(BASE_URL, x) for x in valid_hrefs]


def is_article_match(page, user):
    main_article = get_main_article(page)
    return main_article.text.startswith(user)


def get_main_article(page):
    td_tags = page.select('table table td.row1')
    td_tag = td_tags[4]
    return td_tag


@click.command()
@click.argument('user')
@click.option('--output-filename', default='out.json', help='Output filename.')
def main(user, output_filename):
    loop = asyncio.get_event_loop()
    res = loop.run_until_complete(get_result(user))
    # if you want to return main article, convert html soup into text
    # text_res = [x.text for x in res]
    # else just put res on text_res
    text_res = res
    with open(output_filename, 'w') as f:
        json.dump(text_res, f)


if __name__ == '__main__':
    main()

requirement.txt:

aiohttp>=2.3.7
beautifulsoup4>=4.6.0
click>=6.7

Aquí está la versión python3 del script (probado en python3.5 en Ubuntu 17.10 ).

Cómo utilizar:

  • Para usarlo ponga ambos códigos en los archivos. Como ejemplo, el archivo de código es script.pyy el archivo de paquete es requirement.txt.
  • Ejecutar pip install -r requirement.txt.
  • Ejecute el script como ejemplo python3 script.py pa4080

Utiliza varias bibliotecas:

Cosas que debe saber para desarrollar el programa más allá (aparte del documento del paquete requerido):

  • biblioteca de python: asyncio, json y urllib.parse
  • selectores css ( documentos web mdn ), también algunos html. vea también cómo usar el selector css en su navegador, como este artículo

Cómo funciona:

  • Primero creo un simple descargador html. Es una versión modificada de la muestra dada en aiohttp doc.
  • Después de eso, crea un analizador de línea de comando simple que acepta nombre de usuario y nombre de archivo de salida.
  • Cree un analizador para enlaces de hilo y artículo principal. El uso de pdb y la simple manipulación de URL deberían hacer el trabajo.
  • Combina la función y coloca el artículo principal en json, para que otro programa pueda procesarlo más tarde.

Alguna idea para que pueda desarrollarse más

  • Cree otro subcomando que acepte el enlace del módulo de fecha: se puede hacer separando el método para analizar el módulo de fecha a su propia función y combinarlo con un nuevo subcomando.
  • Almacenamiento en caché del enlace del módulo de fecha: cree un archivo json de caché después de obtener el enlace de hilos. para que el programa no tenga que analizar el enlace nuevamente. o incluso simplemente guardar en caché todo el artículo principal del hilo, incluso si no coincide

Esta no es la respuesta más elegante, pero creo que es mejor que usar bash answer.

  • Utiliza Python, lo que significa que se puede usar multiplataforma.
  • Instalación simple, todo el paquete requerido se puede instalar usando pip
  • Se puede desarrollar más, más legible el programa, más fácil se puede desarrollar.
  • Hace el mismo trabajo que el script bash solo durante 13 minutos .
dan
fuente
Ok, he logrado instalar algunos módulos sudo apt install python3-bs4 python3-click python3-aiohttp python3-async, pero no puedo encontrar, ¿de qué paquete async_timeoutproviene?
pa4080
@ pa4080 lo instalo con pip, por lo que debería incluirse con aiohttp. partes de las primeras 2 funciones se modifican desde aquí aiohttp.readthedocs.io/en/stable . También agregaré instrucciones para instalar el paquete requerido
dan
Instalé con éxito el módulo usando pip. Pero aparece otro error: paste.ubuntu.com/26311694 . Por favor, hágame un ping cuando haga eso :)
pa4080
@ pa4080, no puedo replicar su error, así que simplifico la función de recuperación. el efecto secundario es que el programa puede arrojar un error si el segundo reintento no funciona
dan
1
La desventaja principal es que he logrado ejecutar con éxito el script solo en Ubuntu 17.10. Sin embargo, es 5 veces más rápido que mi script bash, así que decidí aceptar esta respuesta.
pa4080
10

Para resolver esta tarea, he creado el siguiente script bash simple que utiliza principalmente la herramienta CLI wget.

#!/bin/bash

TARGET_URL='http://e-bane.net/modules.php?name=Stories_Archive'
KEY_WORDS=('pa4080' 's0ther')
MAP_FILE='url.map'
OUT_FILE='url.list'

get_url_map() {
    # Use 'wget' as spider and output the result into a file (and stdout) 
    wget --spider --force-html -r -l2 "${TARGET_URL}" 2>&1 | grep '^--' | awk '{ print $3 }' | tee -a "$MAP_FILE"
}

filter_url_map() {
    # Apply some filters to the $MAP_FILE and keep only the URLs, that contain 'article&sid'
    uniq "$MAP_FILE" | grep -v '\.\(css\|js\|png\|gif\|jpg\|txt\)$' | grep 'article&sid' | sort -u > "${MAP_FILE}.uniq"
    mv "${MAP_FILE}.uniq" "$MAP_FILE"
    printf '\n# -----\nThe number of the pages to be scanned: %s\n' "$(cat "$MAP_FILE" | wc -l)"
}

get_key_urls() {
    counter=1
    # Do this for each line in the $MAP_FILE
    while IFS= read -r URL; do
        # For each $KEY_WORD in $KEY_WORDS
        for KEY_WORD in "${KEY_WORDS[@]}"; do
            # Check if the $KEY_WORD exists within the content of the page, if it is true echo the particular $URL into the $OUT_FILE
            if [[ ! -z "$(wget -qO- "${URL}" | grep -io "${KEY_WORD}" | head -n1)" ]]; then
                echo "${URL}" | tee -a "$OUT_FILE"
                printf '%s\t%s\n' "${KEY_WORD}" "YES"
            fi
        done
        printf 'Progress: %s\r' "$counter"; ((counter++))
    done < "$MAP_FILE"
}

# Call the functions
get_url_map
filter_url_map
get_key_urls

El script tiene tres funciones:

  • La primera función get_url_map()usos wgetcomo --spider(lo que significa que se acaba de comprobar que las páginas están allí) y creará recursiva -rURL $MAP_FILEdel $TARGET_URLcon el nivel de profundidad -l2. (Otro ejemplo se puede encontrar aquí: Convertir el sitio web a PDF ). En el caso actual, $MAP_FILEcontiene alrededor de 20 000 URL.

  • La segunda función filter_url_map()simplificará el contenido de $MAP_FILE. En este caso, solo necesitamos las líneas (URL) que contienen la cadena article&sidy son aproximadamente 3000. Aquí se pueden encontrar más ideas: ¿Cómo eliminar palabras particulares de las líneas de un archivo de texto?

  • La tercera función get_key_urls()usará wget -qO-(como el comando curl- ejemplos ) para generar el contenido de cada URL desde el $MAP_FILEe intentará encontrar cualquiera de $KEY_WORDSellos. Si alguno de los $KEY_WORDSfundamentos se encuentra dentro del contenido de una URL en particular, esa URL se guardará en $OUT_FILE.

Durante el proceso de trabajo, el resultado del script se ve como se muestra en la siguiente imagen. Tarda unos 63 minutos en finalizar si hay dos palabras clave y 42 minutos cuando solo se busca una palabra clave.

ingrese la descripción de la imagen aquí

pa4080
fuente
1

He recreado mi guión basado en la respuesta proporcionada por @karel . Ahora el script usa en lynxlugar de wget. Como resultado, se vuelve significativamente más rápido.

La versión actual hace el mismo trabajo durante 15 minutos cuando hay dos palabras clave buscadas y solo 8 minutos si buscamos solo una palabra clave. Eso es más rápido que la solución Python proporcionada por @dan .

Además lynxproporciona un mejor manejo de caracteres no latinos.

#!/bin/bash

TARGET_URL='http://e-bane.net/modules.php?name=Stories_Archive'
KEY_WORDS=('pa4080')  # KEY_WORDS=('word' 'some short sentence')
MAP_FILE='url.map'
OUT_FILE='url.list'

get_url_map() {
    # Use 'lynx' as spider and output the result into a file 
    lynx -dump "${TARGET_URL}" | awk '/http/{print $2}' | uniq -u > "$MAP_FILE"
    while IFS= read -r target_url; do lynx -dump "${target_url}" | awk '/http/{print $2}' | uniq -u >> "${MAP_FILE}.full"; done < "$MAP_FILE"
    mv "${MAP_FILE}.full" "$MAP_FILE"
}

filter_url_map() {
    # Apply some filters to the $MAP_FILE and keep only the URLs, that contain 'article&sid'
    uniq "$MAP_FILE" | grep -v '\.\(css\|js\|png\|gif\|jpg\|txt\)$' | grep 'article&sid' | sort -u > "${MAP_FILE}.uniq"
    mv "${MAP_FILE}.uniq" "$MAP_FILE"
    printf '\n# -----\nThe number of the pages to be scanned: %s\n' "$(cat "$MAP_FILE" | wc -l)"
}

get_key_urls() {
    counter=1
    # Do this for each line in the $MAP_FILE
    while IFS= read -r URL; do
        # For each $KEY_WORD in $KEY_WORDS
        for KEY_WORD in "${KEY_WORDS[@]}"; do
            # Check if the $KEY_WORD exists within the content of the page, if it is true echo the particular $URL into the $OUT_FILE
            if [[ ! -z "$(lynx -dump -nolist "${URL}" | grep -io "${KEY_WORD}" | head -n1)" ]]; then
                echo "${URL}" | tee -a "$OUT_FILE"
                printf '%s\t%s\n' "${KEY_WORD}" "YES"
            fi
        done
        printf 'Progress: %s\r' "$counter"; ((counter++))
    done < "$MAP_FILE"
}

# Call the functions
get_url_map
filter_url_map
get_key_urls
pa4080
fuente