深刻淺出爬蟲之道: Python、Golang與GraphQuery的對比

深刻淺出爬蟲之道: Python、Golang與GraphQuery的對比

本文將分別使用 PythonGolang 以及 GraphQuery 來解析某網站的 素材詳情頁面 ,這個頁面的特點是具備清晰的數據結構,可是DOM結構不夠規範,沒法經過單獨的選擇器定位頁面元素,對頁面的解析形成了一些曲折。經過這個頁面的解析過程,深刻淺出的瞭解爬蟲的解析思想與這些語言之間的異同。css

1、前言

在前言中,爲了防止在後面的章節產生沒必要要的困擾,咱們將會首先了解一些基本的編程理念。html

1. 語義化的DOM結構

這裏咱們講的語義化的DOM結構,不只僅包括 語義化的html標籤,也包括了語義化的選擇器,在前端開發中應該注意的是,全部的動態文本都應該有單獨的 html 標籤包裹,並最好賦予其語義化的 class 屬性或 id 屬性,這在版本功能的迭代中,對前端和後端的開發都是大有裨益的,好比下面的HTML代碼:前端

<div class="main-right fr">
    <p>編號:32490230</p>
    <p class="main-rightStage">模式:RGB</p>
    <p class="main-rightStage">體積:16.659 MB</p>
    <p class="main-rightStage">分辨率:72dpi</p>
</div>

這就是不夠語義化的前端代碼,32504070RGB16.659 MB72dpi這些值都是動態屬性, 會跟隨編號的改變而改變,在規範的開發中,應該將這些 動態變化的屬性,分別用 <span> 這類行內標籤包裹起來,並賦予其必定的語義化選擇器,在上面的HTML結構中大體能夠推測出這是後端直接使用 foreach 渲染出的頁面,這是不符合先後端分離的思想的,若是有一天他們決定使用 jsonpAjax 渲染這些屬性, 由前端進行渲染,工做量無疑會上一個層次。語義化的DOM結構更傾向於下面這樣:node

<p class="main-rightStage property-mode">
    模式:<span>RGB</span>
</p>

也能夠將 property-mode 直接做爲 spanclass 屬性,這樣這些屬性不管是後端渲染,仍是前端動態渲染都減輕了產品迭代產生的負擔。python

2. 穩定的解析代碼

語義化的DOM結構 以後,咱們來談談穩定的解析代碼, 對於下面的DOM結構:linux

<div class="main-right fr">
    <p>編號:32490230</p>
    <p class="main-rightStage">模式:RGB</p>
    <p class="main-rightStage">體積:16.659 MB</p>
    <p class="main-rightStage">分辨率:72dpi</p>
</div>

若是咱們想要提取 模式 信息,固然能夠採起下面的步驟:git

  1. 選取 class 屬性中包含 main-rightdiv
  2. 選取這個 div 中第二個 p 元素,取出其包含的文本
  3. 刪除文本中的 模式:, 獲得模式爲 RGB

雖然成功獲取到了想要的結果,可是這樣的解析方法,咱們認爲它是 不穩定的,這個不穩定是指 在其祖先元素、兄弟元素等自身之外的元素節點發生必定程度的結構改變時,致使解析錯誤或失敗 的狀況, 好比若是有一天在 模式 所在的節點以前增長了一個 尺寸 的屬性:github

<div class="main-right fr">
    <p>編號:32490230</p>
    <p class="main-rightStage">尺寸:4724×6299像素</p>
    <p class="main-rightStage">模式:RGB</p>
    <p class="main-rightStage">體積:16.659 MB</p>
    <p class="main-rightStage">分辨率:72dpi</p>
</div>

那麼咱們以前的解析將會發生錯誤(什麼?你以爲不可能發生這樣的變更?請對比 Page1Page2)。
那咱們應該如何寫出更穩定的解析代碼呢,對於上面的DOM結構,咱們能夠有下面幾種思路:
思路一: 遍歷 class 屬性爲 main-rightStagep 節點,依次判斷節點的文本是否以 模式 開頭, 若是是, 取出其 後的內容,缺點是邏輯太多,不易維護且下降了代碼可讀性。
思路二: 使用正則表達式 模式:([A-Z]+) 進行匹配,缺點是使用不當可能形成效率問題。
思路三: 使用 CSS選擇器中的 contains 方法,好比 .main-rightStage:contains(模式), 就能夠選取文本中包含 模式,且 class 屬性中包含 main-rightStage 的節點了。但缺點是不一樣語言和不一樣庫對這種語法的支持程度各有不一樣,缺少兼容性。
使用哪一種方法,仁者見仁智者見智,不一樣的解析思路帶來的解析的 穩定性、代碼的 複雜程度運行效率兼容性 都是不一樣的, 開發者須要從各類因素中進行權衡, 來寫出最優秀的解析代碼。golang

