如何创建使用关键字和过滤内容的 CLI Web Spider?

pa4*_*080 10 command-line scripts wget curl

我想在已弃用(过时)的文学论坛e-bane.net 中找到我的文章。一些论坛模块被禁用,我无法获得他们作者的文章列表。此外,该网站没有被搜索引擎索引,如 Google、Yndex 等。

找到我所有文章的唯一方法是打开站点的存档页面(图 1)。然后我必须选择特定的年份和月份 - 例如20131 月(图 1)。然后我必须检查每篇文章(图2)开头是否写着我的昵称-pa4080(图3)。但是有几千篇文章。

在此处输入图片说明

在此处输入图片说明

在此处输入图片说明

我已经阅读了以下几个主题,但没有一个解决方案适合我的需求:

我会发布我自己的解决方案。但对我来说很有趣: 有没有更优雅的方法来解决这个任务?

pa4*_*080 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
Run Code Online (Sandbox Code Playgroud)

该脚本具有三个功能:

  • 第一个功能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并尝试在其中查找任何URL $KEY_WORDS。如果$KEY_WORDS在任何特定 URL 的内容中建立了任何 ,则该 URL 将保存在$OUT_FILE.

在工作过程中,脚本的输出看起来就像下一张图片中显示的那样。如果有两个关键字,大约需要 63 分钟,如果只搜索一个关键字,则需要42 分钟

在此处输入图片说明


小智 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()
Run Code Online (Sandbox Code Playgroud)

requirement.txt:

aiohttp>=2.3.7
beautifulsoup4>=4.6.0
click>=6.7
Run Code Online (Sandbox Code Playgroud)

这是脚本的 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 文档上给出的示例的修改版本。
  • 之后创建简单的命令行解析器,它接受用户名和输出文件名。
  • 为线程链接和主要文章创建解析器。使用 pdb 和简单的 url 操作应该可以完成这项工作。
  • 合并该函数并将主要文章放在json上,以便其他程序稍后可以处理它。

一些想法,以便进一步发展

  • 创建另一个接受日期模块链接的子命令:可以通过将解析日期模块的方法分离为其自己的函数并将其与新的子命令组合来完成。
  • 缓存日期模块链接:获取线程链接后创建缓存 json 文件。所以程序不必再次解析链接。甚至只是缓存整个线程主文章,即使它不匹配

这不是最优雅的答案,但我认为它比使用 bash 答案更好。

  • 它使用Python,这意味着它可以跨平台使用。
  • 安装简单,所有需要的包都可以使用pip安装
  • 它可以进一步开发,程序更具可读性,更容易开发。
  • 它与bash 脚本完成相同的工作,但只需要13 分钟