简介
本文记录如何爬取豆瓣电影Top250的数据列表,并存储数据到MySQL数据库中的实践教程。
主要重难点是:
Go实现Http并发请求数据 设计正则表达式匹配数据 存储爬取的数据入库MySQL
- 明确爬取目标 URL 地址 首先在浏览器打开 豆瓣电影 Top250 的网站地址: 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 的数值。
- 发送请求获取响应数据 新建一个 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 main() {
// 固定爬取的起始页 TOP250 每页25条共10页
start := 1
end := 10
channel := make(chan int)
for i := start; i <= end; i++ {
SpiderDouBan(i, channel)
}
}
封装 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
}
- 过滤标签提取有用信息–每一部电影
通过上面的封装函数,我们已经可以获取豆瓣电影 Top250 的全部网页数据。为了测试我们通过查看网页源代码的方法分析网站数据:每一部电影在 HTML 网页中的标签都是固定的,我们只需要找出固定格式的规律,编写 正则表达式,提取出电影名称、主演、图片、评分等信息。
其中一部电影的网页源码:
<li>
<div class="item">
<div class="pic">
<em class="">1</em>
<a href="https://movie.douban.com/subject/1292052/">

</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>
可以看到每一部电影的标签都在
class 为 pic 的 div 节点为电影的排名 ID 号和电影图片以及电影名称的信息的正则:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?)`
节点内包含的是电影的别名其它信息,正则表达式为:
regExp := `<div class="item">[\s\S]*?<div class="pic">[\s\S]*?<em class="">(.*?)<\/em>[\s\S]*?<a href=".*?">[\s\S]*?)[\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]*?)[\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]*?)[\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]*?)[\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 语句遍历数组获取有用信息。
- 使用分析得到有效数据 创建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 的代码请点击: gitee.com/lisgroup/go… 查看
总结实现步骤 根据输入的起始页,创建工作函数 SpiderDouBan() 循环爬取每页生成URL 封装 Request() 方法爬取每个网页数据内容,通过result返回 创建数据库保存爬取的数据 使用goroutine### 简介
SDK社区是一个中立的社区,这里有多样的前端知识,有丰富的api,有爱学习的人工智能开发者,有风趣幽默的开发者带你学python,还有未来火热的鸿蒙,当各种元素组合在一起,让我们一起脑洞大开共同打造专业、好玩、有价值的开发者社区,帮助开发者实现自我价值!