Applied Active Pattern in F# (4)
原创:顾远山
著作权归作者所有,转载请标明出处。
前文提要
活动模式允许你通过定义命名分区对输入数据进行分割 ,并在模式匹配表达式中如可区分联合一般使用。在实际项目中,活动模式的应用场景很多,比如各类解析器,这些工具能助力后续的数据分析任务。活动模式可用于前端输入和后端逻辑的解耦。前端输入通过活动模式解析后可以利用模式匹配连接后端逻辑。利用F#的语言特性,解耦的代码可以接近业务的领域专用语言。
在这之前,这个系列已经有三篇文章,小例(一)介绍了F#中的活动模式,小例(二)把活动模式应用于日期解析器,小例(三)通过活动模式演示了如何把前端输入和后端逻辑解耦。本文是该系列的最后一篇,笔者将用F#实现一只迷你网络爬虫作为例子,对特定网页进行爬取,并借力活动模式从原始文本中抽取字段数据。
问题描述
现有格式相同的公共网页若干,样例如下,网页内的表格有两列,左边是字段名称,右边是字段对应的值,我们只需要把右边那列的值抽取出来即可。比如对应课程名称
这个字段,我们期待获取的值是World Bachelor in Business
。
高阶设计
表格看上去很规整,人肉复制粘贴到Excel再行列转置一下就能达到目的,但随着网页数量增多,操作者难免身心俱疲。所以爬虫是更为现实的解决方案。值得留意的是,这个表面上看着格式非常统一的表格,其实背后各字段值所在的HTML标记却不尽相同,截取其中一小段以说明,如下:
同样都是数据值对应的行列<tr>...<td>...</td>...</tr>
里,以上四个字段就出现了四种不同的情况:
- 黄色目标值
工商管理
出现在唯一的<span>...</span>
标记里 - 绿色目标值
48(以月计)
出现在若干个(此处为两个)<span>...</span>
标记里 - 蓝色目标值
面授
作为若干个<li>...</li>
列表项目(此次为一个)出现在列表<ol>...</ol>
标记里 - 洋红目标值
1其他2語文能力
则被嵌套在一个若干行两行<tr>...</tr>
(此处为两行)若干列<td>...</td>
(此处为两列)的表格<table>...</table>
标记里
所以不同的字段需要在不同的HTML标记里寻值。
当下有些编程语言自带或社区有开源的HTML解析器,能直接把网页的原始文本转换为HTML DOM格式,继而通过操作树形表达结构中的标记抽取数据。F#也有,但本例不使用它现成的解析器,而是通过活动模式进行数据定义、数据分割、数据抽取,以凸显其在数据处理过程中的优势。
逻辑非常清晰,解决方案可以分为以下四个步骤:
- 网页爬取
- 字段抽取
- 数据清洗
- 数据塑形
其中关键的步骤是字段抽取,正是活动模式的强项,易得高阶设计如下:
鉴于输出的直观性,我们直接复用问题描述作为测试用例。
利用活动模式实现
首先,我们需要把网址对应的原始网页文本爬取下来,故有函数getPage
如下:
let getPage url = Http.RequestString(url, responseEncodingOverride = "UTF-8")
其中url
为网址,另外网页内容有繁体中文,应答编码需要重写成UTF-8
。
接下来是字段数据的抽取,在此我们复用小例(二)中实现的正则表达式匹配活动模式如下:
let (|RegexMatch|_|) pattern input = ... //详见《活动模式小例(二)》
易得函数extractColsBy
如下:
let extractColsBy pattern page =
match pageText with
| RegexMatch pattern (_::columns) -> columns
| _ -> []
pattern
为用于匹配数据的正则表达式字符串,page
为原始网页文本。网页原始文本若能匹配正则表达式,则返回由各字段值顺序组成的字符串列表,若不能则返回空列表。
不得不说的是,使用正则表达式匹配活动模式搭配模式匹配对网页原始文本进行按字段分割的操作实在猛如虎,本质上就一行代码,看第一眼什么都没做,再看一眼发现都做完了。我们仔细看一下这行代码。它无非就是一个再普通不过的基于活动模式的模式匹配,其中输出是匹配成功的分组值列表。
为了弥补正则表达式匹配的粗颗粒度,以及消除网页编码和自然语言的差异,我们继续清洗数据的步骤,遂得函数refine
如下:
let refine =
let itemize str = Regex.Replace(str, "<li>","|") //列表转换
let removeTags str = Regex.Replace(str,@"<[^!].+?>","") //移除标记
let removeSpaces str = Regex.Replace(str,@"<\s{2,}","") //移除空格
let removeComments str = Regex.Replace(str,@"<[^!].+?>","") //移除注释
let humanize (str:string) = str.Replace(" "," ").Replace("&","&") //符号转换
itemize >> removeTags >> removeSpaces >> removeComments >> humanize //逐步处理
得益于F#是函数式编程语言,我们很方便就能把几个操作字符串的子函数通过运算符>>
组合起来,而refine
本身也是个函数,类型为string -> string
。
得到清洗数据的refine
函数后,我们可以把它作为入参传到高阶映射函数map
继而应用于列表中每一个值,如下:
let map rawVals = List.map refine rawVals
从数据本身的角度,清洗动作已经完成。
为了方便后续数据利用,我们对数据进行塑形,不妨做个归约,如下:
let reduce refinedVals = List.reduce (fun a b -> a + "\r\n" + b) refinedVals //按行列示
至此,高阶设计全部实现,通过如下代码即可获取结果:
let url = @"..." //略
let pattern = """...""" //详见附录
let result = url |> getPage //网页爬取(高阶设计步骤一)
|> extractColsBy pattern //字段抽取(高阶设计步骤二)
|> map refine //数据清洗(高阶设计步骤三)
|> reduce //数据塑性(高阶设计步骤四)
这段代码与高阶设计完全契合,且接近自然语言,易读性强,而结果与用例相符,测试通过。
结语
F#的活动模式结合正则表达式能对文本进行高效匹配从而实现数据分割抽取。在活动模式的加持下,用F#可以随手以蝇量级代码按需实现自定义的网络爬虫,助力数据分析。
附录
严格意义上本例只能算伪爬虫,因为只有一行代码属于爬取过程(获取网页原始文本),其余代码都是数据处理。
从原始网页文本抽取数据的正则表达式测试参考如下,本例使用的是免费工具Expresso。