使用 shell 构建多进程的 CommandlineFu 爬虫

2024年 7月 18日 65.7k 0

使用 shell 构建多进程的 CommandlineFu 爬虫-1

CommandlineFu 是一个记录脚本片段的网站,每个片段都有对应的功能说明和对应的标签。我想要做的就是尝试用 shell 写一个多进程的爬虫把这些代码片段记录在一个 org 文件中。

参数定义

这个脚本需要能够通过 -n 参数指定并发的爬虫数(默认为 CPU 核的数量),还要能通过 -f 指定保存的 org 文件路径(默认输出到 stdout)。

#!/usr/bin/env bash

proc_num=$(nproc)
store_file=/dev/stdout
while getopts :n:f: OPT; do
    case $OPT in
        n|+n)
            proc_num="$OPTARG"
            ;;
        f|+f)
            store_file="$OPTARG"
            ;;
        *)
            echo "usage: ${0##*/} [+-n proc_num] [+-f org_file} [--]"
            exit 2
    esac
done
shift $(( OPTIND - 1 ))
OPTIND=1

解析命令浏览页面

我们需要一个进程从 CommandlineFu 的浏览列表中抽取各个脚本片段的 URL,这个进程将抽取出来的 URL 存放到一个队列中,再由各个爬虫进程从进程中读取 URL 并从中抽取出对应的代码片段、描述说明和标签信息写入 org 文件中。

这里就会遇到三个问题:

  • 进程之间通讯的队列如何实现
  • 如何从页面中抽取出 URL、代码片段、描述说明、标签等信息
  • 多进程对同一文件进行读写时的乱序问题
  • 实现进程之间的通讯队列

    这个问题比较好解决,我们可以通过一个命名管道来实现:

    queue=$(mktemp --dry-run)
    mkfifo ${queue}
    exec 99${queue}
    trap "rm ${queue} 2>/dev/null" EXIT
    

    从页面中抽取想要的信息

    从页面中提取元素内容主要有两种方法:

  • 对于简单的 HTML 页面,我们可以通过 sedgrepawk 等工具通过正则表达式匹配的方式来从 HTML 中抽取信息。
  • 通过 html-xml-utils 工具集中的 hxselect 来根据 CSS 选择器提取相关元素。
  • 这里我们使用 html-xml-utils 工具来提取:

    function extract_views_from_browse_page()
    {
        if [[ $# -eq 0 ]];then
            local html=$(cat -)
        else
            local html="$*"
        fi
        echo ${html} |hxclean |hxselect -c -s "\n" "li.list-group-item > div:nth-child(1) > div:nth-child(1) > a:nth-child(1)::attr(href)"|sed 's@^@https://www.commandlinefu.com/@'
    }
    
    function extract_nextpage_from_browse_page()
    {
        if [[ $# -eq 0 ]];then
            local html=$(cat -)
        else
            local html="$*"
        fi
        echo ${html} |hxclean |hxselect -s "\n" "li.list-group-item:nth-child(26) > a"|grep '>'|hxselect -c "::attr(href)"|sed 's@^@https://www.commandlinefu.com/@'
    }
    

    这里需要注意的是:hxselect 对 HTML 解析时要求遵循严格的 XML 规范,因此在用 hxselect 解析之前需要先经过 hxclean 矫正。另外,为了防止 HTML 过大,超过参数列表长度,这里允许通过管道的形式将 HTML 内容传入。

    循环读取下一页的浏览页面,不断抽取代码片段 URL 写入队列

    这里要解决的是上面提到的第三个问题: 多进程对管道进行读写时如何保障不出现乱序? 为此,我们需要在写入文件时对文件加锁,然后在写完文件后对文件解锁,在 shell 中我们可以使用 flock 来对文件进行枷锁。 关于 flock 的使用方法和注意事项,请参见另一篇博文 Linux shell flock 文件锁的用法及注意事项。

    由于需要在 flock 子进程中使用函数 extract_views_from_browse_page,因此需要先导出该函数:

    export -f extract_views_from_browse_page
    

    由于网络问题,使用 curl 获取内容可能失败,需要重复获取:

    function fetch()
    {
        local url="$1"
        while ! curl -L ${url} 2>/dev/null;do
            :
        done
    }
    

    collector 用来从种子 URL 中抓取待爬的 URL,写入管道文件中,写操作期间管道文件同时作为锁文件:

    function collector()
    {
        url="$*"
        while [[ -n ${url} ]];do
            echo "从$url中抽取"
            html=$(fetch "${url}")
            echo "${html}"|flock ${queue} -c "extract_views_from_browse_page >${queue}"
            url=$(echo "${html}"|extract_nextpage_from_browse_page)
        done
        # 让后面解析代码片段的爬虫进程能够正常退出,而不至于被阻塞.
        for ((i=0;i${queue}
        done
    }
    

    这里要注意的是, 在找不到下一页 URL 后,我们用一个 for 循环往队列里写入了 =proc_num= 个空行,这一步的目的是让后面解析代码片段的爬虫进程能够正常退出,而不至于被阻塞。

    解析脚本片段页面

    我们需要从脚本片段的页面中抽取标题、代码片段、描述说明以及标签信息,同时将这些内容按 org 模式的格式写入存储文件中。

      function view_page_handler()
    {
    local url="$1"
    local html="$(fetch "${url}")"
    # headline
    local headline="$(echo ${html} |hxclean |hxselect -c -s "\n" ".col-md-8 > h1:nth-child(1)")"
    # command
    local command="$(echo ${html} |hxclean |hxselect -c -s "\n" ".col-md-8 > div:nth-child(2) > span:nth-child(2)"|pandoc -f html -t org)"
    # description
    local description="$(echo ${html} |hxclean |hxselect -c -s "\n" ".col-md-8 > div.description"|pandoc -f html -t org)"
    # tags
    local tags="$(echo ${html} |hxclean |hxselect -c -s ":" ".functions > a")"
    if [[ -n "${tags}" ]];then
    tags=":${tags}"
    fi
    # build org content
    cat

    相关文章

    Linux 命令行的聊天工具 CenterIM
    Linux 桌面年仍未到来 但 Linux 移动之年已到来
    12 个在线学习 Linux 技能网站
    Linux Mint : 会是另一个新的 Ubuntu 吗?
    W3Conf 开发者大会将于下周召开
    Ubuntu 10.04 ARM 处理器上网本版本结束服务期

    发布评论