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

zhoutk發表於2018-07-03

## 背景 要做 IP 地址歸屬地查詢,量比較大,所以想先從網上找到大部分的分配資料,寫個蜘蛛程式來抓取入庫,以後在程式的執行中不斷進行維護、更新、完善。 ## 一些關鍵點

  1. goroutine 的使用,讓程式並行執行。
  2. 正規表示式分組資訊提取的使用,正確的提取我們關注的資訊。
  3. 資料庫批量插入操作。
  4. 資料庫批量更新操作。

## 程式碼解析 按功能模組對核心程式碼進行說明 ###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 內容對應的正則為 ([^']+?) ,第一次遇到單引號結束,問號表示非貪婪匹配,括號是分組,能方便取出所匹配資訊
  • 連結顯示內容對應的正則為 ([^&lt;]+?) , 第一次遇到<時結束,第二個分組
  • FindAllStringSubmatch 函式可以取出所有子分組,子分組從下標 1 開始,0 為正則整體匹配的字串 > goroutine 流程

  • 建立一個無緩衝字串通道,作為所有協程與主程式通訊通道

  • 迴圈正則匹配結果,為每一個省的頁面分配一個協程

  • 協程獲取資料成功並批量寫資料庫,返回成功資訊到通道

  • 協程處理失敗,反回失敗資訊到通道

  • 主程式阻塞等所有協程成功或失敗返回,並列印成功或失敗資訊

### 獲取 ip 地址資訊 > 與主程式類似,注意無資訊時處理。

IpSpider.go

//獲取頁面資料
ctx := common.HttpGet(url)
//reg := regexp.MustCompile(`&lt;li&gt;&lt;a title=&quot;[\S]+&quot; href='([^']+?)'&gt;([^&lt;]+?)&lt;/a&gt;&lt;/li&gt;`)
//兩個分組分別對應IP段開始與結束
reg := regexp.MustCompile(`&lt;dd&gt;&lt;span class=&quot;v_l&quot;&gt;([^&lt;]+?)&lt;/span&gt;&lt;span class=&quot;v_r&quot;&gt;([^&lt;]+?)&lt;/span&gt;&lt;div class=&quot;clearfix&quot;&gt;&lt;/div&gt;&lt;/dd&gt;`)
//&lt;dd&gt;&lt;span class=&quot;v_l&quot;&gt;49.64.0.0&lt;/span&gt;&lt;span class=&quot;v_r&quot;&gt;49.95.255.255&lt;/span&gt;&lt;div class=&quot;clearfix&quot;&gt;&lt;/div&gt;&lt;/dd&gt;
//取得所有匹配的分組資訊
ip := reg.FindAllStringSubmatch(string(ctx), -1)
//沒有取得任何資訊,提前返回,很重要,不然主程式會一直等待結束不了
if len(ip) == 0 {
    ch &lt;- &quot;There are no data exist.&quot;
    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 += &quot;(?,?,?),&quot;        //佔位符
}
vss = vss[0:len(vss) -1]     //去掉最後的逗號
var configs interface{}      //從配置檔案取資料庫資訊
fr, err := os.Open(&quot;./configs.json&quot;)    //配置檔案內容請參照上篇文章《golang實現mysql資料庫備份》
if err != nil {
    ch &lt;- err.Error()
    return err
}
decoder := json.NewDecoder(fr)
err = decoder.Decode(&amp;configs)

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

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

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

### 批量修改資料庫 > 資料庫中的 ip_comp 欄位,是代表運營商資訊,需要從運營商頁面進入進行資料獲取,只需改一下入口 url 重新執行程式就能正確抓取,但這時入庫就不是新增了,而是更新,請簽出到 4729e66 版本。

//前提資料庫表定義要設定唯一索引,主鍵或其它定義的unique索引
...

sqlstr := &quot;insert into ip_addr_info (ip_addr_begin,ip_addr_end,ip_comp) values &quot; + vss +
    //提供更新(唯一索引衝突時)時要對應原欄位與值
    &quot; ON DUPLICATE KEY UPDATE ip_addr_begin = values(ip_addr_begin), ip_addr_end = values(ip_addr_end), ip_comp = values(ip_comp)&quot;
stmt, err := dao.Prepare(sqlstr)
rs, err := stmt.Exec(vs...)
if err != nil {
    ch &lt;- err.Error()
    return err
}else {
    affect, _ := rs.RowsAffected()
    ch &lt;- &quot;Province: &quot; + province + &quot;, affect: &quot; + strconv.FormatInt(affect, 10)
    return nil
}

### 待改進的方面 把入口 url 提到配置中,使用策略模式,讓匹配規則抽象成策略,目標是不改程式,調整配置檔案就可以抓取不同的網頁。 ## 專案地址

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 語言,瞭解了一種全新的併發程式設計模式,熟悉了具體的資料庫操作方法,給自己生成了一個方便的工具。

更多原創文章乾貨分享,請關注公眾號
  • golang實現抓取IP地址的蜘蛛程式
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章