Applied Active Pattern in F# (4)
原創(chuàng):顧遠(yuǎn)山
著作權(quán)歸作者所有逸爵,轉(zhuǎn)載請(qǐng)標(biāo)明出處。
前文提要
活動(dòng)模式允許你通過(guò)定義命名分區(qū)對(duì)輸入數(shù)據(jù)進(jìn)行分割 士嚎,并在模式匹配表達(dá)式中如可區(qū)分聯(lián)合一般使用呜魄。在實(shí)際項(xiàng)目中,活動(dòng)模式的應(yīng)用場(chǎng)景很多莱衩,比如各類(lèi)解析器爵嗅,這些工具能助力后續(xù)的數(shù)據(jù)分析任務(wù)”恳希活動(dòng)模式可用于前端輸入和后端邏輯的解耦睹晒。前端輸入通過(guò)活動(dòng)模式解析后可以利用模式匹配連接后端邏輯趟庄。利用F#的語(yǔ)言特性,解耦的代碼可以接近業(yè)務(wù)的領(lǐng)域?qū)S谜Z(yǔ)言册招。
在這之前岔激,這個(gè)系列已經(jīng)有三篇文章,小例(一)介紹了F#中的活動(dòng)模式是掰,小例(二)把活動(dòng)模式應(yīng)用于日期解析器虑鼎,小例(三)通過(guò)活動(dòng)模式演示了如何把前端輸入和后端邏輯解耦。本文是該系列的最后一篇键痛,筆者將用F#實(shí)現(xiàn)一只迷你網(wǎng)絡(luò)爬蟲(chóng)作為例子炫彩,對(duì)特定網(wǎng)頁(yè)進(jìn)行爬取,并借力活動(dòng)模式從原始文本中抽取字段數(shù)據(jù)絮短。
問(wèn)題描述
現(xiàn)有格式相同的公共網(wǎng)頁(yè)若干江兢,樣例如下,網(wǎng)頁(yè)內(nèi)的表格有兩列丁频,左邊是字段名稱(chēng)杉允,右邊是字段對(duì)應(yīng)的值,我們只需要把右邊那列的值抽取出來(lái)即可。比如對(duì)應(yīng)課程名稱(chēng)
這個(gè)字段,我們期待獲取的值是World Bachelor in Business
口柳。
高階設(shè)計(jì)
表格看上去很規(guī)整捻勉,人肉復(fù)制粘貼到Excel再行列轉(zhuǎn)置一下就能達(dá)到目的店诗,但隨著網(wǎng)頁(yè)數(shù)量增多,操作者難免身心俱疲。所以爬蟲(chóng)是更為現(xiàn)實(shí)的解決方案。值得留意的是秕狰,這個(gè)表面上看著格式非常統(tǒng)一的表格,其實(shí)背后各字段值所在的HTML標(biāo)記卻不盡相同躁染,截取其中一小段以說(shuō)明鸣哀,如下:
同樣都是數(shù)據(jù)值對(duì)應(yīng)的行列<tr>...<td>...</td>...</tr>
里,以上四個(gè)字段就出現(xiàn)了四種不同的情況:
- 黃色目標(biāo)值
工商管理
出現(xiàn)在唯一的<span>...</span>
標(biāo)記里 - 綠色目標(biāo)值
48(以月計(jì))
出現(xiàn)在若干個(gè)(此處為兩個(gè))<span>...</span>
標(biāo)記里 - 藍(lán)色目標(biāo)值
面授
作為若干個(gè)<li>...</li>
列表項(xiàng)目(此次為一個(gè))出現(xiàn)在列表<ol>...</ol>
標(biāo)記里 - 洋紅目標(biāo)值
1其他2語(yǔ)文能力
則被嵌套在一個(gè)若干行兩行<tr>...</tr>
(此處為兩行)若干列<td>...</td>
(此處為兩列)的表格<table>...</table>
標(biāo)記里
所以不同的字段需要在不同的HTML標(biāo)記里尋值吞彤。
當(dāng)下有些編程語(yǔ)言自帶或社區(qū)有開(kāi)源的HTML解析器我衬,能直接把網(wǎng)頁(yè)的原始文本轉(zhuǎn)換為HTML DOM格式,繼而通過(guò)操作樹(shù)形表達(dá)結(jié)構(gòu)中的標(biāo)記抽取數(shù)據(jù)备畦。F#也有低飒,但本例不使用它現(xiàn)成的解析器许昨,而是通過(guò)活動(dòng)模式進(jìn)行數(shù)據(jù)定義懂盐、數(shù)據(jù)分割、數(shù)據(jù)抽取糕档,以凸顯其在數(shù)據(jù)處理過(guò)程中的優(yōu)勢(shì)莉恼。
邏輯非常清晰拌喉,解決方案可以分為以下四個(gè)步驟:
- 網(wǎng)頁(yè)爬取
- 字段抽取
- 數(shù)據(jù)清洗
- 數(shù)據(jù)塑形
其中關(guān)鍵的步驟是字段抽取,正是活動(dòng)模式的強(qiáng)項(xiàng)俐银,易得高階設(shè)計(jì)如下:
鑒于輸出的直觀性尿背,我們直接復(fù)用問(wèn)題描述作為測(cè)試用例。
利用活動(dòng)模式實(shí)現(xiàn)
首先捶惜,我們需要把網(wǎng)址對(duì)應(yīng)的原始網(wǎng)頁(yè)文本爬取下來(lái)田藐,故有函數(shù)getPage
如下:
let getPage url = Http.RequestString(url, responseEncodingOverride = "UTF-8")
其中url
為網(wǎng)址,另外網(wǎng)頁(yè)內(nèi)容有繁體中文吱七,應(yīng)答編碼需要重寫(xiě)成UTF-8
汽久。
接下來(lái)是字段數(shù)據(jù)的抽取,在此我們復(fù)用小例(二)中實(shí)現(xiàn)的正則表達(dá)式匹配活動(dòng)模式如下:
let (|RegexMatch|_|) pattern input = ... //詳見(jiàn)《活動(dòng)模式小例(二)》
易得函數(shù)extractColsBy
如下:
let extractColsBy pattern page =
match pageText with
| RegexMatch pattern (_::columns) -> columns
| _ -> []
pattern
為用于匹配數(shù)據(jù)的正則表達(dá)式字符串踊餐,page
為原始網(wǎng)頁(yè)文本景醇。網(wǎng)頁(yè)原始文本若能匹配正則表達(dá)式,則返回由各字段值順序組成的字符串列表吝岭,若不能則返回空列表三痰。
不得不說(shuō)的是,使用正則表達(dá)式匹配活動(dòng)模式搭配模式匹配對(duì)網(wǎng)頁(yè)原始文本進(jìn)行按字段分割的操作實(shí)在猛如虎窜管,本質(zhì)上就一行代碼散劫,看第一眼什么都沒(méi)做,再看一眼發(fā)現(xiàn)都做完了微峰。我們仔細(xì)看一下這行代碼舷丹。它無(wú)非就是一個(gè)再普通不過(guò)的基于活動(dòng)模式的模式匹配,其中輸出是匹配成功的分組值列表蜓肆。
為了彌補(bǔ)正則表達(dá)式匹配的粗顆粒度颜凯,以及消除網(wǎng)頁(yè)編碼和自然語(yǔ)言的差異,我們繼續(xù)清洗數(shù)據(jù)的步驟仗扬,遂得函數(shù)refine
如下:
let refine =
let itemize str = Regex.Replace(str, "<li>","|") //列表轉(zhuǎn)換
let removeTags str = Regex.Replace(str,@"<[^!].+?>","") //移除標(biāo)記
let removeSpaces str = Regex.Replace(str,@"<\s{2,}","") //移除空格
let removeComments str = Regex.Replace(str,@"<[^!].+?>","") //移除注釋
let humanize (str:string) = str.Replace(" "," ").Replace("&","&") //符號(hào)轉(zhuǎn)換
itemize >> removeTags >> removeSpaces >> removeComments >> humanize //逐步處理
得益于F#是函數(shù)式編程語(yǔ)言症概,我們很方便就能把幾個(gè)操作字符串的子函數(shù)通過(guò)運(yùn)算符>>
組合起來(lái),而refine
本身也是個(gè)函數(shù)早芭,類(lèi)型為string -> string
彼城。
得到清洗數(shù)據(jù)的refine
函數(shù)后,我們可以把它作為入?yún)鞯礁唠A映射函數(shù)map
繼而應(yīng)用于列表中每一個(gè)值退个,如下:
let map rawVals = List.map refine rawVals
從數(shù)據(jù)本身的角度募壕,清洗動(dòng)作已經(jīng)完成。
為了方便后續(xù)數(shù)據(jù)利用语盈,我們對(duì)數(shù)據(jù)進(jìn)行塑形舱馅,不妨做個(gè)歸約,如下:
let reduce refinedVals = List.reduce (fun a b -> a + "\r\n" + b) refinedVals //按行列示
至此刀荒,高階設(shè)計(jì)全部實(shí)現(xiàn)代嗤,通過(guò)如下代碼即可獲取結(jié)果:
let url = @"..." //略
let pattern = """...""" //詳見(jiàn)附錄
let result = url |> getPage //網(wǎng)頁(yè)爬燃(高階設(shè)計(jì)步驟一)
|> extractColsBy pattern //字段抽取(高階設(shè)計(jì)步驟二)
|> map refine //數(shù)據(jù)清洗(高階設(shè)計(jì)步驟三)
|> reduce //數(shù)據(jù)塑性(高階設(shè)計(jì)步驟四)
這段代碼與高階設(shè)計(jì)完全契合干毅,且接近自然語(yǔ)言宜猜,易讀性強(qiáng),而結(jié)果與用例相符硝逢,測(cè)試通過(guò)姨拥。
結(jié)語(yǔ)
F#的活動(dòng)模式結(jié)合正則表達(dá)式能對(duì)文本進(jìn)行高效匹配從而實(shí)現(xiàn)數(shù)據(jù)分割抽取。在活動(dòng)模式的加持下渠鸽,用F#可以隨手以蠅量級(jí)代碼按需實(shí)現(xiàn)自定義的網(wǎng)絡(luò)爬蟲(chóng)垫毙,助力數(shù)據(jù)分析。
附錄
嚴(yán)格意義上本例只能算偽爬蟲(chóng)拱绑,因?yàn)橹挥幸恍写a屬于爬取過(guò)程(獲取網(wǎng)頁(yè)原始文本)综芥,其余代碼都是數(shù)據(jù)處理。
從原始網(wǎng)頁(yè)文本抽取數(shù)據(jù)的正則表達(dá)式測(cè)試參考如下猎拨,本例使用的是免費(fèi)工具Expresso膀藐。