我是如何收集全网行业网站的 - Golang 爬虫示例代码

前言

最近发现服务器磁盘快满了,顺手翻了下服务器上的数据库,惊讶地发现有一个之前写的爬虫程序,它生成的数据竟然占了整整200GB的空间!闲来无事,我决定重新查看这段代码,回顾一下当时我是如何编写这个网站爬虫,并整理成这篇文章,分享给大家。

介绍

这是一款我用 Golang 编写的全网网址采集程序,能够自动爬取和分析互联网上几乎所有能够触及的网站信息。通过它,网站的标题、站点描述、微信号、QQ号、联系电话、运行环境、IP 信息,甚至是网站所使用的框架等都能自动采集和整理。这个项目最初是为了分析行业内的竞争对手而写的,后来我不断优化,扩展成了可以对全网行业网站进行数据收集和分类的工具。

设计思路

这个 Golang 爬虫的核心设计思路是基于广度优先搜索(BFS)的递归式爬取。它会从一个种子 URL 开始,不断抓取页面上的所有链接,并递归地继续访问这些链接,直到所有可触及的页面都被爬取。为了避免陷入死循环和重复抓取,我设计了一些去重机制和 URL 过滤规则。同时,为了避免对目标站点造成压力,爬虫还设置了并发控制。

具体流程

  1. 种子网站的选择:首先要从一组初始 URL 开始,即所谓的种子网站。这些 URL 可以通过一些目录网站或者搜索引擎获取。实际上,我是以 hao123以及另外几个网址导航作为种子网站。
  2. 网页抓取:使用 Golang 的 gorequest 包进行 HTTP 请求,抓取网页内容,由于网页会有GBK等非UTF-8编码的页面,需要进行编码判断,并对非UTF-8编码的页面进行转码。
  3. 页面解析:使用 goquery 包来解析 HTML 页面,提取所需的信息,如标题、描述、联系方式等。
  4. 重复检查和限速:为了避免抓取重复内容,设计了一个 URL 去重机制,同时为了防止爬虫对目标网站的服务器造成负担,加入了爬虫限速和并发限制。
  5. 运行环境检测:通过分析页面的头信息和脚本,可以大致推断出网站所使用的技术栈,比如服务器、框架等。
  6. 数据分析与清洗:采集到的数据通过一定的清洗和筛选,剔除无效信息,然后数据会存储在数据库中,方便后续的检索和分析。我使用了 MySQL 存储结构化数据。

代码实现

接下来,我将展示部分 Golang 爬虫的核心代码,帮助大家更好地理解其中的设计逻辑。

SingleData 函数负责从指定网站抓取数据并存储到数据库。主要步骤包括:

  • 更新网站状态为正在抓取。
  • 调用GetWebsite抓取网站内容,并提取数据。
  • 存储抓取结果和内容数据。
  • 如果存在链接,则处理每个链接,包括限制子域名数量、去重,并将新发现的网站信息存入数据库。

GetWebsite 函数用于抓取单个网站的数据。流程如下:

  • 使用提供的URL发起请求。
  • 处理响应,提取如标题、描述等元信息。
  • 清洗HTML内容,去除脚本和样式标签。
  • 解析文档以获取更多细节,如联系方式等。
  • 收集页面内的链接。

CollectLinks 函数从给定的文档中收集所有有效链接,排除非域名,排除链轮类网址,进行基本验证后返回。

// SingleData 单个数据抓取
func SingleData(website Website) {
	//锁定当前数据
	DB.Model(&Website{}).Where("`domain` = ?", website.Domain).UpdateColumn("status", 2)
	log.Println(fmt.Sprintf("开始采集:%s://%s", website.Scheme, website.Domain))
	err := website.GetWebsite()
	if err == nil {
		website.Status = 1
	} else {
		website.Status = 3
	}
	log.Println(fmt.Sprintf("入库2:%s", website.Domain))
	DB.Where("`domain` = ?", website.Domain).Updates(&website)
	// 同时写入data
	contentData := WebsiteData{
		ID:      website.ID,
		Content: website.Content,
	}
	DB.Where("`id` = ?", contentData.ID).FirstOrCreate(&contentData)
	// end
	if len(website.Links) > 0 {
		for _, v := range website.Links {
			//如果超过了5个子域名,则直接抛弃
			item, itemOk := topDomains.Load(v.TopDomain)
			if itemOk {
				if item.(int) >= 4 {
					//跳过这个记录
					continue
				}
			}
			if _, ok := existsDomain.Load(v.Domain); ok {
				continue
			}
			if itemOk {
				topDomains.Store(v.TopDomain, item.(int)+1)
			} else {
				topDomains.Store(v.TopDomain, 1)
			}
			existsDomain.Store(v.Domain, true)
			runMap.Store(v.Domain, v.Scheme)
			webData := Website{
				Url:       v.Url,
				Domain:    v.Domain,
				TopDomain: v.TopDomain,
				Scheme:    v.Scheme,
				Title:     v.Title,
			}
			DB.Clauses(clause.OnConflict{
				DoNothing: true,
			}).Where("`domain` = ?", webData.Domain).Create(&webData)
			log.Println(fmt.Sprintf("入库:%s", v.Domain))
		}
	}
}

