写软著是一个神奇的工作,最后还要将一些实际代码粘贴到 word 文档中,这种繁琐的工作怎么能手动搞呢,python 脚本走起来。
需求
一股脑粘贴上所有代码即可,不用解释不用组织,字号小点一页50行以上即可。当然像我介么优秀的程序猿要考虑得更通用一点:
- 代码加个语法高亮,支持大多数语言。
- 可以自定义需要收集的文件的扩展名
- 可以设置哪些文件或目录需要忽略,有一些带密码或者私有 key 的源文件不能放。
- 可以限制行数
技术方案
既然要生成 word 文档,先搜了一下 python 生成 docx 格式的库,果然有,就叫 python-docx。简单试了一下,直接生成没有语法高亮的文档很简单,如果要语法高亮就要调用各种 API 来添加格式化的文本。
再搜了一下语法高亮的库,果然也有,Pygments,支持几乎所有语言,看示例代码也很简单。简单说一下原理:Pygments 支持多种语言,需要先判断代码文本的语言,然后根据语言选择词法分析器 lexer,文本通过语法分析器的处理得到结构化的 token 流,再选择一种 formatter 来输出语法高亮的代码。lexer 跟源代码的语言相关,Pygments 支持几乎所有语言,只用考虑怎么判断语言就行。formatter 的支持种类有限,有 html、pdf、各种图片等……就是没有 docx。
理论上可以通过 python-docx 库来自定义 formatter 自行实现一个 docx 的版本,Pygments 对自定义 formatter 支持得很好……但工作量比较大,不是一两天能搞定的。最后选择生成 html,最后一步 html 转到 docx 通过手动进行——看起来比较 low,实际上我还调研了一些自动转换方法:
- 调用 web 服务接口转换:一些免费的在线工具可以在网页上免费用,调用接口就需要购买了,免费额度比较少,还必须联网。
- 还有一个叫 pandoc 的库可以转换,但是不支持 style 的转换,语法高亮没了。
Word 能直接打开 html 文件,只需要「另存为」一下就可以转换成 docx,不需要额外工具。因此,最后决定还是转换为 html,再手工转换为 word。
代码
脚本已放到 Github 上了:codes2html,直接可用。下文解析一下关键代码。
argparse 自定义参数
有一些参数需要配置,因此使用了 argparse 库来定义和解析参数,这个库非常强大,只需定义好参数,help 信息能自动生成。看下面的例子:
import argparse
parser = argparse.ArgumentParser(description='A tool to collect codes and highlight syntax in a single html document.')
parser.add_argument('sources', # 无前缀参数
metavar='source', # 显示在 help 中的名字
nargs='+', # 指定该参数数量,"+" 表示至少一个,还可以设置具体数量
help='source code directory or file') # help 信息
parser.add_argument('-o', '--out', # 可以指定多个名字,哪个都可以
help='output file path. default is output.html',
default='output.html', # 设置默认值
dest='output') # 代码中的标识符,如果不写就用前面参数名称 ”--out“ 指定的 "out"
parser.add_argument('--insert-file-name', help='insert file name as header of a file',
action='store_true',
# 这个 store_true 表示出现这个 '--insert-file-name' 参数
# 就将 .insert_file_name 设置为 True
dest='insert_file_name')
args = parser.parse_args()
args.source # list of str
args.output # str
args.insert_file_name # bool
使用 add_argument()
方法来定义参数,这个方法参数比较多,一般有几种类型的参数:
- 不需要
-x
或--xxx
这类前缀的参数,一般当做主要参数。 - 指定
-x
或--xxx
前缀的参数,一般当做可选参数,并提供默认值。 - 使用
-x
或--xxx
作为开关。
这个模块功能比较多,具体用法可参考以下链接:
name-or-flags
nargs
action
遍历目录、extensions 参数、ignore 文件
遍历目录有几种方法:glob.glob
,os.walk
,os.listdir
。前面两个都是自动递归遍历。os.listdir
需要自己递归调用,由于需要遍历到 ignore 的目录时能终止其子目录的遍历,使用 os.listdir
看起来比较清晰一点。
extensions 参数指定哪些扩展名可以作为源文件。
ignore 文件使用类似 .gitignore
语法:按行分割成一个匹配字符串列表,作为 ignore 规则。遍历过程中如果一个文件匹配了 ignore 规则,直接将这个文件忽略;如果一个目录匹配了 ignore 规则,忽略它并且不进入其中遍历,也就是忽略所有的子目录和文件。
class Codes2HtmlTool:
def _collect_files(self, path):
subfiles = os.listdir(path)
subfiles.sort() # 按字母顺序排个序
for subfile in subfiles:
if self.written_lines >= self.args.lines: # 行数限制判断
break
if subfile.startswith('.'): # 隐藏目录直接忽略
continue
full_path = os.path.join(path, subfile)
# 调用 _should_ignore_file 方法判断是否需要忽略
if self._should_ignore_file(subfile):
print('ignore "', full_path, '"', sep='')
continue
if os.path.isdir(full_path): # 如果是目录,递归调用
self._collect_files(full_path)
elif self._accept_extension(subfile): # 如果是文件,还要检查扩展名
# 如果扩展名符合,调用文件处理方法
self._highlight_and_write_file(full_path)
def _should_ignore_file(self, name):
return _match_any_pattern(name, self.args.ignore_patterns)
def _accept_extension(self, name):
patterns = self.args.extension_patterns
# 没有 patterns 表示对扩展名没有限制
return len(patterns) == 0 or _match_any_pattern(name, patterns)
def _match_any_pattern(name, patterns):
for pattern in patterns:
if fnmatch.fnmatch(name, pattern):
return True
return False
语法高亮
Pygments 其实还可以生成 rtf 格式的文档,它比 html 更接近 docx,因为 word 软件会自动关联 rtf 扩展名。但经过调研发现 Pygments 对 rtf 格式的处理没有 html 灵活,rtf 文件头中的样式定义没有剥离开,多个文件格式化拼接到一起比较麻烦。Pygments 的 html formatter 将 css 定义单独抽象出来,并提供了只返回引用 css 的 html 片段,适合多个文件使用同一配色方案拼接成一个大文件的场景。
from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import get_lexer_for_filename
# hf - HtmlFormatter
class Codes2HtmlTool:
def _highlight_and_write_file(self, full_path):
write_fd = self.write_fd
hf = self.hf
footer = self.args.file_footer
try:
# 先根据文件名获取 lexer,如果无法识别为源代码会直接抛异常
lexer = get_lexer_for_filename(full_path)
with open(full_path) as fd:
lines = fd.readlines()
self.written_lines += len(lines)
content = ''.join(lines)
if full_path.endswith('.h'):
# 如果是 ".h" 文件,根据内容再次判断一下
lexer = get_lexer_for_filename(full_path, code=content)
formatted = highlight(content, lexer, hf) # 高亮代码返回格式化代码
write_fd.write(formatted) # 写入格式化的代码
write_fd.write(footer) # 写入参数中定义的 footer
print('highlighted with ', _short_class_name(lexer), ': "', full_path, '"', sep='')
except:
# 如果正确设置了 extensions 参数,异常情况应该很少出现
print('not source code: "', full_path, '"', sep='')
猜测代码语言,可以通过文件扩展名,也可以通过文件内容。比较有趣的是 .h
文件只通过扩展名,会判定为 C
语言。但 Objective-C
也使用 .h
文件,而且 Objective-C
是 C
的超集,有一些 C
中没有的语法,如果只用文件名,就会导致一些语法解析错误,不能正确高亮。同时使用文件名和文件内容判断才可以正确判定使用的是 Objective-C
还是 C
。
用法简介
- 需要 python3
- 需要安装 Pygments
pip install Pygments
- 最简单用法:python codes2html.py [目录],会遍历指定的目录,收集 3500 行代码到一个单独的 html 文件中。
- 参数
-e
,--extensions
:限定源代码文件扩展名,用逗号分割的字符串,如果不限制可以传 "*" 或者不写。默认值"*"
。 - 参数
-l
,--lines
:限制读入的源代码行数,但不会截断一个完整的文件,所以最终行数可能会大于这个值。0 表示不限制行数。默认值3500
。 - 参数
-o
,--out
:指定输出的 html 文件名。默认值output.html
- 参数
-i
,--ignore
:指定 ignore 文件。默认值ignore.txt
- 参数
-f
,--footer
:指定每个文件末尾插入的内容,HTML 格式字符串。默认值</br>
。
举个例子
python codes2html.py ~/texthere/ ~/next/ -e h,c,cpp,m,mm,swift -l 5000 -i xcode_ignore.txt -o all_ios_projects.html
ignore 文件示例,iOS 项目
Pods
Assets.xcassets
*.framework
AppDelegate.*
(ole)