小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
与其每天翻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容器
- 创建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"]
- 创建shell脚本 start.sh
#!/bin/sh
cd $(dirname "$0")
cd ..
./app > run.log 2>&1
- 构建镜像
docker build -t martinfan/house .
- 后台运行容器
docker run -itd martinfan/house
- 查看容器状态
docker ps -a
代码目录结构:
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容器同时只能管理一个进程,如果这个进程退出那么容器也就退出了,但这不表示容器只能运行一个进程(其他进程可在后台运行),但是要使容器不退出必须有一个前台执行的进程。