寫軟著是一個神奇的工作,最后還要將一些實(shí)際代碼粘貼到 word 文檔中硫戈,這種繁瑣的工作怎么能手動搞呢锰什,python 腳本走起來。
需求
一股腦粘貼上所有代碼即可丁逝,不用解釋不用組織汁胆,字號小點(diǎn)一頁50行以上即可。當(dāng)然像我介么優(yōu)秀的程序猿要考慮得更通用一點(diǎn):
- 代碼加個語法高亮果港,支持大多數(shù)語言沦泌。
- 可以自定義需要收集的文件的擴(kuò)展名
- 可以設(shè)置哪些文件或目錄需要忽略,有一些帶密碼或者私有 key 的源文件不能放辛掠。
- 可以限制行數(shù)
技術(shù)方案
既然要生成 word 文檔谢谦,先搜了一下 python 生成 docx 格式的庫,果然有萝衩,就叫 python-docx回挽。簡單試了一下,直接生成沒有語法高亮的文檔很簡單猩谊,如果要語法高亮就要調(diào)用各種 API 來添加格式化的文本千劈。
再搜了一下語法高亮的庫,果然也有牌捷,Pygments墙牌,支持幾乎所有語言,看示例代碼也很簡單暗甥。簡單說一下原理:Pygments 支持多種語言喜滨,需要先判斷代碼文本的語言,然后根據(jù)語言選擇詞法分析器 lexer撤防,文本通過語法分析器的處理得到結(jié)構(gòu)化的 token 流虽风,再選擇一種 formatter 來輸出語法高亮的代碼。lexer 跟源代碼的語言相關(guān),Pygments 支持幾乎所有語言辜膝,只用考慮怎么判斷語言就行无牵。formatter 的支持種類有限,有 html厂抖、pdf茎毁、各種圖片等……就是沒有 docx。
理論上可以通過 python-docx 庫來自定義 formatter 自行實(shí)現(xiàn)一個 docx 的版本验游,Pygments 對自定義 formatter 支持得很好……但工作量比較大充岛,不是一兩天能搞定的保檐。最后選擇生成 html耕蝉,最后一步 html 轉(zhuǎn)到 docx 通過手動進(jìn)行——看起來比較 low,實(shí)際上我還調(diào)研了一些自動轉(zhuǎn)換方法:
- 調(diào)用 web 服務(wù)接口轉(zhuǎn)換:一些免費(fèi)的在線工具可以在網(wǎng)頁上免費(fèi)用夜只,調(diào)用接口就需要購買了垒在,免費(fèi)額度比較少,還必須聯(lián)網(wǎng)扔亥。
- 還有一個叫 pandoc 的庫可以轉(zhuǎn)換场躯,但是不支持 style 的轉(zhuǎn)換,語法高亮沒了旅挤。
Word 能直接打開 html 文件踢关,只需要「另存為」一下就可以轉(zhuǎn)換成 docx,不需要額外工具粘茄。因此签舞,最后決定還是轉(zhuǎn)換為 html,再手工轉(zhuǎn)換為 word柒瓣。
代碼
腳本已放到 Github 上了:codes2html儒搭,直接可用。下文解析一下關(guān)鍵代碼芙贫。
argparse 自定義參數(shù)
有一些參數(shù)需要配置搂鲫,因此使用了 argparse 庫來定義和解析參數(shù),這個庫非常強(qiáng)大磺平,只需定義好參數(shù)魂仍,help 信息能自動生成〖鹋玻看下面的例子:
import argparse
parser = argparse.ArgumentParser(description='A tool to collect codes and highlight syntax in a single html document.')
parser.add_argument('sources', # 無前綴參數(shù)
metavar='source', # 顯示在 help 中的名字
nargs='+', # 指定該參數(shù)數(shù)量擦酌,"+" 表示至少一個,還可以設(shè)置具體數(shù)量
help='source code directory or file') # help 信息
parser.add_argument('-o', '--out', # 可以指定多個名字媒吗,哪個都可以
help='output file path. default is output.html',
default='output.html', # 設(shè)置默認(rèn)值
dest='output') # 代碼中的標(biāo)識符仑氛,如果不寫就用前面參數(shù)名稱 ”--out“ 指定的 "out"
parser.add_argument('--insert-file-name', help='insert file name as header of a file',
action='store_true',
# 這個 store_true 表示出現(xiàn)這個 '--insert-file-name' 參數(shù)
# 就將 .insert_file_name 設(shè)置為 True
dest='insert_file_name')
args = parser.parse_args()
args.source # list of str
args.output # str
args.insert_file_name # bool
使用 add_argument()
方法來定義參數(shù),這個方法參數(shù)比較多,一般有幾種類型的參數(shù):
- 不需要
-x
或--xxx
這類前綴的參數(shù)锯岖,一般當(dāng)做主要參數(shù)介袜。 - 指定
-x
或--xxx
前綴的參數(shù),一般當(dāng)做可選參數(shù)出吹,并提供默認(rèn)值遇伞。 - 使用
-x
或--xxx
作為開關(guān)。
這個模塊功能比較多捶牢,具體用法可參考以下鏈接:
name-or-flags
nargs
action
遍歷目錄鸠珠、extensions 參數(shù)、ignore 文件
遍歷目錄有幾種方法:glob.glob
秋麸,os.walk
渐排,os.listdir
。前面兩個都是自動遞歸遍歷灸蟆。os.listdir
需要自己遞歸調(diào)用驯耻,由于需要遍歷到 ignore 的目錄時能終止其子目錄的遍歷,使用 os.listdir
看起來比較清晰一點(diǎn)炒考。
extensions 參數(shù)指定哪些擴(kuò)展名可以作為源文件可缚。
ignore 文件使用類似 .gitignore
語法:按行分割成一個匹配字符串列表,作為 ignore 規(guī)則斋枢。遍歷過程中如果一個文件匹配了 ignore 規(guī)則帘靡,直接將這個文件忽略;如果一個目錄匹配了 ignore 規(guī)則瓤帚,忽略它并且不進(jìn)入其中遍歷描姚,也就是忽略所有的子目錄和文件。
class Codes2HtmlTool:
def _collect_files(self, path):
subfiles = os.listdir(path)
subfiles.sort() # 按字母順序排個序
for subfile in subfiles:
if self.written_lines >= self.args.lines: # 行數(shù)限制判斷
break
if subfile.startswith('.'): # 隱藏目錄直接忽略
continue
full_path = os.path.join(path, subfile)
# 調(diào)用 _should_ignore_file 方法判斷是否需要忽略
if self._should_ignore_file(subfile):
print('ignore "', full_path, '"', sep='')
continue
if os.path.isdir(full_path): # 如果是目錄缘滥,遞歸調(diào)用
self._collect_files(full_path)
elif self._accept_extension(subfile): # 如果是文件轰胁,還要檢查擴(kuò)展名
# 如果擴(kuò)展名符合,調(diào)用文件處理方法
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 表示對擴(kuò)展名沒有限制
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 其實(shí)還可以生成 rtf 格式的文檔朝扼,它比 html 更接近 docx赃阀,因?yàn)?word 軟件會自動關(guān)聯(lián) rtf 擴(kuò)展名。但經(jīng)過調(diào)研發(fā)現(xiàn) Pygments 對 rtf 格式的處理沒有 html 靈活擎颖,rtf 文件頭中的樣式定義沒有剝離開榛斯,多個文件格式化拼接到一起比較麻煩。Pygments 的 html formatter 將 css 定義單獨(dú)抽象出來搂捧,并提供了只返回引用 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:
# 先根據(jù)文件名獲取 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" 文件王凑,根據(jù)內(nèi)容再次判斷一下
lexer = get_lexer_for_filename(full_path, code=content)
formatted = highlight(content, lexer, hf) # 高亮代碼返回格式化代碼
write_fd.write(formatted) # 寫入格式化的代碼
write_fd.write(footer) # 寫入?yún)?shù)中定義的 footer
print('highlighted with ', _short_class_name(lexer), ': "', full_path, '"', sep='')
except:
# 如果正確設(shè)置了 extensions 參數(shù)搪柑,異常情況應(yīng)該很少出現(xiàn)
print('not source code: "', full_path, '"', sep='')
猜測代碼語言,可以通過文件擴(kuò)展名索烹,也可以通過文件內(nèi)容工碾。比較有趣的是 .h
文件只通過擴(kuò)展名,會判定為 C
語言百姓。但 Objective-C
也使用 .h
文件渊额,而且 Objective-C
是 C
的超集,有一些 C
中沒有的語法垒拢,如果只用文件名旬迹,就會導(dǎo)致一些語法解析錯誤,不能正確高亮求类。同時使用文件名和文件內(nèi)容判斷才可以正確判定使用的是 Objective-C
還是 C
奔垦。
用法簡介
- 需要 python3
- 需要安裝 Pygments
pip install Pygments
- 最簡單用法:python codes2html.py [目錄],會遍歷指定的目錄仑嗅,收集 3500 行代碼到一個單獨(dú)的 html 文件中宴倍。
- 參數(shù)
-e
,--extensions
:限定源代碼文件擴(kuò)展名张症,用逗號分割的字符串仓技,如果不限制可以傳 "*" 或者不寫。默認(rèn)值"*"
俗他。 - 參數(shù)
-l
,--lines
:限制讀入的源代碼行數(shù)脖捻,但不會截斷一個完整的文件,所以最終行數(shù)可能會大于這個值兆衅。0 表示不限制行數(shù)地沮。默認(rèn)值3500
。 - 參數(shù)
-o
,--out
:指定輸出的 html 文件名羡亩。默認(rèn)值output.html
- 參數(shù)
-i
,--ignore
:指定 ignore 文件摩疑。默認(rèn)值ignore.txt
- 參數(shù)
-f
,--footer
:指定每個文件末尾插入的內(nèi)容,HTML 格式字符串畏铆。默認(rèn)值</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 項(xiàng)目
Pods
Assets.xcassets
*.framework
AppDelegate.*
(ole)