Як створити веб-павук CLI, який використовує ключові слова та фільтрує вміст?


10

Я хочу знайти свої статті на застарілому (застарілому) форумі літератури e-bane.net . Деякі модулі форуму вимкнено, і я не можу отримати список статей їх автора. Також сайт не індексується пошуковими системами як Google, Yndex тощо.

Єдиний спосіб знайти всі мої статті - це відкрити сторінку архіву сайту (рис.1). Тоді я повинен вибрати певний рік та місяць - наприклад, січень 2013 року (рис.1). І тоді я повинен оглянути кожну статтю (мал.2), чи на початку написано моє прізвисько - pa4080 (мал.3). Але є кілька тисяч статей.

введіть тут опис зображення

введіть тут опис зображення

введіть тут опис зображення

Я читав декілька тем, як слід, але жодне з рішень не відповідає моїм потребам:

Я опублікую власне рішення . Але для мене цікаво: чи є якийсь більш елегантний спосіб вирішити це завдання?

Відповіді:


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

Ось версія сценарію python3 (протестована на python3.5 на Ubuntu 17.10 ).

Як користуватись:

  • Для його використання покладіть обидва коди у файли. Наприклад, файл коду script.pyта файл пакета requirement.txt.
  • Біжи pip install -r requirement.txt.
  • Запустіть сценарій як приклад python3 script.py pa4080

Тут використовується кілька бібліотек:

Що потрібно знати для подальшої розробки програми (крім документа необхідного пакету):

  • бібліотека пітона: asyncio, json та urllib.parse
  • css-селектори ( mdn web docs ), також деякі html. дивіться також, як використовувати селектор css у своєму браузері, наприклад, у цій статті

Як це працює:

  • Спочатку я створю простий завантажувач html. Це модифікована версія із зразка, поданого на doc. Aiohttp.
  • Після цього створюється простий синтаксичний аналізатор командного рядка, який приймає ім'я користувача та вихідне ім'я файлу.
  • Створіть аналізатор посилань на потоки та основну статтю. Використання pdb та простих маніпуляцій з URL-адресою повинно зробити цю роботу.
  • Поєднайте функцію та поставте головну статтю на json, щоб інша програма змогла її обробити пізніше.

Якась ідея, щоб вона могла розвиватися далі

  • Створіть ще одну підкоманду, яка приймає посилання модуля дати: це можна зробити, розділивши метод розбору модуля дати на власну функцію та поєднати його з новою підкомандою.
  • Кешування посилання модуля дати: створити файл кеша json після отримання посилання. тому програмі не доведеться знову розбирати посилання. або навіть просто кешувати всю основну статтю теми, навіть якщо вона не відповідає

Це не найелегантніша відповідь, але я вважаю, що це краще, ніж використовувати баш-відповідь.

  • Він використовує Python, що означає, що він може використовуватися крос-платформою.
  • Проста установка, весь необхідний пакет можна встановити за допомогою pip
  • Її можна розвивати далі, програму читати, легше її розробляти.
  • Він виконує ту саму роботу, що і сценарій bash лише протягом 13 хвилин .

Гаразд, мені вдалося встановити деякі модулі:, sudo apt install python3-bs4 python3-click python3-aiohttp python3-asyncале я не можу знайти - з якого пакета async_timeoutпоходить?
pa4080

@ pa4080 я встановлюю за допомогою pip, тому він повинен включати aiohttp. частини першої 2 функції модифікуються звідси aiohttp.readthedocs.io/en/stable . Також я додам інструкцію встановити необхідний пакет
дан

Я успішно встановив модуль за допомогою pip. Але з’являється якась інша помилка: paste.ubuntu.com/26311694 . Будь ласка,
підкажіть

@ pa4080, я не можу повторити вашу помилку, тому я спрощую функцію отримання. побічний ефект полягає в тому, що програма може видалити помилку, якщо друга спроба не працює
дан

1
Основні мінуси в тому, що мені вдалося успішно запустити сценарій тільки на Ubuntu 17.10. Однак це в 5 разів швидше, ніж мій баш сценарій, тому я вирішив прийняти цю відповідь.
pa4080

10

Для вирішення цього завдання я створив наступний простий скрипт bash, який в основному використовує інструмент 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

Сценарій має три функції:

  • Перша функція get_url_map()використовує wgetяк --spider(що означає, що вона просто перевірить, чи існують сторінки) та створить рекурсивну -rURL $MAP_FILE-адресу $TARGET_URLз рівнем глибини -l2. (Інший приклад можна знайти тут: Перетворити веб-сайт у PDF ). У поточному випадку він $MAP_FILEмістить близько 20 000 URL-адрес.

  • Друга функція filter_url_map()спростить вміст $MAP_FILE. У цьому випадку нам потрібні лише рядки (URL-адреси), які містять рядок, article&sidа їх близько 3000. Більше ідей можна знайти тут: Як видалити конкретні слова з рядків текстового файлу?

  • Третя функція get_key_urls()використовуватиме wget -qO-(як команда curl- приклади ) для виведення вмісту кожної URL-адреси з $MAP_FILEі намагатиметься знайти будь-яку з $KEY_WORDSних. Якщо будь-який із $KEY_WORDSфайлів заснований у вмісті будь-якої конкретної URL-адреси, ця URL-адреса буде збережена у $OUT_FILE.

Під час робочого процесу вихід сценарію виглядає так, як показано на наступному зображенні. Вона займає близько 63 хвилин , щоб закінчити , якщо є два ключових слів і 42 хвилини , коли шукається тільки одне ключове слово.

введіть тут опис зображення


1

Я відтворив свій сценарій на основі цієї відповіді, наданої @karel . Тепер сценарій використовує lynxзамість wget. В результаті вона стає значно швидшою.

Поточна версія виконує ту саму роботу протягом 15 хвилин, коли є два ключові слова, які шукаються, і лише 8 хвилин, якщо ми шукаємо лише одне ключове слово. Це швидше, ніж рішення Python, яке надає @dan .

Крім того, lynxзабезпечує краще поводження з не латинськими символами.

#!/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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.