深入淺出爬蟲之道: Python、Golang與GraphQuery的
一、前言
在前言中,為了防止在後面的章節產生不必要的困擾,我們將會首先了解一些基本的程式設計理念。
1. 語義化的DOM結構
這裡我們講的語義化的DOM結構,不僅僅包括 ,也包括了語義化的選擇器,在前端開發中應該注意的是,所有的動態文字都應該有單獨的 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>
這就是不夠語義化的前端程式碼,32504070
,RGB
,16.659 MB
,72dpi
這些值都是動態屬性, 會跟隨編號的改變而改變,在規範的開發中,應該將這些 動態變化的屬性
,分別用 <span>
這類行內標籤包裹起來,並賦予其一定的語義化選擇器,在上面的HTML結構中大致可以推測出這是後端直接使用 foreach 渲染出的頁面,這是不符合前後端分離的思想的,如果有一天他們決定使用 jsonp
或 Ajax
渲染這些屬性, 由前端進行渲染,工作量無疑會上一個層次。語義化的DOM結構更傾向於下面這樣:
<p class="main-rightStage property-mode"> 模式:<span>RGB</span></p>
也可以將 property-mode
直接作為 span
的 class
屬性,這樣這些屬性無論是後端渲染,還是前端動態渲染都減輕了產品迭代產生的負擔。
2. 穩定的解析程式碼
在 語義化的DOM結構
之後,我們來談談穩定的解析程式碼, 對於下面的DOM結構:
<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>
如果我們想要提取 模式
資訊,當然可以採取下面的步驟:
選取
class
屬性中包含main-right
的div
選取這個
div
中第二個p
元素,取出其包含的文字刪除文字中的
模式:
, 得到模式為RGB
雖然成功獲取到了想要的結果,但是這樣的解析方法,我們認為它是 不穩定的
,這個不穩定是指 在其祖先元素、兄弟元素等自身以外的元素節點發生一定程度的結構改變時,導致解析錯誤或失敗 的情況, 比如如果有一天在 模式
所在的節點之前增加了一個 尺寸
的屬性:
<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>
那麼我們之前的解析將會發生錯誤(什麼?你覺得不可能發生這樣的變動?請對比 和 )。
那我們應該如何寫出更穩定的解析程式碼呢,對於上面的DOM結構,我們可以有下面幾種思路:
思路一: 遍歷 class
屬性為 main-rightStage
的 p
節點,依次判斷節點的文字是否以 模式
開頭, 如果是, 取出其 :
後的內容,缺點是邏輯太多,不易維護且降低了程式碼可讀性。
思路二: 使用正規表示式 模式:([A-Z]+)
進行匹配,缺點是使用不當可能造成效率問題。
思路三: 使用 CSS選擇器中的 contains
方法,比如 .main-rightStage:contains(模式)
, 就可以選取文字中包含 模式
,且 class
屬性中包含 main-rightStage
的節點了。但缺點是不同語言和不同庫對這種語法的支援程度各有不同,缺乏相容性。
使用哪種方法,仁者見仁智者見智,不同的解析思路帶來的解析的 穩定性
、程式碼的 複雜程度
、執行效率
和 相容性
都是不同的, 開發者需要從各種因素中進行權衡, 來寫出最優秀的解析程式碼。
二、進行頁面的解析
在進行頁面資料的抽取之前,首先要做的是明確我們需要哪些資料、頁面上提供了哪些資料,然後設計出我們需要的資料結構。首先開啟 , 由於其最上方的 瀏覽量
、收藏量
、下載量
等資料是動態載入的, 在我們的演示中暫時不需要,而這個頁面右邊的 尺寸
、模式
等資料,透過上面 和 的對比,可以得知這些屬性是不一定存在的,因此將它們一起歸到 metainfo
中。因此我們需要獲得的資料如下圖所示:
datastruct.png
由此我們可以很快設計出我們的資料結構:
{ title pictype number type metadata { size volume mode resolution } author images [] tags [] }
其中 size
、volume
、mode
、resolution
由於可能不存在,因此歸入到了 metadata
下, images
是一個圖片地址的陣列,tags
是標籤陣列,在確定了要提取的資料結構,就可以開始進行解析。
使用Python進行頁面的解析
Python庫的數量非常龐大,有很多優秀的庫可以幫助到我們,在使用Python進行頁面的解析時,我們通常用到下面這些庫:
提供
正規表示式
支援的re
庫提供
CSS選擇器
支援的pyquery
和beautifulsoup4
提供
Xpath
支援的lxml
庫提供
JSON PATH
支援的jsonpath_rw
庫
這些庫在 Python 3
下獲得支援的,可以透過 pip install
進行安裝。
由於 CSS選擇器
的語法比 Xpath
語法要更加簡潔,而在方法的呼叫上,pyquery
比 beautifulsoup4
要更加方便,因此在 2 和 3 之間我們選擇了 pyquery
。
下面我們會以 title
和 type
屬性的獲取作為例子進行講解, 其他節點的獲取是同理的。首先我們先使用 requests
庫下載這個頁面的原始檔:
import requestsfrom pyquery import PyQuery as pq response = requests.get("") document = pq(response.content.decode('gb2312'))
下面使用Python進行的解析都將依次為前提進行。
1. 獲取title節點
開啟 ,在標題上右鍵, 點選 檢視元素
,可以看到它的DOM結構如下:
title.png
這時我們注意到, 我們想要提取出的標題文字 大俠海報金庸武俠水墨中國風黑白
,並沒有被html標籤包裹,這是不符合我們上面提到的 的。同時,使用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結構:
metainfo.png
我們發現這些節點不具有語義化的選擇器,並且這些屬性不一定都存在(詳見 和 的對比)。在 中我們也講到了對於這種結構的文件可以採取的幾種思路,這裡我們採用正則解析的方法:
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)
由於獲取 size
、volume
、mode
、resolution
這些屬性,都可以採取類似的方法,因此我們可以歸結出一個正則提取的函式:
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 requestsimport refrom pyquery import PyQuery as pqdef regex_get(text, expr): matches = re.compile(expr).findall(text) if len(matches) == 0: return "" return matches[0] conseq = {}## 下載文件response = requests.get("") 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"))## 獲取tagconseq['tags'] = []for node_image in document.find(".mainRight-tagBox .fl"): conseq['tags'].append(pq(node_image).text()) print(conseq)
使用Golang進行頁面的解析
在 Golang
中解析 html
和 xml
文件, 常用到的庫有以下幾種:
提供
正規表示式
支援的regexp
庫提供
CSS選擇器
支援的github.com/PuerkitoBio/goquery
提供
Xpath
支援的gopkg.in/xmlpath.v2
庫提供
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 mainimport ( "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("").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)) }
解析邏輯完全相同,程式碼量和複雜程度相較 差不多,下面我們來看一下新出現的 是如何做的。
使用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()` ] }
透過對比可以看出, 它只是在我們設計的資料結構之中新增了一些由反引號包裹起來的函式。驚豔的是,它能完全還原我們上面在 Python
和 Golang
中的解析邏輯,而且從它的語法結構上,更能清晰的讀出返回的資料結構。這段 的執行結果如下:
{ "data": { "author": "Ice bear", "images": [ "!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a0", "!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a1024", "!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a2048", "!/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}
是一個文字查詢語言,它不依賴於任何後端語言,可以被任何後端語言呼叫,一段 查詢語句,在任何語言中可以得到相同的解析結果。
它內建了 xpath
選擇器,css
選擇器,jsonpath
選擇器和 正規表示式
,以及足量的文字處理函式,結構清晰易讀,能夠保證 資料結構
、解析程式碼
、返回結果
結構的一致性。
專案地址:
的語法簡潔易懂, 即使你是第一次接觸它, 也能很快的上手, 它的語法設計理念之一就是 符合直覺
, 我們應該如何執行它呢:
1. 在Golang中呼叫GraphQuery
在 golang
中,你只需要首先使用 go get -u github.com/storyicon/graphquery
獲得 並在程式碼中呼叫即可:
package mainimport ( "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("").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.ParseFromString
的第二個引數傳入,得到的結果與預期完全相同。
2. 在Python中呼叫GraphQuery
在 Python
等其他後端語言中,呼叫 需要首先啟動其服務,服務已經為 windows
、mac
和 linux
編譯好,到 中下載即可。
在解壓並啟動服務後,我們就可以愉快的使用 在任何後端語言中對任何文件以圖形的方式進行解析了。Python呼叫的示例程式碼如下:
import requestsdef GraphQuery(document, expr): response = requests.post("", data={ "document": document, "expression": expr, }) return response.text response = requests.get("") 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": [ "!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a0", "!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a1024", "!/fw/1024/watermark/url/L2ltYWdlcy93YXRlcm1hcmsvZGF0dS5wbmc=/repeat/true/crop/0x1024a0a2048", "!/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}
三、後記
複雜的解析邏輯帶來的不僅僅是程式碼可讀性的問題,在程式碼的維護和移植上也會造成很大的困擾,不同的語言和不同的庫也為程式碼的解析結果造成了差異, 是一個全新的開源專案,它的主旨就是讓開發者從這些重複繁瑣的解析邏輯中解脫出來,寫出高可讀性、高可移植性、高可維護性的程式碼。歡迎實踐、持續關注與程式碼貢獻,一起見證 與開源社群的發展!
作者:Ox1系統管理員
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/964/viewspace-2818991/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java爬蟲與Python爬蟲的區別?Java爬蟲Python
- Golang福利爬蟲Golang爬蟲
- 【Python學習】爬蟲爬蟲爬蟲爬蟲~Python爬蟲
- Python爬蟲與Java爬蟲有何區別?Python爬蟲Java
- 第 64 期深入淺出 Golang RuntimeGolang
- 不踩坑的Python爬蟲:Python爬蟲開發與專案實戰,從爬蟲入門 PythonPython爬蟲
- IPIDEA乾貨|Java爬蟲與Python爬蟲的區別IdeaJava爬蟲Python
- 【python爬蟲】python爬蟲demoPython爬蟲
- Python爬蟲開發(二):整站爬蟲與Web挖掘Python爬蟲Web
- 深入淺出 Golang 資源嵌入方案:前篇Golang
- 最近要寫爬蟲,大家有推薦 Golang 的爬蟲框架嗎?爬蟲Golang框架
- 通用爬蟲與聚焦爬蟲爬蟲
- python爬蟲---網頁爬蟲,圖片爬蟲,文章爬蟲,Python爬蟲爬取新聞網站新聞Python爬蟲網頁網站
- 深入淺出的“深拷貝與淺拷貝”
- Golang爬蟲+正規表示式Golang爬蟲
- 爬蟲技術淺析爬蟲
- WebMagic 爬蟲框架淺析Web爬蟲框架
- 深入淺出FE(十四)深入淺出websocketWeb
- 3 行寫爬蟲 - 使用 Goribot 快速構建 Golang 爬蟲爬蟲Golang
- python就是爬蟲嗎-python就是爬蟲嗎Python爬蟲
- Python爬蟲之路-chrome在爬蟲中的使用Python爬蟲Chrome
- Python爬蟲(1.爬蟲的基本概念)Python爬蟲
- 深入淺出深拷貝與淺拷貝
- Python爬蟲開發與專案實戰——基礎爬蟲分析Python爬蟲
- Python爬蟲開發與專案實戰 3: 初識爬蟲Python爬蟲
- python 爬蟲Python爬蟲
- python爬蟲Python爬蟲
- Python爬蟲的用途Python爬蟲
- 深入淺出 Golang 資源嵌入方案:go-bindata篇Golang
- Golang爬蟲,Go&&正則爬取資料,槓桿的Golang爬蟲
- 用Golang寫爬蟲(六) - 使用collyGolang爬蟲
- 『No20: Golang 爬蟲上手指南』Golang爬蟲
- Golang 網路爬蟲框架gocolly/collyGolang爬蟲框架
- Python爬蟲之路-selenium在爬蟲中的使用Python爬蟲
- CSS深入淺出-寬度與高度CSS
- [Python] 網路爬蟲與資訊提取(1) 網路爬蟲之規則Python爬蟲
- 反射的深入淺出反射
- 深入淺出this的理解