Comment créer une CLI Web Spider qui utilise des mots clés et filtre le contenu?

10

Je souhaite retrouver mes articles dans le forum de littérature obsolète (obsolète) e-bane.net . Certains modules du forum sont désactivés et je ne peux pas obtenir la liste des articles de leur auteur. De plus, le site n'est pas indexé par les moteurs de recherche comme Google, Yndex, etc.

La seule façon de retrouver tous mes articles est d'ouvrir la page archive du site (fig.1). Ensuite, je dois sélectionner une certaine année et un certain mois - par exemple janvier 2013 (fig.1). Et puis je dois inspecter chaque article (fig.2) si au début est écrit mon surnom - pa4080 (fig.3). Mais il y a quelques milliers d'articles.

entrez la description de l'image ici

entrez la description de l'image ici

entrez la description de l'image ici

J'ai lu quelques sujets comme suit, mais aucune des solutions ne correspond à mes besoins:

Je posterai ma propre solution . Mais pour moi, c'est intéressant: existe-t-il un moyen plus élégant de résoudre cette tâche?

pa4080
la source

Réponses:

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

Voici la version python3 du script (testé sur python3.5 sur Ubuntu 17.10 ).

Comment utiliser:

  • Pour l'utiliser, mettez les deux codes dans des fichiers. Par exemple, le fichier de code est script.pyet le fichier de package est requirement.txt.
  • Courez pip install -r requirement.txt.
  • Exécutez le script comme exemple python3 script.py pa4080

Il utilise plusieurs bibliothèques:

Choses à savoir pour développer davantage le programme (autre que le doc du package requis):

  • bibliothèque python: asyncio, json et urllib.parse
  • sélecteurs css ( mdn web docs ), également certains html. voir aussi comment utiliser le sélecteur css sur votre navigateur tel que cet article

Comment ça fonctionne:

  • Je crée d'abord un simple téléchargeur html. Il s'agit d'une version modifiée de l'exemple donné sur aiohttp doc.
  • Après cela, créer un analyseur de ligne de commande simple qui accepte le nom d'utilisateur et le nom de fichier de sortie.
  • Créez un analyseur pour les liens de discussion et l'article principal. L'utilisation de pdb et d'une simple manipulation d'url devrait faire l'affaire.
  • Combinez la fonction et mettez l'article principal sur json, afin qu'un autre programme puisse le traiter plus tard.

Une idée pour qu'elle puisse être développée davantage

  • Créez une autre sous-commande qui accepte le lien du module de date: cela peut être fait en séparant la méthode pour analyser le module de date à sa propre fonction et le combiner avec la nouvelle sous-commande.
  • Mise en cache du lien du module de date: créer un fichier json de cache après avoir obtenu le lien des threads. afin que le programme n'ait pas à analyser à nouveau le lien. ou même simplement mettre en cache l'intégralité de l'article principal du fil même s'il ne correspond pas

Ce n'est pas la réponse la plus élégante, mais je pense que c'est mieux que d'utiliser la réponse bash.

  • Il utilise Python, ce qui signifie qu'il peut être utilisé sur plusieurs plates-formes.
  • Installation simple, tous les packages requis peuvent être installés à l'aide de pip
  • Il peut être développé davantage, plus lisible par le programme, plus facile à développer.
  • Il ne fait le même travail que le script bash que pendant 13 minutes .
dan
la source
Ok, j'ai réussi à installer certains modules:, sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncmais je ne trouve pas - de quel paquet async_timeoutvient-il?
pa4080
@ pa4080 j'installe avec pip donc il devrait être inclus avec aiohttp. certaines parties de la première fonction 2 sont modifiées à partir d'ici aiohttp.readthedocs.io/en/stable . je vais également ajouter des instructions pour installer le package requis
dan
J'ai réussi à installer le module à l'aide de pip. Mais une autre erreur apparaît: paste.ubuntu.com/26311694 . Veuillez me cingler quand vous faites cela :)
pa4080
@ pa4080, je ne peux pas reproduire votre erreur, donc je simplifie la fonction de récupération. l'effet secondaire est que le programme peut générer une erreur si la deuxième tentative ne fonctionne pas
dan
1
Le principal inconvénient est que j'ai réussi à exécuter le script avec succès uniquement sur Ubuntu 17.10. Cependant, il est 5 fois plus rapide que mon script bash, j'ai donc décidé d'accepter cette réponse.
pa4080
10

Pour résoudre cette tâche, j'ai créé le prochain script bash simple qui utilise principalement l'outil 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

Le script a trois fonctions:

  • La première fonction get_url_map()utilise wgetas --spider(ce qui signifie qu'elle vérifiera simplement que les pages sont là) et créera l' -rURL récursive $MAP_FILEdu $TARGET_URLniveau de profondeur -l2. (Un autre exemple peut être trouvé ici: Convertir un site Web en PDF ). Dans le cas actuel, le $MAP_FILEcontient environ 20 000 URL.

  • La deuxième fonction filter_url_map()simplifiera le contenu du $MAP_FILE. Dans ce cas, nous n'avons besoin que des lignes (URL) qui contiennent la chaîne article&sidet elles sont d'environ 3000. Plus d'idées peuvent être trouvées ici: Comment supprimer des mots particuliers des lignes d'un fichier texte?

  • La troisième fonction get_key_urls()utilisera wget -qO-(comme la commande curl- exemples ) pour sortir le contenu de chaque URL à partir de la $MAP_FILEet essaiera de trouver n'importe laquelle de celle- $KEY_WORDSci. Si l'un des $KEY_WORDSest fondé dans le contenu d'une URL particulière, cette URL sera enregistrée dans le $OUT_FILE.

Pendant le processus de travail, la sortie du script ressemble à ce qu'elle est montrée sur l'image suivante. Il faut environ 63 minutes pour terminer s'il y a deux mots clés et 42 minutes lorsqu'un seul mot clé est recherché.

entrez la description de l'image ici

pa4080
la source
1

J'ai recréé mon script sur la base de cette réponse fournie par @karel . Maintenant, le script utilise à la lynxplace de wget. En conséquence, il devient beaucoup plus rapide.

La version actuelle fait le même travail pendant 15 minutes lorsqu'il y a deux mots clés recherchés et seulement 8 minutes si nous recherchons un seul mot clé. C'est plus rapide que la solution Python fournie par @dan .

En outre, lynxoffre une meilleure gestion des caractères non latins.

#!/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
la source