Linux中國

使用 shell 構建多進程的 CommandlineFu 爬蟲

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 文件中。

這裡就會遇到三個問題:

  1. 進程之間通訊的隊列如何實現
  2. 如何從頁面中抽取出 URL、代碼片段、描述說明、標籤等信息
  3. 多進程對同一文件進行讀寫時的亂序問題

實現進程之間的通訊隊列

這個問題比較好解決,我們可以通過一個命名管道來實現:

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

從頁面中抽取想要的信息

從頁面中提取元素內容主要有兩種方法:

  1. 對於簡單的 HTML 頁面,我們可以通過 sedgrepawk 等工具通過正則表達式匹配的方式來從 HTML 中抽取信息。
  2. 通過 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 &apos;s@^@https://www.commandlinefu.com/@&apos;
}

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 &apos;>&apos;|hxselect -c "::attr(href)"|sed &apos;s@^@https://www.commandlinefu.com/@&apos;
}

這裡需要注意的是: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<${proc_num};i++))
    do
        echo >${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 <<EOF |flock -x ${store_file} tee -a ${store_file}
* ${headline}      ${tags}

:PROPERTIES:
:URL:       ${url}
:END:

${description}
#+begin_src shell
${command}
#+end_src

EOF
  }

這裡抽取信息的方法跟上面的類似,不過代碼片段和描述說明中可能有一些 HTML 代碼,因此通過 pandoc 將之轉換為 org 格式的內容。

注意最後輸出 org 模式的格式並寫入存儲文件中的代碼不要寫成下面這樣:

    flock -x ${store_file} cat <<EOF >${store_file}
    * ${headline}tt ${tags}
    ${description}
    #+begin_src shell
    ${command}
    #+end_src
EOF

它的意思是使用 flockcat 命令進行加鎖,再把 flock 整個命令的結果通過重定向輸出到存儲文件中,而重定向輸出的這個過程是沒有加鎖的。

spider 從管道文件中讀取待抓取的 URL,然後實施真正的抓取動作。

function spider()
{
    while :
    do
        if ! url=$(flock ${queue} -c &apos;read -t 1 -u 99 url && echo $url&apos;)
        then
            sleep 1
            continue
        fi

        if [[ -z "$url" ]];then
            break
        fi
        view_page_handler ${url}
    done
}

這裡要注意的是,為了防止發生死鎖,從管道中讀取 URL 時設置了超時,當出現超時就意味著生產進程趕不上消費進程的消費速度,因此消費進程休眠一秒後再次檢查隊列中的 URL。

組合起來

collector "https://www.commandlinefu.com/commands/browse" &

for ((i=0;i<${proc_num};i++))
do
    spider &
done
wait

抓取其他網站

通過重新定義 extract_views_from_browse_pageextract_nextpage_from-browse_pageview_page_handler 這幾個函數, 以及提供一個新的種子 URL,我們可以很容易將其改造成抓取其他網站的多進程爬蟲。

例如通過下面這段代碼,就可以用來爬取 xkcd 上的漫畫:

function extract_views_from_browse_page()
{
    if [[ $# -eq 0 ]];then
        local html=$(cat -)
    else
        local html="$*"
    fi
    max=$(echo "${html}"|hxclean |hxselect -c -s "n" "#middleContainer"|grep "Permanent link to this comic" |awk -F "/" &apos;{print $4}&apos;)
    seq 1 ${max}|sed &apos;s@^@https://xkcd.com/@&apos;
}

function extract_nextpage_from_browse_page()
{
    echo ""
}

function view_page_handler()
{
    local url="$1"
    local html="$(fetch "${url}/")"
    local image="https:$(echo ${html} |hxclean |hxselect -c -s "n" "#comic > img:nth-child(1)::attr(src)")"
    echo ${image}
    wget ${image}
}

collector "https://xkcd.com/" &

本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國