一、初始版本:使用定时任务执行
- 配置方式:
配置项中使用jobStatus控制任务启停,jobCron设定 Cron 表达式,任务调度使用robfig/cron库。 - 执行逻辑:
启动时根据配置注册定时任务,任务执行时通过exec.Command打开 Chrome 浏览器访问指定网址(如百度),随后立即关闭浏览器。 - 存在问题:
-
- 浏览器打开后立即关闭,实际网页还未加载完成;
- 任务每次都会强行开启和关闭浏览器,资源浪费严重;
- 定时执行缺乏灵活性,容易错过或重复执行。
job:
jobStatus:
runstartsync: true
jobCron:
runstartsync: "0 0 2 * * 0"
var Cfg = &Config{}
type Config struct {
Job CronJobConfig `json:"job"`
}
type CronJobConfig struct {
JobStatus map[string]bool `json:"jobStatus"`
JobCron map[string]string `json:"jobCron"`
}
func InitConfig(filePath string) {
// 读取配置文件
if filePath == "" {
viper.SetConfigFile("config.yaml")
} else {
viper.SetConfigFile(filePath)
}
err := viper.ReadInConfig()
if err != nil {
fmt.Printf("Error reading config file: %v\n", err)
return
}
err = viper.Unmarshal(Cfg)
if err != nil {
fmt.Printf("Error unmarshaling config: %v\n", err)
return
}
fmt.Println(Cfg)
}
添加定时任务
package job
import (
"fmt"
"github.com/robfig/cron/v3"
)
var (
JobList = make([]*Job, 0)
)
type Job struct {
Name string
Status bool
Cron string
Func func()
}
var cronScheduler *cron.Cron
func StartCronJob() {
addJob()
// 创建一个支持秒级的 cron 调度器
cronScheduler = cron.New(cron.WithSeconds())
for _, job := range JobList {
if job.Status {
fmt.Println("添加定时任务", job.Name+" "+job.Cron)
cronScheduler.AddFunc(job.Cron, job.Func)
}
}
// 启动 cron 调度器
cronScheduler.Start()
fmt.Println("定时任务启动")
}
// StopCronJob 停止定时任务
func StopCronJob() {
if cronScheduler != nil {
// 停止 cron 调度器
stop := cronScheduler.Stop()
// 等待所有任务完成
<-stop.Done()
fmt.Println("定时任务已停止")
}
}
package job
import (
"context"
"jz-scraw/internal/app/consts"
"jz-scraw/internal/app/logic"
"jz-scraw/internal/app/model"
"jz-scraw/internal/app/types"
"jz-scraw/internal/config"
"jz-scraw/pkg/http_call"
)
func AddRunStartSync() {
name := "runstartsync"
JobList = append(JobList, &Job{
Status: config.Cfg.Job.JobStatus[name],
Name: name,
Cron: config.Cfg.Job.JobCron[name],
Func: addRunStartSync,
})
}
func addRunStartSync() {
cmd := exec.Command(`cmd`, `/c`, `start`, "chrome.exe", "www.baidu.com")
cmd.Start()
exec.Command(`taskkill`, `/im`, `chrome.exe`, `/f`).Run()
}
func main() {
job.StopCronJob()
}
二、优化方向一:增加随机延迟与协程执行
为缓解资源压力及避免浏览器闪退,改用协程加延迟控制。
- 优化点:
-
- 打开网页后延迟关闭,延迟时间为 10~20 秒随机;
- 使用协程异步执行任务,防止阻塞主线程;
- 独立协程定时关闭浏览器,每 10 分钟执行一次,避免长期驻留。
- 改进效果:
-
- 网页有更充足的时间加载;
- 控制更灵活,避免 CPU 和内存飙升。
- 仍存在问题:
-
- 定时关闭存在误杀风险(网页刚打开就被关闭);
- 无法感知任务是否正在运行;
- 缺乏运行状态判断和窗口数量控制。
func AddRunStartSync() {
addRunStartSync()
}
func addRunStartSync() {
cmd := exec.Command(`cmd`, `/c`, `start`, "chrome.exe", "www.baidu.com")
cmd.Start()
randomSec := 10 + rand.Intn(10)
time.Sleep(time.Duration(randomSec) * time.Second)
exec.Command(`taskkill`, `/im`, `chrome.exe`, `/f`).Run()
}
func CloseChrome() {
fmt.Println("关闭了窗口")
exec.Command(`taskkill`, `/im`, `chrome.exe`, `/f`).Run()
}
func main() {
go func() {
for {
AddRunStartSync()
}
}()
go func() {
for {
// 每 10分钟 关闭浏览器
time.Sleep(60 * 10 * time.Second)
job.CloseChrome()
}
}()
}
三、优化方向二:引入状态控制和窗口数量感知机制
针对误杀浏览器窗口和任务冲突的问题,引入状态控制结构 isFuckRun,实现自动化流程更精细的调度策略:
- 状态标识:
-
- 使用原子变量
isRun表示当前是否允许执行任务; windowCount记录当前已打开窗口数量,用于判断是否需关闭浏览器。
- 使用原子变量
- 执行逻辑调整:
-
- 仅在
isRun == true的情况下执行任务; - 执行一次后增加窗口计数;
- 若窗口数超过阈值(如 12 个),自动关闭浏览器并清零计数;
- 若
isRun == false,则休眠 30 分钟后尝试重新启用。
- 仅在
- 优势:
-
- 实现了任务运行过程的“开关控制”;
- 避免浏览器被重复启动或错误关闭;
- 支持自我恢复,具备一定容错能力。
func main() {
go func() {
for {
fuckRunClient := job.GetIsFuckRun()
fuckRunClient.ConsumeJobMqOne()
}
}()
}
type isFuckRun struct {
isRun atomic.Bool
windowCount atomic.Int64
}
var isFuckRunClient *isFuckRun
func InitIsFuckRun() {
isFuckRunClient = &isFuckRun{}
isFuckRunClient.Start()
}
func GetIsFuckRun() *isFuckRun {
return isFuckRunClient
}
func (f *isFuckRun) ConsumeJobMqOne() {
if f.isRun.Load() {
addRunStartSync()
logger.Logger.Info("执行自动化流程中")
// 到达12个窗口以上就关闭浏览器(具体根据业务修改关闭位置)
if f.GetWindowCount() > 12 {
CloseChrome()
}
} else {
// 休眠30分钟
logger.Logger.Info("等待自动化流程开启")
time.Sleep(30 * 60 * time.Second)
f.Start()
logger.Logger.Info("自动开启自动化流程")
}
}
func (f *isFuckRun) Close() {
logger.Logger.Info("自动化流程已经关闭")
f.isRun.Store(false)
f.windowCount.Store(0)
}
func (f *isFuckRun) Start() {
f.isRun.Store(true)
}
func (f *isFuckRun) IsRun() bool {
return f.isRun.Load()
}
// AddWindowCount 并返回当前窗口数量
func (f *isFuckRun) AddWindowCount() {
f.windowCount.Add(1)
}
func (f *isFuckRun) SetWindowCountZero() {
f.windowCount.Store(0)
}
func (f *isFuckRun) GetWindowCount() int64 {
logger.Logger.Info(fmt.Sprintf("窗口数: %d", f.windowCount.Load()))
return f.windowCount.Load()
}
func addRunStartSync() {
cmd := exec.Command(`cmd`, `/c`, `start`, "chrome.exe", "www.baidu.com")
cmd.Start()
randomSec := 10 + rand.Intn(10)
time.Sleep(time.Duration(randomSec) * time.Second)
exec.Command(`taskkill`, `/im`, `chrome.exe`, `/f`).Run()
}
func CloseChrome() {
fmt.Println("关闭了窗口")
exec.Command(`taskkill`, `/im`, `chrome.exe`, `/f`).Run()
}