爬虫总体架构:
爬虫实现方案:
单机版
单机版架构
主要模块有:
- 处理引擎(Engine)
- 解析器(Parser)
- 下载器(Fetcher)
流程:
请求处理模型=请求URL + URL对应的Parse
- 0: 先向引擎发起一个种子请求处理模型(URL+Parse)
- 1: 引擎将请求放入任务队列中
- 2: 引擎从任务队列中取出请求处理模型(如果还有请求的话)
- 3. 将请求的URL发送给下载器
- 4. 下载器根据URL返回对应的html内容(utf-8编码的文本)给引擎
- 5. 引擎将html内容转发给请求处理模型对应的解析器(Parse)
- 6. 解析器返回处理后的得到的新的请求处理模型及Item信息(URL+Parse,Item)
- 7. 重复1-6
单机版总体算法:
目标:爬取每个城市第一页的所有用户
URL : http://www.zhenai.com/zhenghun
所以需要三个解析器:
- 城市列表解析器
- 城市解析器
- 用户解析器
解析器的
- 输入:utf8编码的文本
- 输出:Request{URL,对应Parse}列表 ,Item列表
输出一定是一个Request列表,因为一个城市的输入,会返回多个用户的输出。
Item是在返回Request列表时,里面的每个Request对应的一些附加信息
代码结构
重点在于Request请求模型的设计
type Request struct {
Url string
ParserFunc func([]byte) ParseResult // 参数[]byte就是下载器根据这个URL返回的内容
}
type ParseResult struct {
Requests []Request
Items []interface{}
}
- 就是说一个请求处理模型包括URL和其对应的Parse
- 解析器的返回模型包括请求处理模型列表和Item列表
当然有时候,解析器之间是需要参数传递的,比如城市解析器传递参数给用户解析器
有两种处理方式:
1. ParseFunc加参数
type Request struct {
Url string
ParserFunc func([]byte, string) ParseResult
}
2. 使用函数式编程
一般因为ParseFunc的结构,定义的解析器如下
func ParseProfile(contents []byte) engine.ParseResult{
...
}
但是如果城市解析器,需要传递用户名称给用户解析器,而使用第一种方法对其它解析器影响较大的话,就可以使用如下手段:
result.Requests = append(
result.Requests, engine.Request{
Url: string(m[1]),
ParserFunc: func(c []byte) engine.ParseResult {
return ParseProfile(c ,userName)
},
})
然后定义用户解析器的时候
func ParseProfile(contents []byte, name string) engine.ParseResult{
...
}
引擎处理:
func Run(seeds ...Request){
var requests []Request
for _, r := range seeds{
requests = append(requests, r)
}
for len(requests) > 0 {
r := requests[0] //获取任务列表中的第一个请求处理模型
requests = requests[1:]
log.Printf("Fetching %s",r.Url)
body, err := fetcher.Fetch(r.Url) //将请求处理模型的URL发送给下载器,得到下载的内容
if err != nil {
log.Printf("Fetching error, url %s, %s", r.Url, err)
continue
}
parseResult := r.ParserFunc(body) //将下载器返回的内容交给请求处理模型的Parse处理
requests = append(requests, parseResult.Requests...) //将返回的新的请求处理模型列表加入任务列表中
//在一次请求处理结束后
for _, item := range parseResult.Items {
log.Printf("Got item %v", item)
}
}
}