找回密码
立即注册
搜索
发回帖 发新帖

6392

积分

0

好友

773

主题
发表于 2 小时前 | 查看: 4| 回复: 0

前两天翻到一个老 Python 项目,换了下基础镜像,结果业务逻辑还没开始跑就挂了。

ModuleNotFoundError: No module named 'distutils'

另一个脚本更直接:

ModuleNotFoundError: No module named 'cgi'

遇到这种报错,我一般不会急着 pip install。

先看 Python 版本。

因为有些模块根本不是“你没装”,而是 Python 官方已经把它从标准库里清出去了。

Python 3.12 移除了 distutils、imp、asyncore、asynchat、smtpd 等模块;到了 Python 3.13,又一次性移除了 19 个 PEP 594 里列出的老模块,包括 cgi、imghdr、telnetlib、pipes、crypt 等。

所以如果你的项目里还躺着下面这些代码,建议别等到升级 Python 时才处理。


distutils,别再拿它做版本判断和打包了

以前很多脚本喜欢这么写:

from distutils.version import LooseVersion

if LooseVersion(remote_ver) > LooseVersion(local_ver):
    upgrade()

这代码在老项目里相当常见。

问题是 distutils 从 Python 3.10 开始弃用,到 Python 3.12 已经正式从标准库移除。

版本比较我现在直接用 packaging。

比如一个部署脚本,要判断服务端插件是不是比本地新:

from packaging.version import Version

defneed_pull(current: str, published: str) -> bool:
return Version(published) > Version(current)

if need_pull("2.9.8", "2.10.1"):
    print("发现新版本,准备拉取")

这地方千万别自己:

"2.10.1" > "2.9.8"

字符串比较版本号,迟早给自己挖坑。

还有一种情况比较迷惑。

你升级 Python 3.12 后,某些环境里 import distutils 居然还能成功,于是觉得官方不是说移除了吗?

很可能是 setuptools 给你兜了一层兼容。

Python 官方文档也明确提到,第三方的 Setuptools 仍然可以提供 distutils,但 Python 3.12 创建虚拟环境时连 setuptools 都不再默认预装。

所以别拿“我电脑还能跑”当依据。

CI 换台机器可能就炸了。


imp,动态加载代码的老项目里特别多

我见过不少插件系统、规则脚本、测试工具还留着这种东西:

import imp

module = imp.load_source("checker", "/opt/rules/checker.py")

这个模块其实早就该清了。

imp 从 Python 3.4 就开始弃用,一直拖到 Python 3.12 才彻底移除,官方给的迁移方向就是 importlib。

现在动态加载本地规则文件,我一般这么写:

import importlib.util
from pathlib import Path

defload_rule(rule_file: Path):
    module_name = f"_rule_{rule_file.stem}"

    spec = importlib.util.spec_from_file_location(module_name, rule_file)

if spec isNoneor spec.loader isNone:
raise RuntimeError(f"规则文件无法加载: {rule_file}")

    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)

return module

rule = load_rule(Path("/srv/rules/check_amount.py"))
result = rule.check({"amount": 3800})

代码是多了几行。

但动态加载这种事情,我反而不喜欢一句话全藏掉。

模块从哪里来的、loader 有没有拿到、哪一步加载失败,都应该看得见。

出问题的时候好查。


asyncore、asynchat,老 TCP 服务别再往上续命

这个我印象比较深。

维护过一些年份比较久的 TCP 服务,尤其设备通信、内部代理、Socket 服务,很容易翻出:

import asyncore

你看到它的时候基本就该提高警惕了。

asyncore 和 asynchat 从 Python 3.6 就已经弃用,到 Python 3.12 正式删除,官方给出的替代就是 asyncio。

比如我现在写一个很轻量的设备探测服务,不会再围着 asyncore.dispatcher 搭东西:

import asyncio

asyncdefreceive_probe(reader, writer):
    peer = writer.get_extra_info("peername")

try:
        payload = await asyncio.wait_for(
            reader.readline(),
            timeout=5
        )

        device_code = payload.decode(errors="replace").strip()

        print(f"probe from={peer}, device={device_code}")

        writer.write(b"OK\n")
await writer.drain()

except TimeoutError:
        print(f"probe timeout, peer={peer}")

finally:
        writer.close()
await writer.wait_closed()

asyncdefserve():
    server = await asyncio.start_server(
        receive_probe,
        host="0.0.0.0",
        port=9108
    )

asyncwith server:
await server.serve_forever()

asyncio.run(serve())

不是说换成 asyncio 就天然高性能。

线程、事件循环、阻塞调用这些坑一个都不会自动消失。

但你现在还继续给 asyncore 写新代码,就有点跟 Python 版本升级硬扛了。

没必要。


smtpd 被删了,但别把 smtplib 一起误杀

这两个名字很容易看串。

smtpd 是拿来做 SMTP Server 的,Python 3.12 已经移除了,官方推荐考虑基于 asyncio 的第三方库 aiosmtpd。

但是:

import smtplib

这个不是一回事。

你业务代码只是连接公司的 SMTP 服务发邮件,smtplib 还在。

所以代码扫描的时候别看到 SMTP 三个字母就开始全局替换。

这种升级我最烦“一把梭”。

先看模块到底拿来干什么。


cgi,看到这个名字基本已经有年代感了

Python 3.13 项目里再执行:

import cgi

已经没了。

cgi 从 Python 3.11 弃用,Python 3.13 正式移除,最后一个自带它的版本是 Python 3.12。

老代码里比较典型的是:

cgi.FieldStorage()

用来自己处理表单和上传参数。

这种代码新项目我一般不会迁成另一套“手撸 HTTP 表单解析”。

