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

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

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

在此处输入图像描述

在此处输入图像描述

在此处输入图像描述

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

  • Ubuntu的网络蜘蛛
  • 如何在Linux系统上编写Web蜘蛛
  • 从站点获取URL列表

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

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运行脚本

它使用了几个库:

  • 单击参数解析器
  • beautifulsoup for html parser
  • aiohttp for html downloader

进一步开发程序的事情(除了所需包的文档):

  • python库:asyncio,json和urllib.parse
  • css选择器( mdn web docs ),也有些html。 另请参阅如何在浏览器上使用css选择器,例如本文

这个怎么运作:

  • 首先,我创建一个简单的html下载器。 它是aiohttp doc上给出的样本的修改版本。
  • 之后创建简单的命令行解析器,接受用户名和输出文件名。
  • 为线程链接和主要文章创建解析器。 使用pdb和简单的url操作应该可以完成这项工作。
  • 结合函数并将主要文章放在json上,以便其他程序可以在以后处理它。

有些想法可以进一步发展

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

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

  • 它使用Python,这意味着它可以跨平台使用。
  • 安装简单,可以使用pip安装所有必需的软件包
  • 它可以进一步开发,程序更易读,更容易开发。
  • 它与bash脚本的工作相同,仅持续13分钟

为了解决这个问题,我创建了下一个主要使用CLI工具wget 简单 bash脚本。

 #!/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 

该脚本有三个function:

  • 第一个函数get_url_map()使用wget作为--spider (这意味着它只会检查页面是否存在)并将创建$MAP_FILE的递归-r URL $MAP_FILE ,深度级别为-l2 。 (另一个例子可以在这里找到: 将网站转换为PDF )。 在当前情况下, $MAP_FILE包含大约20 000个URL。

  • 第二个函数filter_url_map()将简化$MAP_FILE的内容。 在这种情况下,我们只需要包含字符串article&sid的行(URL),它们大约是3000.更多想法可以在这里找到: 如何从文本文件的行中删除特定的单词?

  • 第三个函数get_key_urls()将使用wget -qO- (作为命令curl - examples )从$MAP_FILE输出每个URL的内容,并尝试查找其中的任何$KEY_WORDS 。 如果在任何特定URL的内容中建立了任何$KEY_WORDS ,则该URL将保存在$OUT_FILE

在工作过程中,脚本的输出看起来如下图所示。 如果有两个关键字则需要大约63分钟才能完成,而只搜索一个关键字需要42分钟

在此处输入图像描述

我根据@karel提供的答案重新创建了我的脚本 。 现在该脚本使用lynx而不是wget 。 结果它变得明显更快。

当有两个搜索关键字时,当前版本执行相同的工作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 'wget' as spider and output the result into a file (and stdout) 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