Wie erstelle ich einen CLI-Web-Spider, der Schlüsselwörter verwendet und Inhalte filtert?

10

Ich möchte meine Artikel im veralteten (veralteten) Literaturforum e-bane.net finden . Einige der Forummodule sind deaktiviert, und ich kann keine Liste der Artikel des Autors abrufen. Außerdem wird die Website von den Suchmaschinen nicht als Google, Yndex usw. indiziert.

Die einzige Möglichkeit, alle meine Artikel zu finden, besteht darin, die Archivseite der Site zu öffnen (Abb. 1). Dann muss ich ein bestimmtes Jahr und einen bestimmten Monat auswählen - z. B. Januar 2013 (Abb.1). Und dann muss ich jeden Artikel überprüfen ( Abb . 2), ob am Anfang mein Spitzname geschrieben steht - pa4080 ( Abb . 3 ). Aber es gibt einige tausend Artikel.

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Geben Sie hier die Bildbeschreibung ein

Ich habe einige Themen wie folgt gelesen, aber keine der Lösungen passt zu meinen Anforderungen:

Ich werde meine eigene Lösung veröffentlichen . Aber für mich ist interessant: Gibt es einen eleganteren Weg, um diese Aufgabe zu lösen?

pa4080
quelle

Antworten:

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

Hier ist die Python3-Version des Skripts (getestet unter Python3.5 unter Ubuntu 17.10 ).

Wie benutzt man:

  • Um es zu verwenden, legen Sie beide Codes in Dateien. Als Beispiel ist die Codedatei script.pyund die Paketdatei requirement.txt.
  • Ausführen pip install -r requirement.txt.
  • Führen Sie das Skript als Beispiel aus python3 script.py pa4080

Es werden mehrere Bibliotheken verwendet:

Wissenswertes zur Weiterentwicklung des Programms (außer dem Dokument des erforderlichen Pakets):

Wie es funktioniert:

  • Zuerst erstelle ich einen einfachen HTML-Downloader. Es handelt sich um eine modifizierte Version aus dem Beispiel in aiohttp doc.
  • Danach erstellen Sie einen einfachen Befehlszeilen-Parser, der den Benutzernamen und den Ausgabedateinamen akzeptiert.
  • Erstellen Sie einen Parser für Thread-Links und Hauptartikel. Die Verwendung von PDF und einfache URL-Manipulation sollte den Job erledigen.
  • Kombinieren Sie die Funktion und stellen Sie den Hauptartikel auf json, damit andere Programme ihn später verarbeiten können.

Eine Idee, damit es weiterentwickelt werden kann

  • Erstellen Sie einen weiteren Unterbefehl, der eine Verknüpfung mit dem Datumsmodul akzeptiert: Trennen Sie dazu die Methode, um das Datumsmodul in eine eigene Funktion zu analysieren und mit einem neuen Unterbefehl zu kombinieren.
  • Zwischenspeichern des Datumsmodul-Links: Erstellen Sie eine Cache-JSON-Datei, nachdem Sie den Thread-Link erhalten haben. Das Programm muss den Link also nicht erneut analysieren. oder zwischenspeichern Sie einfach den gesamten Thread-Hauptartikel, auch wenn er nicht übereinstimmt

Dies ist nicht die eleganteste Antwort, aber ich denke, es ist besser als die Verwendung einer Bash-Antwort.

  • Es verwendet Python, was bedeutet, dass es plattformübergreifend verwendet werden kann.
  • Einfache Installation, alle erforderlichen Pakete können mit pip installiert werden
  • Es kann weiterentwickelt werden, das Programm ist besser lesbar und es kann einfacher entwickelt werden.
  • Es macht den gleichen Job wie das Bash-Skript nur für 13 Minuten .
Dan
quelle
Ok, ich habe es geschafft, einige Module zu installieren: sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncaber ich kann nicht finden - von welchem ​​Paket async_timeoutkommt?
pa4080
@ pa4080 Ich installiere mit Pip, so sollte es in Aiohttp enthalten sein. Teile der ersten 2 Funktionen werden von hier aus geändert: aiohttp.readthedocs.io/en/stable . Ich werde auch Anweisung fügen Sie das gewünschte Paket zu installieren
dan
Ich habe das Modul erfolgreich mit pip installiert. Es wird jedoch ein anderer Fehler angezeigt: paste.ubuntu.com/26311694 . Bitte
pingen
@ pa4080, ich kann Ihren Fehler nicht replizieren, daher vereinfache ich die Abruffunktion. der Nebeneffekt ist , dass Programmfehler werfen können , wenn die zweite Wiederholung nicht funktioniert
dan
1
Die Hauptnachteile sind, dass ich es geschafft habe, das Skript nur unter Ubuntu 17.10 erfolgreich auszuführen. Es ist jedoch fünfmal schneller als mein Bash-Skript, daher habe ich beschlossen, diese Antwort zu akzeptieren.
pa4080
10

Um diese Aufgabe zu lösen, habe ich das nächste einfache Bash-Skript erstellt, das hauptsächlich das CLI-Tool verwendet 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

Das Skript hat drei Funktionen:

  • Die erste Funktion get_url_map()verwendet wgetas --spider(was bedeutet, dass nur überprüft wird, ob Seiten vorhanden sind) und erstellt eine rekursive -rURL $MAP_FILEder $TARGET_URLmit Tiefenebene -l2. (Ein weiteres Beispiel finden Sie hier: Website in PDF konvertieren ). Im aktuellen Fall $MAP_FILEenthält die ca. 20 000 URLs.

  • Die zweite Funktion filter_url_map()vereinfacht den Inhalt der $MAP_FILE. In diesem Fall benötigen wir nur die Zeilen (URLs), die die Zeichenfolge enthalten, article&sidund sie sind ungefähr 3000. Weitere Ideen finden Sie hier: Wie entferne ich bestimmte Wörter aus Zeilen einer Textdatei?

  • Die dritte Funktion get_key_urls()verwendet wget -qO-(als Befehl curl- Beispiele ), um den Inhalt jeder URL von der auszugeben, $MAP_FILEund versucht, einen der $KEY_WORDSdarin enthaltenen zu finden. Wenn eine der $KEY_WORDSURLs im Inhalt einer bestimmten URL enthalten ist, wird diese URL in der gespeichert $OUT_FILE.

Während des Arbeitsprozesses sieht die Ausgabe des Skripts so aus, wie sie im nächsten Bild gezeigt wird. Es dauert ungefähr 63 Minuten, bis zwei Schlüsselwörter vorhanden sind, und 42 Minuten, wenn nur ein Schlüsselwort gesucht wird.

Geben Sie hier die Bildbeschreibung ein

pa4080
quelle
1

Ich habe mein Skript basierend auf dieser Antwort von @karel neu erstellt . Jetzt verwendet das Skript lynxanstelle von wget. Im Ergebnis wird es deutlich schneller.

Die aktuelle Version erledigt den gleichen Job 15 Minuten lang, wenn zwei Schlüsselwörter gesucht werden, und nur 8 Minuten, wenn nur ein Schlüsselwort gesucht wird. Das ist schneller als die Python- Lösung von @dan .

lynxBietet außerdem eine bessere Handhabung von nicht-lateinischen Zeichen.

#!/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
quelle