一句话总结:一个 100 行的 Python 脚本,定期从 sitemap.xml 提取 URL、对比已提交记录、分批提交到搜索引擎 API。核心设计是”只提交新的,不浪费每日配额,断点续传”。


目录

  1. 为什么需要这个脚本
  2. 脚本架构
  3. 核心模块实现
  4. 完整代码
  5. 运行与维护
  6. 遇到的问题与解决
  7. 扩展思路

为什么需要这个脚本

静态站点(GitHub Pages、Vercel 等)没有后端服务器,无法像动态网站那样在发布文章时自动通知搜索引擎。每次新增文章后,需要手动将新 URL 提交给搜索引擎,操作繁琐且容易遗漏。

手动提交的痛点:

  • 需要打开站长平台 → 手动粘贴 URL → 提交
  • 文章多了记不清哪些提交过
  • 每日配额有限,胡乱提交浪费配额

针对这些问题,目标很清晰:

1
2
3
输入:sitemap.xml(包含站点所有 URL)
处理:自动找出"尚未提交过"的 URL → 分批提交
输出:已提交记录 → 下次运行时跳过

脚本架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    ┌─────────────────┐
│ sitemap.xml │
│ (所有 URL) │
└────────┬────────┘

┌──────────────────────┐
│ 对比历史提交记录 │
│ baidu_submitted.json │
└────────┬─────────────┘

┌──────────────────────┐
│ 挑出未提交的 URL │
│ → 分批(5条/批) │
└────────┬─────────────┘

┌──────────────────────┐
│ POST 到搜索引擎 API │
│ → 解析返回结果 │
└────────┬─────────────┘

┌──────────────────────┐
│ 更新提交记录文件 │
│ → 下次跳过已提交的 │
└──────────────────────┘

核心模块实现

模块一:sitemap 解析

Hexo 生成的 sitemap.xml 遵循标准的 sitemap 协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import xml.etree.ElementTree as ET

SITEMAP_PATH = 'public/sitemap.xml'

def parse_sitemap(path):
"""从 sitemap.xml 提取所有 URL"""
tree = ET.parse(path)
root = tree.getroot()
ns = {'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'}

urls = []
for url in root.findall('ns:url', ns):
loc = url.find('ns:loc', ns)
if loc is not None and loc.text:
urls.append(loc.text.strip())
return urls

模块二:提交记录管理

使用 JSON 文件持久化记录,set 结构确保唯一性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json

RECORD_PATH = 'baidu_submitted.json'

def load_submitted():
"""读取已提交记录"""
if os.path.exists(RECORD_PATH):
with open(RECORD_PATH, 'r', encoding='utf-8') as f:
return set(json.load(f))
return set()

def save_submitted(urls):
"""保存已提交记录"""
with open(RECORD_PATH, 'w', encoding='utf-8') as f:
json.dump(sorted(urls), f, ensure_ascii=False, indent=2)

模块三:API 提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

BAIDU_API = (
'http://data.zz.baidu.com/urls'
'?site=https://example.com'
'&token=YOUR_TOKEN'
)

def submit_batch(urls, api_url):
"""提交一批 URL"""
body = '\n'.join(urls)
headers = {'Content-Type': 'text/plain'}
resp = requests.post(api_url, data=body.encode('utf-8'), headers=headers)
return resp.json()

模块四:主控逻辑

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
def main():
# 1. 加载已提交记录
submitted = load_submitted()
print(f'已提交记录:{len(submitted)} 条')

# 2. 解析 sitemap
all_urls = parse_sitemap(SITEMAP_PATH)
print(f'sitemap 总计:{len(all_urls)} 条')

# 3. 挑出未提交的
new_urls = [u for u in all_urls if u not in submitted]
print(f'尚未提交:{len(new_urls)} 条')

if not new_urls:
return

# 4. 分批提交
BATCH_SIZE = 5
total_submitted = 0
idx = 0

while idx < len(new_urls):
batch = new_urls[idx:idx + BATCH_SIZE]
result = submit_batch(batch, BAIDU_API)

if 'error' in result:
handle_error(result)
break

success = result.get('success', 0)
remain = result.get('remain', 0)

if success > 0:
submitted.update(batch[:success])
total_submitted += success
idx += success

if remain == 0:
break

# 5. 保存记录
if total_submitted > 0:
save_submitted(submitted)

完整代码

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
"""
收录提交脚本
用法:python baidu_submit.py
"""
import os
import sys
import json
import io
import requests
import xml.etree.ElementTree as ET

# ===== 配置 =====
SITEMAP_PATH = os.path.join(os.path.dirname(__file__), 'public', 'sitemap.xml')
RECORD_PATH = os.path.join(os.path.dirname(__file__), 'baidu_submitted.json')
BAIDU_API = 'http://data.zz.baidu.com/urls?site=https://example.com&token=YOUR_TOKEN'
# ================

def load_submitted():
if os.path.exists(RECORD_PATH):
with open(RECORD_PATH, 'r', encoding='utf-8') as f:
return set(json.load(f))
return set()

