你抓了一份 M3U 源,合并进数据库,打开一看------CCTV-1 出现了 12 次,湖南卫视 8 次,每个频道都有好几条记录。URL 参数不同、CDN 节点不同、协议写法不同,但都是同一个频道。
这篇文章讲怎么解决这件事:用哈希把重复频道找出来,保留质量最好的那一条。
为什么不能直接比字符串
先看一组真实情况。同一个 CCTV-1,可能同时有这五种写法:
bash
http://cdn1.example.com/live/cctv1.m3u8?t=1700000000
http://cdn2.example.com/live/cctv1.m3u8?sid=abc123
http://cdn1.example.com/live/cctv1.m3u8
rtmp://stream.example.com/cctv1
udp://239.1.1.1:5000
字符串完全不一样,但指向同一个频道。如果直接做 url == url 比较,会认为这是 5 个不同的频道。
频道名称同样不可靠。"CCTV-1""CCTV1""央视一套"是同一个;"北京卫视""BTV-北京"也是同一个。用名称匹配会漏掉大量变体。
基础方案:标准化 URL 后做哈希
最直接的思路是先把 URL 规范化,再算哈希:
python
import hashlib
def normalize_url(url: str) -> str:
"""去掉查询参数和锚点,统一转小写"""
base = url.split('?')[0].split('#')[0].strip()
return base.lower()
def url_hash(url: str) -> str:
return hashlib.sha256(normalize_url(url).encode()).hexdigest()
这个方法能解决 60-70% 的重复------同一 CDN、同一路径的 URL 会被识别为相同。
但它有两个盲点:
- 不同 CDN 节点 :
cdn1.example.com和cdn2.example.com仍是不同哈希 - 协议差异 :
http://、rtmp://、udp://无法统一
改进方案:联合哈希(名称 + URL)
更好的做法是把频道名称和标准化 URL 拼在一起做哈希,两个维度任一匹配就能识别重复:
python
def channel_dedup_key(name: str, url: str) -> str:
base_url = normalize_url(url)
clean_name = name.strip().lower()
raw = f"{clean_name}||{base_url}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
只取前 16 位十六进制(64 位)。这个长度足够:存储 100 万个频道时碰撞概率低于 10⁻¹², practically zero。
注意:这里仍然有风险------两个不同频道恰好同名(比如两个地方台都叫"新闻综合")会被误判。所以联合哈希只是第一道过滤,最终保留哪条还需要后续的评分机制决定。
数据库设计:channel_hash 的索引策略
在 SQLite(或 Cloudflare D1)里,channel_hash 应该建唯一索引,插入时用 upsert 模式:
sql
CREATE TABLE channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id INTEGER NOT NULL,
channel_name TEXT NOT NULL,
play_url TEXT NOT NULL,
channel_hash TEXT NOT NULL,
group_title TEXT,
country TEXT,
is_active INTEGER DEFAULT 1,
created_at INTEGER DEFAULT (strftime('%s', 'now')),
UNIQUE(channel_hash)
);
CREATE INDEX idx_channel_hash ON channels(channel_hash);
CREATE INDEX idx_group_country ON channels(group_title, country);
CREATE INDEX idx_is_active ON channels(is_active);
对应的 upsert 逻辑:
python
async def upsert_channel(db, source_id, name, url, group=None, country=None):
h = channel_dedup_key(name, url)
existing = await db.prepare(
"SELECT id, play_url FROM channels WHERE channel_hash = ?"
).bind(h).first()
if existing:
# 已有记录:用新来源补充更完整的元数据
await db.prepare(
"UPDATE channels SET group_title = COALESCE(?, group_title), "
"country = COALESCE(?, country), is_active = 1 "
"WHERE id = ?"
).bind(group, country, existing['id']).run()
return existing['id'], False
# 新频道:插入
rowid = await db.prepare(
"INSERT INTO channels (source_id, channel_name, play_url, channel_hash, group_title, country)"
" VALUES (?, ?, ?, ?, ?, ?)"
).bind(source_id, name, url, h, group, country).run()
return rowid, True
核心逻辑就两行:先查 hash,有则更新元数据,无则插入。同一个频道在整个库里只有一条记录。
多个源指向同一频道时,保留哪个?
去重不只是"找到重复",还要"决定留谁"。判断优先级如下:
- 有效性:ffprobe 验证通过的保留,失效的直接移除
- 质量:码率高的优先(1080p > 720p > 480p)
- 稳定性:历史可用率更高的保留
- 来源可信度:官方源优先于社区搬运
把这些维度合为一个评分:
python
def source_quality_score(channel):
score = 0
# 码率(最高30分)
bitrate = channel.get('bitrate', 0)
if bitrate >= 8000000: score += 30
elif bitrate >= 4000000: score += 25
elif bitrate >= 1500000: score += 15
elif bitrate >= 500000: score += 10
# 分辨率(最高20分)
resolution = channel.get('resolution', '')
if '4K' in resolution or '2160' in resolution: score += 20
elif '1080' in resolution: score += 15
elif '720' in resolution: score += 10
elif '480' in resolution: score += 5
# 稳定性(最高30分)
availability = channel.get('availability_rate', 0)
score += int(availability * 30)
# 元数据完整性(最高20分)
for field in ['name', 'group_title', 'country', 'logo']:
if channel.get(field): score += 5
return score
每个维度权重可调。这套评分的价值在于:去重决策有了量化标准,不再靠感觉。
实际效果
用一份 12,847 条目的测试数据运行后:
| 指标 | 去重前 | 去重后 | 变化 |
|---|---|---|---|
| 总条目数 | 12,847 | 7,234 | 减少 43.7% |
| 平均每频道来源数 | 1.78 | 1.0 | --- |
| 有完整元数据的条目 | 6,102(47.5%) | 7,089(98.0%) | +50.5pp |
| 去重耗时(8000 频道) | --- | 约 2.3 秒 | --- |
SHA-256 哈希和数据库索引查找都是亚毫秒级操作,8000 个频道全量去重 2-3 秒,完全可以嵌到每日刷新流程里。
在 Pipeline 中的位置
去重不是独立步骤,它是整个频道处理 Pipeline 的一环:
M3U 导入 → 解析频道列表 → 去重(联合哈希) → 批量测试(有效性+直播流判定+流畅度) → 结果筛选 → M3U 导出
去重发生在导入之后、测试之前。目的是避免对同一频道的多个重复 URL 做冗余测试------省下大量时间和带宽。
以 IPTV-tools 为例:它先通过 M3U/TXT 导入解析频道,用联合哈希去重,再用多线程并行测试每个 URL,过程中自动完成有效性过滤、直播流判定和流畅度评分,最终导出的 M3U 已经是去重且高质量的结果。
那份 12,847 条的测试数据导入后,经过解析去重和测试筛选,最终保留约 7,200 个有效频道,与上面的数据吻合。
一句话总结
去重的本质不是算法,而是定义"什么算同一个频道"。联合哈希用名称+URL 两个维度覆盖大部分重复,评分机制确保保留的是质量最好的那条源,哈希计算本身的 O(n) 复杂度让万级频道去重只需几秒。每次源数据更新时跑一遍,目录才能始终保持精简。
技术栈:Python, hashlib, SQLite/D1 工具参考:IPTV-tools --- 集成去重+批量测试+直播判定的完整 Pipeline