golang實現抓取IP地址的蜘蛛程序

背景

要作IP地址歸屬地查詢,量比較大,因此想先從網上找到大部分的分配數據,寫個蜘蛛程序來抓取入庫,之後在程序的運行中不斷進行維護、更新、完善。mysql

一些關鍵點

  1. goroutine的使用,讓程序並行運行。
  2. 正則表達式分組信息提取的使用,正確的提取咱們關注的信息。
  3. 數據庫批量插入操做。
  4. 數據庫批量更新操做。

代碼解析

按功能模塊對核心代碼進行說明git

ip.go

主進程,實現goroutine的調用。
func main()  {
    //利用go基本庫封裝的網頁抓取函數,後面有說明
    ctx := common.HttpGet("http://ips.chacuo.net/")  
    //正則表達式,有兩個分組(兩組小括號),分別取城市信息與url,具體分析代碼後面有說明
    reg := regexp.MustCompile(`<li><a title="[\S]+" href='([^']+?)'>([^<]+?)</a></li>`)        
    //取得頁面上全部的城市及相應url
    ips := reg.FindAllStringSubmatch(string(ctx), -1)

    ch := make(chan string)    //創建無緩衝字符串通道

    for _, el := range ips {    //一個協程處理一個具體頁面
        go ipSpider.SpiderOnPage(el[1], el[2], ch)
    }

    for range ips {            //阻塞等待全部抓取工做所有完成
        fmt.Println(<-ch)
    }
}
正則表達式說明
  • 主進程針對全部省有入口頁面,取得每省的入口分配給一個協程去處理,每個入口是這個樣子