如果已经用了 FastAPI、Django、Flask 之类的框架,就让框架处理。

如果只是一个很薄的 WSGI 工具,需要读 Query String,标准库已经够了:

from urllib.parse import parse_qs

defread_filters(environ):
    raw_query = environ.get("QUERY_STRING", "")

    params = parse_qs(
        raw_query,
        keep_blank_values=True
    )

return {
"keyword": params.get("keyword", [""])[0],
"page": int(params.get("page", ["1"])[0])
    }

至于 multipart/form-data 文件上传,别为了少装一个依赖,自己在那里切 boundary。

我见过这种代码。

刚开始几十行,后面修中文文件名、重复字段、大文件、异常报文,越修越不像人写的。

Python 官方对 cgi.FieldStorage 的迁移建议里,也给出了 urllib.parse、email.message 以及 multipart 第三方库这些方向。

PyPI 上虽然还有 legacy-cgi 可以给老系统续一下命,但如果是新项目,我不会重新把它接回来。


imghdr,上传图片的代码里经常还能翻出来

这个更容易中招:

import imghdr

image_type = imghdr.what(file_path)

很多头像上传、图片导入、批处理脚本以前都这么判断图片类型。

Python 3.13 已经把 imghdr 移除了。

官方现在给出的替代包括 filetype、puremagic、python-magic。

比如上传目录里做第一层文件检查,可以这么搞:

from pathlib import Path
import filetype

ALLOW_MIME = {
"image/jpeg",
"image/png",
"image/webp",
}

defcheck_upload(path: Path):
    kind = filetype.guess(path)

if kind isNone:
raise ValueError("无法识别文件格式")

if kind.mime notin ALLOW_MIME:
raise ValueError(f"不允许上传该类型: {kind.mime}")

return kind.mime

不过这里我还得多说一句。

识别出来是 image/jpeg,不代表这个文件就绝对安全。

真是用户上传场景,我通常还会让图片库实际解码一次,再做尺寸、像素、文件大小限制。

只看:

xxx.jpg

这个后缀是最不值得信的东西。


telnetlib,设备项目和运维脚本重点搜一下

一般 Web 项目不太能碰见它。

做过交换机、路由器、工控设备、老服务器自动化的,代码仓库里没准现在还有:

import telnetlib

Python 3.13 已经移除了 telnetlib。官方列出的可选替代包括 telnetlib3 和 Exscript。

这种代码我不会直接机械替换。

我一般先问一句:

这台设备现在还必须用 Telnet 吗?

如果设备已经支持 SSH,那我宁可顺手把协议也换掉。

如果是二十年前的设备,固件动不了,现场就认 Telnet,那再考虑 telnetlib3。

技术债有时候不是换个 import 就算还完。


pipes,拼 Shell 命令的代码该收拾了

老脚本里还可能见到:

import pipes

尤其是拼 shell 参数。

Python 3.13 已经把它移除,官方迁移建议很直接:

  • 跑子进程,用 subprocess
  • 只是需要 shell 参数转义,用 shlex.quote()

但我写自动化脚本时,更喜欢从源头上少拼 shell 字符串。

比如备份一个目录:

import subprocess
from pathlib import Path

defarchive_dir(source: Path, output: Path):
    cmd = [
"tar",
"-czf",
        str(output),
"-C",
        str(source.parent),
        source.name,
    ]

    result = subprocess.run(
        cmd,
        check=True,
        capture_output=True,
        text=True,
    )

return result.stderr

这里故意没有:

shell=True

路径也不用自己在那里加引号、转义空格、判断特殊字符。

能传参数列表的时候,就别先拼成一坨字符串再交给 Shell 重新解析。

省很多烂问题。


还有一个我建议认证系统重点搜:crypt

Python 3.13 同样把 crypt 模块移除了。对于密码哈希,官方文档给出的替代方向包括 bcrypt、argon2-cffi 等。

新代码如果还在自己折腾:

crypt.crypt(password, salt)

我一般直接改。

比如使用 Argon2:

from argon2 import PasswordHasher

password_hasher = PasswordHasher()

defencode_password(raw_password: str) -> str:
return password_hasher.hash(raw_password)

defverify_password(encoded: str, raw_password: str) -> bool:
try:
return password_hasher.verify(encoded, raw_password)
except Exception:
returnFalse

认证这一块尤其别有“原来能跑就不动”的想法。

能跑和该不该继续跑,是两回事。


如果手上有个几年没升级过 Python 的项目,我不会一个目录一个目录地肉眼翻。

先扫一遍:

rg -n '\b(import|from)\s+(distutils|imp|asyncore|asynchat|smtpd|cgi|imghdr|telnetlib|pipes|crypt)\b' .

再把测试环境拉到准备升级的 Python 版本:

python -Wd -m pytest

-Wd 我挺喜欢加。

有些弃用警告平时被藏得太好了,开发几年都没人看,直到某个大版本直接把 API 删除,CI 才突然一片红。

Python 这几年清理标准库的动作并不小。

特别是 3.12 和 3.13。后者按照 PEP 594 一口气清掉了 19 个被认为过时、历史遗留或存在更好替代方案的标准库模块。

所以升级 Python,我现在第一件事一般不是研究新语法。

先搜 import。

老代码里那几个看着特别眼熟、十几年没动过的库,往往比业务代码先给你一拳。




上一篇:iLoader 上手:iPhone 侧载 IPA 的流程,砍成了三步
下一篇:OpenAI Codex 负责人 Tibo 万字访谈:28天改进承诺、Dots 路线图与智能体生态
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-6 02:56 , Processed in 0.094679 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表