爬蟲平臺Crawlab核心原理--自動提取欄位演算法

MarvinZhang發表於2019-06-04

⚠注意: 可配置爬蟲現在僅在Python版本(v0.2.1-v0.2.4)可用,在最新版本Golang版本(v0.3.0)還暫時不可用,後續會加上,請關注近期更新

背景

實際的大型爬蟲開發專案中,爬蟲工程師會被要求抓取監控幾十上百個網站。一般來說這些網站的結構大同小異,不同的主要是被抓取項的提取規則。傳統方式是讓爬蟲工程師寫一個通用框架,然後將各網站的提取規則做成可配置的,然後將配置工作交給更初級的工程師或外包出去。這樣做將爬蟲開發流水線化,提高了部分生產效率。但是,配置的工作還是一個苦力活兒,還是非常消耗人力。因此,自動提取欄位應運而生。

自動提取欄位是Crawlab在版本v0.2.2中在可配置爬蟲基礎上開發的新功能。它讓使用者不用做任何繁瑣的提取規則配置,就可以自動提取出可能的要抓取的列表項,做到真正的“一鍵抓取”,順利的話,開發一個網站的爬蟲可以半分鐘內完成。市面上有利用機器學習的方法來實現自動抓取要提取的抓取規則,有一些可以做到精準提取,但遺憾的是平臺要收取高額的費用,個人開發者或小型公司一般承擔不起。

Crawlab的自動提取欄位是根據人為抓取的模式來模擬的,因此不用經過任何訓練就可以使用。而且,Crawlab的自動提取欄位功能不會向使用者收取費用,因為Crawlab本身就是免費的。

演算法介紹

演算法的核心來自於人的行為本身,通過查詢網頁中看起來像列表的元素來定位列表及抓取項。一般我們查詢列表項是怎樣的一個過程呢?有人說:這還不容易嗎,一看就知道那個是各列表呀!兄弟,拜託... 我們們是在程式的角度談這個的,它只理解HTML、CSS、JS這些程式碼,並不像你那樣智慧。

我們識別一個列表,首先要看它是不是有很多類似的子項;其次,這些列表通常來說看起來比較“複雜”,含有很多看得見的元素;最後,我們還要關注分頁,分頁按鈕一般叫做“下一頁”、“下頁”、“Next”、“Next Page”等等。

用程式可以理解的語言,我們把以上規則總結如下:

列表項

  1. 從根節點自上而下遍歷標籤;
  2. 對於每一個標籤,如果包含多個同樣的子標籤,判斷為列表標籤候選;
  3. 取子標籤(遞迴)個數最多的列表標籤候選為列表標籤;

列表子項

  1. 對以上規則提取的列表標籤,對每個子標籤(遞迴)進行遍歷
  2. 將有href的a標籤為加入目標欄位;
  3. 將有text的標籤為加入目標欄位。

分頁

  1. 對於每一個標籤,如果標籤文字為特定文字(“下一頁”、“下頁”、“next page”、“next”),選取該標籤為目標標籤。

這樣,我們就設計好了自動提取列表項、列表子項、分頁的規則。剩下的就是寫程式碼了。我知道這樣的設計過於簡單,也過於理想,沒有考慮到一些特殊情況。後面我們將通過在一些知名網站上測試看看我們的演算法表現如何。

演算法實現

演算法實現很簡單。為了更好的操作HTML標籤,我們選擇了lxml庫作為HTML的操作庫。lxml是python的一個解析庫,支援HTML和XML的解析,支援XPath、CSS解析方式,而且解析效率非常高。

自上而下的遍歷語法是sel.iter()seletree.Element,而iter會從根節點自上而下遍歷各個元素,直到遍歷完所有元素。它是一個generator

構造解析樹

在獲取到頁面的HTML之後,我們需要呼叫lxml中的etree.HTML方法構造解析樹。程式碼很簡單如下,其中rrequests.getResponse

# get html parse tree
sel = etree.HTML(r.content)
複製程式碼

這段帶程式碼在SpiderApi._get_html方法裡。原始碼請見這裡

輔助函式

在開始構建演算法之前,我們需要實現一些輔助函式。所有函式是封裝在SpiderApi類中的,所以寫法與類方法一樣。

@staticmethod
def _get_children(sel):
    # 獲取所有不包含comments的子節點
    return [tag for tag in sel.getchildren() if type(tag) != etree._Comment]
複製程式碼
@staticmethod
def _get_text_child_tags(sel):
    # 遞迴獲取所有文字子節點(根節點)
    tags = []
    for tag in sel.iter():
        if type(tag) != etree._Comment and tag.text is not None and tag.text.strip() != '':
            tags.append(tag)
    return tags
複製程式碼
@staticmethod
def _get_a_child_tags(sel):
    # 遞迴獲取所有超連結子節點(根節點)
    tags = []
    for tag in sel.iter():
        if tag.tag == 'a':
            if tag.get('href') is not None and not tag.get('href').startswith('#') and not tag.get(
                    'href').startswith('javascript'):
                tags.append(tag)
    return tags
複製程式碼

獲取列表項

下面是核心中的核心!同學們請集中注意力。

我們來編寫獲取列表項的程式碼。以下是獲得列表標籤候選列表list_tag_list的程式碼。看起來稍稍有些複雜,但其實邏輯很簡單:對於每一個節點,我們獲得所有子節點(一級),過濾出高於閾值(預設10)的節點,然後過濾出節點的子標籤類別唯一的節點。這樣候選列表就得到了。

