使用 GitHub Actions 定时爬取任意网址(域名)的 DNS 解析记录,并生成Hosts文件格式,是一个非常实用的自动化需求。我们可以利用 Linux 自带的 dig 命令来完成查询,并结合 GitHub Actions 的 schedule 功能实现定时触发,最后将结果提交回仓库保存历史记录。
以下是完整的操作步骤和代码实现。
核心思路
- 触发机制:利用 GitHub Actions 的
cron语法定时运行(如每小时一次)。 - 解析逻辑:在运行器中提取网址中的域名,使用
dig命令查询 DNS 记录。 - 数据存储:将查询结果追加到一个文件中,并利用
git命令提交回仓库,形成历史监控日志。
第一步:创建仓库和文件
- 在 GitHub 上新建一个仓库(例如命名为
dns-monitor)。 - 在仓库中创建目录结构:
.github/workflows/ - 在该目录下创建一个 YAML 文件,例如
scratchdns-check.yml。
第二步:编写 Workflow 脚本
将以下代码复制到 scratchdns-check.yml 中。
第三步:创建网址配置文件
监控多个网址,最好的做法是将网址列表放在一个单独的配置文件中(例如 urls.txt),然后通过脚本循环读取这个文件进行批量查询。
在你的仓库根目录下新建一个名为 urls.txt 的文件,把你要监控的网址一行写一个。支持写 http:// 开头的完整链接,也可以直接写域名。
这样管理起来非常方便,以后只需要修改 urls.txt 文件,不用去动 GitHub Actions 的配置代码。
urls.txt 内容示例:
https://www.google.com
https://github.com
bing.com
https://www.apple.com
# 这是注释,以 # 开头的一行会被自动忽略
example.com
代码关键点解析
-
Cron 定时 (
schedule):- cron: '0 * * * *'代表每小时第 0 分钟执行一次。- 注意时区:GitHub Actions 默认使用 UTC 时间。如果你需要北京时间(UTC+8)早上 8 点运行,你需要设置 UTC 时间 0 点(即
0 0 * * *)。最小间隔通常是 5 分钟。
-
域名提取:
- 因为 DNS 只能解析域名(如
baidu.com),不能解析带路径的完整 URL。脚本中sed命令负责把https://www.baidu.com/s?wd=test处理成www.baidu.com。
- 因为 DNS 只能解析域名(如
-
dig 命令:
dig +short $DOMAIN A:这是最核心的命令。A代表查询 IPv4 地址。如果你想查询 IPv6,可以改成AAAA;查询 CNAME(别名),可以改成CNAME。
-
日志文件 (
dns_current.log):- 文件里永远只保留最新一次的数据。
- 格式严格按照你的要求:
IP 域名 # 注释。
-
get_old_ip函数:- 这是核心。在生成新日志前,它利用
awk命令去读取现有的dns_current.log。 - 它会比较每一行的第2列(
$2),如果匹配当前的域名,就返回第1列($1,即旧的IP)。
- 这是核心。在生成新日志前,它利用
-
失败处理逻辑:
- 如果
dig查不到 IP:- 先调用
get_old_ip。 - 如果拿到了旧 IP,就输出
1.2.3.4 example.com # Timeout。 - 如果从来没查到过这个域名(旧日志里也没有),就输出
Unknown example.com # Timeout,保证日志格式的一致性。
- 先调用
- 如果
-
只读取输出的第一行: 非常简单,只需要在
dig命令后加上管道符| head -n 1即可。
head -n 1 的作用是只读取输出的第一行,这样即使 DNS 返回了多个 IP(比如 CDN 返回的多个节点),我们也只取第一个,保证日志格式整洁(一行一个域名)。
关键代码在这一行:
NEW_IP=$(dig +short "$DOMAIN" A +time=3 +tries=1 2>/dev/null | head -n 1)
-
没有
head -n 1时:如果查询google.com,dig可能会返回:142.250.1.100 142.250.1.101 142.250.1.102这会导致你的日志文件变成:
142.250.1.100 142.250.1.101 142.250.1.102 www.google.com格式完全错乱。
-
加上
head -n 1后:只抓取第一行142.250.1.100。 日志文件保持完美格式:142.250.1.100 www.google.com
-
临时文件 (
dns_current.tmp):- 我们先往
.tmp文件里写,写完了再把.tmp改名为.log。这样做是为了防止万一脚本中途出错,导致原来的日志文件被清空或损坏。
- 我们先往
-
自动提交:
- 为了让机器人能提交代码,我们需要
permissions: contents: write。 - 使用了
git status --porcelain来检查是否有变化,如果 DNS 记录没变(且之前没跑过),就不会触发无效的 commit。
- 为了让机器人能提交代码,我们需要
以下是更新后的完整 Workflow 代码:
name: DNS Monitor (Latest Only)
on:
schedule:
# 每小时运行一次
- cron: '0 * * * *'
workflow_dispatch: # 允许手动触发
permissions:
contents: write
jobs:
check-dns:
runs-on: ubuntu-latest
steps:
# 1. 检出代码
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
# 2. 配置 Git
- name: Configure Git
run: |
git config --global user.name "GitHub Actions Bot"
git config --global user.email "actions@github.com"
# 3. 执行 DNS 查询与日志生成
- name: Query DNS and Update Log
run: |
# 定义文件名
LOG_FILE="current_hosts"
TMP_LOG="dns_current.tmp"
# --- 函数:获取某个域名的上一次IP ---
function get_old_ip() {
local domain=$1
# 从旧日志查找对应域名的IP (精确匹配第2列)
awk -v d="$domain" '$2 == d {print $1}' "$LOG_FILE" 2>/dev/null | head -n 1
}
# --- 开始处理 ---
# 创建临时文件,写入表头
echo "# Latest DNS Status - Updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" > "$TMP_LOG"
echo "# Format: IP Domain # Optional Comment" >> "$TMP_LOG"
if [ ! -f "urls.txt" ]; then
echo "Error: urls.txt not found!"
exit 1
fi
# 循环读取 urls.txt
while IFS= read -r line || [[ -n "$line" ]]; do
# 跳过空行和注释
if [[ -z "$line" || "$line" =~ ^# ]]; then
continue
fi
# 提取域名
DOMAIN=$(echo "$line" | sed -e 's|^[^/]*//||' -e 's|/.*$||')
# 查询 DNS (A记录)
# --- 核心修改在这里 ---
# 新增了: | head -n 1
# 作用: 无论 dig 返回多少个 IP,只取第一个
NEW_IP=$(dig +short "$DOMAIN" A +time=3 +tries=1 2>/dev/null | head -n 1)
# --- 判断结果 ---
if [ -n "$NEW_IP" ]; then
# 1. 查询成功:写入 "IP 域名"
echo "$NEW_IP $DOMAIN" >> "$TMP_LOG"
else
# 2. 查询失败:尝试获取上一次的 IP
OLD_IP=$(get_old_ip "$DOMAIN")
if [ -n "$OLD_IP" ]; then
# 有历史记录:写入 "旧IP 域名 # Timeout"
echo "$OLD_IP $DOMAIN # Timeout" >> "$TMP_LOG"
else
# 无历史记录:标记为 Unknown
echo "Unknown $DOMAIN # Timeout (No History)" >> "$TMP_LOG"
fi
fi
done < "urls.txt"
# 覆盖旧日志
mv "$TMP_LOG" "$LOG_FILE"
# 输出结果预览
echo "=== Current Log File Contents ==="
cat "$LOG_FILE"
# 4. 提交更改
- name: Commit changes
run: |
if [[ -n $(git status --porcelain) ]]; then
git add current_hosts
git commit -m "Update latest DNS snapshot"
git push
else
echo "No changes to commit."
fi
第五步:如何使用
- 将上述文件保存并推送到 GitHub。
- 你可以点击仓库上方的 Actions 标签页。
- 找到 "DNS Monitor" workflow,点击右侧的 "Run workflow" 按钮进行第一次手动测试。
- 测试成功后,代码库根目录下会出现一个
dns_current.log文件。 - 之后,它会根据你设定的时间自动运行并更新这个文件。
这样你就可以清晰地看到所有网址在不同时间点的解析状态变化了。
进阶玩法:查询多条记录并发送通知
如果你需要监控多个网址,或者希望解析结果发生变化时发送邮件/钉钉通知,可以修改脚本逻辑:
# 示例:简单的变化检测
# 假设上次的结果存在 last_ip.txt
if [ "$CURRENT_IP" != "$(cat last_ip.txt 2>/dev/null)" ]; then
echo "IP Changed! New IP: $CURRENT_IP"
echo "$CURRENT_IP" > last_ip.txt
# 这里可以调用 curl 发送 Webhook 通知
# curl -X POST "你的钉钉/Slack Webhook URL" -d '{"text":"DNS Changed"}'
fi
这个方案无需任何服务器,完全依托于 GitHub 的免费资源,非常适合个人开发者做长期监控。 欢迎fork github.com/aooom/dns-m…
Hosts管理,推荐用SwitchHosts一键切换,SwitchHosts 的 官网(switchhosts.vercel.app/ ) 或 Github仓库(github.com/oldj/Switch… ) ,下载对应系统版本的安装包