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


10

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

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

在此处输入图片说明

在此处输入图片说明

在此处输入图片说明

我读过以下主题,但没有一种解决方案适合我的需求:

我将发布自己的解决方案。但是对我来说有趣的还有没有更优雅的方式来解决此任务?

Answers:


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安装所有必需的软件包
  • 它可以进一步开发,使程序更具可读性,也可以更容易地开发。
  • 它做相同的工作的bash脚本只为13分钟

好的,我已经成功安装了一些模块:sudo apt install python3-bs4 python3-click python3-aiohttp python3-async,但我找不到-哪个软件包async_timeout来自?
pa4080 '18

@ pa4080我使用pip安装,因此它应该包含在aiohttp中。前2个功能的部分可从此处修改aiohttp.readthedocs.io/en/stable。我还将添加说明以安装所需的软件包

我使用pip成功安装了模块。但是出现了一些其他错误: paste.ubuntu.com/26311694。执行此操作时,请ping我:)
pa4080 '18年

@ pa4080,我无法复制您的错误,因此我简化了提取功能。副作用是,如果第二次重试不起作用,程序可能会引发错误

1
主要缺点是,我设法仅在Ubuntu 17.10上成功运行了脚本。但是它比我的bash脚本快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其中包含约20000个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,则该URL将保存在中$OUT_FILE

在工作过程中,脚本的输出如下图所示。如果有两个关键字,则大约需要63分钟,而仅搜索一个关键字时,则需要42分钟

在此处输入图片说明


1

我根据@karel提供的答案重新创建了脚本。现在,脚本使用代替。结果,它变得明显更快。lynxwget

如果有两个搜索到的关键字,当前版本将执行15分钟的相同工作,而如果仅搜索一个关键字,则只有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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.