Как создать CLI Web Spider, который использует ключевые слова и фильтрует контент?

10

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

Единственный способ найти все мои статьи - открыть страницу архива сайта (рис.1). Затем я должен выбрать определенный год и месяц - например, январь 2013 года (рис.1). И тогда я должен проверить каждую статью (рис.2), написано ли в начале мой ник - pa4080 (рис.3). Но есть несколько тысяч статей.

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

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

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

Я прочитал несколько тем следующим образом, но ни одно из решений не соответствует моим потребностям:

Я опубликую свое собственное решение . Но для меня интересно: есть ли более изящный способ решить эту задачу?

pa4080
источник

Ответы:

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

Он использует несколько библиотек:

Что нужно знать для дальнейшего развития программы (кроме документации необходимого пакета):

  • библиотека python: asyncio, json и urllib.parse
  • Селекторы CSS ( MDN Web Docs ), а также некоторые HTML. см. также, как использовать css selector в вашем браузере, например, в этой статье

Как это устроено:

  • Сначала я создаю простой HTML-загрузчик. Это модифицированная версия из образца, приведенного на aiohttp doc.
  • После этого создаем простой парсер командной строки, который принимает имя пользователя и имя выходного файла.
  • Создайте парсер для ссылок на темы и основной статьи. Использование pdb и простое манипулирование URL должны сделать эту работу.
  • Объедините функцию и поместите основную статью в json, чтобы другая программа могла обработать ее позже.

Некоторая идея, чтобы она могла развиваться дальше

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

Это не самый элегантный ответ, но я думаю, что это лучше, чем использовать bash.

  • Он использует 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
@ pa4080, я не могу воспроизвести вашу ошибку, поэтому я упрощаю функцию извлечения. побочный эффект в том , что программа может бросить ошибку , если вторая повторная попытка не работает
дан
1
Основным минусом является то, что мне удалось успешно запустить скрипт только в Ubuntu 17.10. Однако это в 5 раз быстрее, чем мой bash-скрипт, поэтому я решил принять этот ответ.
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 минуты, когда выполняется поиск только по одному ключевому слову.

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

pa4080
источник
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
pa4080
источник