list_tag_list = []
threshold = spider.get('item_threshold') or 10
# iterate all child nodes in a top-down direction
for tag in sel.iter():
    # get child tags
    child_tags = self._get_children(tag)

    if len(child_tags) < threshold:
        # if number of child tags is below threshold, skip
        continue
    else:
        # have one or more child tags
        child_tags_set = set(map(lambda x: x.tag, child_tags))

        # if there are more than 1 tag names, skip
        if len(child_tags_set) > 1:
            continue

        # add as list tag
        list_tag_list.append(tag)
複製程式碼

接下來我們將從候選列表中篩選出包含最多文字子節點的節點。聽起來有些拗口,打個比方:一個電商網站的列表子項,也就是產品項,一定是有許多例如價格、產品名、賣家等資訊的,因此會包含很多文字節點。我們就是通過這種方式過濾掉文字資訊不多的列表(例如選單列表、類別列表等等),得到最終的列表。在程式碼裡我們存為max_tag

# find the list tag with the most child text tags
max_tag = None
max_num = 0
for tag in list_tag_list:
    _child_text_tags = self._get_text_child_tags(self._get_children(tag)[0])
    if len(_child_text_tags) > max_num:
        max_tag = tag
        max_num = len(_child_text_tags)
複製程式碼

下面,我們將生成列表項的CSS選擇器。以下程式碼實現的邏輯主要就是根據上面得到的目標標籤根據其idclass屬性來生成CSS選擇器。

# get list item selector
item_selector = None
if max_tag.get('id') is not None:
    item_selector = f'#{max_tag.get("id")} > {self._get_children(max_tag)[0].tag}'
elif max_tag.get('class') is not None:
    cls_str = '.'.join([x for x in max_tag.get("class").split(' ') if x != ''])
    if len(sel.cssselect(f'.{cls_str}')) == 1:
        item_selector = f'.{cls_str} > {self._get_children(max_tag)[0].tag}'
複製程式碼

找到目標列表項之後,我們需要做的就是將它下面的文字標籤和超連結標籤提取出來。程式碼如下,就不細講了。感興趣的讀者可以看原始碼來理解。

# get list fields
fields = []
if item_selector is not None:
    first_tag = self._get_children(max_tag)[0]
    for i, tag in enumerate(self._get_text_child_tags(first_tag)):
        if len(first_tag.cssselect(f'{tag.tag}')) == 1:
            fields.append({
                'name': f'field{i + 1}',
                'type': 'css',
                'extract_type': 'text',
                'query': f'{tag.tag}',
            })
        elif tag.get('class') is not None:
            cls_str = '.'.join([x for x in tag.get("class").split(' ') if x != ''])
            if len(tag.cssselect(f'{tag.tag}.{cls_str}')) == 1:
                fields.append({
                    'name': f'field{i + 1}',
                    'type': 'css',
                    'extract_type': 'text',
                    'query': f'{tag.tag}.{cls_str}',
                })

    for i, tag in enumerate(self._get_a_child_tags(self._get_children(max_tag)[0])):
        # if the tag is <a...></a>, extract its href
        if tag.get('class') is not None:
            cls_str = '.'.join([x for x in tag.get("class").split(' ') if x != ''])
            fields.append({
                'name': f'field{i + 1}_url',
                'type': 'css',
                'extract_type': 'attribute',
                'attribute': 'href',
                'query': f'{tag.tag}.{cls_str}',
            })
複製程式碼

分頁的程式碼很簡單,實現也很容易,就不多說了,大家感興趣的可以看原始碼

這樣我們就實現了提取列表項以及列表子項的演算法。

使用方法

要使用自動提取欄位,首先得安裝Crawlab。如何安裝請檢視Github

Crawlab安裝完畢執行起來後,得建立一個可配置爬蟲,詳細步驟請參考[爬蟲手記] 我是如何在3分鐘內開發完一個爬蟲的

建立完畢後,我們來到建立好的可配置爬蟲的爬蟲詳情的配置標籤,輸入開始URL,點選提取欄位按鈕,Crawlab將從開始URL中提取列表欄位。

爬蟲平臺Crawlab核心原理--自動提取欄位演算法

接下來,點選預覽看看這些欄位是否為有效欄位,可以適當增刪改。可以的話點選執行,爬蟲就開始爬資料了。

好了,你需要做的就是這幾步,其餘的交給Crawlab來做就可以了。

爬蟲平臺Crawlab核心原理--自動提取欄位演算法

測試結果

本文在對排名前10的電商網站上進行了測試,僅有3個網站不能識別(分別是因為“動態內容”、“列表沒有id/class”、“lxml定位元素問題”),成功率為70%。讀者們可以嘗試用Crawlab自動提取欄位功能對你們自己感興趣的網站進行測試,看看是否符合預期。結果的詳細列表如下。

網站 成功提取 原因
淘寶 N 動態內容
京東 Y
阿里巴巴1688 Y
搜了網 Y
蘇寧易購 Y
糯米網 Y
買購網 N 列表沒有id/class
天貓 Y
噹噹網 N lxml定位元素問題

Crawlab的演算法當然還需要改進,例如考慮動態內容和列表沒有id/class等定位點的時候。也歡迎各位前來試用,甚至貢獻該專案。

Github: tikazyq/crawlab

如果您覺得Crawlab對您的日常開發或公司有幫助,請加作者微信拉入開發交流群,大家一起交流關於Crawlab的使用和開發。

爬蟲平臺Crawlab核心原理--自動提取欄位演算法

相關文章