为友情链接添加自动检测脚本

友情链接是博客的重要组成部之一,很多博主日常也会时常进行链接的交换,即是发展自己的互联网友人圈子,也算得上是为自己的博客引流。不过在如今信息大爆炸的时代,也渐渐有些博主实在“有些乏力”来维护自己的站点,其中的缘由可能有诸多,最终便是出现我们偶尔会遇到的情况,某个站点突然之间可能就无法访问。为此便有想法给自己的站点添加个友情链接检测脚本,剔除掉那些已经失效的友情链接,让用户访问时的跳转体验更好加。

方案设计

考虑到我们的博客站点是生成静态文件的部署方式,所以友情链接的检测频率也不会特别高,就是在每次发表新文章构建博客站点的时候检测一次。并结合Github Actions的CI流程可以实现自动化的处理,大致的流程如下:

graph TD; node1([fa:fa-play 开始 ]) --> node2[/站点内容更新or配置调整/] --> |fa:fa-upload Git提交&推送&触发GithubAction| node3[[fa:fa-scroll 调用脚本验证友链可访问性]] --> |fa:fa-trash-can-arrow-up 剔除无法访问的链接| node4[/fa:fa-list-check 生成新的友链配置文件/] --> node5[fa:fa-blog 构建构静态文件] --> node6([fa:fa-stop 结束 ]);

创建检测脚本

根据Hugo NexT主题中有关于友情链接的配置信息,我们可以获取到友链的具体URL地址,直接使用Get方法探查其可访问性,然后调整友链的状态标识,重新生成新的友情链接配置文件,具体的脚本代码可参考如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"
	"sync"
	"time"

	"gopkg.in/yaml.v3"
)

var (
	timeOut int
)

type FLink struct {
	Name     string `yaml:"name"`
	Desc     string `yaml:"desc"`
	Avatar   string `yaml:"avatar"`
	Link     string `yaml:"link"`
	Visible  bool   `yaml:"visible"`
	Skip     bool   `yaml:"skip"`
	Attempts int    `yaml:"-"`
}

type FLinkGroup struct {
	Title string  `yaml:"FLinksTitle"`
	Desc  string  `yaml:"FLinksDesc"`
	List  []FLink `yaml:"FLinksList"`
}

func main() {
	// 读取命令行参数
	filePath := flag.String("f", "", "友情链接的Yaml文件路径")
	flag.IntVar(&timeOut, "t", 3, "检测网站的超时时长")
	help := flag.Bool("h", false, "显示帮助信息")
	flag.Parse()

	// 验证文件路径是否为空
	if *help || *filePath == "" {
		flag.Usage()
		return
	}

	// 读取YAML文件
	data, err := ioutil.ReadFile(*filePath)
	if err != nil {
		fmt.Println("读取文件失败:", err)
		return
	}

	// 解析YAML文件
	var groups []FLinkGroup
	err = yaml.Unmarshal(data, &groups)
	if err != nil {
		fmt.Println("解析YAML文件失败:", err)
		return
	}

	// 统计站点数量
	totalLinks := 0
	for _, group := range groups {
		totalLinks += len(group.List)
	}

	// 创建并发控制的通道
	ch := make(chan FLink)
	wg := sync.WaitGroup{}

	// 启动并发检测
	for groupIndex, group := range groups {
		for linkIndex := range group.List {
			wg.Add(1)
			go checkLink(&groups[groupIndex].List[linkIndex], ch, &wg)
		}
	}

	// 启动结果处理
	go func() {
		wg.Wait()
		close(ch)
	}()

	// 统计结果
	successCount := 0
	failureCount := 0
	failureLinks := make(map[string]string)
	startTime := time.Now()

	for link := range ch {
		if link.Visible {
			successCount++
		} else {
			failureCount++
			failureLinks[link.Name] = link.Link
		}

		// 输出检测进度
		finishLinks := successCount + failureCount
		progress := float64(finishLinks) / float64(totalLinks) * 100
		remainingTime := time.Since(startTime).Seconds() / progress * (100 - progress)
		fmt.Printf("正在检测:%d/%d,检测进度:%.2f%%,预计剩余时间:%.2f秒\n", finishLinks, totalLinks, progress, remainingTime)
	}

	// 输出统计信息
	fmt.Println("\n站点检测统计信息")
	fmt.Println(strings.Repeat("-", 30))
	fmt.Printf("总共检测站点:%-d\n", totalLinks)
	fmt.Printf("正常访问站点:%-d\n", successCount)
	fmt.Printf("失去连接站点:%-d\n", failureCount)
	fmt.Printf("检测总共耗时:%-.2f秒\n", time.Since(startTime).Seconds())
	fmt.Println(strings.Repeat("-", 30))
	fmt.Println("")

	// 输出连接失败的站点名称和地址
	if failureLinks != nil && len(failureLinks) > 0 {
		fmt.Println("连接失败的站点列表")
		fmt.Println(strings.Repeat("-", 50))

		for name, link := range failureLinks {
			fmt.Printf("%s:%s\n", name, link)
		}
		fmt.Println(strings.Repeat("-", 50))
		fmt.Println("")
	}

	// 将结果保存到原文件
	resultData, err := yaml.Marshal(groups)
	if err != nil {
		fmt.Println("保存结果到文件失败:", err)
		return
	}

	err = ioutil.WriteFile(*filePath, resultData, 0644)
	if err != nil {
		fmt.Println("保存结果到文件失败:", err)
		return
	}

	fmt.Printf("结果已保存到%s文件\n", *filePath)
}

