Como crio uma CLI Web Spider que usa palavras-chave e filtra conteúdo?

10

Quero encontrar meus artigos no fórum de literatura obsoleto (obsoleto) e-bane.net . Alguns dos módulos do fórum estão desativados e não consigo obter uma lista de artigos do autor. Além disso, o site não é indexado pelos mecanismos de pesquisa como Google, Yndex etc.

A única maneira de encontrar todos os meus artigos é abrir a página de arquivo do site (fig.1). Depois, devo selecionar um determinado ano e mês - por exemplo, janeiro de 2013 (fig.1). E então devo inspecionar cada artigo (fig.2) se no começo está escrito meu apelido - pa4080 (fig.3). Mas existem alguns milhares de artigos.

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Li alguns tópicos a seguir, mas nenhuma das soluções se encaixa às minhas necessidades:

Vou postar minha própria solução . Mas para mim é interessante: existe alguma maneira mais elegante de resolver essa tarefa?

pa4080
fonte

Respostas:

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

Aqui está a versão python3 do script (testada em python3.5 no Ubuntu 17.10 ).

Como usar:

  • Para usá-lo, coloque os dois códigos nos arquivos. Como exemplo, o arquivo de código é script.pye o pacote é requirement.txt.
  • Corra pip install -r requirement.txt.
  • Execute o script como exemplo python3 script.py pa4080

Ele usa várias bibliotecas:

Informações importantes para desenvolver ainda mais o programa (além do documento do pacote necessário):

  • biblioteca python: asyncio, json e urllib.parse
  • seletores de css ( mdn web docs ), também alguns html. veja também como usar o seletor de css no seu navegador, como este artigo

Como funciona:

  • Primeiro, crio um simples downloader de html. É a versão modificada da amostra fornecida no aiohttp doc.
  • Depois disso, crie um analisador de linha de comando simples que aceite nome de usuário e nome de arquivo de saída.
  • Crie um analisador para links de tópicos e artigo principal. O uso de pdb e manipulação simples de URL deve fazer o trabalho.
  • Combine a função e coloque o artigo principal no json, para que outro programa possa processá-lo posteriormente.

Alguma idéia para que possa ser desenvolvida ainda mais

  • Crie outro subcomando que aceite o link do módulo de data: isso pode ser feito separando o método para analisar o módulo de data para sua própria função e combiná-lo com o novo subcomando.
  • Armazenando em cache o link do módulo de data: crie um arquivo json de cache após obter o link de threads. para que o programa não precise analisar o link novamente. ou até apenas armazenar em cache todo o artigo principal do thread, mesmo que ele não corresponda

Esta não é a resposta mais elegante, mas acho que é melhor do que usar a resposta do bash.

  • Ele usa Python, o que significa que pode ser usado em várias plataformas.
  • Instalação simples, todo o pacote necessário pode ser instalado usando pip
  • Ele pode ser desenvolvido ainda mais, mais legível o programa, mais fácil ele pode ser desenvolvido.
  • Ele faz o mesmo trabalho que o script bash apenas por 13 minutos .
dan
fonte
Ok, eu consegui instalar alguns módulos:, sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncmas não consigo encontrar - de qual pacote async_timeoutvem?
pa4080
@ pa4080 eu instalo com o pip, então ele deve ser incluído no aiohttp. partes da primeira função 2 são modificadas aqui aiohttp.readthedocs.io/en/stable . Também vou adicionar instruções para instalar o pacote necessário
dan
Eu instalei com sucesso o módulo usando pip. Mas algum outro erro aparece: paste.ubuntu.com/26311694 . Por favor de ping-me quando você faz isso :)
pa4080
@ pa4080, não consigo replicar o seu erro, por isso simplifico a função de busca. o efeito colateral é que o programa pode lançar erro se a segunda tentativa não está funcionando
dan
1
Os principais contras é que eu consegui executar o script com sucesso apenas no Ubuntu 17.10. No entanto, é 5 vezes mais rápido que o meu script bash, então decidi aceitar esta resposta.
pa4080
10

Para resolver esta tarefa, criei o próximo script bash simples que usa principalmente a ferramenta 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

O script tem três funções:

  • A primeira função get_url_map()usa wgetas --spider(o que significa que apenas verificará se as páginas estão lá) e criará um -rURL recursivo $MAP_FILEdo $TARGET_URLnível de profundidade -l2. (Outro exemplo pode ser encontrado aqui: Converter site em PDF ). No caso atual, ele $MAP_FILEcontém cerca de 20.000 URLs.

  • A segunda função filter_url_map()simplificará o conteúdo do $MAP_FILE. Nesse caso, precisamos apenas das linhas (URLs) que contêm a string article&side elas são cerca de 3000. Mais ideias podem ser encontradas aqui: Como remover palavras específicas das linhas de um arquivo de texto?

  • A terceira função get_key_urls()usará wget -qO-(como o comando curl- exemplos ) para gerar o conteúdo de cada URL a partir do $MAP_FILEe tentará encontrar qualquer um deles $KEY_WORDS. Se qualquer um $KEY_WORDSdeles for encontrado no conteúdo de qualquer URL específico, esse URL será salvo no $OUT_FILE.

Durante o processo de trabalho, a saída do script é exibida como na próxima imagem. Demora cerca de 63 minutos para concluir se houver duas palavras-chave e 42 minutos quando apenas uma palavra-chave for pesquisada.

insira a descrição da imagem aqui

pa4080
fonte
1

Eu recriado o meu script com base em esta resposta fornecida pelo @karel . Agora, o script usa em lynxvez de wget. Em resultado, torna-se significativamente mais rápido.

A versão atual faz o mesmo trabalho por 15 minutos quando há duas palavras-chave pesquisadas e apenas 8 minutos se estivermos pesquisando apenas uma palavra-chave. Isso é mais rápido que a solução Python fornecida pelo @dan .

Além disso, lynxfornece uma melhor manipulação de caracteres não 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
fonte