golang+docker实现一个简易的爬虫

198 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

与其每天翻app,不如自己写程序~

简单实现一个定时任务,每天定时获取第三方的某小区房价

1.实现爬虫代码

package main

import (
   "errors"
   "fmt"
   "github.com/jinzhu/gorm"
   _ "github.com/jinzhu/gorm/dialects/mysql"
   "github.com/robfig/cron/v3"
   "io/ioutil"
   "log"
   "net/http"
   "strconv"
   "strings"
   "time"
)

type House struct {
   Id   int64  `json:"id"`
   Name string `json:"name"`
   Url  string `json:"url"`
}

type Price struct {
   Id      int64  `json:"id"`
   HouseId int64  `json:"house_id"`
   Price   int64  `json:"price"`
   Date    string `json:"date"`
}

var Parser cron.Parser

func main() {
   Parser = cron.NewParser(
      cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow,
   )
   c := cron.New(cron.WithParser(Parser))
   c.AddFunc("0 27 17 * * ? ", func() {
      err := RecordPrice()
      if err != nil {
         log.Fatalf("err:%v", err)
         return
      }
   })
   c.Start()
   log.Println("service start:")
   select {}
}

func RecordPrice() error {
   pwd := "123456"
   db, err := gorm.Open("mysql", fmt.Sprintf("root:%s@(127.0.0.1:3306)/martin?charset=utf8mb4&parseTime=True&loc=Local", pwd))
   if err != nil {
      panic(err)
   }
   defer db.Close()

   //打开日志(查看mysql执行log,生产就不需要了)
   db.LogMode(true)

   now := time.Now()
   today := fmt.Sprintf("%d-%d-%d", now.Year(), now.Month(), now.Day())
   count := 0
   db.Model(&Price{}).Where("date = ?", today).Count(&count)
   if db.Error != nil {
      return db.Error
   }
   if count == 0 {
      price := getPrice("https://ks.anjuke.com/community/view/xxxxxx")
      if price == 0 {
         return errors.New("页面结果有误")
      } else {
         db.Create(&Price{
            HouseId: 1,
            Price:   price,
            Date:    today,
         })
         log.Printf("今日(%s):同步完成\n", today)
      }
   } else {
      log.Printf("今日(%s)已经同步过数据\n", today)
   }

   return nil
}

//根据页面元素获取价格
func getPrice(url string) int64 {
   body := fetch(url)
   index := strings.Index(body, "comm_midprice")
   if index == 0 {
      return 0
   }

   priceStr := body[index+16:]
   endIndex := strings.Index(priceStr, """)
   finalPriceStr := priceStr[:endIndex]
   price, err := strconv.ParseInt(finalPriceStr, 10, 64)
   if err != nil {
      log.Printf("format err:%v\n", err)
      return 0
   }
   return price
}

//爬取网页元素
func fetch(url string) string {
   client := &http.Client{}
   req, _ := http.NewRequest("GET", url, nil)
   //设置ua,模拟浏览器访问
   req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36")
   resp, err := client.Do(req)
   if err != nil {
      log.Printf("Http get err:%v\n", err)
      return ""
   }
   if resp.StatusCode != 200 {
      log.Printf("Http status code:%v\n", resp.StatusCode)
      return ""
   }
   defer resp.Body.Close()
   body, err := ioutil.ReadAll(resp.Body)
   if err != nil {
      log.Printf("Read err:%v\n", err)
      return ""
   }
   return string(body)
}

Tips:
短时间多次爬取页面,网站基本都会有反爬机制,页面会跳转到验证页面。所以这里只是一个简易版本。如果要实现一个健壮的爬虫程序,selenium + PhantomJs,有兴趣的童鞋可以去研究研究~

2.放入docker容器

  1. 创建Dockerfile
FROM golang:1.13.5-alpine
WORKDIR /go/src/app
COPY . .

ENV GOPROXY=https://goproxy.cn
RUN go get -d -v ./...
RUN go build -o app

RUN sed -i 's/\r//g' ./bin/start.sh
RUN chmod +x ./bin/start.sh
ENTRYPOINT ["./bin/start.sh"]
  1. 创建shell脚本 start.sh
#!/bin/sh
cd $(dirname "$0")
cd ..
./app > run.log 2>&1
  1. 构建镜像
docker build -t martinfan/house .
  1. 后台运行容器
docker run -itd martinfan/house
  1. 查看容器状态
docker ps -a

image.png

代码目录结构:

house
    |--bin
          |--start.sh
    |--main.go
    |--go.mod
    |--Dockerfile

至此,基础功能已实现~

Tips:
1.在windows平台下,换行符是\r\n;而在linux平台下,换行符是\n。于是\r被vim解释成了^M。所以可以利用sed -i 's/\r//g' filename 替换掉。

2.Docker容器同时只能管理一个进程,如果这个进程退出那么容器也就退出了,但这不表示容器只能运行一个进程(其他进程可在后台运行),但是要使容器不退出必须有一个前台执行的进程。