tokenize.generate_tokens() 是提取注释的正确入口,需逐个检查 token.type == tokenize.comment,构造完整可解析输入流,过滤编码声明,单次遍历分类收集以兼顾准确性和性能。

tokenize.generate_tokens() 是提取注释的正确入口
Python 标准库 tokenize 模块不提供“直接获取所有注释”的快捷函数,必须用 generate_tokens() 迭代器逐个检查 token 类型。注释在词法层面被识别为 tokenize.COMMENT 类型,但仅当源码以合法 Python 语法形式传入时才可靠触发——这意味着不能直接对字符串片段(如 "# hello")调用,而必须构造完整可解析的输入流。
常见错误是试图用 tokenize.tokenize()(需要 bytes 流且以 encoding 开头)或误把文档字符串(tokenize.STRING)当成注释。文档字符串本质是字符串字面量,不属于注释 token。
实操建议:
- 用
io.StringIO(source_code).readline构造 readline 函数,传给generate_tokens() - 严格过滤
tok.type == tokenize.COMMENT,忽略tokenize.NL(空行)和tokenize.NEWLINE - 确保源码末尾有换行符,否则最后一行注释可能被截断(
generate_tokens内部依赖换行结束 token 解析)
处理多行注释和嵌套井号的边界情况
Python 本身不支持多行注释语法,所有 # 后内容均视为单行注释,但实际代码中常出现形如 """# not a comment""" 或 print("# inside string") 的干扰项。这些不会产生 COMMENT token,因为 tokenize 在字符串和注释之间有明确优先级:字符串字面量先于注释识别。
真正需警惕的是缩进后紧跟 # 的场景,比如:
if True:
x = 1 # inline comment
# standalone comment
两者都会生成 COMMENT token,但位置信息不同:tok.start 和 tok.end 是 (行号, 列号) 元组,inline 注释的起始列号大于缩进列数。
实操建议:
- 用
tok.string获取原始注释文本(含开头#),用.lstrip('#').strip()提取内容 - 若需区分 inline / standalone,比较
tok.start[1]与该行首非空白字符列号(需自行从tok.line计算) - 不要依赖正则匹配
#.*,它会误伤字符串内、f-string 中的#
兼容性陷阱:Python 3.7+ 的 token.EXACT_TOKEN_TYPES 影响注释识别
Python 3.7 引入了 token.EXACT_TOKEN_TYPES,但 tokenize.COMMENT 不在此映射中,它始终是独立常量。不过,某些旧脚本会错误地用 token.NAME 或 token.OP 做兜底判断,导致漏掉注释。
更隐蔽的问题是编码声明:如果源码开头含 # -*- coding: utf-8 -*-,它会被识别为 COMMENT,但这是编码声明而非业务注释。是否过滤取决于需求——多数静态分析工具会跳过首行或前两行的编码/模式声明。
实操建议:
- 显式导入
import tokenize,不要用from token import *,避免混淆token和tokenize模块 - 检查
tok.line.strip().startswith('# -*- coding:') or 'coding:' in tok.line来跳过编码行 - 在 CI 或多版本环境中测试 Python 3.6–3.12,确认
tokenize.COMMENT值未变化(目前恒为 54)
性能敏感场景下避免重复解析
每次调用 generate_tokens() 都会重新做完整的词法扫描,若需同时提取注释、函数名、import 行等,不要多次遍历;应一次迭代中分类收集。反复调用 tokenize.detect_encoding() 或 tokenize.open() 也会引入开销。
另一个易忽略点:generate_tokens() 对空文件或只有空白符的输入会抛 TokenError: EOF in multi-line statement,需包裹 try-except。
实操建议:
- 用单次循环 + 字典分组:
comments = [],imports = [],funcs = [],按tok.type分流 - 对大文件(>1MB),考虑用
tokenize.TokenInfo的line属性做懒加载,避免提前读取整行内容 - 若只需注释位置而不需内容,用
tok.start和len(tok.string)推算范围,比切片tok.line更快
注释 token 的行号列号是基于原始源码的,一旦源码经过 minify、preprocess 或 AST 修改,tokenize 输出的位置信息就不再对齐。这点在构建 LSP 插件或代码高亮时最容易被忽略。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











