キーワードを使用してコンテンツをフィルタリングするCLI Webスパイダーを作成するにはどうすればよいですか?


10

廃止された(廃止された)文献フォーラムe-bane.net内で自分の記事を見つけたいです。一部のフォーラムモジュールが無効になっており、著者の記事のリストを取得できません。また、サイトはGoogle、Yndexなどの検索エンジンによってインデックスに登録されていません。

私の記事をすべて見つける唯一の方法は、サイトのアーカイブページを開くことです(図1)。次に、特定の年と月を選択する必要があります-たとえば、2013年1月(図1)。そして、最初に私のニックネーム-pa4080(図3)が書かれているかどうか、各記事(図2)を検査する必要があります。しかし、数千の記事があります。

ここに画像の説明を入力してください

ここに画像の説明を入力してください

ここに画像の説明を入力してください

以下のようにいくつかのトピックを読みましたが、どのソリューションも私のニーズに適合しません。

自分で解決策を投稿します。しかし、私にとって興味深い です。このタスクを解決するためのよりエレガントな方法はありますか?

回答:


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バージョンです(Ubuntu 17.10の python3.5でテスト済み)。

使い方:

  • これを使用するには、両方のコードをファイルに入れます。例として、コードファイルはscript.py、パッケージファイルはrequirement.txtです。
  • を実行しますpip install -r requirement.txt
  • 例としてスクリプトを実行します python3 script.py pa4080

それはいくつかのライブラリを使用します:

プログラムをさらに開発するために知っておくべきこと(必要なパッケージのドキュメント以外):

  • Pythonライブラリ:asyncio、json、urllib.parse
  • cssセレクター(mdn web docs)、いくつかのhtml。この記事のように、ブラウザーでcssセレクターを使用する方法もご覧ください。

使い方:

  • まず、シンプルなhtmlダウンローダーを作成します。これは、aiohttp docで提供されているサンプルの修正バージョンです。
  • その後、ユーザー名と出力ファイル名を受け入れる単純なコマンドラインパーサーを作成します。
  • スレッドリンクとメイン記事のパーサーを作成します。pdbと単純なURL操作を使用することでうまくいくはずです。
  • 他のプログラムが後でそれを処理できるように、関数を組み合わせてjsonにメイン記事を置きます。

さらに発展させるためのアイデア

  • 日付モジュールリンクを受け入れる別のサブコマンドを作成します。これは、日付モジュールを解析するメソッドを独自の関数に分離し、それを新しいサブコマンドと組み合わせることで実行できます。
  • 日付モジュールリンクのキャッシュ:スレッドリンクを取得した後、キャッシュjsonファイルを作成します。したがって、プログラムはリンクを再度解析する必要はありません。またはそれが一致しなくてもスレッドのメイン記事全体をキャッシュする

これは最もエレガントな回答ではありませんが、bash回答を使用するよりも優れていると思います。

  • Pythonを使用しているため、クロスプラットフォームで使用できます。
  • 簡単なインストール、必要なすべてのパッケージはpipを使用してインストールできます
  • それはさらに開発することができ、プログラムをより読みやすく、より簡単に開発できます。
  • 13分間だけ、bashスクリプトと同じジョブを実行します。

はいsudo apt install python3-bs4 python3-click python3-aiohttp python3-async、いくつかのモジュールをインストールできました:が見つかりません-どのパッケージasync_timeoutからのものですか?
pa4080

@ pa4080私はpioでインストールするので、aiohttpに含める必要があります。最初の2つの関数の一部がここから変更されていますaiohttp.readthedocs.io/en/stable。また、必要なパッケージをインストールする手順も追加します
dan

pipを使用してモジュールを正常にインストールしました。しかし、他のいくつかのエラーが表示されます: paste.ubuntu.com/26311694。それをするとき私にpingしてください:)
pa4080

@ pa4080、私はあなたのエラーを複製することができないので、フェッチ機能を簡略化します。副作用は、2回目の再試行が機能しない場合、プログラムがエラーをスローする可能性があることです
dan

1
主な短所は、Ubuntu 17.10でのみスクリプトを正常に実行できたことです。ただし、bashスクリプトより5倍高速であるため、この回答を受け入れることにしました。
pa4080

10

このタスクを解決するために、主にCLIツールを使用する次の単純な bashスクリプトを作成しました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

スクリプトには3つの関数があります。

  • 最初の関数get_url_map()wgetas --spider(つまり、ページが存在することを確認するだけです)を使用し、深さレベルの再帰的な-rURL $MAP_FILEを作成し$TARGET_URLます-l2。(別の例はここにあります:ウェブサイトをPDFに変換)。現在の場合、に$MAP_FILEは約20 000のURLが含まれています。

  • 2番目の関数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

作業プロセス中、スクリプトの出力は、次の画像に示すようになります。キーワードが2つある場合は終了までに約63分、キーワードが1つだけの場合は42分かかります。

ここに画像の説明を入力してください


1

@karelから提供されたこの回答に基づいてスクリプトを再作成しました。これで、スクリプトはの代わりにを使用します。その結果、大幅に速くなります。lynxwget

現在のバージョンでは、2つの検索キーワードがある場合は15分間、1つのキーワードのみを検索する場合は8分間だけ同じ処理を実行します。@danが提供するPythonソリューションよりも高速です

さらに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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.