Come faccio a creare un Web Spider CLI che utilizza parole chiave e filtra il contenuto?


10

Voglio trovare i miei articoli nel forum di letteratura deprecato (obsoleto) e-bane.net . Alcuni moduli del forum sono disabilitati e non riesco a ottenere un elenco di articoli dal loro autore. Inoltre il sito non è indicizzato dai motori di ricerca come Google, Yndex, ecc.

L'unico modo per trovare tutti i miei articoli è aprire la pagina di archivio del sito (fig.1). Quindi devo selezionare determinati anni e mesi, ad esempio gennaio 2013 (fig.1). E poi devo controllare ogni articolo (fig.2) se all'inizio è scritto il mio soprannome - pa4080 (fig.3). Ma ci sono poche migliaia di articoli.

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

inserisci qui la descrizione dell'immagine

Ho letto alcuni argomenti come segue, ma nessuna delle soluzioni si adatta alle mie esigenze:

Pubblicherò la mia soluzione . Ma per me è interessante: esiste un modo più elegante per risolvere questo compito?

Risposte:


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

Ecco la versione di python3 dello script (testato su python3.5 su Ubuntu 17.10 ).

Come usare:

  • Per usarlo inserisci entrambi i codici nei file. Ad esempio il file di codice è script.pye il file di pacchetto è requirement.txt.
  • Corri pip install -r requirement.txt.
  • Esegui lo script come esempio python3 script.py pa4080

Utilizza diverse librerie:

Cose da sapere per sviluppare ulteriormente il programma (oltre alla documentazione del pacchetto richiesto):

  • libreria python: asyncio, json e urllib.parse
  • selettori css ( documenti web mdn ), anche alcuni HTML. vedi anche come usare il selettore css sul tuo browser come questo articolo

Come funziona:

  • Per prima cosa creo un semplice downloader HTML. Viene modificata la versione dell'esempio fornito nel documento aiohttp.
  • Dopo di che, crea un semplice parser da riga di comando che accetta nome utente e nome file di output.
  • Creare un parser per i collegamenti di thread e l'articolo principale. L'uso di pdb e la semplice manipolazione dell'URL dovrebbero fare il lavoro.
  • Combina la funzione e inserisci l'articolo principale su json, in modo che altri programmi possano elaborarlo in seguito.

Qualche idea in modo che possa essere ulteriormente sviluppata

  • Creare un altro sottocomando che accetta il collegamento del modulo data: è possibile separare il metodo per analizzare il modulo data nella propria funzione e combinarlo con un nuovo sottocomando.
  • Memorizzazione nella cache del collegamento del modulo data: crea il file json della cache dopo aver ottenuto il collegamento dei thread. quindi il programma non deve analizzare nuovamente il collegamento. o anche solo memorizzare nella cache l'intero articolo principale del thread anche se non corrisponde

Questa non è la risposta più elegante, ma penso sia meglio che usare la risposta bash.

  • Utilizza Python, il che significa che può essere utilizzato su più piattaforme.
  • Installazione semplice, tutto il pacchetto richiesto può essere installato usando pip
  • Può essere sviluppato ulteriormente, più leggibile il programma, più facile da sviluppare.
  • Fa lo stesso lavoro dello script bash solo per 13 minuti .

Ok, sono riuscito a installare alcuni moduli:, sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncma non riesco a trovare - da quale pacchetto async_timeoutproviene?
pa4080,

@ pa4080 installo con pip, quindi dovrebbe essere incluso con aiohttp. parti della prima funzione 2 vengono modificate da qui aiohttp.readthedocs.io/en/stable . aggiungerò anche le istruzioni per installare il pacchetto richiesto
dan

Ho installato il modulo con successo usando pip. Ma appare qualche altro errore: paste.ubuntu.com/26311694 . Per favore, chiamami quando lo fai :)
pa4080,

@ pa4080, non riesco a replicare il tuo errore, quindi semplifico la funzione di recupero. l'effetto collaterale è che il programma potrebbe generare errori se il secondo tentativo non funziona
dan

1
I contro principali sono che sono riuscito a eseguire correttamente lo script solo su Ubuntu 17.10. Tuttavia è 5 volte più veloce del mio script bash, quindi ho deciso di accettare questa risposta.
pa4080,

10

Per risolvere questo compito ho creato il prossimo semplice script bash che utilizza principalmente lo strumento 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

Lo script ha tre funzioni:

  • La prima funzione get_url_map()usi wgetcome --spider(il che significa che sarà solo controllare che le pagine esistono) e creerà ricorsivo -rURL $MAP_FILEdel $TARGET_URLcon livello di profondità -l2. (Un altro esempio può essere trovato qui: Converti il ​​sito Web in PDF ). Nel caso attuale $MAP_FILEcontiene circa 20.000 URL.

  • La seconda funzione filter_url_map()semplifica il contenuto di $MAP_FILE. In questo caso abbiamo bisogno solo delle righe (URL) che contengono la stringa article&side sono circa 3000. Altre idee possono essere trovate qui: Come rimuovere determinate parole dalle righe di un file di testo?

  • La terza funzione get_key_urls()utilizzerà wget -qO-(come il comando curl- esempi ) per generare il contenuto di ciascun URL da $MAP_FILEe proverà a trovarne uno al $KEY_WORDSsuo interno. Se uno qualsiasi degli elementi $KEY_WORDSè presente all'interno del contenuto di un determinato URL, tale URL verrà salvato in $OUT_FILE.

Durante il processo di lavoro, l'output dello script appare come mostrato nell'immagine successiva. Ci vogliono circa 63 minuti per finire se ci sono due parole chiave e 42 minuti quando viene cercata una sola parola chiave.

inserisci qui la descrizione dell'immagine


1

Ho ricreato la mia sceneggiatura basandomi su questa risposta fornita da @karel . Ora lo script utilizza lynxinvece di wget. Di conseguenza diventa significativamente più veloce.

La versione corrente fa lo stesso lavoro per 15 minuti quando ci sono due parole chiave cercate e solo 8 minuti se stiamo cercando solo una parola chiave. È più veloce della soluzione Python fornita da @dan .

Inoltre lynxoffre una migliore gestione dei caratteri non latini.

#!/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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.