2、進行頁面的解析

在進行頁面數據的抽取以前,首先要作的是明確咱們須要哪些數據、頁面上提供了哪些數據,而後設計出咱們須要的數據結構。首先打開 待解析頁面, 因爲其最上方的 瀏覽量收藏量下載量等數據是動態加載的, 在咱們的演示中暫時不須要,而這個頁面右邊的 尺寸模式 等數據,經過上面 Page1Page2 的對比,能夠得知這些屬性是不必定存在的,所以將它們一塊兒歸到 metainfo 中。所以咱們須要得到的數據以下圖所示:正則表達式

由此咱們能夠很快設計出咱們的數據結構:

{
    title
    pictype
    number
    type
    metadata {
        size
        volume
        mode
        resolution
    }
    author
    images []
    tags []
}

其中 sizevolumemoderesolution因爲可能不存在,所以納入到了 metadata 下, images 是一個圖片地址的數組,tags 是標籤數組,在肯定了要提取的數據結構,就能夠開始進行解析。

使用Python進行頁面的解析

Python庫的數量很是龐大,有不少優秀的庫能夠幫助到咱們,在使用Python進行頁面的解析時,咱們一般用到下面這些庫:

  1. 提供 正則表達式 支持的 re
  2. 提供 CSS選擇器 支持的 pyquerybeautifulsoup4
  3. 提供 Xpath 支持的 lxml
  4. 提供 JSON PATH 支持的 jsonpath_rw

這些庫在 Python 3 下得到支持的,能夠經過 pip install 進行安裝。
因爲 CSS選擇器 的語法比 Xpath 語法要更加簡潔,而在方法的調用上,pyquerybeautifulsoup4 要更加方便,所以在 2 和 3 之間咱們選擇了 pyquery
下面咱們會以 titletype 屬性的獲取做爲例子進行講解, 其餘節點的獲取是同理的。首先咱們先使用 requests 庫下載這個頁面的源文件:

import requests
from pyquery import PyQuery as pq
response = requests.get("http://www.58pic.com/newpic/32504070.html")
document = pq(response.content.decode('gb2312'))

下面使用Python進行的解析都將依次爲前提進行。

1. 獲取title節點

打開 待解析頁面,在標題上右鍵, 點擊 查看元素,能夠看到它的DOM結構以下:

這時咱們注意到, 咱們想要提取出的標題文本 大俠海報金庸武俠水墨中國風黑白,並無被html標籤包裹,這是不符合咱們上面提到的 語義化的dom結構 的。同時,使用CSS選擇器,也是沒法直接選取到這個文本節點的(可使用Xpath直接選取到,本文略)。對於這樣的節點,咱們能夠有下面兩種思路:
思路一: 先選取其父元素節點, 獲取其 HTML 內容,使用正則表達式, 匹配在 </div><p 之間的文本。
思路二: 先選取其父元素節點,而後刪除文本節點以外的其餘節點,再直接經過獲取父元素節點的文本,獲得想要的標題文本。
咱們採起思路二,寫出下面的Python代碼:

title_node = document.find(".detail-title")
title_node.find("div").remove()
title_node.find("p").remove()
print(title_node.text())

輸出結果與咱們指望的相同, 爲 大俠海報金庸武俠水墨中國風黑白

2. 獲取size節點

尺寸 上右鍵查看元素,能夠看到下圖所示的DOM結構:

咱們發現這些節點不具備語義化的選擇器,而且這些屬性不必定都存在(詳見Page1Page2 的對比)。在 穩定的解析代碼 中咱們也講到了對於這種結構的文檔能夠採起的幾種思路,這裏咱們採用正則解析的方法:

import re
context = document.find(".mainRight-file").text()
file_type_matches = re.compile("尺寸:(.*?像素)").findall(context)
filetype = ""
if len(file_type_matches) > 0:
    filetype =  file_type_matches[0]
print(filetype)