func checkLink(link *FLink, ch chan<- FLink, wg *sync.WaitGroup) {
	defer wg.Done()

	if link.Skip {
		link.Visible = true
	} else {
		// 默认重试3次
		for i := 0; i < 3; i++ {
			// 等待时间逐次增加
			if i > 1 {
				time.Sleep(time.Duration(i) * time.Second)
			}

			// 发起HTTP请求
			//fmt.Printf("正在检测%s....\n", link.Link)
			client := http.Client{Timeout: time.Duration(timeOut) * time.Second}
			resp, err := client.Get(link.Link)
			if err == nil && resp.StatusCode == http.StatusOK {
				link.Visible = true
				ch <- *link
				return
			}
		}

		// 所有重试都失败,设置visible为false
		link.Visible = false
	}

	ch <- *link
}

加入Action触发

在站点的根目录下创建.github文件夹,接着分别创建名为workflowsscripts 的子目录,并将上述提供的友情链接检测脚本保存到scripts目录下,同时在workflows目录下创建名为deploy.yml的文件,将生成静态站点文件相关步骤配置在此文件,配置代码参考如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
name: Deploy Site

on:
  push:
    branches:
      - main

jobs:
  # 本地生成静态站点文件
  deploy-site:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true
          fetch-depth: 0 

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '^1.22.0'
    
      - name: Check All Friend's links
        run: | 
          cd .github/scripts/ 
          go version
          go run main.go -f ../../data/flinks.yaml

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          extended: true

      - name: Build Site
        run: |
          hugo --minify          

上线效果

至此就实现了对友情链接进检测的自动化流程,站点的每次发布都会触发脚本的运行,可以看下上线后台日志输出的效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
go version go1.22.3 linux/amd64
go: downloading gopkg.in/yaml.v3 v3.0.1
正在检测:1/27,检测进度:3.70%,预计剩余时间:0.01秒
......
正在检测:27/27,检测进度:100.00%,预计剩余时间:0.00秒

站点检测统计信息
------------------------------
总共检测站点:27
正常访问站点:20
失去连接站点:7
检测总共耗时:4.27秒
------------------------------

连接失败的站点列表
--------------------------------------------------
心流:https://blog.panghai.top
永恆遊戲研究院 | 巧合還是緣份:https://forevergame.org
......
--------------------------------------------------

结果已保存到../../../data/flinks.yaml文件
注:由于Github Action使用的机器大多是国外资源,为此这个脚本的检测可能会出现“误杀”,原因自然是有些站点不支持全球的化的访问所影响的。