最近学习 Go 语言,打算写个爬虫,爬取相亲网站数据
网站分析
打开网页 http://www.zhenai.com/zhenghun
, 这是一个城市列表,进入某个城市后可以看到用户列表(分页),点击用户可以进入用户详情,这个就是我们要爬取的数据。整个的爬取流程可以归结为下图:
热身
爬取网页并打印
先从简单的开始,让我们爬取网页,并打印出来:
1 | package main |
注意到这里使用了一些第三方库来推测并转换编码格式。
正则表达式提取城市 url
拿到网页内容后,我们可以利用正则表达式来提取城市的 url:
1 | func printCityList(contents []byte) { |
单任务版爬虫
热身结束,让我们来构建一个简单的单任务版爬虫。下面是单任务版爬虫的架构图:
- 传入初始的种子到
Engine
中启动整个爬取过程,种子会被保存到任务队列中 Engine
从队列中拿出一个Request
对象request
,将所要爬取的网页 URL 发送给Fetcher
,Fethcer
将网页下载下来给Engine
- 拿到
Fetcher
返回的网页文本后交给Parser
,Parser
从文本中提取出所要爬取的数据items
,或者根据需要提取出需要进一步访问的网页 url 并组装成Request
对象 Engine
拿到返回的数据后,如果是items
则保存下来,而如果是request
,则将其添加到任务队列中。然后重复 2 - 4 步,直到任务队列为空。
项目目录结构
1 | ├── engine |
types
types 里面是用到的数据结构,如下所示:
1 | type Request struct { |
fetcher
fetcher 目录下是网页下载器,热身篇中有提到过,这里不赘述,不过实际爬取过程中加入了限制爬取速率的功能:
1 | ... |
model
model 中定义了目标数据的结构:
1 | package model |
parser
zhenai/parser
下面定义的是各网页对应的解析器,其中 citylist
用来解析城市列表页,city
用来解析单个城市页,profile
用来解析个人详情页。
citylist
1 | const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[0-9a-z]+)"[^>]*>([^<]+)</a>` |
city
1 | const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>` |
profile
1 | var ageRe = regexp.MustCompile(`<td><span class="label">年龄:</span>(\d+)岁</td>`) |
engine
该目录下面是爬虫引擎,目前只有一个单任务版 SimpleEngine
:
1 | package engine |
说了这么多,最后我们来跑一跑吧:
1 | e := engine.SimpleEngine{} |
总结
本文实现了一个简单的单任务版爬虫,整个爬虫的架构还是比较清晰易懂的。而且,通过自定义 parser,可以实现一个比较灵活的工作流。但是这个版本的爬虫爬取的效率很慢,远远达不到实际应用的需求。所以,借助 Go 语言独特的 goroutine,下一节我们要来实现一个多任务版本的爬虫。