直接用 ssl 和 socket 获取域名证书,需显式设置 server_hostname 并调用 getpeercert() 解析 notafter 时间,复用 sslcontext,控制并发,精确捕获错误并记录。

用 ssl 模块直接抓取证书信息,别依赖第三方库
Python 标准库的 ssl 和 socket 足够完成域名证书获取,不需要装 requests 或 certifi —— 那些反而可能绕过真实握手、拿不到服务端实际下发的证书。
关键在于建立原始 TLS 连接并调用 getpeercert(),它返回的是 DER 编码的证书字典,含 notAfter 字段(字符串格式如 'Sep 12 08:24:56 2025 GMT')。
常见错误:直接用 urllib.request.urlopen() 访问 HTTPS 地址,拿到的是响应头,不是证书;或者用 ssl.create_default_context().wrap_socket() 但没传 server_hostname,导致 SNI 不匹配,某些 CDN(如 Cloudflare)会返回默认证书或拒绝连接。
- 必须显式设置
server_hostname参数,否则多数现代服务会返回错误证书或空结果 - 连接需设超时(
timeout=10),否则 DNS 卡住或服务无响应时脚本会长时间挂起 -
getpeercert()返回的notAfter是 GMT 时间,需用ssl._ssl._parse_asn1_time()解析(私有函数但稳定可用),或手动用datetime.strptime()+ 固定格式
解析 notAfter 时间时,格式错一个字符就报 ValueError
证书里的 notAfter 字符串格式固定但易错:月份是英文缩写(Jan/Feb…),日是无前导零的数字(5 不是 05),年是四位,时分秒带前导零,结尾恒为 ' GMT'。用 strptime 必须严格匹配:'%b %d %H:%M:%S %Y GMT' —— 注意 %d 对应的是空格+数字(如 ' 5'),所以实际要用 '%b %d %H:%M:%S %Y GMT' 并提前 .strip()。
更稳妥的做法是调用 ssl._ssl._parse_asn1_time(not_after_str),它内部已处理所有 ASN.1 时间变体(UTCTime / GeneralizedTime),返回 datetime 对象,无需手动适配格式。
- 别用
dateutil.parser.parse(),它对'Sep 12 08:24:56 2025 GMT'解析不稳定,有时把GMT当时区有时忽略 - 解析失败时不要静默吞掉异常,至少打印原始
notAfter字符串,方便定位格式问题 - 拿到
datetime后立刻转为 UTC 时间(.replace(tzinfo=timezone.utc)),避免本地时区干扰剩余天数计算
批量监测多个域名时,并发控制比多线程更重要
证书检查本质是网络 I/O 密集型任务,开太多并发连接容易触发目标服务器限流,或本地端口耗尽(尤其 Windows 默认 ephemeral 端口少)。用 concurrent.futures.ThreadPoolExecutor 时,max_workers 设为 5~10 比设成 cpu_count()*5 更实际。
真正要注意的是连接复用和错误隔离:每个域名独立建连,失败不中断其他;DNS 查询也应单独超时(socket.getaddrinfo 可能卡住),建议用 threading.Timer 或 asyncio.wait_for 包裹整个流程。
- 为每个域名单独捕获
socket.timeout、ssl.SSLCertVerificationError、ConnectionRefusedError,不同错误含义不同(超时可能是防火墙拦截,CertVerificationError可能是自签名) - 记录失败域名时附带错误类型和
str(e),比如"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)",说明本地 CA 信任链缺失,不是证书过期 - 避免在循环里反复创建
ssl.SSLContext实例,复用同一个上下文对象即可
判断“即将过期”不能只看剩余天数,得考虑证书重签窗口
运维实践中,证书通常提前 30 天续签,但不同团队策略不同:有的提前 15 天告警,有的要求预留 7 天人工操作时间。脚本里硬编码 days_left 做告警阈值,不如把阈值作为参数传入,或从配置文件读取。
更关键的是区分“过期”和“即将过期”:过期是 now > notAfter(已失效),即将过期是 0 。两者处理逻辑应分离——前者要立即通知,后者可汇总日报。
- 注意
datetime比较必须同 tzinfo,否则抛TypeError: can't compare offset-naive and offset-aware datetimes - 如果域名返回的证书
notAfter为空(某些内网服务或错误配置),应归类为 “证书不可读”,而非视为“永不过期” - 一次扫描后生成简明报告:字段包括
domain、expires_at(ISO 格式)、days_left、status(OK / EXPIRED / SOON / ERROR)
证书过期检测看似简单,真正上线后最常出问题的是 DNS 解析超时未设限、SNI 主机名拼写错误、以及 notAfter 时间解析时忽略 GMT 时区导致剩余天数算错。这些点不写进日志,排查起来比重写逻辑还费时间。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











