手把手教你用 Gitee 替代 DDNS:家庭 IP 自动更新 + 本地快捷访问
## 前言
很多人家中搭建了 NAS、树莓派或小型服务器,希望通过固定域名或地址随时访问内网服务。传统的 DDNS 需要依赖域名服务商,并且要开放端口、配置防火墙。
本文提供一种极简替代方案:**利用 Gitee(码云)私有仓库存储家庭公网 IP,通过 Python 脚本自动更新 IP,并在本地电脑生成快捷方式**,实现类似 DDNS 的效果,无需公网域名,完全免费。
---
## 一、方案设计思路
1. **家庭服务器**:定时获取当前公网 IPv4 和 IPv6,通过 Gitee API 写入私有仓库的两个文件 `ipv4.txt` 和 `ipv6.txt`。
2. **本地电脑**:无 Git 依赖,通过 Gitee API 直接读取最新 IP,并在脚本所在目录生成可直接双击打开的网页快捷方式(`.url` / `.webloc` / `.desktop`)。
3. **运行环境**:Windows / macOS / Linux 均可,最终可将脚本打包为单文件 exe,方便多台设备使用。
优点:
- **无需购买域名**,完全利用 Gitee 免费私有仓库。
- **同时支持 IPv4 和 IPv6**,未来兼容性好。
- **纯 Python 实现**,可打包为独立可执行文件,无需安装 Python 环境也能运行。
- **安全可控**:私有仓库 + 个人令牌,IP 不会公开。
---
## 二、准备工作
### 1. 注册 Gitee 并创建私有仓库
- 访问 [gitee.com](https://gitee.com) 注册账号。
- 创建私有仓库,例如命名为 `homeddns`(本文使用用户名 `georgecn`,仓库 `homeddns`)。
- 在 **设置 → 私人令牌** 生成一个新令牌,勾选 `projects` 权限,**复制保存好**(只显示一次)。
### 2. 确认家庭网络具备公网 IP
- 访问 [ipv4.ip.sb](https://ipv4.ip.sb) 和 [ipv6.ip.sb](https://ipv6.ip.sb) 检查是否有公网地址。
- 若没有公网 IPv4,可考虑使用 IPv6 或内网穿透方案(本文暂不展开)。
### 3. 路由器端口转发
如果家庭 Web 服务运行在某个内网设备(如 `192.168.1.100:8080`),需在路由器中设置端口转发(NAT),将外网请求的 8080 端口映射到该内网地址。
---
## 三、家庭服务器端:自动更新 IP 到 Gitee
### 1. 脚本功能
- 依次尝试多个 IP 查询服务(`ipv4.ip.sb`、`v4.ident.me` 等),获取当前公网 IPv4 和 IPv6。
- 通过 Gitee API 将 IP 写入仓库文件(若文件不存在则创建;若存在则更新,自动处理 `sha`)。
- 若 IP 未变化则跳过更新,节省 API 调用。
### 2. 完整代码 `update_ip_to_gitee.py`
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import io
import base64
import requests
# 解决 Windows 控制台 GBK 编码问题
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# ========== 配置区域 ==========
GITEE_TOKEN = '你的私人令牌'
OWNER = '你的用户名'
REPO = 'homeddns' # 仓库名
BRANCH = 'master' # 分支名
# =============================
IPV4_SERVICES = [
'https://ipv4.ip.sb',
'https://v4.ident.me',
'https://api.ipify.org',
'https://ipv4.icanhazip.com'
]
IPV6_SERVICES = [
'https://ipv6.ip.sb',
'https://v6.ident.me',
'https://api6.ipify.org',
'https://ipv6.icanhazip.com'
]
def fetch_ip(service_list, ip_type='IPv4'):
for url in service_list:
try:
print(f'尝试从 {url} 获取{ip_type}...')
resp = requests.get(url, timeout=5)
if resp.status_code == 200:
ip = resp.text.strip()
if ip_type == 'IPv4' and '.' in ip and ':' not in ip:
return ip
elif ip_type == 'IPv6' and ':' in ip:
return ip
except Exception as e:
print(f'从 {url} 获取失败: {e}')
continue
return None
def get_file_sha(file_path):
api_url = f'https://gitee.com/api/v5/repos/{OWNER}/{REPO}/contents/{file_path}'
headers = {'Authorization': f'token {GITEE_TOKEN}'}
params = {'ref': BRANCH}
try:
resp = requests.get(api_url, headers=headers, params=params)
if resp.status_code == 200:
data = resp.json()
if isinstance(data, dict):
return data.get('sha')
elif isinstance(data, list):
print(f'警告:{file_path} 是目录,不是文件')
return None
elif resp.status_code == 404:
return None
else:
print(f'获取文件信息异常: {resp.status_code}')
return None
except Exception as e:
print(f'请求异常: {e}')
return None
def update_gitee_file(file_path, content):
api_url = f'https://gitee.com/api/v5/repos/{OWNER}/{REPO}/contents/{file_path}'
headers = {'Authorization': f'token {GITEE_TOKEN}'}
new_content_b64 = base64.b64encode(content.encode()).decode('utf-8')
data = {
'access_token': GITEE_TOKEN,
'content': new_content_b64,
'message': f'Update {file_path}',
'branch': BRANCH
}
try:
resp = requests.put(api_url, json=data, headers=headers)
if resp.status_code in (200, 201):
print(f'[OK] 文件 {file_path} 更新成功')
return True
elif resp.status_code == 400 and 'sha is missing' in resp.text:
print(f'文件 {file_path} 已存在,获取 sha 重试...')
sha = get_file_sha(file_path)
if sha:
data['sha'] = sha
retry_resp = requests.put(api_url, json=data, headers=headers)
if retry_resp.status_code in (200, 201):
print(f'[OK] 文件 {file_path} 更新成功')
return True
print(f'[ERROR] 更新失败: {resp.status_code}')
return False
except Exception as e:
print(f'请求异常: {e}')
return False
def main():
print("===== 开始获取公网IP =====")
ipv4 = fetch_ip(IPV4_SERVICES, 'IPv4')
if ipv4:
print(f'[INFO] IPv4: {ipv4}')
update_gitee_file('ipv4.txt', ipv4)
else:
print('[ERROR] 无法获取 IPv4')
ipv6 = fetch_ip(IPV6_SERVICES, 'IPv6')
if ipv6:
print(f'[INFO] IPv6: {ipv6}')
update_gitee_file('ipv6.txt', ipv6)
else:
print('[WARN] 无法获取 IPv6')
print("===== 执行完成 =====")
if __name__ == '__main__':
main()
```
### 3. 部署到家庭服务器
- 将脚本保存为 `update_ip_to_gitee.py`。
- 安装依赖:`pip install requests`。
- 手动执行测试:`python update_ip_to_gitee.py`。
- 设置定时任务(例如每 5 分钟运行一次):
- **Windows**:使用任务计划程序。
- **Linux**:`crontab -e` 添加 `*/5 * * * * /usr/bin/python3 /path/to/update_ip_to_gitee.py`
- **宝塔面板**:直接添加计划任务,执行命令 `python /path/to/update_ip_to_gitee.py`
---
## 四、本地电脑:一键生成访问快捷方式
我们希望在任何电脑上(无需 Git)拉取最新 IP,并生成一个双击就能打开家庭网页的快捷方式。
### 1. 脚本功能
- 通过 Gitee API 直接获取 `ipv4.txt` 和 `ipv6.txt` 的内容。
- 自动识别操作系统(Windows/macOS/Linux),生成对应格式的快捷方式(`.url` / `.webloc` / `.desktop`)。
- 快捷方式保存在脚本所在目录,方便移动使用。
### 2. 完整代码 `sync_and_create_shortcut.py`
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import requests
import base64
# ========== 配置区域 ==========
GITEE_TOKEN = '你的私人令牌'
OWNER = '你的用户名'
REPO = 'homeddns'
BRANCH = 'master'
WEB_PORT = 8080 # 你的家庭 Web 服务端口
# =============================
# 获取脚本所在目录(兼容 PyInstaller 打包)
if getattr(sys, 'frozen', False):
SCRIPT_DIR = os.path.dirname(sys.executable)
else:
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# 如果当前目录不可写,降级到桌面
if not os.access(SCRIPT_DIR, os.W_OK):
SCRIPT_DIR = os.path.join(os.path.expanduser('~'), 'Desktop')
print(f'[WARN] 原目录不可写,改用桌面目录: {SCRIPT_DIR}')
def get_file_content(file_path):
url = f'https://gitee.com/api/v5/repos/{OWNER}/{REPO}/contents/{file_path}'
headers = {'Authorization': f'token {GITEE_TOKEN}'}
params = {'ref': BRANCH}
try:
resp = requests.get(url, headers=headers, params=params, timeout=10)
if resp.status_code == 200:
data = resp.json()
content_b64 = data.get('content', '')
if content_b64:
return base64.b64decode(content_b64).decode('utf-8').strip()
elif resp.status_code == 404:
print(f'[ERROR] 文件 {file_path} 不存在')
else:
print(f'[ERROR] 获取 {file_path} 失败,状态码 {resp.status_code}')
return None
except Exception as e:
print(f'[ERROR] 网络错误: {e}')
return None
def create_shortcut_windows(filepath, url):
content = f"""[InternetShortcut]
URL={url}
IconFile=%SystemRoot%\\System32\\SHELL32.dll
IconIndex=13
"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
def create_shortcut_macos(filepath, url):
content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>URL</key>
<string>{url}</string>
</dict>
</plist>
"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
def create_shortcut_linux(filepath, url):
content = f"""[Desktop Entry]
Type=Link
Name=Home Web
URL={url}
Icon=text-html
"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
def main():
if sys.platform == 'win32':
create_func = create_shortcut_windows
ext = '.url'
elif sys.platform == 'darwin':
create_func = create_shortcut_macos
ext = '.webloc'
else:
create_func = create_shortcut_linux
ext = '.desktop'
print(f'快捷方式将保存在: {SCRIPT_DIR}')
ipv4 = get_file_content('ipv4.txt')
ipv6 = get_file_content('ipv6.txt')
if ipv4:
url4 = f'http://{ipv4}:{WEB_PORT}/'
path4 = os.path.join(SCRIPT_DIR, f'Home_IPv4{ext}')
create_func(path4, url4)
print(f'[OK] IPv4 快捷方式: {path4}')
print(f' URL: {url4}')
else:
print('[WARN] 无法获取 IPv4')
if ipv6:
url6 = f'http://[{ipv6}]:{WEB_PORT}/'
path6 = os.path.join(SCRIPT_DIR, f'Home_IPv6{ext}')
create_func(path6, url6)
print(f'[OK] IPv6 快捷方式: {path6}')
print(f' URL: {url6}')
else:
print('[WARN] 无法获取 IPv6')
# 非打包模式或打包但不需要等待时,可省略 input
if not getattr(sys, 'frozen', False):
input('按 Enter 退出...')
if __name__ == '__main__':
main()
```
### 3. 使用方式
- **直接运行 Python 脚本**:需安装 `requests` 库。
- **打包为 exe**(推荐,适合无 Python 环境):
```bash
pip install pyinstaller
pyinstaller --onefile --noconsole sync_and_create_shortcut.py
```
生成的 exe 文件可以放在任意文件夹(如 U 盘、桌面),双击运行即生成快捷方式。
### 4. 高级用法:定时自动更新
Windows 下可将 exe 添加到任务计划程序,设置每天运行一次,确保快捷方式指向最新 IP。
---
## 五、常见问题与解决方案
### 1. 控制台报错 `UnicodeEncodeError: 'gbk' codec can't encode character`
**原因**:Windows 默认控制台编码为 GBK,而脚本中使用了 emoji(✅❌)或中文字符。
**解决**:已在脚本开头强制将 stdout 编码设为 UTF-8,并将 emoji 替换为 `[OK]`、`[ERROR]` 等 ASCII 文本。
### 2. 打包后出现 `RuntimeError: input0: lost sys.stdIn`
**原因**:使用 `--noconsole` 打包后没有控制台,`input()` 无法调用。
**解决**:在脚本中判断是否打包环境,仅当非打包时调用 `input`,或完全删除该行。
### 3. Gitee API 返回 `sha is missing`
**原因**:文件已存在但更新时未提供 `sha` 字段。
**解决**:脚本已实现自动获取 `sha` 并重试。
### 4. 获取不到公网 IPv6
**原因**:家庭网络可能未分配公网 IPv6 或被光猫/路由器阻止。
**解决**:检查 [ipv6.ip.sb](https://ipv6.ip.sb) 是否显示地址;若没有,可只用 IPv4 方案。
### 5. 访问快捷方式时打不开网页
- 确认路由器已正确配置端口转发。
- 确认家庭 Web 服务正在运行。
- 尝试用 IPv6 地址直接访问(如果支持),IPv6 通常不需要端口转发。
---
## 六、总结
通过这套方案,你拥有了一个完全自主可控、免费且稳定的动态 IP 同步系统。
- **服务器端**:每 5 分钟自动将 IPv4/IPv6 写入 Gitee 私有仓库。
- **客户端**:一键生成快捷方式,随时访问家庭服务。
你可以将客户端 exe 分享给家人或朋友,他们只需双击即可访问你的家庭网页,无需安装任何环境。
**扩展思路**:
- 搭配 `frp` 或 `ZeroTier` 实现内网穿透,彻底解决无公网 IP 的问题。
- 将快捷方式部署在手机桌面(iOS 可通过“添加到主屏幕”,Android 可使用 URL 快捷方式)。
如果你在使用中遇到任何问题,欢迎在评论区留言交流。
---
**项目源码已开源**:
Gitee 仓库地址:`https://gitee.com/你的用户名/homeddns`(私有,仅自己可见)
客户端脚本可任意分发,只需修改配置中的令牌和用户名即可。
**感谢阅读,祝玩得开心!**
