必须用 sys.meta_path 插入自定义 MetaPathFinder 才能可靠拦截所有 import 行为,因其是唯一能覆盖 from x import y、importlib.import_module 及 frozen module 的层级;直接删 sys.modules 或改 import 不可靠,因底层走 importlib._bootstrap._find_and_load 流程。

为什么直接删 sys.modules 或改 __import__ 不可靠
很多人试过在导入前清空 sys.modules['requests'],或重写 __import__ 函数,结果发现:模块仍能被成功导入(尤其在多线程、importlib.import_module 或第三方库内部调用时失效)。根本原因是 Python 的 import 机制分多层——__import__ 只是高层封装,底层走的是 importlib._bootstrap 中的 _find_and_load 流程,绕过它等于没控制。
必须用 sys.meta_path 插入自定义 MetaPathFinder
这是唯一能稳定拦截所有 import 行为的位置,包括 from x import y、importlib.import_module、甚至 frozen module 加载。你需要实现一个类,重写 find_spec 方法,在其中判断模块名并拒绝加载:
import sys
from importlib.abc import MetaPathFinder
from importlib.util import spec_from_file_location
<p>class BlockImportFinder(MetaPathFinder):
def <strong>init</strong>(self, blocked_modules):
self.blocked = set(blocked_modules)</p><pre class="brush:php;toolbar:false;">def find_spec(self, fullname, path, target=None):
if fullname in self.blocked:
# 关键:返回 None 表示找不到,触发 ImportError
return None
# 其他模块交给后续 finder 处理
return None注册(必须放在任何被禁模块 import 之前)
sys.meta_path.insert(0, BlockImportFinder(['requests', 'os', 'subprocess']))
-
sys.meta_path是一个 list,越靠前的 finder 优先级越高,务必用insert(0, ...) - 返回
None是标准做法,返回假值(如False)不会触发 ImportError,Python 会继续查下一个 finder - 模块名是完整路径(
'requests.api'、'django.db'),需按需做前缀匹配或通配逻辑
如何支持通配符和子模块拦截(比如禁掉整个 urllib)
单纯比对 fullname == 'urllib' 不够——用户仍可导入 urllib.request。得在 find_spec 中加前缀判断:
def find_spec(self, fullname, path, target=None):
if any(fullname == mod or fullname.startswith(mod + '.') for mod in self.blocked):
return None
return None
- 注意顺序:先检查完全匹配(
'urllib'),再检查前缀('urllib.'),避免把'urllib2'误杀 - 不要用正则——import 过程极频繁,正则编译和匹配开销大,且
find_spec要求无副作用 - 如果要放行某个子模块(如允许
urllib.parse但禁其他),得写更细粒度的白名单逻辑,此时建议用字典树或预编译集合
真实环境里最容易被忽略的三个坑
很多方案本地测试 OK,一上生产就失效,问题通常出在这儿:
- 注册时机太晚:
import requests已执行完,sys.meta_path修改无效——必须在任何相关 import 语句前完成注册,推荐放在入口文件最顶部,或用-m启动前通过python -c "import block; block.install(); exec(open('main.py').read())" - 被
zipimport或frozen modules绕过:某些打包工具(PyInstaller、cx_Freeze)会把模块打进 zip 或冻结成字节码,此时sys.meta_path不生效,得配合修改sys.path_hooks或 patchzipimport.zipimporter.find_spec - 动态导入逃逸:
eval("import requests")、exec(compile(...))或字符串拼接 import,这种不属于 import 机制管控范围,只能靠 AST 扫描或限制eval/exec来补防
权限控制不是设个钩子就完事,关键看模块是否真没进内存——检查 'requests' in sys.modules 和 importlib.util.find_spec('requests') 返回值,比只信异常更可靠。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











