Golang分布式爬虫:抓取煎蛋文章|Redis/Mysql|56,961 篇文章
---
layout: post
title: "Golang分布式爬虫:抓取煎蛋文章"
date: 2017-04-15
author: hunterhug
categories: [代码]
desc: "Golang分布式爬虫:抓取煎蛋文章"
tags: ["爬虫","Golang"]
permalink: "/spider/jiandan.html"
---
版权所有,转载请注明:www.lenggirl.com/spider/jiandan.html
项目地址:https://github.com/hunterhug/GoSpiderExample
- 克隆
https://github.com/hunterhug/GoSpider
仓库到GOPATH路径下,地址:https://github.com/hunterhug/GoSpider - 示例仅供学习,爬虫有风险,如果太暴力,会给别人带来损失,在此申明不承担相应责任,查看Example
一、介绍
多浏览器持久化cookie分布式爬虫爬取数据,使用到redis,mysql,将网页数据保存在磁盘中,详情页解析后存入数据库。中级示例!
二、架构
使用Redis进行分布式,多只爬虫并发抓取,保存cookie,且利用保存在本地的文件。
我们先看如何运行,再看代码
代码结构
clear.go 清除Redis中抓取失败的URL,从doing队列移到todo队列
cont.go 配置
detail.go 详情页抓取
index.go 首页页面抓取
parse.go 解析功能
store.go 存储功能
--main
main.go 程序入口
三、使用方法
- cont.go编辑配置,
RootDir = "E:\\jiandan"
为数据目录 - 进main文件夹运行
- 数据保存在
RootDir
下文件夹,和Mysql数据库中 - 重抓要删除Redis数据库和相应文件夹,否则已抓将不再抓。
四、代码解释
各种解释均写在代码中
1.main.go入口
package main
import (
//"fmt"
"github.com/hunterhug/GoSpiderExample/jiandan"
"os"
"os/signal"
)
var Clear = false
func main() {
if Clear {
// Reids中Doing的迁移到Todo,需手动,var Clear = true
go jiandan.Clear()
} else {
// 首页爬虫爬取
go jiandan.IndexSpiderRun()
// 详情页抓取
go jiandan.DetailSpidersRun()
}
c := make(chan os.Signal)
//监听指定信号
signal.Notify(c, os.Interrupt)
//阻塞直至有信号传入
<-c
}
Reids中Doing的迁移到Todo,需手动设置var Clear = true
,然后两个协程一起抓取,信号量监听才退出!
2.cont.go配置
package jiandan
import (
"fmt"
"github.com/hunterhug/GoSpider/spider"
"github.com/hunterhug/GoSpider/store/myredis"
"github.com/hunterhug/GoSpider/store/mysql"
"github.com/hunterhug/GoSpider/util"
"path/filepath"
)
// 可抽离到配置文件中
const (
// 网站
Url = "http://jandan.net"
Host = "jandan.net"
// 详情页爬虫数量
DetailSpiderNum = 30
DetailSpiderNamePrefix = "detail"
// 首页爬虫数量
IndexSpiderNum = 3
IndexSpiderNamePrefix = "index"
// 爬虫暂停时间
StopTime = 1
// 日志级别
LogLevel = "info"
)
var (
// 首页页数
IndexPage int
// 根目录
//RootDir = util.CurDir()
RootDir = "E:\\jiandan"
// Redis配置
RedisConfig = myredis.RedisConfig{
DB: 0,
Host: "127.0.0.1:6379",
Password: "smart2016",
}
RedisClient myredis.MyRedis
RedisListTodo = "jiandantodo"
RedisListDoing = "jiandandoing"
RedisListDone = "jiandandone"
// mysql config
mysqlconfig = mysql.MysqlConfig{
Username: "root",
Password: "smart2016",
Ip: "127.0.0.1",
Port: "3306",
Dbname: "jiandan",
}
MysqlClient mysql.Mysql
)
// 设置全局
func init() {
e := util.MakeDir(filepath.Join(RootDir, "data", "detail"))
if e != nil {
spider.Log().Panic(e.Error())
}
spider.SetGlobalTimeout(StopTime)
spider.SetLogLevel(LogLevel)
indexstopchan = make(chan bool, 1)
// 初始化爬虫们,一种多爬虫方式,设置到全局MAP中
for i := 0; i <= IndexSpiderNum; i++ {
s, e := spider.New(nil)
if e != nil {
spider.Log().Panicf("index spider %d new error: %s", i, e.Error())
}
// 设置随机UA
s.SetUa(spider.RandomUa())
spider.Pool.Set(fmt.Sprintf("%s-%d", IndexSpiderNamePrefix, i), s)
}
for i := 0; i <= DetailSpiderNum; i++ {
s, e := spider.New(nil)
if e != nil {
spider.Log().Panicf("detail spider %d new error: %s", i, e.Error())
}
s.SetUa(spider.RandomUa())
spider.Pool.Set(fmt.Sprintf("%s-%d", DetailSpiderNamePrefix, i), s)
}
}
首先进行各种常量定义,然后REDIS和MYSQL配置,新建数据保存文件夹,初始化多只爬虫
3.store.go存储功能
package jiandan
import (
"github.com/hunterhug/GoSpider/spider"
"github.com/hunterhug/GoSpider/store/myredis"
"github.com/hunterhug/GoSpider/store/mysql"
"github.com/hunterhug/GoSpider/util"
)
func init() {
// 新建Redis池,方便爬虫们插和抽!!
client, err := myredis.NewRedisPool(RedisConfig, DetailSpiderNum+IndexSpiderNum+2)
if err != nil {
spider.Log().Error(err.Error())
}
RedisClient = client
// 新建数据库
e := mysqlconfig.CreateDb()
if e != nil {
spider.Log().Error(e.Error())
}
// a new db connection
MysqlClient = mysql.New(mysqlconfig)
// open connection
MysqlClient.Open(500, 300)
// create sql
sql := `
CREATE TABLE IF NOT EXISTS pages (
id varchar(255) NOT NULL,
url varchar(255) NOT NULL,
title varchar(255) NOT NULL,
shortcontent varchar(255) NOT NULL DEFAULT '',
tags varchar(255) NOT NULL DEFAULT '',
content longtext NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='煎蛋文章';`
// create
_, err = MysqlClient.Create(sql)
if err != nil {
spider.Log().Error(err.Error())
}
}
func SentRedis(urls []string) {
var interfaceSlice []interface{} = make([]interface{}, len(urls))
for i, d := range urls {
interfaceSlice[i] = d
}
_, e := RedisClient.Lpush(RedisListTodo, interfaceSlice...)
if e != nil {
spider.Log().Errorf("sent redis error:%s", e.Error())
}
}
func SaveToMysql(url string, m map[string]string) {
if m["title"] == "" {
return
}
_, e := MysqlClient.Insert("INSERT INTO `jiandan`.`pages`(`id`,`url`,`title`,`shortcontent`,`tags`,`content`)VALUES(?,?,?,?,?,?)", util.Md5(url), url, m["title"], m["shortcontent"], m["tags"], m["content"])
if e != nil {
spider.Log().Error("save mysql error:" + e.Error())
}
}
首先初始化Redis池和新建MYSQL数据库和表,数据库采用编码:utf8mb4,因为字节数会造成错误
CREATE TABLE IF NOT EXISTS pages (
id varchar(255) NOT NULL,
url varchar(255) NOT NULL,
title varchar(255) NOT NULL,
shortcontent varchar(255) NOT NULL DEFAULT '',
tags varchar(255) NOT NULL DEFAULT '',
content longtext NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='煎蛋文章';
函数SentRedis
和SaveToMysql
分别是发送网址到redis,和发送文章到Mysql中。
4.parse.go 解析库
package jiandan
import (
"errors"
"github.com/PuerkitoBio/goquery"
"github.com/hunterhug/GoSpider/query"
"github.com/hunterhug/GoSpider/util"
"strings"
)
// 解析页面数量
func ParseIndexNum(data []byte) error {
doc, e := query.QueryBytes(data)
if e != nil {
return e
}
s := doc.Find(".pages").Text()
temp := strings.Split(s, "/")
if len(temp) != 2 {
return errors.New("index page not found")
}
result := strings.Replace(strings.TrimSpace(temp[1]), ",", "", -1)
i, e := util.SI(result)
if e != nil {
return e
}
IndexPage = i
return nil
}
// 提取信息
func ParseIndex(data []byte) []string {
list := []string{}
doc, e := query.QueryBytes(data)
if e != nil {
return list
}
doc.Find(".post").Each(func(num int, node *goquery.Selection) {
//title := node.Find("h2").Text()
//if title == "" {
// return
//}
url, _ := node.Find("h2").Find("a").Attr("href")
if url == "" {
return
}
//tag := node.Find(".time_s").Text()
//if strings.Contains(tag, "·") {
// tag = strings.Split(tag, "·")[1]
//}
//fmt.Printf("%s,%s,%s\n", title, url, tag)
list = append(list, url)
})
return list
}
func ParseDetail(data []byte) map[string]string {
returnmap := map[string]string{
"title": "", "tags": "", "content": "", "shortcontent": "",
}
doc, e := query.QueryBytes(data)
if e != nil {
return returnmap
}
// 标题
title := doc.Find("title").Text()
if strings.TrimSpace(title) == "" {
return returnmap
}
shortcontent, _ := doc.Find(`meta[name="description"]`).Attr("content")
tags, _ := doc.Find(`meta[name="keywords"]`).Attr("content")
result := ""
doc.Find("#content").Find(".post p").Each(func(num int, node *goquery.Selection) {
temp, _ := node.Html()
result = result + "<p>" + temp + "</p>"
})
returnmap["title"] = strings.Replace(title,"\"","'",-1)
returnmap["tags"] = strings.Replace(tags,"\"","'",-1)
returnmap["shortcontent"] = strings.Replace(shortcontent,"\"","'",-1)
returnmap["content"] = strings.Replace(result,"\"","'",-1)
return returnmap
}
三个函数分别是获取首页中页数,抽取非详情页的URl和抽取详情页的信息。
5.clear.go 失败清除功能
package jiandan
import "github.com/hunterhug/GoSpider/spider"
// 将Doing移到Todo
func Clear() {
for {
s, _ := RedisClient.Brpoplpush(RedisListDoing, RedisListTodo, 0)
spider.Log().Info("movw :" + s)
}
}
由于爬虫中途会死亡,所以doing的Redis池会反应出来,所以我们每次失败后先将Doing的URL移到Todo
6.index.go 非详情页抓取,保存本地,打Redis
package jiandan
import (
"fmt"
"github.com/hunterhug/GoSpider/spider"
"github.com/hunterhug/GoSpider/util"
"path/filepath"
)
var (
// 信号量
indexstopchan chan bool
)
// 首页启动入口,包括所有非详情页面的抓取
// 抓取网址到redis,因为页数经常变动,所以这个爬虫比较暴力,借助文件夹功能接力,如果页面更新,请将data数据夹删除
func IndexSpiderRun() {
// 获取首页页数并把首页网址打到redis
IndexStep()
// 按顺序抓取页面,打到redis
PagesStep()
}
// 步骤1:首页随便取只爬虫抓取
func IndexStep() {
s, ok := spider.Pool.Get(IndexSpiderNamePrefix + "-0")
if !ok {
spider.Log().Panic("IndexStep:Get Index Spider error!")
}
// 爬取首页
s.SetUrl(Url).SetMethod("get").SetHost(Host)
data, e := s.Go()
if e != nil {
// 错误直接退出
spider.Log().Panicf("Get Index Error:%s", e.Error())
}
spider.Log().Info("Catch Index!")
// 实验的
indexfile := filepath.Join(RootDir, "data", "index.html")
e = util.SaveToFile(indexfile, data)
if e != nil {
spider.Log().Errorf("Save Index Error:%s", e.Error())
}
// 获取页数
e = ParseIndexNum(data)
if e != nil {
spider.Log().Panic(e.Error())
}
SentRedis(ParseIndex(data))
}
// 步骤2:分配任务
func PagesStep() {
urllist := []string{}
for i := 2; i <= IndexPage; i++ {
urllist = append(urllist, fmt.Sprintf("%s/page/%d", Url, i))
}
// 分配任务
tasks, e := util.DevideStringList(urllist, IndexSpiderNum)
if e != nil {
spider.Log().Panic(e.Error())
}
// 任务开始
for i, task := range tasks {
go PagesTaskGoStep(i, task)
}
for i, _ := range tasks {
// 等待爬虫结束
<-indexstopchan
spider.Log().Infof("index spider %s-%d finish", IndexSpiderNamePrefix, i)
}
}
// 步骤2接力:多只爬虫并发抓页面
func PagesTaskGoStep(name int, task []string) {
var e error
var data []byte
// 获取池中爬虫
spidername := fmt.Sprintf("%s-%d", IndexSpiderNamePrefix, name)
s, ok := spider.Pool.Get(spidername)
if !ok {
spider.Log().Panicf("Pool Spider %s not get", spidername)
}
Outloop:
for _, url := range task {
// 文件存在,那么不抓
pagename := fmt.Sprintf("%s.html", util.ValidFileName(url))
savepath := filepath.Join(RootDir, "data", pagename)
if util.FileExist(savepath) {
spider.Log().Infof("page %s Exist", pagename)
data, e = util.ReadfromFile(savepath)
if e != nil {
spider.Log().Errorf("take data from exist file error:%s", e.Error())
} else {
SentRedis(ParseIndex(data))
}
continue
}
s.SetUrl(url)
s.SetRefer(s.Preurl)
retrynum := 5
for {
if retrynum == 0 {
goto Outloop
}
data, e = s.Go()
if e != nil {
spider.Log().Errorf("%s: index page %s fetch error:%s,remain %d times", spidername, url, e.Error(), retrynum)
retrynum = retrynum - 1
continue
}
SentRedis(ParseIndex(data))
spider.Log().Infof("%s:index page %s fetch!", spidername, url)
break
}
// 保存文件
e = util.SaveToFile(savepath, data)
if e != nil {
spider.Log().Errorf("Save page %s Fail:%s", pagename, e.Error())
}
spider.Log().Infof("Save page %s Done", pagename)
}
indexstopchan <- true
}
首先,先设置信号量,方便结束,我们先抓取首页,解析首页页数,存到本地,打Redis,再以此多只爬虫分配任何,抓取各个页面,先判断本地是否存在文件,存在读取解析插到Redis,不存在抓取存入本地,解析,打到Redis。
同时,打到Redis的Url,这时另外的爬虫会分布式抓取这些详情页,见下面:
7.detail.go 抓取详情页,保存本地,和数据库
package jiandan
import (
"fmt"
"github.com/hunterhug/GoSpider/spider"
"github.com/hunterhug/GoSpider/util"
"path/filepath"
)
// 详情页爬虫
func DetailSpidersRun() {
for i := 0; i < DetailSpiderNum; i++ {
go DetailTaskStep(i)
}
}
func DetailTaskStep(name int) {
spidername := fmt.Sprintf("%s-%d", DetailSpiderNamePrefix, name)
detailpath := filepath.Join(RootDir, "data", "detail")
s, ok := spider.Pool.Get(spidername)
if !ok {
spider.Log().Panicf("Pool Spider %s not get", spidername)
}
for {
// 将Todo移到Doing
url, e := RedisClient.Brpoplpush(RedisListTodo, RedisListDoing, 0)
if e != nil {
spider.Log().Errorf("BrpopLpush % error:%s", url, e.Error())
break
}
// Done已经存在
ok, _ := RedisClient.Hexists(RedisListDone, url)
if ok {
// 删除Doing!
RedisClient.Lrem(RedisListDoing, 0, url)
continue
}
// 文件存在不抓!
filename := filepath.Join(detailpath, util.ValidFileName(url))
if util.FileExist(filename) {
spider.Log().Infof("file:%s exist", filename)
// 删除Doing!
RedisClient.Lrem(RedisListDoing, 0, url)
// 读取后解析存储
data, e := util.ReadfromFile(filename)
if e != nil {
spider.Log().Errorf("take from file %s error: %s", filename, e.Error())
} else {
SaveToMysql(url, ParseDetail(data))
}
RedisClient.Hset(RedisListDone, url, "")
continue
}
s.SetUrl(url)
retrynum := 5
for {
if retrynum == 0 {
break
}
data, e := s.Go()
if e != nil {
spider.Log().Errorf("%s:detail url %s catch error:%s remian %d times", spidername, url, e.Error(), retrynum)
retrynum = retrynum - 1
continue
} else {
spider.Log().Infof("catch url:%s", url)
e := util.SaveToFile(filename, data)
if e != nil {
spider.Log().Errorf("file %s save error:%s", filename, e.Error())
}
SaveToMysql(url, ParseDetail(data))
// 删除Doing!
RedisClient.Lrem(RedisListDoing, 0, url)
// 送到Done中
RedisClient.Hset(RedisListDone, url, "")
break
}
}
}
}
会阻塞拿取todo队列的URL,打到doing后开始抓取,抓取结束删除doing URL,如果检测到本地存在详情页,则直接删除doing,然后读取文件,解析文件,打到MYSQL。如果抓取失败,会重试5次!
五、结果
结果,总共抓取了56,961 篇文章
分布式爬虫,下一篇,抓取妹子图:啥Redis都不用,准备好网速就行!
Golang分布式爬虫:抓取煎蛋文章|Redis/Mysql|56,961 篇文章的更多相关文章
- [Java]使用HttpClient实现一个简单爬虫,抓取煎蛋妹子图
第一篇文章,就从一个简单爬虫开始吧. 这只虫子的功能很简单,抓取到”煎蛋网xxoo”网页(http://jandan.net/ooxx/page-1537),解析出其中的妹子图,保存至本地. 先放结果 ...
- python爬虫学习(1)__抓取煎蛋图片
#coding=utf-8 #python_demo 爬取煎蛋妹子图在本地文件夹 import requests import threading import time import os from ...
- python3爬虫爬取煎蛋网妹纸图片(上篇)
其实之前实现过这个功能,是使用selenium模拟浏览器页面点击来完成的,但是效率实际上相对来说较低.本次以解密参数来完成爬取的过程. 首先打开煎蛋网http://jandan.net/ooxx,查看 ...
- 用python来抓取“煎蛋网”上面的美女图片,尺度很大哦!哈哈
所用Python环境为:python 3.3.2 用到的库为:urllib.request re 废话不多说,先上代码: import urllib.request import re #获 ...
- python爬虫–爬取煎蛋网妹子图片
前几天刚学了python网络编程,书里没什么实践项目,只好到网上找点东西做. 一直对爬虫很好奇,所以不妨从爬虫先入手吧. Python版本:3.6 这是我看的教程:Python - Jack -Cui ...
- 基于scrapy的分布式爬虫抓取新浪微博个人信息和微博内容存入MySQL
为了学习机器学习深度学习和文本挖掘方面的知识,需要获取一定的数据,新浪微博的大量数据可以作为此次研究历程的对象 一.环境准备 python 2.7 scrapy框架的部署(可以查看上一篇博客的简 ...
- Python分布式爬虫抓取知乎用户信息并进行数据分析
在以前的文章中,我写过一篇使用selenium来模拟登录知乎的文章,然后在很长一段时间里都没有然后了... 不过在最近,我突然觉得,既然已经模拟登录到了知乎了,为什么不继续玩玩呢?所以就创了一个项目, ...
- Python 爬虫 爬取 煎蛋网 图片
今天, 试着爬取了煎蛋网的图片. 用到的包: urllib.request os 分别使用几个函数,来控制下载的图片的页数,获取图片的网页,获取网页页数以及保存图片到本地.过程简单清晰明了 直接上源代 ...
- python3爬虫爬取煎蛋网妹纸图片(下篇)2018.6.25有效
分析完了真实图片链接地址,下面要做的就是写代码去实现了.想直接看源代码的可以点击这里 大致思路是:获取一个页面的的html---->使用正则表达式提取出图片hash值并进行base64解码--- ...
随机推荐
- 【JSON学习之道】js操作JSON
JSON (JavaScript Object Notation)一种简单的数据格式,比xml更轻巧. JSON 是 JavaScript 原生格式,这意味着在 JavaScript 中处理 JSON ...
- Centos7 安装 zabbix3.2
简介: Zabbix的一个很优秀的分布式监控服务器, 它有两部分组成: 1. “zabbix-server”用来收集并且在web端展示数据 2. “zabbix-agent”用来采集数据,发送给ser ...
- 关于hession 随笔
今天遇到一个问题,纠结了很久也没有解决,情况是这样的, 我这个项目使用的是 hession 通信.我做的业务很简单,只是新加了一个接口 ,这 个接口是广告那一块的,数据库在之前的项目里面都没有使用到 ...
- JavaWeb之MVC模式
一.什么是MVC设计模式? MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model).视图(View)和控制器(Contr ...
- HTML&CSS Table元素详细解说
1.预热 css样式多如牛毛,我不可能一个一个去讲,那样好像背字典一样,我相信你们也不喜欢这样的方式.所以,我会在实战中慢慢和你讲解,然后,你记住一些重要的css属性就可以了.关键是,你要学会去查资料 ...
- 网络信息安全攻防学习平台 上传,解密通关writeup
上传关 [1]查看源代码,发现JS代码.提交时onclick进行过验证.ctrl+shift+i 打开开发者工具,将conclick修改为 return True,即可以上传上传php文件,拿到KEY ...
- MySQL相关信息(二)
1.修改MySQL提示符 (1)连接客户端时通过参数指定 shell>mysql -u root -p --prompt 提示符 C:\Users\Administrator>mysql ...
- ABP入门系列(14)——应用BootstrapTable表格插件
ABP入门系列目录--学习Abp框架之实操演练 源码路径:Github-LearningMpaAbp 1. 引言 之前的文章ABP入门系列(7)--分页实现讲解了如何进行分页展示,但其分页展示仅适用于 ...
- 连接池 DBCP c3p0以及分页的案例
1. 连接池 思考: 程序中连接如何管理? 连接资源宝贵:需要对连接管理 连接: a) 操作数据库,创建连接 b) 操作结束, 关闭! 分析: 涉及频繁的连接的打开.关闭,影响程序的运行效率! 连接 ...
- 运行错误:应用程序无法启动因为并行配置不正确。the application has failed to start because its side-by-side configuration is incorrect 解决方法
问题描述: 当电脑同时安装VS2008和VS2008 SP1时,编译出来的Visual C++程序的manifest 文件会默认引用VS2008的MFC版本和CRT版本.如下: <depende ...