Jak mogę utworzyć Spider Web CLI, który używa słów kluczowych i filtruje zawartość?

10

Chcę znaleźć moje artykuły na przestarzałym (przestarzałym) forum literatury e-bane.net . Niektóre moduły forum są wyłączone i nie mogę uzyskać listy artykułów ich autora. Witryna nie jest również indeksowana przez wyszukiwarki jako Google, Yndex itp.

Jedynym sposobem na znalezienie wszystkich moich artykułów jest otwarcie strony archiwum witryny (ryc. 1). Następnie muszę wybrać określony rok i miesiąc - np. Styczeń 2013 (ryc. 1). A potem muszę sprawdzić każdy artykuł (ryc. 2), czy na początku jest napisany mój pseudonim - pa4080 ( ryc. 3). Ale jest kilka tysięcy artykułów.

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

Przeczytałem kilka poniższych tematów, ale żadne z rozwiązań nie odpowiada moim potrzebom:

Zamieszczę własne rozwiązanie . Ale dla mnie jest interesujące: czy istnieje bardziej elegancki sposób na rozwiązanie tego zadania?

pa4080
źródło

Odpowiedzi:

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

Oto wersja skryptu python3 (przetestowana na python3.5 na Ubuntu 17.10 ).

Jak używać:

  • Aby go użyć, umieść oba kody w plikach. Przykładowo plik kodu to script.pyplik pakietu requirement.txt.
  • Uruchom pip install -r requirement.txt.
  • Uruchom skrypt jako przykład python3 script.py pa4080

Wykorzystuje kilka bibliotek:

Co należy wiedzieć, aby dalej rozwijać program (inny niż dokument wymaganego pakietu):

  • biblioteka python: asyncio, json i urllib.parse
  • selektory css ( mdn web docs ), także niektóre pliki HTML. Zobacz także, jak korzystać z selektora css w przeglądarce, na przykład w tym artykule

Jak to działa:

  • Najpierw tworzę prosty downloader HTML. Jest to zmodyfikowana wersja z próbki podanej na aiohttp doc.
  • Po utworzeniu prostego parsera wiersza poleceń, który akceptuje nazwę użytkownika i wyjściową nazwę pliku.
  • Utwórz parser dla linków wątków i głównego artykułu. Używanie pdb i prostych manipulacji adresami URL powinno wystarczyć.
  • Połącz funkcję i umieść główny artykuł w json, aby inny program mógł go przetworzyć później.

Trochę pomysłu, dzięki czemu można go dalej rozwijać

  • Utwórz kolejną komendę, która akceptuje łącze do modułu daty: można to zrobić, oddzielając metodę parsowania modułu daty do jego własnej funkcji i łącząc go z nową komendą.
  • Buforowanie łącza modułu daty: utwórz plik json pamięci podręcznej po uzyskaniu łącza wątków. więc program nie musi ponownie analizować łącza. lub nawet po prostu buforuj cały główny artykuł wątku, nawet jeśli nie pasuje

To nie jest najbardziej elegancka odpowiedź, ale myślę, że jest lepsza niż użycie bash.

  • Korzysta z Pythona, co oznacza, że ​​można go używać na różnych platformach.
  • Prosta instalacja, wszystkie wymagane pakiety można zainstalować za pomocą pip
  • Można go dalej rozwijać, bardziej czytelny program, łatwiej można go rozwijać.
  • Działa tak samo jak skrypt bash tylko przez 13 minut .
dan
źródło
Ok, udało mi się zainstalować kilka modułów: sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncale nie mogę znaleźć - z którego pakietu async_timeoutpochodzi?
pa4080
@ pa4080 instaluję z pipem, więc powinien być dołączony do aiohttp. części pierwszej 2 funkcji zostały zmodyfikowane stąd aiohttp.readthedocs.io/en/stable . dodam także instrukcję instalacji wymaganego pakietu
dan
Z powodzeniem zainstalowałem moduł za pomocą pip. Ale pojawia się inny błąd: paste.ubuntu.com/26311694 . Proszę,
pingujcie,
@ pa4080, nie mogę zreplikować twojego błędu, więc uprościłem funkcję pobierania. efektem ubocznym jest to, że program może zgłosić błąd, jeśli druga próba nie działa
dn
1
Główną wadą jest to, że udało mi się pomyślnie uruchomić skrypt tylko na Ubuntu 17.10. Jest jednak 5 razy szybszy niż mój skrypt bash, więc zdecydowałem się zaakceptować tę odpowiedź.
pa4080
10

Aby rozwiązać to zadanie, stworzyłem kolejny prosty skrypt bash, który używa głównie narzędzia 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

Skrypt ma trzy funkcje:

  • Pierwsza funkcja get_url_map()zastosowań wgetjak --spider(co oznacza, że będzie to po prostu sprawdzić, czy strony są tam) i stworzy rekurencyjną -rURL $MAP_FILEz poniższych $TARGET_URLz poziomu głębokości -l2. (Inny przykład można znaleźć tutaj: Konwertuj stronę internetową na PDF ). W obecnym przypadku $MAP_FILEzawiera około 20 000 adresów URL.

  • Druga funkcja filter_url_map()uprości zawartość $MAP_FILE. W tym przypadku potrzebujemy tylko wierszy (adresów URL), które zawierają ciąg, article&sida są one około 3000. Więcej pomysłów można znaleźć tutaj: Jak usunąć określone słowa z wierszy pliku tekstowego?

  • Trzecia funkcja get_key_urls()użyje wget -qO-(jako polecenia curl- przykładów ) do wyprowadzenia zawartości każdego adresu URL z $MAP_FILEi spróbuje znaleźć dowolny z $KEY_WORDSnich. Jeśli którykolwiek z elementów $KEY_WORDSjest oparty na treści określonego adresu URL, ten adres URL zostanie zapisany w pliku $OUT_FILE.

Podczas procesu roboczego dane wyjściowe skryptu wyglądają tak, jak pokazano na następnym obrazie. Zakończenie jest około 63 minut, jeśli są dwa słowa kluczowe, i 42 minuty, gdy wyszukiwane jest tylko jedno słowo kluczowe.

wprowadź opis zdjęcia tutaj

pa4080
źródło
1

Odtworzyłem swój skrypt na podstawie odpowiedzi udzielonej przez @karel . Teraz skrypt używa lynxzamiast wget. W rezultacie staje się znacznie szybszy.

Obecna wersja wykonuje tę samą pracę przez 15 minut, gdy są dwa wyszukiwane słowa kluczowe i tylko 8 minut, jeśli szukamy tylko jednego słowa kluczowego. Jest to szybsze niż rozwiązanie Python dostarczone przez @dan .

Ponadto lynxzapewnia lepszą obsługę znaków spoza alfabetu łacińskiego.

#!/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
źródło