<a title="北京最新IP地址段" href="http://ips.chacuo.net/view/s_BJ">北京</a>
  • 請注意,這裏面變化只有三個部分(title內容,href內容,連接顯示內容),其中兩個部分是咱們須要的
  • title內容對應正則爲 [\S]+ ,非空白符
  • href內容對應的正則爲 ([^']+?) ,第一次遇到單引號結束,問號表示非貪婪匹配,括號是分組,能方便取出所匹配信息
  • 連接顯示內容對應的正則爲 ([^<]+?) , 第一次遇到<時結束,第二個分組
  • FindAllStringSubmatch函數能夠取出全部子分組,子分組從下標1開始,0爲正則總體匹配的字符串
goroutine 流程
  • 創建一個無緩衝字符串通道,做爲全部協程與主進程通訊通道
  • 循環正則匹配結果,爲每個省的頁面分配一個協程
  • 協程獲取數據成功並批量寫數據庫,返回成功信息到通道
  • 協程處理失敗,反回失敗信息到通道
  • 主進程阻塞等全部協程成功或失敗返回,並打印成功或失敗信息

獲取ip地址信息

與主進程相似,注意無信息時處理。

IpSpider.gogithub

//獲取頁面數據
    ctx := common.HttpGet(url)
    //reg := regexp.MustCompile(`<li><a title="[\S]+" href='([^']+?)'>([^<]+?)</a></li>`)
    //兩個分組分別對應IP段開始與結束
    reg := regexp.MustCompile(`<dd><span class="v_l">([^<]+?)</span><span class="v_r">([^<]+?)</span><div class="clearfix"></div></dd>`)
    //<dd><span class="v_l">49.64.0.0</span><span class="v_r">49.95.255.255</span><div class="clearfix"></div></dd>
    //取得全部匹配的分組信息
    ip := reg.FindAllStringSubmatch(string(ctx), -1)
    //沒有取得任何信息,提早返回,很重要,否則主進程會一直等待結束不了
    if len(ip) == 0 {
        ch <- "There are no data exist."
        return nil
    }

數據庫表結構生成語句

CREATE TABLE `ip_addr_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '索引,自動增加',
  `ip_addr_begin` varchar(32) NOT NULL DEFAULT '' COMMENT 'ip地址段開始',
  `ip_addr_end` varchar(32) DEFAULT '' COMMENT 'ip地址段結束',
  `province` varchar(32) DEFAULT '' COMMENT '所屬省',
  `ip_comp` varchar(32) DEFAULT '' COMMENT '運營商',
  PRIMARY KEY (`id`),
  UNIQUE KEY `ip_addr` (`ip_addr_begin`,`ip_addr_end`)
) ENGINE=InnoDB AUTO_INCREMENT=7268 DEFAULT CHARSET=utf8 COMMENT='表';

批量寫入數據庫

循環處理抓取數據,生成批量寫入語句及輸入參數,請簽到出到afc9ebd版本。
var vs [] interface{}        //存儲輸入參數的接口數組
    var vss string               //待拼接的輸入參數佔位符字符串
    for _, el := range ip {      //處理全部的數據
        vs = append(vs, el[1], el[2], province)    //每一列包括開始地址、結束地址與省份
        vss += "(?,?,?),"        //佔位符
    }
    vss = vss[0:len(vss) -1]     //去掉最後的逗號
    var configs interface{}      //從配置文件取數據庫信息
    fr, err := os.Open("./configs.json")    //配置文件內容請參照上篇文章《golang實現mysql數據庫備份》
    if err != nil {
        ch <- err.Error()
        return err
    }
    decoder := json.NewDecoder(fr)
    err = decoder.Decode(&configs)

    confs := configs.(map[string]interface{})
    dialect := confs["database_dialect"].(string)

    dbConf := confs["db_"+dialect+"_config"].(map[string]interface{})
    dbHost := dbConf["db_host"].(string)
    dbPort := strconv.FormatFloat(dbConf["db_port"].(float64), 'f', -1, 32)
    dbUser := dbConf["db_user"].(string)
    dbPass := dbConf["db_pass"].(string)
    dbName := dbConf["db_name"].(string)
    dbCharset := dbConf["db_charset"].(string)

    dao, err := mysql.Open(dialect, dbUser + ":"+dbPass+"@tcp("+dbHost+":"+dbPort+")/"+dbName+"?charset="+dbCharset)
    defer dao.Close()
    if err != nil {
        ch <- err.Error()
        return err
    }
    //批量插入語句拼接
    sqlstr := "insert into ip_addr_info (ip_addr_begin,ip_addr_end,province) values " + vss
    stmt, err := dao.Prepare(sqlstr)    //預處理帶參數的sql語句
    rs, err := stmt.Exec(vs...)         //帶參數執行sql語句
    if err != nil {                    //出錯,返回錯誤信息
        ch <- err.Error()
        return err
    }else {                            //成功,返回成功信息
        affect, _ := rs.RowsAffected()
        ch <- "Province: " + province + ", affect: " + strconv.FormatInt(affect, 10)
        return nil
    }

批量修改數據庫

數據庫中的ip_comp字段,是表明運營商信息,須要從運營商頁面進入進行數據獲取,只需改一下入口url從新運行程序就能正確抓取,但這時入庫就不是新增了,而是更新,請簽出到4729e66版本。
//前提數據庫表定義要設定惟一索引,主鍵或其它定義的unique索引
    ...
    
    sqlstr := "insert into ip_addr_info (ip_addr_begin,ip_addr_end,ip_comp) values " + vss +
        //提供更新(惟一索引衝突時)時要對應原字段與值
        " ON DUPLICATE KEY UPDATE ip_addr_begin = values(ip_addr_begin), ip_addr_end = values(ip_addr_end), ip_comp = values(ip_comp)"
    stmt, err := dao.Prepare(sqlstr)
    rs, err := stmt.Exec(vs...)
    if err != nil {
        ch <- err.Error()
        return err
    }else {
        affect, _ := rs.RowsAffected()
        ch <- "Province: " + province + ", affect: " + strconv.FormatInt(affect, 10)
        return nil
    }

待改進的方面

把入口url提到配置中,使用策略模式,讓匹配規則抽象成策略,目標是不改程序,調整配置文件就能夠抓取不一樣的網頁。golang

項目地址

https://github.com/zhoutk/goTools

使用方法

git clone https://github.com/zhoutk/goTools
cd goTools
go get
go run ip.go

go buid ip.go
./ip

小結

熟悉了golang語言,瞭解了一種全新的併發編程模式,熟悉了具體的數據庫操做方法,給本身生成了一個方便的工具。正則表達式

相關文章
相關標籤/搜索