目录
引言
环境准备
如何使用
main入口代码实现
实现采集网络接口
总结
其他资源
引言
Go-Zero 是一个高性能、可扩展的微服务框架,专为 Go 语言设计。它提供了丰富的功能,如 RPC、RESTful API 支持、服务发现、熔断器、限流器等,使开发者能够快速构建分布式系统。在众多数据存储选项中,MongoDB 作为一种流行的 NoSQL 数据库,以其灵活性和高性能受到广泛欢迎。本文将介绍如何在 Go-Zero 项目中集成 MongoDB,来看下借助go-zero的goctl工具,实现golang采集网络数据入MongoDB库是多么的简单。
这里以一个小目标任务为例:
采集知乎日报每天的日报数据入MongoDB库(注:仅用于个人学习研究目的,禁止用于其它用途),详细介绍下 Go-Zero 框架集成 MongoDB 数据库的使用。
顺便推荐下go-zero微服务框架,即强大又好用。不但用于微服务,它里面的各个模块也是可以独立拿来用。这里的小任务(采集数据入mongo库),没有使用go-zero的微服务,仅使用了它里面的logx日志模块、httpc客户端模块,config配置文件操作模块和goctl自动化生成 MongoDB的model层代码。
环境准备
goctl:确保你已经安装了 Go-Zero的goctl命令行工具。goctl 是 go-zero 的内置脚手架,是提升开发效率的一大利器,可以一键生成代码、文档、部署 k8s yaml、dockerfile 等。
$ go install github.com/zeromicro/go-zero/tools/goctl@latest
或者手动下载安装:
goctl 安装 | go-zero Documentation
MongoDB:在本地或远程服务器上安装并运行 MongoDB 实例。
MongoDB: The Developer Data Platform | MongoDB
Go:安装最新版本的 Go 语言环境。
https://go.dev/dl/
如何使用
goctl model 为 goctl 提供的数据库模型代码生成指令,目前支持 MySQL、PostgreSQL、Mongo 的代码生成,MySQL 支持从 sql 文件和数据库连接两种方式生成。
Mongo 模型层代码的生成不同于 MySQL,MySQL 可以从 scheme_information 库中读取到一张表的信息(字段名称,数据类型,索引等), 而 Mongo 是文档型数据库,我们暂时无法从 db 中读取某一条记录来实现字段信息获取。
新建一项目目录文件夹,假设为collect。
#进入项目根目录
cd collect
#开始生成
goctl model mongo -t ZhiNews -dir model/zhiNews
各个参数的含义,主要用的是 -t -e -dir
-t是生成文件的前缀名称
-e表示的是生成一个简单的增删改查接口,-dir是生成文档放在的目录
-c是带缓存的
生成model层代码执行以上命令即可。很简单,不需要提前编写什么模型文件,以上命令将自动在model/zhiNews目录下生成模型框架代码,如果需要扩展其他字段类型,直接修改生成的zhinewstypes.go文件。
zhinewsmodelgen.go
这个文件是自动生成的,不要修改。
// Code generated by goctl. DO NOT EDIT.
package model
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type zhiNewsModel interface {
Insert(ctx context.Context, data *ZhiNews) error
FindOne(ctx context.Context, id string) (*ZhiNews, error)
Update(ctx context.Context, data *ZhiNews) (*mongo.UpdateResult, error)
Delete(ctx context.Context, id string) (int64, error)
}
type defaultZhiNewsModel struct {
conn *mon.Model
}
func newDefaultZhiNewsModel(conn *mon.Model) *defaultZhiNewsModel {
return &defaultZhiNewsModel{conn: conn}
}
func (m *defaultZhiNewsModel) Insert(ctx context.Context, data *ZhiNews) error {
if data.ID.IsZero() {
data.ID = primitive.NewObjectID()
data.CreateAt = time.Now()
data.UpdateAt = time.Now()
}
_, err := m.conn.InsertOne(ctx, data)
return err
}
func (m *defaultZhiNewsModel) FindOne(ctx context.Context, id string) (*ZhiNews, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, ErrInvalidObjectId
}
var data ZhiNews
err = m.conn.FindOne(ctx, &data, bson.M{"_id": oid})
switch err {
case nil:
return &data, nil
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultZhiNewsModel) Update(ctx context.Context, data *ZhiNews) (*mongo.UpdateResult, error) {
data.UpdateAt = time.Now()
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
func (m *defaultZhiNewsModel) Delete(ctx context.Context, id string) (int64, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return 0, ErrInvalidObjectId
}
res, err := m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return res, err
}
zhinewstypes.go
在这个文件里根据需要扩展类型字段。
package model
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type Stories struct {
Title string `bson:"title" json:"title"`
Url string `bson:"url" json:"url"`
Hint string `bson:"hint" json:"hint"`
Images string `bson:"images" json:"images"`
Id int64 `bson:"id" json:"id"`
}
type TopStories struct {
Title string `bson:"title" json:"title"`
Url string `bson:"url" json:"url"`
Hint string `bson:"hint" json:"hint"`
Images string `bson:"images" json:"images"`
Id int64 `bson:"id" json:"id"`
}
type ZhiNews struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
// TODO: Fill your own fields
Date string `bson:"date" json:"date"`
Storys []Stories `bson:"stories" json:"stories"`
TopStorys []TopStories `bson:"top_stories" json:"top_stories"`
UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
}
zhinewsmodel.go
注意,那个zhinewsmodelgen.go,文件后带gen的文件是自动生成的,默认提供的接口实现,但肯定不够用,但也不要在这里编辑。可以在zhinewsmodel.go文件中扩展实现。
如下所示:
package model
import (
"context"
"time"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
var _ ZhiNewsModel = (*customZhiNewsModel)(nil)
type (
// ZhiNewsModel is an interface to be customized, add more methods here,
// and implement the added methods in customZhiNewsModel.
ZhiNewsModel interface {
zhiNewsModel
// 增加 插入重复则更新
InsertWithUpdate(ctx context.Context, data *ZhiNews) error
// 增加 根据日期来更新
UpdateOneByDate(ctx context.Context, data *ZhiNews) (*mongo.UpdateResult, error)
// 增加 根据日期查找
FindOneByDate(ctx context.Context, date string) (*ZhiNews, error)
}
customZhiNewsModel struct {
*defaultZhiNewsModel
}
)
// NewZhiNewsModel returns a model for the mongo.
func NewZhiNewsModel(url, db, collection string) ZhiNewsModel {
conn := mon.MustNewModel(url, db, collection)
return &customZhiNewsModel{
defaultZhiNewsModel: newDefaultZhiNewsModel(conn),
}
}
func (m *customZhiNewsModel) InsertWithUpdate(ctx context.Context, data *ZhiNews) error {
err := m.Insert(ctx, data)
if mongo.IsDuplicateKeyError(err) {
_, err = m.UpdateOneByDate(ctx, data)
}
return err
}
func (m *customZhiNewsModel) UpdateOneByDate(ctx context.Context, data *ZhiNews) (*mongo.UpdateResult, error) {
dat, err := m.FindOneByDate(ctx, data.Date)
if err != nil {
return nil, err
}
data.ID = dat.ID
data.UpdateAt = time.Now()
res, err := m.conn.UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
return res, err
}
func (m *customZhiNewsModel) FindOneByDate(ctx context.Context, date string) (*ZhiNews, error) {
var data ZhiNews
err := m.conn.FindOne(ctx, &data, bson.M{"date": date})
switch err {
case nil:
return &data, nil
case mon.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
config.yaml:
MongoDB:
Url: "mongodb://test1:xxxx@175.xxx.126.10:27017/?tls=false&authSource=test1"
DbName: "test1"
Collection: "zhiNews"
main入口代码实现
package main
import (
model "collect/model/zhiNews"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"time"
"github.com/tidwall/gjson"
"github.com/zeromicro/go-zero/core/conf"
log "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/httpc"
)
type Config struct {
MongoDB struct {
Url string `json:",default=mongodb://localhost:27017"`
DbName string `json:",default=testdb"`
Collection string `json:",default=zhiNews"`
}
}
var f = flag.String("f", "etc/config.yaml", "config file")
func getZhiNews(date string) (resp *model.ZhiNews, err error) {
url := "https://news-at.zhihu.com/api/4/news/latest"
parsedDate, err := time.Parse("20060102", date)
if err != nil {
log.Errorf("Error parsing date:%s", err)
}
now := time.Now().Format("20060102")
if now != date {
url = "https://news-at.zhihu.com/api/4/news/before/" + parsedDate.AddDate(0, 0, 1).Format("20060102")
}
// 创建一个请求
r, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Error(err)
return nil, err
}
//设置常见的HTTP头
r.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3")
//ctx := context.Background()
//res, err_ := httpc.Do(ctx, http.MethodGet, url, nil)
res, err_ := httpc.DoRequest(r)
if err_ != nil {
log.Error(err_)
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
log.Errorf("Failed to read response body:%s", err)
return nil, err
}
log.Debug(res.StatusCode)
var keyVal map[string]interface{}
err = json.Unmarshal(body, &keyVal)
if err != nil {
log.Errorf("Failed to extract key value:%s", err)
}
//log.Debugf("keyVal:%v", keyVal)
if res.StatusCode != 200 {
log.Errorf("Failed to request,%s", res.Status)
return nil, errors.New(res.Status)
}
var zhi model.Stories
var responseData []model.Stories
list_ := gjson.GetBytes(body, "stories").Array()
for _, item := range list_ {
zhi.Id = item.Get("id").Int()
zhi.Title = item.Get("title").String()
zhi.Images = item.Get("images.0").String()
zhi.Url = item.Get("url").String()
zhi.Hint = item.Get("hint").String()
responseData = append(responseData, zhi)
}
var top model.TopStories
var topData []model.TopStories
list_1 := gjson.GetBytes(body, "top_stories").Array()
for _, item := range list_1 {
top.Id = item.Get("id").Int()
top.Title = item.Get("title").String()
top.Images = item.Get("images.0").String()
top.Url = item.Get("url").String()
top.Hint = item.Get("hint").String()
topData = append(topData, top)
}
log.Debugf("len list_:%d", len(list_))
if len(list_) != 0 {
resp = &model.ZhiNews{
Storys: responseData,
TopStorys: topData,
Date: gjson.GetBytes(body, "date").String(),
}
} else {
resp = &model.ZhiNews{
Storys: nil,
TopStorys: nil,
Date: date,
}
}
return resp, nil
}
func main() {
fmt.Printf("collect: %s\n", "start")
flag.Parse()
var c Config
conf.MustLoad(*f, &c)
log.Info("config: ", c)
mo := model.NewZhiNewsModel(c.MongoDB.Url, c.MongoDB.DbName, c.MongoDB.Collection)
ctx := context.Background()
// 获取今天的日期
today := time.Now()
// 设置采集的天数范围
daysAgo := 49 // 例如,采集从今天往前的7天数据
for i := 0; i < daysAgo; i++ {
date := today.AddDate(0, 0, -i).Format("20060102")
log.Info("date: ", date)
resp, err := getZhiNews(date)
if err != nil {
log.Errorf("getZhiNews error:" + err.Error())
return
}
err = mo.InsertWithUpdate(ctx, resp)
if err != nil {
log.Errorf("InsertWithUpdate error: %v", err)
return
}
log.Info("getZhiNews Insert ok")
}
fmt.Printf("collect: %s\n", "end")
}
注意,直接使用httpc模块的do方法,可能会被后台认为是机器人操作而直接返回403错误。
//ctx := context.Background()
//res, err_ := httpc.Do(ctx, http.MethodGet, url, nil)
这里使用其支持自定义http报文头的写法:
// 创建一个请求
r, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Error(err)
return nil, err
}
//设置常见的HTTP头
r.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3")
//ctx := context.Background()
//res, err_ := httpc.Do(ctx, http.MethodGet, url, nil)
res, err_ := httpc.DoRequest(r)
if err_ != nil {
log.Error(err_)
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
log.Errorf("Failed to read response body:%s", err)
return nil, err
}
log.Debug(res.StatusCode)
model层的代码使用
package main
import (
model "collect/model/zhiNews"
"context"
"errors"
"flag"
"fmt"
"io"
"net/http"
"time"
"github.com/tidwall/gjson"
"github.com/zeromicro/go-zero/core/conf"
log "github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/httpc"
)
func main() {
fmt.Printf("collect: %s\n", "start")
flag.Parse()
var c Config
conf.MustLoad(*f, &c)
log.Info("config: ", c)
//mo := model.NewZhiNewsModel("mongodb://test1:psdddd@localhost:27017/?tls=false&authSource=test1", "test1", "zhiNews")
//model的使用
mo := model.NewZhiNewsModel(c.MongoDB.Url, c.MongoDB.DbName, c.MongoDB.Collection)
ctx := context.Background()
......
}
实现采集网络接口
知乎日报接口:
#获取最新每日一报
get https://news-at.zhihu.com/api/4/news/latest
###历史日报
get https://news-at.zhihu.com/api/4/news/before/20240617
#curl访问
curl -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3" https://news-at.zhihu.com/api/4/news/before/20240622
func getZhiNews(date string) (resp *model.ZhiNews, err error) {
url := "https://news-at.zhihu.com/api/4/news/latest"
parsedDate, err := time.Parse("20060102", date)
if err != nil {
log.Errorf("Error parsing date:%s", err)
}
now := time.Now().Format("20060102")
if now != date {
url = "https://news-at.zhihu.com/api/4/news/before/" + parsedDate.AddDate(0, 0, 1).Format("20060102")
}
// 创建一个请求
r, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Error(err)
return nil, err
}
//设置常见的HTTP头
r.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3")
//ctx := context.Background()
//res, err_ := httpc.Do(ctx, http.MethodGet, url, nil)
res, err_ := httpc.DoRequest(r)
if err_ != nil {
log.Error(err_)
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
log.Errorf("Failed to read response body:%s", err)
return nil, err
}
log.Debug(res.StatusCode)
var keyVal map[string]interface{}
err = json.Unmarshal(body, &keyVal)
if err != nil {
log.Errorf("Failed to extract key value:%s", err)
}
//log.Debugf("keyVal:%v", keyVal)
if res.StatusCode != 200 {
log.Errorf("Failed to request,%s", res.Status)
return nil, errors.New(res.Status)
}
var zhi model.Stories
var responseData []model.Stories
list_ := gjson.GetBytes(body, "stories").Array()
for _, item := range list_ {
zhi.Id = item.Get("id").Int()
zhi.Title = item.Get("title").String()
zhi.Images = item.Get("images.0").String()
zhi.Url = item.Get("url").String()
zhi.Hint = item.Get("hint").String()
responseData = append(responseData, zhi)
}
var top model.TopStories
var topData []model.TopStories
list_1 := gjson.GetBytes(body, "top_stories").Array()
for _, item := range list_1 {
top.Id = item.Get("id").Int()
top.Title = item.Get("title").String()
top.Images = item.Get("images.0").String()
top.Url = item.Get("url").String()
top.Hint = item.Get("hint").String()
topData = append(topData, top)
}
log.Debugf("len list_:%d", len(list_))
if len(list_) != 0 {
resp = &model.ZhiNews{
Storys: responseData,
TopStorys: topData,
Date: gjson.GetBytes(body, "date").String(),
}
} else {
resp = &model.ZhiNews{
Storys: nil,
TopStorys: nil,
Date: date,
}
}
return resp, nil
}
设置采集的天数范围,开始采集
// 获取今天的日期
today := time.Now()
// 设置采集的天数范围
daysAgo := 49 // 例如,采集从今天往前的49天数据
for i := 0; i < daysAgo; i++ {
date := today.AddDate(0, 0, -i).Format("20060102")
log.Info("date: ", date)
resp, err := getZhiNews(date)
if err != nil {
log.Errorf("getZhiNews error:" + err.Error())
return
}
err = mo.InsertWithUpdate(ctx, resp)
if err != nil {
log.Errorf("InsertWithUpdate error: %v", err)
return
}
log.Info("getZhiNews Insert ok")
}
总结
以上完成实现了采集知乎日报的新闻列表信息,采集入mongoDB数据库。使用了go-zero 的goctl工具自动生成操作mongoDB的代码,使用了go-zero框架中的部分模块如日志模块,配置文件操作模块、网络访问模块等。可以看到借助goctl自动生成代码,采集数据入mongoDB数据库是多么的简单方便。再次推荐下go-zero这一优秀的微服务框架。
其他资源
配置文件 | go-zero Documentation
goctl model | go-zero Documentation