因爲獲取 sizevolumemoderesolution 這些屬性,均可以採起相似的方法,所以咱們能夠歸結出一個正則提取的函數:

def regex_get(text, expr):
    matches = re.compile(expr).findall(text)
    if len(matches) == 0:
        return ""
    return matches[0]

所以,在獲取 size 節點時,咱們的代碼就能夠精簡爲:

size = regex_get(context, r"尺寸:(.*?像素)")

3. 完整的Python代碼

到這裏,咱們解析頁面可能遇到的問題就已經解決了大半,整個Python代碼以下:

import requests
import re
from pyquery import PyQuery as pq

def regex_get(text, expr):
    matches = re.compile(expr).findall(text)
    if len(matches) == 0:
        return ""
    return matches[0]

conseq = {}

## 下載文檔
response = requests.get("http://www.58pic.com/newpic/32504070.html")
document = pq(response.text)

## 獲取文件標題
title_node = document.find(".detail-title")
title_node.find("div").remove()
title_node.find("p").remove()
conseq["title"] = title_node.text()

## 獲取素材類型
conseq["pictype"] = document.find(".pic-type").text()

## 獲取文件格式
conseq["filetype"] =  regex_get(document.find(".mainRight-file").text(), r"文件格式:([a-z]+)")

## 獲取元數據
context = document.find(".main-right p").text()
conseq['metainfo'] = {
    "size": regex_get(context, r"尺寸:(.*?像素)"),
    "volume": regex_get(context, r"體積:(.*? MB)"),
    "mode": regex_get(context, r"模式:([A-Z]+)"),
    "resolution": regex_get(context, r"分辨率:(\d+dpi)"),
}

## 獲取做者
conseq['author'] = document.find('.user-name').text()

## 獲取圖片
conseq['images'] = []
for node_image in document.find("#show-area-height img"):
    conseq['images'].append(pq(node_image).attr("src"))

## 獲取tag
conseq['tags'] = []
for node_image in document.find(".mainRight-tagBox .fl"):
    conseq['tags'].append(pq(node_image).text())

print(conseq)

使用Golang進行頁面的解析

Golang 中解析 htmlxml 文檔, 經常使用到的庫有如下幾種:

  1. 提供 正則表達式 支持的 regexp
  2. 提供 CSS選擇器 支持的 github.com/PuerkitoBio/goquery
  3. 提供 Xpath 支持的 gopkg.in/xmlpath.v2
  4. 提供 JSON PATH 支持的 github.com/tidwall/gjson

這些庫,你均可以經過 go get -u 來獲取,因爲在上面的Python解析中咱們已經整理出瞭解析邏輯,在Golang中只須要復現便可,與 Python 不一樣的是,咱們最好先爲咱們的數據結構定義一個 struct,像下面這樣:

type Reuslt struct {
    Title    string
    Pictype  string
    Number   string
    Type     string
    Metadata struct {
        Size       string
        Volume     string
        Mode       string
        Resolution string
    }
    Author string
    Images []string
    Tags   []string
}

同時,因爲咱們的 待解析頁面 是非主流的 gbk 編碼,因此在下載下來文檔以後,須要手動將 utf-8 的編碼轉換爲 gbk 的編碼,這個過程雖然不在解析的範疇以內,可是也是必需要作的步驟之一, 咱們使用了 github.com/axgle/mahonia 這個庫進行編碼的轉換,並整理出了編碼轉換的函數 decoderConvert

func decoderConvert(name string, body string) string {
    return mahonia.NewDecoder(name).ConvertString(body)
}

所以, 最終的 golang 代碼應該是下面這樣的:

package main

import (
    "encoding/json"
    "log"
    "regexp"
    "strings"

    "github.com/axgle/mahonia"
    "github.com/parnurzeal/gorequest"

    "github.com/PuerkitoBio/goquery"
)

type Reuslt struct {
    Title    string
    Pictype  string
    Number   string
    Type     string
    Metadata struct {
        Size       string
        Volume     string
        Mode       string
        Resolution string
    }
    Author string
    Images []string
    Tags   []string
}

func RegexGet(text string, expr string) string {
    regex, _ := regexp.Compile(expr)
    return regex.FindString(text)
}

func decoderConvert(name string, body string) string {
    return mahonia.NewDecoder(name).ConvertString(body)
}