def save_submitted(urls):
with open(RECORD_PATH, 'w', encoding='utf-8') as f:
json.dump(sorted(urls), f, ensure_ascii=False, indent=2)

def parse_sitemap(path):
tree = ET.parse(path)
root = tree.getroot()
ns = {'ns': 'http://www.sitemaps.org/schemas/sitemap/0.9'}
urls = []
for url in root.findall('ns:url', ns):
loc = url.find('ns:loc', ns)
if loc is not None and loc.text:
urls.append(loc.text.strip())
return urls

def submit_batch(urls, api_url):
body = '\n'.join(urls)
headers = {'Content-Type': 'text/plain'}
resp = requests.post(api_url, data=body.encode('utf-8'), headers=headers)
return resp.json()

def main():
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')

if not os.path.exists(SITEMAP_PATH):
print('[错误] 找不到 sitemap.xml,先执行 hexo g')
sys.exit(1)

submitted = load_submitted()
print('已提交记录:%d 条' % len(submitted))

all_urls = parse_sitemap(SITEMAP_PATH)
print('sitemap 总计:%d 条' % len(all_urls))

new_urls = [u for u in all_urls if u not in submitted]
print('尚未提交:%d 条' % len(new_urls))

if not new_urls:
print('没有新 URL,跳过。')
return

BATCH_SIZE = 5
total_submitted = 0
idx = 0

while idx < len(new_urls):
batch = new_urls[idx:idx + BATCH_SIZE]
print('\n正在提交第 %d~%d 条...' % (idx + 1, min(idx + BATCH_SIZE, len(new_urls))))
result = submit_batch(batch, BAIDU_API)

if 'error' in result:
msg = result.get('message', '')
if 'quota' in msg:
print(' [i] 当日配额已用完')
else:
print(' [!] 提交失败:' + msg)
break

success = result.get('success', 0)
remain = result.get('remain', 0)
print(' [OK] 成功:%d 条 | 剩余配额:%d 条' % (success, remain))

if success > 0:
submitted.update(batch[:success])
total_submitted += success
idx += success

if remain == 0:
print(' [i] 配额已用完,停止。')
break

if total_submitted > 0:
save_submitted(submitted)
print('\n本次共提交 %d 条' % total_submitted)
else:
print('\n本次未提交成功。')

if __name__ == '__main__':
main()

运行与维护

1
2
3
# 部署站点后运行
hexo g -d
python baidu_submit.py

脚本特点:

  • 幂等:重复运行不会重复提交已提交过的 URL
  • 断点续传:每日运行,每次只提交新增的
  • 配额感知:单批次不超过剩余配额,避免整批被拒
  • 自动跳过:已提交的记录自动跳过

遇到的问题与解决

问题 1:Windows 编码问题

1
UnicodeEncodeError: 'gbk' codec can't encode character

原因:Windows 控制台默认 GBK 编码,print 包含特殊字符时报错。

解决:重定向 stdout:

1
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')

同时,不要在 print 中使用 emoji——Windows 对 emoji 支持不稳定。

问题 2:单次提交超过剩余配额

API 返回 {"error":400, "message":"over quota"}

原因:剩余配额 7 条时,提交了 10 条,整批被拒。

解决:批次大小设为 5 条,且每次提交后检查剩余配额。

问题 3:配额不重置

GitHub Pages 无备案站点的 API 配额可能是一次性总限额而不是每日重置。

解决:配额用完后改用手动提交补充。

扩展思路

这个脚本可以扩展到更多场景:

多搜索引擎支持

1
2
3
4
5
6
7
8
9
10
11
ENGINES = {
'baidu': {
'api': 'http://data.zz.baidu.com/urls?...',
'record': 'baidu_submitted.json',
},
'bing': {
'api': 'https://ssl.bing.com/webmaster/api?...',
'record': 'bing_submitted.json',
},
# 更多...
}

CI/CD 集成

在 GitHub Actions 中每次部署后自动运行:

1
2
3
4
5
6
7
8
9
# .github/workflows/submit.yml
on:
push:
branches: [main]
jobs:
submit:
runs-on: ubuntu-latest
steps:
- run: python baidu_submit.py

增量监控

配合定时任务(cron job)定期检查 sitemap 变化:

1
2
# 每天凌晨运行
0 3 * * * cd /path/to/site && python baidu_submit.py

总结

  1. 设计原则:幂等(不重复提交)+ 增量(只提交新的)+ 配额感知(不超过限制)
  2. 关键技术:sitemap 解析 + JSON 记录 + 分批 POST + 错误处理
  3. 维护成本:几乎为零——每次部署后跑一次脚本就行,自动跳过已提交的
  4. 扩展性好:可从单一搜索引擎扩展到多个,也可集成到 CI/CD 流程

总结:这个 100 行脚本的核心价值不在于代码量,而在于设计思想——幂等性、增量处理、配额感知。这些原则同样适用于其他 API 限速场景的任务自动化。