// GetWebsite 一个域名数据抓取
func (website *Website) GetWebsite() error {
	if website.Url == "" {
		website.Url = website.Scheme + "://" + website.Domain
	}
	ops := &Options{}
	if ProxyValid {
		ops.ProxyIP = JsonData.DBConfig.Proxy
	}
	requestData, err := Request(website.Url, ops)
	if err != nil {
		log.Println(err)
		return err
	}
	// 注入内容
	website.Content = requestData.Body

	if requestData.Domain != "" {
		website.Domain = requestData.Domain
		website.TopDomain = getTopDomain(website.Domain)
	}
	if requestData.Scheme != "" {
		website.Scheme = requestData.Scheme
	}
	website.Server = requestData.Server

	//获取IP
	conn, err := net.ResolveIPAddr("ip", website.Domain)
	if err == nil {
		website.IP = conn.String()
	}

	//尝试判断cms
	website.Cms = getCms(requestData.Body)

	//先删除一些不必要的标签
	re, _ := regexp.Compile("\\<style[\\S\\s]+?\\</style\\>")
	requestData.Body = re.ReplaceAllString(requestData.Body, "")
	re, _ = regexp.Compile("\\<script[\\S\\s]+?\\</script\\>")
	requestData.Body = re.ReplaceAllString(requestData.Body, "")
	//解析文档内容
	htmlR := strings.NewReader(requestData.Body)
	doc, err := goquery.NewDocumentFromReader(htmlR)
	if err != nil {
		fmt.Println(err)
		return err
	}
	contentText := doc.Text()
	contentText = strings.ReplaceAll(contentText, "\n", " ")
	contentText = strings.ReplaceAll(contentText, "\r", " ")
	contentText = strings.ReplaceAll(contentText, "\t", " ")
	website.Title = doc.Find("title").Text()
	desc, exists := doc.Find("meta[name=description]").Attr("content")
	if exists {
		website.Description = desc
	} else {
		website.Description = strings.ReplaceAll(contentText, " ", "")
	}
	nameRune := []rune(website.Description)
	curLen := len(nameRune)
	if curLen > 200 {
		website.Description = string(nameRune[:200])
	}
	nameRune = []rune(website.Title)
	curLen = len(nameRune)
	if curLen > 200 {
		website.Title = string(nameRune[:200])
	}
	//尝试获取微信
	reg := regexp.MustCompile(`(?i)(微信|微信客服|微信号|微信咨询|微信服务)\s*(:|:|\s)\s*([a-z0-9\-_]{4,30})`)
	match := reg.FindStringSubmatch(contentText)
	if len(match) > 1 {
		website.WeChat = match[3]
	}
	//尝试获取QQ
	reg = regexp.MustCompile(`(?i)(QQ|QQ客服|QQ号|QQ号码|QQ咨询|QQ联系|QQ交谈)\s*(:|:|\s)\s*([0-9]{5,12})`)
	match = reg.FindStringSubmatch(contentText)
	if len(match) > 1 { //
		website.QQ = match[3]
	}
	//尝试获取电话
	reg = regexp.MustCompile(`([0148][1-9][0-9][0-9\-]{4,15})`)
	match = reg.FindStringSubmatch(contentText)
	if len(match) > 1 {
		website.Cellphone = match[1]
	}
	website.Links = CollectLinks(doc)

	return nil
}

// CollectLinks 读取页面链接
func CollectLinks(doc *goquery.Document) []Link {
	var links []Link
	aLinks := doc.Find("a")
	//读取所有连接
	existsLinks := map[string]bool{}
	for i := range aLinks.Nodes {
		href, exists := aLinks.Eq(i).Attr("href")
		title := strings.TrimSpace(aLinks.Eq(i).Text())
		if exists {
			scheme, host := ParseDomain(href)
			if host != "" && scheme != "" {
				hosts := strings.Split(host, ".")
				if len(hosts) < 2 || (len(hosts) > 3 && hosts[0] != "www") || (len(hosts) == 3 && hosts[0] != "www" && len(hosts[1]) > 4) {
					//refuse
				} else {
					if !existsLinks[host] {
						//去重
						existsLinks[host] = true
						links = append(links, Link{
							Title:     title,
							Url:       scheme + "://" + host,
							Domain:    host,
							TopDomain: getTopDomain(host),
							Scheme:    scheme,
						})
					}
				}
			}
		}
	}

	return links
}

结语

这就是我使用 Golang 编写的全网网址爬取程序的基本思路和部分实现。整个项目虽然简单,但在实际应用中也遇到了一些挑战,如去重、大量无效链接、泛域名、限速和防止封禁等问题。如果你也对 Golang 爬虫感兴趣,不妨尝试自己动手写一个,结合自己的需求来定制化实现!

  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值