简介:这是一款面向需要批量处理海量网址的运维、爬虫开发与数据分析人员的实用工具,由AI虎哥定制,专门解决几十万乃至上百万条URL的指定后缀筛选、过滤与去重保存问题。压缩包共11个文件,约66KB,包含可执行程序、批处理脚本、常用与不用后缀清单、安装教程及说明文档等,其中exe与bat负责核心处理逻辑,txt提供后缀规则参考,html与url文件用于查看更新和更多资源,jpg为配套图示。已有58人学习下载。工具采用智能多线程处理技术,可实现秒级响应,用户可自定义保留或排除特定后缀,并自动去重,避免重复条目影响后续爬取与分析效率。包内附必看文件与安装教程,便于快速上手,适合市场分析、数据采集等场景下需要高效整理网址列表的读者参考使用。
1. 后缀筛选这件事,为什么手工做迟早要翻车
手里攒了几万条 URL,要按后缀挑出.com、.cn、.org,顺手把重复的干掉——听起来是个十分钟的活。真上手就知道,Excel 打开十万行就开始转圈,Ctrl+H替换到一半发现把域名里的字符也误伤了,去重时又分不清http和https、带不带www、末尾有没有斜杠算不算同一条。更别提从各种 App 分享链接里扒出来的地址,长这样:snssdk1128://webview?url=https%3a%2f%2faweme.snssdk.com%2ffalcon...,里面套着一层 URL 编码,直接按后缀切根本切不对。
这就是「URL 网址指定后缀批量筛选处理工具」要解决的问题:把一堆杂乱网址喂进去,按你指定的后缀过滤,同时完成去重和清洗,输出一份能直接用的清单。它适合做数据清洗、爬虫结果整理、外链资产盘点、SEO 竞品域名归集的从业者。核心动作只有三个——解析、筛选、去重,但每一个都有坑。下面按我实际做过的路径,从原理到能跑起来的脚本,一步步拆开讲。
2. 先搞清楚 URL 的哪一段才叫「后缀」
2.1 后缀到底指域名后缀还是路径后缀
很多人说「按后缀筛选」,脑子里想的其实是两件不同的事。一种是顶级域名后缀(TLD),比如.com、.cn、.net、.org,你要的是「所有 .cn 结尾的站点」;另一种是路径后缀,比如.html、.php、.jpg、.zip,你要的是「所有静态页或压缩包链接」。这两者的解析位置完全不同:前者取的是 host 部分的最后一段,后者取的是 path 部分的扩展名。
如果不先分清,脚本写出来就是错的。比如https://blog.example.com/article.zip,按 TLD 筛.com它命中,按路径后缀筛.zip它也命中,但如果你把整条 URL 拿去endswith('.com'),这条就漏了,因为它结尾是.zip。所以第一步永远是把 URL 拆成 scheme、host、path、query 四段,再决定在哪一段上做后缀匹配。
常见做法是用语言自带的 URL 解析库,而不是自己写正则去切。Python 里就是urllib.parse.urlparse,它能稳定拆出这几段,遇到畸形 URL 也不会直接崩。
2.2 用 urlparse 拆解一条 URL 的最小验证
先跑通一条,确认拆出来的结构符合预期,再批量。下面这段是验证脚本:
from urllib.parse import urlparse # 故意选一条带编码、带 query 的复杂 URL raw = "https://sub.example.com/path/page.html?id=123&ref=abc" p = urlparse(raw) print("scheme:", p.scheme) # https print("host :", p.hostname) # sub.example.com print("path :", p.path) # /path/page.html print("query :", p.query) # id=123&ref=abc # 取 TLD:host 按点切,取最后一段 tld = p.hostname.split(".")[-1] if p.hostname else "" print("tld :", tld) # com # 取路径后缀:path 里最后一个点之后的部分 ext = p.path.rsplit(".", 1)[-1] if "." in p.path else "" print("ext :", ext) # html逻辑说明:urlparse把 URL 拆成命名元组,hostname会自动去掉端口和大小写归一,比手动split('/')可靠得多。参数说明:p.hostname返回的是小写主机名,p.path保留原始大小写,所以做后缀比较时统一.lower()更稳。注意rsplit(".", 1)只切最后一个点,避免page.min.html这种被切错。
提示:
urlparse对没有 scheme 的裸域名(如example.com/a.html)会把整串当成 path,host 为空。批量处理前要先判断有没有 scheme,没有就补一个http://再解析。
2.3 后缀匹配用 endswith 还是正则
确定好取哪一段之后,匹配本身很简单,str.endswith()就够。但有个细节:.com会误伤.com.cn吗?不会,因为.com.cn的 TLD 是cn,不是com。真正会误伤的是用整条 URL 做 endswith的场景,比如https://a.com/download结尾不是.com,但它是 .com 站点。所以匹配对象必须是拆出来的 host 或 path,不是原始字符串。
如果后缀规则多且要支持通配,比如「所有.gov和.edu结尾」,用元组传给 endswith 即可:host.endswith(('.gov', '.edu'))。正则只在需要复杂模式(如「二级域名是数字」)时才上,日常筛选没必要,正则写错反而更难排查。
3. 批量筛选与去重的完整脚本怎么写
3.1 读取、清洗、筛选、去重四步流水线
把前面拆解的逻辑串成一条流水线。输入是一个文本文件,每行一条 URL;输出是筛选去重后的结果。核心顺序是:先规范化,再筛选,最后去重。顺序不能反,否则HTTP://A.com和http://a.com会被当成两条。
from urllib.parse import urlparse, urlunparse def normalize(url: str) -> str: """规范化:补 scheme、host 转小写、去掉末尾斜杠""" url = url.strip() if not url: return "" if "://" not in url: url = "http://" + url p = urlparse(url) if not p.hostname: return "" # 重建:scheme 和 host 小写,path 去掉末尾多余的 / path = p.path.rstrip("/") or "/" return urlunparse(( p.scheme.lower(), p.hostname.lower(), path, "", p.query, "" )) def keep_by_tld(url: str, tlds: tuple) -> bool: """按顶级域名后缀筛选""" host = urlparse(url).hostname or "" return host.endswith(tlds) def process(infile: str, outfile: str, tlds=(".com", ".cn")): seen = set() kept = [] with open(infile, encoding="utf-8") as f: for line in f: u = normalize(line) if not u: continue if not keep_by_tld(u, tlds): continue if u in seen: # 去重靠 set,O(1) continue seen.add(u) kept.append(u) with open(outfile, "w", encoding="utf-8") as f: f.write("\n".join(kept)) print(f"输入处理完成,保留 {len(kept)} 条") process("urls.txt", "urls_clean.txt", tlds=(".com", ".cn", ".org"))逻辑说明:normalize负责把各种写法统一成一种标准形式,这是去重能生效的前提。keep_by_tld只做筛选,职责单一。process里用set记录已见 URL,去重是 O(1),十万条也就几秒。参数说明:tlds传元组,想加后缀直接往里塞;normalize里path.rstrip("/") or "/"保证根路径统一成/,避免a.com和a.com/被算成两条。
3.2 处理 URL 编码嵌套的分享链接
从 App 分享出来的链接经常是xxx://webview?url=https%3a%2f%2f...,真正的目标 URL 藏在url=参数里,而且是编码过的。这种要先解码再提取。用urllib.parse的parse_qs和unquote组合:
from urllib.parse import urlparse, parse_qs, unquote def extract_real_url(raw: str) -> str: """从带 url= 参数的分享链接里抠出真实 URL""" p = urlparse(raw) # 只看 query 部分,找 url 参数 qs = parse_qs(p.query) if "url" in qs: inner = qs["url"][0] # parse_qs 已经做过一次解码,这里再兜底一次 return unquote(inner) return raw # 不是嵌套链接,原样返回 # 测试 test = "snssdk1128://webview?url=https%3a%2f%2faweme.snssdk.com%2ffalcon%2fdouyi" print(extract_real_url(test)) # 输出: https://aweme.snssdk.com/falcon/douyi逻辑说明:parse_qs会把 query 拆成字典并自动做一次百分号解码,所以url的值拿到时已经是https://...了。unquote是二次兜底,防止有些链接编码了两层。参数说明:parse_qs默认丢弃空值,如果url=后面为空会拿不到,需要加keep_blank_values=True。注意这种嵌套链接的 scheme 是自定义的(snssdk1128://),urlparse照样能拆,不用特殊处理。
注意:不是所有分享链接都用
url=这个参数名,有的用target=、link=。实际处理时先把参数名收集全,再统一提取,别只认一个。
3.3 大文件流式处理与内存控制
十万条以内,上面的脚本直接读进内存没问题。上到百万级,kept列表和seen集合会吃掉几百 MB。这时候改成流式写:边读边写,seen集合保留(去重必须记),但结果不攒在内存里。
def process_stream(infile: str, outfile: str, tlds=(".com",)): seen = set() count = 0 with open(infile, encoding="utf-8") as fin, \ open(outfile, "w", encoding="utf-8") as fout: for line in fin: u = normalize(line) if not u or not keep_by_tld(u, tlds) or u in seen: continue seen.add(u) fout.write(u + "\n") count += 1 print(f"保留 {count} 条")逻辑说明:用with同时打开两个文件,读一行处理一行写一行,内存占用只跟seen集合大小相关。参数说明:如果连seen都放不下(千万级以上),就得换布隆过滤器或先外部排序再去重,那是另一个量级的方案,日常业务用不到。
4. 去重规则没定好,等于白干
4.1 什么算「重复」:三种判定口径
去重最容易被忽略的是判定标准。同样两条 URL,按不同口径结论完全不同:
| 口径 | 判定依据 | http://a.com/xvshttps://a.com/x | a.com/xvsa.com/x/ |
|---|---|---|---|
| 严格字符串 | 完全一致 | 不重复 | 不重复 |
| 忽略协议 | scheme 不参与 | 重复 | 不重复 |
| 规范化后 | 统一 scheme+host+path | 重复 | 重复 |
我一般用第三种,也就是前面normalize的做法。因为业务上要的是「同一个页面只留一条」,协议和末尾斜杠的差异没有意义。但如果你的场景要区分 http 和 https(比如做证书巡检),就得用第一种,别盲目规范化。
4.2 query 参数要不要参与去重
https://a.com/list?page=1和?page=2算不算重复?取决于你要什么。做站点清单,它们是同一个页面,算重复;做爬虫 URL 队列,它们是不同内容,不能去重。我的习惯是默认保留 query,因为丢掉 query 可能把不同内容合并成一条,属于不可逆的信息损失。真要按页面去重,再单独写一个「去掉 query 后去重」的开关。
def dedup_ignore_query(urls): """按 host+path 去重,忽略 query""" seen = set() result = [] for u in urls: p = urlparse(u) key = (p.hostname, p.path.rstrip("/")) if key in seen: continue seen.add(key) result.append(u) return result逻辑说明:用(host, path)元组做 key,query 不参与,这样?page=1和?page=2只留第一条。参数说明:path.rstrip("/")保证/x和/x/归一。这个函数适合「站点页面盘点」,不适合「内容抓取」。
4.3 用 SQL 做去重时的写法差异
数据量再大或者已经在数据库里,用 SQL 去重更省事。MySQL 和 SQL Server 的写法有区别,别混用。MySQL 常用GROUP BY或ROW_NUMBER():
-- MySQL 8.0+:按 url 去重,保留 id 最小的一条 DELETE t1 FROM urls t1 JOIN urls t2 ON t1.url = t2.url AND t1.id > t2.id; -- 或者查询去重结果 SELECT url, MIN(id) AS id FROM urls GROUP BY url;SQL Server 用窗口函数更直观:
-- SQL Server:ROW_NUMBER 标记重复,删掉序号大于 1 的 WITH ranked AS ( SELECT id, url, ROW_NUMBER() OVER (PARTITION BY url ORDER BY id) AS rn FROM urls ) DELETE FROM ranked WHERE rn > 1;逻辑说明:MySQL 的自连接删除靠t1.id > t2.id保留最小 id;SQL Server 的ROW_NUMBER按 url 分组编号,rn > 1的就是重复。参数说明:PARTITION BY url里的 url 字段如果是长文本,建议先建索引或加哈希列,否则大表上窗口函数会慢得离谱。这也是「sql 语句去重」类需求最常见的性能坑。
5. 批量处理里最容易翻车的几个地方
5.1 编码问题:GBK 文件读成乱码
现象:脚本跑完输出一堆\ufffd或者直接抛UnicodeDecodeError。原因:输入文件是 Windows 下用记事本存的 GBK 编码,脚本按 UTF-8 读。解决:先探测编码再读,或者显式指定。
# 方案一:显式指定 open("urls.txt", encoding="gbk") # 方案二:探测后读(需要 chardet) import chardet with open("urls.txt", "rb") as f: enc = chardet.detect(f.read(10000))["encoding"] with open("urls.txt", encoding=enc or "utf-8") as f: ...血泪经验:别信「文件都是 UTF-8」这个假设,尤其是别人发来的 txt。
5.2 后缀匹配误伤:.com匹配到了.community
现象:筛.com时把example.community也带进来了。原因:用了host.endswith("com")而不是endswith(".com"),少了那个点。解决:后缀规则统一带前导点,或者匹配时补上。
# 错误 host.endswith("com") # example.community 也命中 # 正确 host.endswith(".com") # 只命中真正的 .com5.3 空行和注释行没过滤
现象:输出文件里混进空行、#开头的注释,后续导入数据库报错。原因:读取时没做行级清洗。解决:normalize里先strip(),空串直接返回,注释行按业务决定是否跳过。
5.4 去重后顺序乱了
现象:去重结果顺序和输入不一致,排查时对不上。原因:用了set存结果,集合无序。解决:用set只做判重,结果单独用list按输入顺序追加,就像 3.1 里的kept列表。
5.5 大文件一次性 readlines 撑爆内存
现象:百万行文件跑脚本时内存飙到几个 G 然后被系统杀掉。原因:用了f.readlines()把整个文件读进列表。解决:改成for line in f逐行迭代,Python 的文件对象本身就是迭代器,不会一次性加载。
6. 把筛选规则做成可配置的进阶玩法
脚本能跑之后,真正提升效率的是把规则从代码里抽出来。每次改后缀都要动代码、重跑,太笨。我一般会配一个规则文件,比如rules.json:
{ "tlds": [".com", ".cn", ".org"], "path_exts": [".html", ".php"], "ignore_query": false, "dedup_mode": "normalized" }然后脚本读这个配置来决定行为:
import json def load_rules(path="rules.json"): with open(path, encoding="utf-8") as f: return json.load(f) def match(url: str, rules: dict) -> bool: p = urlparse(url) host = p.hostname or "" # TLD 命中 或 路径后缀命中,任一满足即可 tld_ok = host.endswith(tuple(rules["tlds"])) ext = p.path.rsplit(".", 1)[-1].lower() if "." in p.path else "" ext_ok = ("." + ext) in rules["path_exts"] return tld_ok or ext_ok逻辑说明:match把「域名后缀」和「路径后缀」两种筛选合并,任一命中就保留,适合「既要 .cn 站点、又要所有 .html 页面」的混合需求。参数说明:rules["tlds"]必须是带点的列表,path_exts同理;dedup_mode可以扩展成strict/normalized/ignore_query三档,对应第 4 章讲的三种口径。
验证方法很简单:准备一个 20 行的小样本,每行覆盖一种边界情况(带编码、带端口、裸域名、末尾斜杠、大小写混合),跑一遍看输出是否符合预期。这个样本文件我建议长期留着,每次改规则先跑它,比拿几万条真实数据试错快得多。
一个具体技巧:处理前先统计一遍输入里出现的所有 TLD 和路径后缀,用Counter排序,你会对数据长什么样心里有数,规则也能定得更准。
from collections import Counter from urllib.parse import urlparse def survey(infile): tlds, exts = Counter(), Counter() with open(infile, encoding="utf-8") as f: for line in f: p = urlparse(line.strip()) if p.hostname: tlds[p.hostname.split(".")[-1]] += 1 if "." in p.path: exts[p.path.rsplit(".", 1)[-1]] += 1 print("TLD 分布:", tlds.most_common(10)) print("后缀分布:", exts.most_common(10))这套东西我从最早的手工 Excel 替换,到写脚本,再到规则外置,前后改了三版。最大的教训是:别急着写筛选逻辑,先花十分钟看看数据长什么样。TLD 分布、编码情况、有没有嵌套链接,这三样摸清了,脚本一次就能写对,省下的返工时间远超那十分钟。希望帮到你。
本文还有配套的精品资源,点击获取