简介:这是一款面向IPTV爱好者、家庭媒体中心搭建者及Python自动化实践者的实用工具集,解决IPTV直播源长期失效、手动筛选低效、多平台频道难聚合等痛点。工具支持从Tonkiang、IPTV365、Hacks等多个公开数据源自动抓取M3U频道列表,并集成内置速度测试模块,可批量验证URL可用性与响应延迟,显著提升频道库维护效率。压缩包共35个文件,含11个核心Python脚本(如gui.py、speed_tester.py、各源爬虫模块)、5个XML配置与IDE设置文件、4个TOC文档、3个说明类TXT、2个Markdown文档(含README与CHANGELOG),以及可直接运行的exe可执行文件和图标资源,整体大小25.4MB,结构清晰、模块解耦,便于二次开发与本地化适配。目前已有110人学习下载,用户可直接运行GUI界面操作,亦可深入阅读base_scraper.py等基础类、config.py配置逻辑及allinone_scraper.py整合流程,掌握多源并发抓取、异步测速、异常重试与日志追踪等实战技巧。 如果你也收藏过一堆 IPTV 直播源、每隔几天就要逐个测试能不能播,肯定懂手工整理频道的崩溃感。把网页里零散的 m3u 地址复制下来,再在播放器里一个个试,最后还要删掉失效线路……这套流程我做过大半年,直到花一个周末用 Python 写了这版频道抓取工具,才彻底从重复劳动里解脱出来。这个工具解决的核心问题只有三件:从多个数据源自动抓取频道列表、按关键词快速搜索过滤、对候选线路做速度测试并排序。今天我把完整设计思路和实现细节拆开聊,适合正在学 Python 网络编程、或者想做一个真正能日常用的自动化工具体系的朋友参考。
1. 为什么要自己写一只IPTV频道抓取工具:先聊聊找源的痛苦
1.1 手工整理源的效率瓶颈
市面上其实有一些“IPTV频道列表采集工具”,但用起来总觉得隔靴搔痒。有的只支持特定网站,有的搜索逻辑太弱,有的测速方式就是 ping 一下网关,根本不代表真实播放体验。我最早也是用别人写好的脚本,后来发现几个很现实的问题:
- 数据源格式五花八门:有的网站直接给 m3u 文本,有的返回 JSON,还有藏在网页 HTML 里的乱码表格。
- 频道命名规则不统一:“CCTV-1”、“中央一台”、“CCTV1高清”其实是同一个台,但字符串完全对不上。
- 失效源淘汰太快:今天还能播的地址,下周可能就 404,必须高频重测。
- 我只想看几个地方频道和新闻频道,但每次都要从几千行频道里手动筛。
这些事情用 Excel 做太累,用现成工具又不顺手。于是决定用 Python 自己写一个,需求很明确:能抓、能搜、能测速。这个项目练到的东西也特别多,requests 网络请求、lxml/BeautifulSoup 解析、asyncio 并发测速、PyInstaller 打包、配置文件管理,几乎把 Python 后端常用的技能点都串了一遍。
1.2 这个工具应该具备哪些能力
定方案之前先列了功能清单,不搞大而全,只做日常必须的:
- 多数据源接入:至少支持远程 HTTP 文本源、本地 txt/m3u 文件、网页 URL 三种输入方式。
- 频道标准化解析:把不同格式的输入统一转成结构化字典,字段包括
name、url、group、resolution。 - 多关键词搜索:输入“央视 新闻”能同时匹配“CCTV-13 新闻高清”这类含多个关键字的频道,也支持逗号分隔的 OR 逻辑。
- 速度测试:不是 ping,而是真实读取流媒体数据,按“连接耗时 + 下载速度”综合打分。
- 结果导出:排序后导出为 m3u 文件或 CSV,方便直接导入播放器。
1.3 合规与道德边界
先说一个很重要的前提:这个工具本质上只是一个“抓取和检测框架”,它本身不生产内容。我用它只处理公开可访问的测试源和运营商允许个人使用的频道列表,不去碰需要特殊权限的付费内容,也不做任何绕过认证机制的操作。如果你要在自己项目里用,请务必确认数据源有合法授权,抓取频率也别太激进,避免给源站造成压力。下面所有代码示例,都只演示技术流程,不指向任何具体商业源。
2. 工具的整体形态:模块拆分与数据流设计
2.1 从数据源到播放列表的完整流转
整个工具可以拆成五个清晰模块,箭头方向就是数据流转路径:
DataSource(多数据源获取) ↓ Parser(格式解析,输出统一频道模型) ↓ Filter/Search(去重、关键词过滤) ↓ SpeedTester(并发测速、打分) ↓ Exporter(导出 m3u / CSV)每个模块之间只通过标准数据结构沟通,这样后期加一个新数据源,只需要写一个 DataSource 子类;加一种测速逻辑,不影响前面的解析和过滤逻辑。
2.2 项目目录结构
我实际用的是这样一个结构:
iptv_fetcher/ ├── main.py # 入口,CLI/GUI 启动 ├── config.yaml # 数据源列表、过滤关键词、测速参数 ├── requirements.txt ├── core/ │ ├── __init__.py │ ├── models.py # Channel 数据类 │ ├── fetcher.py # 数据源获取器,支持 HTTP / 本地文件 │ ├── parser.py # m3u / txt / json 解析 │ ├── filter.py # 去重和关键词过滤 │ ├── speed.py # 测速器 │ └── exporter.py # 导出 m3u / csv ├── sources/ │ ├── __init__.py │ ├── http_source.py │ └── local_source.py └── tests/ # 针对 parser 和 filter 的单元测试刚开始没必要拆得太细,但core和sources分层是值得保留的,因为数据源是会持续增加的部分。用yaml管理配置也很有必要,源地址、搜索关键词、并发数这些参数都应该从配置文件读,而不是硬编码在代码里。
2.3 为什么选择 requests + lxml + asyncio 这个组合
- requests:同步 HTTP 请求足够稳,像获取 m3u 这类文本内容不需要太高性能,重试和 header 设置非常直观。
- lxml:解析 HTML 表格或者处理乱码比正则表达式省心得多,特别是当数据源是网页而非纯文本时。
- asyncio + aiohttp:测速阶段必须并发,几百个频道串行测试能等死人,用 asyncio 并发同时开 20~30 个连接,效率会高很多。
- PyYAML:读配置文件,简单可靠。
有一点需要注意:requests 和 aiohttp 不能混用同一个 session,所以抓取阶段用 requests,测速阶段用 aiohttp,两者之间通过中间数据传递,不互相依赖。
3. 多数据源抓取与解析:从网络请求到标准化频道列表
3.1 定义统一的频道数据模型
解析之前,先定义一个Channel数据类,所有源最终都转成这个结构:
from dataclasses import dataclass, field @dataclass class Channel: name: str url: str group: str = "未分组" resolution: str = "" source: str = "" # 标记来自哪个数据源 raw_name: str = "" # 原始名称,排查问题用字段不需要多,够用就行。source字段很关键,测速后如果发现某个数据源整体失效,可以直接按这个字段剔除,不用猜。
3.2 数据源获取器:HTTP 与本地文件统一接口
我写了一个BaseFetcher,再把 HTTP 和本地文件分别实现:
import requests from pathlib import Path class BaseFetcher: def fetch(self) -> str: raise NotImplementedError class HttpFetcher(BaseFetcher): def __init__(self, url, timeout=10, headers=None): self.url = url self.timeout = timeout self.headers = headers or {"User-Agent": "Mozilla/5.0"} def fetch(self): resp = requests.get(self.url, timeout=self.timeout, headers=self.headers) resp.raise_for_status() # 处理编码:优先从 Content-Type 提取,否则用 apparent_encoding if not resp.encoding: resp.encoding = resp.apparent_encoding return resp.text class LocalFetcher(BaseFetcher): def __init__(self, path): self.path = path def fetch(self): text = Path(self.path).read_text(encoding="utf-8", errors="ignore") return text这里有个很容易踩的坑:resp.text的编码判断。很多直播源是 GBK 编码,但服务器没有返回 charset,直接取.text会乱码。稳妥做法是:
if not resp.encoding or resp.encoding.lower() == "iso-8859-1": resp.encoding = resp.apparent_encodingapparent_encoding会基于内容自动判断,准确率足够应付大多数直播源文本。
3.3 解析器:搞定 m3u、TXT 和 JSON 三类格式
m3u 是最常见的格式,长这样:
#EXTM3U #EXTINF:-1 group-title="央视",CCTV-1 综合 http://example.com/live/cctv1.m3u8解析逻辑用正则加逐行 scan:
import re def parse_m3u(content: str) -> list[Channel]: channels = [] lines = content.splitlines() extinf_re = re.compile(r'#EXTINF:-?[0-9]+(?:.*?group-title="(.*?)")?,(.*)') current_name = None current_group = "未分组" for line in lines: line = line.strip() if line.startswith("#EXTINF"): m = extinf_re.match(line) if m: current_group = m.group(1) or "未分组" current_name = m.group(2).strip() elif line.startswith("#"): continue elif line: if current_name: channels.append(Channel(name=current_name, url=line, group=current_group, source="m3u")) current_name = None return channelsTXT 格式通常是“频道名,URL”一行一个,CSV 同理:
def parse_txt(content: str) -> list[Channel]: channels = [] for line in content.splitlines(): line = line.strip() if not line or line.startswith("#"): continue if "," in line: parts = line.split(",", 1) # 有些源会写 "name,url,resolution" name = parts[0].strip() url = parts[1].strip().split(",")[0].strip() channels.append(Channel(name=name, url=url, source="txt")) return channelsJSON 格式没有统一标准,常见是数组对象,字段可能是name/url/resolution:
import json def parse_json_content(content: str) -> list[Channel]: channels = [] data = json.loads(content) items = data if isinstance(data, list) else data.get("channels", []) for item in items: channels.append(Channel( name=item.get("name", ""), url=item.get("url", item.get("link", "")), group=item.get("group", "未分组"), resolution=item.get("resolution", ""), source="json" )) return channels我在实际开发中是把Parser做成一个按输入内容自动选择策略的分发器,根据文件头判断格式:
class Parser: @staticmethod def parse(content: str) -> list[Channel]: stripped = content.lstrip() if stripped.startswith("#EXTM3U"): return parse_m3u(content) if stripped.startswith("["): return parse_json_content(content) return parse_txt(content)文本开头是#EXTM3U就是 m3u,是[假设 JSON,否则默认按 TXT 处理。这个启发式准确率非常高。
4. 多关键词搜索与频道过滤:在几百个源里快速定位目标频道
4.1 搜索语法设计:AND 与 OR 的取舍
我最开始只做了最简单的in判断,比如用户输入“央视新闻”,就只能匹配“央视新闻”这个连续字符串,遇到“CCTV-13 新闻”就匹配不到。后来改成把关键词按空格拆成多个 term,默认是“AND”关系,也就是频道名必须同时包含所有关键词;如果用户用逗号分隔,则改成“OR”关系。
举个例子:
央视 新闻 => 频道名包含“央视” AND 包含“新闻” 央视,新闻 => 频道名包含“央视” OR 包含“新闻”注意空格和逗号的语义不同,这样能兼顾精确和宽泛两种需求。实现很简单:
def match_channel(channel: Channel, query: str) -> bool: query = query.strip() if not query: return True if "," in query: # OR: 任意一个关键词命中即通过 terms = [t.strip() for t in query.split(",") if t.strip()] return any(term.lower() in channel.name.lower() or term.lower() in channel.url.lower() for term in terms) # AND: 默认按空格拆分 terms = [t.strip() for t in query.split() if t.strip()] text = f"{channel.name} {channel.url}".lower() return all(term.lower() in text for term in terms)如果你还想支持分类过滤,比如只看“高清”或者“央视”分组,可以在group字段上做同样的匹配,但要注意中文大小写和下划线变体,建议先把\s+和_统一替换成空字符串再比较。
4.2 去重:不只是 URL 去重,还要处理频道别名
去重是最容易忽略的一环。两个数据源可能给出同一个频道的不同 URL,也可能给出不同频道名但 URL 一样。我做了两层去重:
- URL 层:去掉
http://和https://前缀、去掉末尾/、去掉 URL 中的时间戳参数(很多直播源会带?token=xxx),然后做精确去重。 - 名称层:先把频道名里常见的杂音去掉,比如“高清”、“超清”、“HD”、“FHD”、“1080P”等,再比较核心名称,如果核心名称相同且 URL 域名相同,就给别名打分,保留高分那个。
名称归一化函数大概长这样:
import re def normalize_name(name: str) -> str: name = name.lower() name = re.sub(r"[\((].*?[\))]", "", name) # 去掉括号备注 name = re.sub(r"[\s_\-]+", "", name) name = re.sub(r"(高清|超清|原画|hd|fhd|uhd|4k|1080p|720p|标清)", "", name) return name这个函数不可能做到完美,但对最常见的直播源命名习惯已经够用。如果两个名称归一化后一样,就保留resolution更高、或者之前测速分数更高的那条。
4.3 过滤黑名单与自定义源优先级
我还加了一个黑名单机制,针对某些明显的广告地址或者无效地址。黑名单规则用正则列表配置在 yaml 里:
blacklist: - "zhibo.ads" - "\\.mp4$" # 一部分 .mp4 直播流可能不适用于本机网络环境过滤逻辑很简单:
def is_blacklisted(url: str) -> bool: import re for pattern in config["blacklist"]: if re.search(pattern, url): return True return False自定义源优先级也很有用:如果你自己维护了一个手动验证过的频道列表,并且和抓取到的源同名,就优先用手动列表里的源。方法是在过滤后,把手动源插到结果最前面,播放器默认会加载第一个可用源。
5. 速度测试的实现细节:不是简单 ping 一下就完事
5.1 为什么 ping 的延迟不能代表播放卡不卡
很多工具测速用的是ping,但 ping 走的是 ICMP 协议,它只能说明网络连通性和 RTT,完全看不出来 HTTP 流媒体服务能不能给你稳定输出视频数据。你遇到的情况可能是:ping 通了但视频一直转圈,或者 ping 延迟很低但下载速度只有几十 KB/s。
真实播放体验和这几个因素相关:
- TCP 连接建立时间:这个能通过
socket.connect测出来。 - HTTP 响应头耗时:包括 DNS 解析、服务端处理、重定向。
- 首包和数据传输速度:视频流是持续不断的数据,需要读取一定字节数来计算平均速率。
- 协议类型:有些源只支持 HLS(m3u8 分段拉流),有些是 HTTP FLV 流,测速逻辑要能兼容。
5.2 用 asyncio + aiohttp 实现并发测速
我最终的测速逻辑是:对每个 URL 发起一个GET请求,设置timeout,读取前 1MB 数据后断开连接,记录开始时间、首字节时间和读取完成时间,计算两个指标:
- 连接时间:从发起请求到收到响应头(包含首字节)的耗时。
- 下载速度:读取数据量 / 读取耗时。
代码如下:
import asyncio import aiohttp import time async def test_single(channel: Channel, timeout=5, read_bytes=1_000_000): url = channel.url start = time.monotonic() speed = 0.0 connect_time = -1.0 try: async with aiohttp.ClientSession() as session: async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as resp: if resp.status != 200: return connect_time, speed, False # 第一个 chunk 到达时间减去开始时间,近似作为首包时间 first_chunk = await resp.content.read(1024) first_byte_time = time.monotonic() - start if not first_chunk: return connect_time, speed, False # 继续读,直到读满 read_bytes 或者超时 total_read = len(first_chunk) read_start = time.monotonic() while total_read < read_bytes: chunk = await resp.content.read(8192) if not chunk: break total_read += len(chunk) read_time = time.monotonic() - read_start if read_time > 0: speed = total_read / read_time / 1024 # KB/s connect_time = first_byte_time return connect_time, speed, True except Exception: return connect_time, speed, False然后批量并发:
async def run_speed_test(channels, concurrency=20): sem = asyncio.Semaphore(concurrency) results = [] async def worker(ch): async with sem: connect_time, speed, ok = await test_single(ch) results.append((ch, connect_time, speed, ok)) await asyncio.gather(*[worker(ch) for ch in channels]) return results注意aiohttp.ClientSession不建议每个请求都新建,但如果你的频道数量不是特别大(几百个),这样写问题不大,代码反而更清晰。如果追求性能,可以提前创建一个 session 传入。
5.3 测速评分排序策略
测速结果不能光看速度,还要考虑连接稳定性。我用一个简单评分公式:
score = 100 * (1 - connect_time / timeout) + min(speed / 1024, 10) * 10- 连接时间越快,得分越高。
- 下载速度超过 1MB/s 就能拿满速度分,避免超高带宽源占据绝对优势。
- 任何失败项得 0 分。
按分数降序排完,取前 N 个输出到播放列表。你还可以记录测速日期,在下一次运行时优先测试“上次可用”的频道,进一步减少请求量。
6. 工程化落地:CLI 还是 GUI?打包成 exe 的经验
6.1 命令行工具和小型 GUI 的分工
这个工具最开始是纯命令行,适合放在服务器上定时跑:
python main.py --config config.yaml --search "央视 新闻" --test-speed --output result.m3u但实际用下来,我给身边朋友用的时候,大部分人还是喜欢有个窗口能看进度。后来用PySide6做了一版极简 GUI,只在主界面上放三个区域:数据源列表、搜索框和结果表格。CLI 和 GUI 共用同一个core模块,业务逻辑没有任何重复。
我的建议是:如果只是自己用,命令行足够;如果要做成能分发的工具,GUI 的价值更大,但工作量也更多。不要一开始就上 GUI,先把核心流程跑通。
6.2 参数配置与默认值管理
配置文件config.yaml是核心,我会把数据源地址、搜索关键词、并发数、超时时间、导出的文件名都放在里面:
sources: - name: "公开测试源 A" type: "http" url: "https://example.com/list.m3u" - name: "本地维护源" type: "local" path: "./my_channels.txt" search: query: "央视,新闻" # 默认搜索关键词 max_results: 200 speed: concurrency: 20 timeout: 5 read_bytes: 1048576 export: output: "result.m3u" sort_by: "score"用 PyYAML 读取后直接传给入口函数,这样每次改数据源不用碰代码。给朋友分享工具时,他只需要改这个文件。
6.3 PyInstaller 打包的三个实用教训
用 PyInstaller 打包成 exe 时,我踩过几个坑:
- UPX 压缩导致杀毒软件误报:加
--noupx参数可以降低误报率,但体积会大一些。 - aiohttp 等库的隐式导入:PyInstaller 有时检测不到
aiohttp内部用到的aiodns或frozenlist,打包后运行会报ModuleNotFoundError。解决方式是手动在 spec 文件的hiddenimports里加上。 - 单文件模式的临时目录问题:
--onefile启动慢,且程序内若用到相对路径读取配置文件,会跑到临时解压目录。稳妥做法是判断sys.frozen,并始终从可执行文件所在目录读取外部配置文件:
import sys from pathlib import Path def app_base_path(): if getattr(sys, "frozen", False): return Path(sys.executable).parent return Path(__file__).parent.resolve()打包命令供参考:
pyinstaller --noconfirm --noupx --onefile --name iptv_fetcher ^ --hidden-import aiohttp --hidden-import aiodns ^ --collect-all yaml main.py--collect-all yaml是为了确保 PyYAML 的元数据一起打进去。
7. 那些坑与合规提醒:给后来者的实在建议
7.1 字符集、超时与重试
我遇到最多的解析问题就是 HTTP 头没有声明编码。后来统一在fetcher里抓完文本后先用charset-normalizer做一次检测,再把乱码文本丢给解析器。某些源连接很慢,必须设置合理的超时,我一般用 5 秒连接超时 + 10 秒总超时。如果某个数据源连续 3 次获取失败,就把整个源标记为不可用,并在日志里输出源名称,而不是让程序卡死。
还有一点,很多直播源地址会定期更换 token,URL 里带的时间戳参数会影响去重。我在解析后统一把?token=以后的部分清掉再做 URL 去重,但导出时保留完整 URL。
7.2 并发数控制与服务器友好性
测速阶段并发数不是越高越好。并发 100 很容易触发目标服务器的防抖策略,导致 IP 被封。我自己的经验是控制在 15~30 之间,测试间隔可以加一个 0.05~0.1 秒的小延迟。如果你要频繁跑,建议对同一个域名只测一次,减少重复请求。这个工具的价值是给你一个清晰的候选列表,而不是在短时间内把源站流量打满。
7.3 合法使用边界与后续扩展
最后还是想强调:这个工具应该用在你有权访问的直播源上,比如运营商给你开放的频道列表、公开测试频道、自己拿摄像头推流的测试地址。不要拿它去抓需要登录才能看的付费频道,更不要批量下载别人的私有源再二次分发。从技术上讲,所有抓取、解析、测速逻辑都是中性技能,但使用场景决定了性质。
如果你有兴趣继续往下扩展,我列几个值得做的方向:
- 加入 EPG(电子节目指南)解析,让搜索结果能同时显示当前正在播什么节目。
- 定时任务自动更新源,用 cron 或 Windows 计划任务每天跑一次,输出带日期标记的 m3u。
- 把测速结果写入 SQLite,分析不同时段某个频道的可用率变化。
- 做一个简单的 Web 页面,通过局域网在电视盒子上直接访问搜索结果。
我在实际使用中的体会是:这种工具的价值不在代码量,而在“搜索策略 + 测速策略”的迭代。第一次跑通可能只要一个下午,但后面每次遇到新格式、新场景,都是在给这个系统打补丁。尤其是测速部分,从“ping 一下就算 OK”进化到“读取实际视频流并评分”,整个可用性提升非常明显。如果你正卡在“源多但不知道哪个能用”的烦恼中,建议直接动手写一个最小版本,跑通之后你会回来感谢自己的。
本文还有配套的精品资源,点击获取