概述###
Markdown 很適合於技術寫作,因為技術寫作並不需要花哨的排版和內容, 只要內容生動而嚴謹,文筆朴實而優美。
為了編寫對讀者更友好的文章,有必要生成文章的標題導航,讓讀者有個預期的閱讀概覽。當文章標題比較多時,手工去編寫導航錨點比較費時,因此決定使用Python解析Markdown文檔自動生成標題導航。
知識與思路###
寫過Markdown的人知道,Markdown的標題是使用一到六個# 左右包圍住標題文字,而錨點是 [標題](#標題)。 比如 ## 知識與思路 ## ,錨點連接是 [知識與思路](#知識與思路)。標題文字允許包含空格,不過最好不要用空格或特殊不可打印字符,因為在網頁上點擊帶錨點的鏈接時可能失效,並且網站對Markdown錨點的支持可能有特殊規則,比如博客園會將標題中的大寫字母自動轉成小寫字母,並且對於有空格的標題可能無法鏈接到錨點。
這樣,就需要解析標題行,從中提取出標題並生成錨點鏈接。通常使用正則表達式來匹配和提取文本內容。此外,為了排版,也要根據標題的級別生成相應的縮進。考慮到段落中很可能含有#字符,因此標題最好從第二級開始,不超過四級。
標題行的正則表達式是:
\s*(#{2,6})\s*(.*?)\s*(?:\1)\s+
知識點:
- 2-6 個#號使用 #{2,6} 匹配;
- 由於行起始和標題前后都可能含有空格,因此需要使用\s*來兼容;行末尾至少有個換行符,因此要使用 \s+;
- 使用(subregex)捕獲 subregex 匹配的文本, 使用 \1 表示后面與前面對應的被匹配的分組文本, 使用 (?:subregex) 表示匹配 subregex 的文本,但在捕獲分組時忽略,不作為提取內容;
- 使用 (.*?) 匹配標題內容, ? 表示非貪婪模式,避免將 # 也包含到標題中;
- 標題的縮進比較簡單,第二級的不縮進,第N級縮進 (N-2)個指定字符,這里字符選定為中文空格
 
- 程序會接受一系列 Markdown 文檔的文件路徑參數,並輸出對應於每個文件的導航文本;由於參數比較簡單,就不使用 argparse 這樣的參數解析模塊了;
- 使用: python mdnav.py filename1 filename2 ... filenameN
代碼實現###
#!/usr/bin/python
#_*_encoding:utf-8_*_
##########################################################################
# mdnav.py: Generate nav anchors for given markdown files
# usage: python mdnav.py md_filename1 md_filename2 ... md_filenameN
##########################################################################
import re
import sys
mdTitleRegex = r'\s*(#{2,6})\s*(.*?)\s*(?:\1)\s+'
mdTitlePatt = re.compile(mdTitleRegex)
def parseLineByRegex(line, regex_patt):
m = regex_patt.match(line)
return m.groups() if m else ()
def outputAnchor(titleTuple):
if len(titleTuple) == 2:
intents = ' ' * (len(titleTuple[0])-2)
title = titleTuple[1]
anchor = '[%s](#%s)' % (title, title)
print intents,anchor
def procLine(line):
outputAnchor(parseLineByRegex(line, mdTitlePatt))
def help():
print '%s usage: need at least one param as markdown filename' % sys.argv[0]
print 'python %s filename1 filename2 ... filenameN' % sys.argv[0]
if __name__ == '__main__':
if len(sys.argv) < 2:
help()
exit(1)
for ind in range(1, len(sys.argv)):
filename = sys.argv[ind]
print 'file: ', filename
with open(filename) as mdtext:
map(procLine, mdtext.readlines())