简介
本文记录如何爬取豆瓣电影Top250的数据列表,并存储数据到MySQL数据库中的实践教程。
主要重难点是:
- Go实现Http并发请求数据
- 设计正则表达式匹配数据
- 存储爬取的数据入库MySQL
1. 明确爬取目标 URL 地址
首先在浏览器打开 豆瓣电影 Top250
的网站地址: https://movie.douban.com/top250 ,
打开后我们可以看到每一页显示 25 部电影,点击最下面的第二页按钮后分页参数变为 ?start=25&filter=
,第三页分页参数 ?start=50&filter=
。。。
可以看出 250 部电影分为 10 页显示,要抓取全部电影,我们只需要循环生成这 10 页 URL 链接即可。下面展示的是每一页的链接地址:
https://movie.douban.com/top250?start=0&filter= #首页,相当于 https://movie.douban.com/top250
https://movie.douban.com/top250?start=25&filter= #第2页
https://movie.douban.com/top250?start=50&filter= #第3页
...
https://movie.douban.com/top250?start=225&filter= #第10页
通过分析链接地址,我们得出规律:start=0 显示 1 到 25 部电影数据,start=25 显示 26 到 50 部电影,同理在抓取下一页的数据时,只需要改变 start
的数值。
2. 发送请求获取响应数据
- 新建一个
main.go
文件,并新建main()
主函数控制循环次数,用于生成 URL 的输入参数:
func main() {
// 固定爬取的起始页 TOP250 每页25条共10页
start := 1
end := 10
channel := make(chan int)
for i := start; i <= end; i++ {
SpiderDouBan(i, channel)
}
}
- 下一步新建
SpiderDouBan()
函数用于爬取控制器:
func SpiderDouBan(index int) {
body := map[string]string{}
// strconv.Itoa() 将数字转字符串
urls := "https://movie.douban.com/top250?start=" + strconv.Itoa((index-1)*25) + "&filter="
headers := map[string]string{"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36"}
// Request() 函数是封装的简单请求方法,见第 3 步
result, err := Request(urls, "GET", body, headers, 4)
//fmt.Println(result, err)
if err != nil {
fmt.Println("HttpGet err: ", err)
}
}
- 封装 HTTP 请求方法
Request()
:
对于很多网站都有反爬虫的措施,如果没有 headers 头信息的请求一律认为是爬虫请求,就会禁止请求。所以我们每次爬取网页时,都会加上一些 headers 的头信息。
但是对于访问过于频繁的请求,客户端的 IP
就会被服务端禁止访问,因此设置代理 proxies
也可以将请求伪装成来自不同 IP 的访问,前提是保证代理的 IP 地址是有效的。
// 简单封装一个Http请求方法
// rawUrl 请求的接口地址
// method 请求的方法,GET/POST
// bodyMap 请求的 body 内容
// header 请求的头信息
// timeout 超时时间
func Request(rawUrl, method string, bodyMaps, headers map[string]string, timeout time.Duration) (result string, err error) {
if timeout <= 0 {
timeout = 5
}
client := &http.Client{
Timeout: timeout * time.Second,
}
// 请求的 body 内容
data := url.Values{}
for key, value := range bodyMaps {
data.Set(key, value)
}
// 提交请求
request, err1 := http.NewRequest(method, rawUrl, strings.NewReader(data.Encode())) // URL-encoded payload
if err1 != nil {
err = err1
return
}
// 增加header头信息
for key, val := range headers {
request.Header.Set(key, val)
}
// 处理返回结果
response, _ := client.Do(request)
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("get content failed status code is %d ", response.StatusCode)
}
res, err2 := ioutil.ReadAll(response.Body)
if err2 != nil {
err = err2
return
}
return string(res), nil
}
3. 过滤标签提取有用信息–每一部电影
通过上面的封装函数,我们已经可以获取豆瓣电影 Top250 的全部网页数据。为了测试我们通过查看网页源代码的方法分析网站数据:每一部电影在 HTML
网页中的标签都是固定的,我们只需要找出固定格式的规律,编写 正则表达式
,提取出电影名称、主演、图片、评分等信息。
其中一部电影的网页源码:
<li>
<div class="item">
<div class="pic">
<em class="">1</em>
<a href="https://movie.douban.com/subject/1292052/">
<img width="100" alt="肖申克的救赎" src="https://img3.doubanio.com/view/photo/s_ratio_poster/public/p480747492.webp" class="">
</a>
</div>
<div class="info">
<div class="hd">
<a href="https://movie.douban.com/subject/1292052/" class="">
<span class="title">肖申克的救赎</span>
<span class="title"> / The Shawshank Redemption</span>
<span class="other"> / 月黑高飞(港) / 刺激1995(台)</span>
</a>
<span class="playable">[可播放]</span>
</div>
<div class="bd">
<p class="">
导演: 弗兰克·德拉邦特 Frank Darabont 主演: 蒂姆·罗宾斯 Tim Robbins /...<br>
1994 / 美国 / 犯罪 剧情
</p>
<div class="star">
<span class="rating5-t"></span>
<span class="rating_num" property="v:average">9.7</span>
<span property="v:best" content="10.0"></span>
<span>2153218人评价</span>
</div>
<p class="quote">
<span class="inq">希望让人自由。</span>
</p>
</div>
</div>
</div>
</li>
可以看到每一部电影的标签都在 <li>
标签的 <div class="item">
节点里面,可以先用站长工具 https://tool.oschina.net/regex/ 测试编写的正则表达式提取到的数据是否正确::
- class 为 pic 的 div 节点为电影的排名
ID
号和电影图片以及电影名称的信息的正则:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">`
- 节点内包含的是电影的别名其它信息,正则表达式为:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>`
-
的标签内包含了电影的导演和主演信息,其中
标签内是电影的导演和演员信息,其中使用
换行,所以可以提取出导演和演员的数据,正则表达式改写为:regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>[\s\S]*?<div class="bd">[\s\S]*?<p class=".*?">([\s\S]*?)<br>([\s\S]*?)<\/p>`
-
的标签中包含的是电影的星级和评分数据。提取星级和评分的规则和上面分析的类型,所以最终的正则表达式为:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>[\s\S]*?<div class="bd">[\s\S]*?<p class=".*?">([\s\S]*?)<br>([\s\S]*?)<\/p>[\s\S]*?span class="rating_num".*?average">(.*?)<\/span>`
完整的提取每页中所有电影数据的代码如下:
// 使用正则匹配 regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?<img width=".*?" alt="(.*?)" src="(.*?)" class=".*?">[\s\S]*?div class="info[\s\S]*?class="hd"[\s\S]*?class="title">(.*?)<\/span>[\s\S]*?class="other">(.*?)<\/span>[\s\S]*?<div class="bd">[\s\S]*?<p class=".*?">([\s\S]*?)<br>([\s\S]*?)<\/p>[\s\S]*?span class="rating_num".*?average">(.*?)<\/span>` // 使用正则匹配 find := regexp.MustCompile(regExp) // 其中 result 为上一步获取的网页HTML数据 content := find.FindAllStringSubmatch(result, -1) // 返回匹配的详细二维切片
其中
content
数据格式为[][]string
二维切片,所以我们可以使用for
语句遍历数组获取有用信息。4. 使用分析得到有效数据
- 创建MySQL的一张数据表存储保存的数据
CREATE TABLE `top250` ( `id` int(20) NOT NULL AUTO_INCREMENT, `title` varchar(20) DEFAULT '', `image` varchar(100) DEFAULT '', `subtitle` varchar(255) DEFAULT '', `other` varchar(255) DEFAULT NULL, `personnel` varchar(255) DEFAULT '', `info` varchar(255) DEFAULT '', `score` varchar(10) DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
- 使用
database/sql
包并使用 MySQL 驱动:MySQL drivers
其中数据库教程点击: Go SQL 数据库教程 查看
func insert(movies [][]string) { // 存放 (?, ?, ...) 的slice valueStrings := make([]string, 0, len(movies)) // 存放values的slice valueArgs := make([]interface{}, 0, len(movies)*8) // 遍历切片准备数据 for _, val := range movies { // 占位符 valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?)") for i := 1; i < len(val); i++ { valueArgs = append(valueArgs, val[i]) } } db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test") if err != nil { log.Fatal(err) } defer db.Close() // 自行拼接要执行的具体语句 sqlName := fmt.Sprintf("INSERT INTO `top250` (id,title,image,subtitle,other,personnel,info,score) VALUES %s", strings.Join(valueStrings, ",")) fmt.Println(sqlName) // insert stmt, err := db.Prepare(sqlName) if err != nil { log.Fatal(err) } fmt.Println(valueArgs) res, err := stmt.Exec(valueArgs...) if err != nil { log.Fatal(err) } lastId, err := res.LastInsertId() if err != nil { log.Fatal(err) } rowCnt, err := res.RowsAffected() if err != nil { log.Fatal(err) } log.Printf("ID = %d, affected = %d\n", lastId, rowCnt) }
5.启动 goroutine
我们只需要在
main()
函数中创建chan
通道就可以并发调用SpiderDouBan()
函数:channel := make(chan int) for i := start; i <= end; i++ { go SpiderDouBan(i, channel) } for i := start; i <= end; i++ { fmt.Println("第" + strconv.Itoa(<-channel) + "页任务完成") }
下面在
SpiderDouBan()
函数执行的最后面,向chan
通道中发送数据:func SpiderDouBan(index int, ch chan int) { // ... 省略代码 // ... // chan 记录 ch <- index }
其中完整 Go爬取豆瓣电影Top250 的代码请点击: https://gitee.com/lisgroup/go-learn/blob/master/spider/douban.go 查看
总结实现步骤
- 根据输入的起始页,创建工作函数
SpiderDouBan()
- 循环爬取每页生成URL
- 封装
Request()
方法爬取每个网页数据内容,通过result返回 - 创建数据库保存爬取的数据
- 使用goroutine
-
评论区