活动模式小例(四)

Applied Active Pattern in F# (4)

原创:顾远山
著作权归作者所有,转载请标明出处。

前文提要

活动模式允许你通过定义命名分区对输入数据进行分割 ,并在模式匹配表达式中如可区分联合一般使用。在实际项目中,活动模式的应用场景很多,比如各类解析器,这些工具能助力后续的数据分析任务。活动模式可用于前端输入和后端逻辑的解耦。前端输入通过活动模式解析后可以利用模式匹配连接后端逻辑。利用F#的语言特性,解耦的代码可以接近业务的领域专用语言。

在这之前,这个系列已经有三篇文章,小例(一)介绍了F#中的活动模式,小例(二)把活动模式应用于日期解析器,小例(三)通过活动模式演示了如何把前端输入和后端逻辑解耦。本文是该系列的最后一篇,笔者将用F#实现一只迷你网络爬虫作为例子,对特定网页进行爬取,并借力活动模式从原始文本中抽取字段数据。

问题描述

现有格式相同的公共网页若干,样例如下,网页内的表格有两列,左边是字段名称,右边是字段对应的值,我们只需要把右边那列的值抽取出来即可。比如对应课程名称这个字段,我们期待获取的值是World Bachelor in Business

网页样例

高阶设计

表格看上去很规整,人肉复制粘贴到Excel再行列转置一下就能达到目的,但随着网页数量增多,操作者难免身心俱疲。所以爬虫是更为现实的解决方案。值得留意的是,这个表面上看着格式非常统一的表格,其实背后各字段值所在的HTML标记却不尽相同,截取其中一小段以说明,如下:

字段值分布于形式各异的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("&nbsp;"," ").Replace("&amp;","&") //符号转换
    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。

正则表达式测试
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容