func main() {
    //下載文檔
    request := gorequest.New()
    _, body, _ := request.Get("http://www.58pic.com/newpic/32504070.html").End()
    document, err := goquery.NewDocumentFromReader(strings.NewReader(decoderConvert("gbk", body)))
    if err != nil {
        panic(err)
    }
    conseq := &Reuslt{}
    //獲取文件標題
    titleNode := document.Find(".detail-title")
    titleNode.Find("div").Remove()
    titleNode.Find("p").Remove()
    conseq.Title = titleNode.Text()

    // 獲取素材類型
    conseq.Pictype = document.Find(".pic-type").Text()
    // 獲取文件格式
    conseq.Type = document.Find(".mainRight-file").Text()
    // 獲取元數據
    context := document.Find(".main-right p").Text()
    conseq.Metadata.Mode = RegexGet(context, `尺寸:(.*?)像素`)
    conseq.Metadata.Resolution = RegexGet(context, `體積:(.*? MB)`)
    conseq.Metadata.Size = RegexGet(context, `模式:([A-Z]+)`)
    conseq.Metadata.Volume = RegexGet(context, `分辨率:(\d+dpi)`)
    // 獲取做者
    conseq.Author = document.Find(".user-name").Text()
    // 獲取圖片
    document.Find("#show-area-height img").Each(func(i int, element *goquery.Selection) {
        if attribute, exists := element.Attr("src"); exists && attribute != "" {
            conseq.Images = append(conseq.Images, attribute)
        }
    })
    // 獲取tag
    document.Find(".mainRight-tagBox .fl").Each(func(i int, element *goquery.Selection) {
        conseq.Tags = append(conseq.Tags, element.Text())
    })
    bytes, _ := json.Marshal(conseq)
    log.Println(string(bytes))
}

解析邏輯徹底相同,代碼量和複雜程度相較 python版 差很少,下面咱們來看一下新出現的 GraphQuery 是如何作的。

使用GraphQuery進行解析

已知咱們想要獲得的數據結構以下:

{
    title
    pictype
    number
    type
    metadata {
        size
        volume
        mode
        resolution
    }
    author
    images []
    tags []
}

GraphQuery 的代碼是下面這樣的:

{
    title `xpath("/html/body/div[4]/div[1]/div/div/div[1]/text()")`
    pictype `css(".pic-type")`
    number `css(".detailBtn-down");attr("data-id")`
    type `regex("文件格式:([a-z]+)")`
    metadata `css(".main-right p")` {
        size `regex("尺寸:(.*?)像素")`
        volume `regex("體積:(.*? MB)")`
        mode `regex("模式:([A-Z]+)")`
        resolution `regex("分辨率:(\d+dpi)")`  
    }
    author `css(".user-name")`
    images `css("#show-area-height img")` [
        src `attr("src")`
    ]
    tags `css(".mainRight-tagBox .fl")` [
        tag `text()`
    ]
}

經過對比能夠看出, 它只是在咱們設計的數據結構之中添加了一些由反引號包裹起來的函數。驚豔的是,它能徹底還原咱們上面在 PythonGolang 中的解析邏輯,並且從它的語法結構上,更能清晰的讀出返回的數據結構。這段 GraphQuery 的執行結果以下:

{
    "data": {
        "author": "Ice bear",
        "images": [
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a0", 
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a1024", 
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a2048", 
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a3072"
        ],
        "metadata": {
            "mode": "RGB",
            "resolution": "200dpi",
            "size": "4724×6299",
            "volume": "196.886 MB"
        },
        "number": "32504070",
        "pictype": "原創",
        "tags": ["大俠", "海報", "黑白", "金庸", "水墨", "武俠", "中國風"],
        "title": "大俠海報金庸武俠水墨中國風黑白",
        "type": "psd"
    },
    "error": "",
    "timecost": 10997800
}

GraphQuery 是一個文本查詢語言,它不依賴於任何後端語言,能夠被任何後端語言調用,一段 GraphQuery 查詢語句,在任何語言中能夠獲得相同的解析結果。
它內置了 xpath選擇器,css選擇器,jsonpath 選擇器和 正則表達式 ,以及足量的文本處理函數,結構清晰易讀,可以保證 數據結構解析代碼返回結果 結構的一致性。

項目地址: github.com/storyicon/graphquery

GraphQuery 的語法簡潔易懂, 即便你是第一次接觸它, 也能很快的上手, 它的語法設計理念之一就是 符合直覺, 咱們應該如何執行它呢:

1. 在Golang中調用GraphQuery

golang 中,你只須要首先使用 go get -u github.com/storyicon/graphquery 得到 GraphQuery 並在代碼中調用便可:

package main

import (
    "log"

    "github.com/axgle/mahonia"
    "github.com/parnurzeal/gorequest"
    "github.com/storyicon/graphquery"
)

func decoderConvert(name string, body string) string {
    return mahonia.NewDecoder(name).ConvertString(body)
}

func main() {
    request := gorequest.New()
    _, body, _ := request.Get("http://www.58pic.com/newpic/32504070.html").End()
    body = decoderConvert("gbk", body)
    response := graphquery.ParseFromString(body, "{ title `xpath(\"/html/body/div[4]/div[1]/div/div/div[1]/text()\")` pictype `css(\".pic-type\")` number `css(\".detailBtn-down\");attr(\"data-id\")` type `regex(\"文件格式:([a-z]+)\")` metadata `css(\".main-right p\")` { size `regex(\"尺寸:(.*?)像素\")` volume `regex(\"體積:(.*? MB)\")` mode `regex(\"模式:([A-Z]+)\")` resolution `regex(\"分辨率:(\\d+dpi)\")` } author `css(\".user-name\")` images `css(\"#show-area-height img\")` [ src `attr(\"src\")` ] tags `css(\".mainRight-tagBox .fl\")` [ tag `text()` ] }")
    log.Println(response)
}

咱們的 GraphQuery 表達式以 單行 的形式, 做爲函數 graphquery.ParseFromString 的第二個參數傳入,獲得的結果與預期徹底相同。

2. 在Python中調用GraphQuery

Python 等其餘後端語言中,調用 GraphQuery 須要首先啓動其服務,服務已經爲 windowsmaclinux 編譯好,到 GraphQuery-http 中下載便可。
在解壓並啓動服務後,咱們就能夠愉快的使用 GraphQuery 在任何後端語言中對任何文檔以圖形的方式進行解析了。Python調用的示例代碼以下:

import requests

def GraphQuery(document, expr):
    response = requests.post("http://127.0.0.1:8559", data={
        "document": document,
        "expression": expr,
    })
    return response.text

response = requests.get("http://www.58pic.com/newpic/32504070.html")
conseq = GraphQuery(response.text, r"""
    {
        title `xpath("/html/body/div[4]/div[1]/div/div/div[1]/text()")`
        pictype `css(".pic-type")`
        number `css(".detailBtn-down");attr("data-id")`
        type `regex("文件格式:([a-z]+)")`
        metadata `css(".main-right p")` {
            size `regex("尺寸:(.*?)像素")`
            volume `regex("體積:(.*? MB)")`
            mode `regex("模式:([A-Z]+)")`
            resolution `regex("分辨率:(\d+dpi)")`  
        }
        author `css(".user-name")`
        images `css("#show-area-height img")` [
            src `attr("src")`
        ]
        tags `css(".mainRight-tagBox .fl")` [
            tag `text()`
        ]
    }
""")
print(conseq)

輸出結果爲:

{
    "data": {
        "author": "Ice bear",
        "images": [
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a0", 
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a1024", 
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a2048", 
            "http://pic.qiantucdn.com/58pic/32/50/40/70d58PICZfkRTfbnM2UVe_PIC2018.jpg!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a3072"
        ],
        "metadata": {
            "mode": "RGB",
            "resolution": "200dpi",
            "size": "4724×6299",
            "volume": "196.886 MB"
        },
        "number": "32504070",
        "pictype": "原創",
        "tags": ["大俠", "海報", "黑白", "金庸", "水墨", "武俠", "中國風"],
        "title": "大俠海報金庸武俠水墨中國風黑白",
        "type": "psd"
    },
    "error": "",
    "timecost": 10997800
}

3、後記

複雜的解析邏輯帶來的不只僅是代碼可讀性的問題,在代碼的維護和移植上也會形成很大的困擾,不一樣的語言和不一樣的庫也爲代碼的解析結果形成了差別,GraphQuery 是一個全新的開源項目,它的主旨就是讓開發者從這些重複繁瑣的解析邏輯中解脫出來,寫出高可讀性、高可移植性、高可維護性的代碼。歡迎實踐、持續關注與代碼貢獻,一塊兒見證 GraphQuery 與開源社區的發展!

相關文章
相關標籤/搜索