我是如何收集全网行业网站的 - 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 爬虫感兴趣,不妨尝试自己动手写一个,结合自己的需求来定制化实现!
如果你想看完整的源码,可以访问我的Github仓库:https://github.com/fesiong/cobweb

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容