文章目錄
  1. 1. Python3实现简易的markdown解释器
  • 2016-2017学年第1学期
    1. 实验报告
  • Python3实现简易的markdown解释器
    1. 一、介绍(说明文档)
      1. 1. 使用前的准备
      2. 2. 支持的语法
        1. 1. 1. 标题
        2. 2. 2. 列表
        3. 3. 3. 代码块
        4. 4. 4. 链接和图片
        5. 5. 5. 引用
        6. 6. 6. 粗体和斜体
        7. 7. 7. 分割线
      3. 3. 使用说明
    2. 二、代码分析
      1. 1. docopt文档
      2. 2. 需要导入的库和全局状态
      3. 3. 主函数
      4. 4. 执行函数
      5. 5. 转换函数
        1. 1. 5.1 处理文本块
        2. 2. 5.2 链接处理函数
        3. 3. 5.3 处理加粗、斜体标识
        4. 4. 5.4 加粗、斜体辅助函数
        5. 5. 5.5 两个处理函数和一个简单的判断函数
    3. 三. 其他说明
    4. 四. 自评分数
    5. 五. 评价理由以及心得体会
  • Python3实现简易的markdown解释器

    2016-2017学年第1学期

    实验报告

    • 课程名称:Python 程序设计基础
    • 实验项目:大作业
    • 专业班级:软件工程1501
    • 学生学号:31501293
    • 学生姓名:陈哲凡
    • 实验指导教师:郭鸣, 李飞

    Python3实现简易的markdown解释器

    一、介绍(说明文档)

    该简易的 markdown 解释器最终是一个.py文件,能在shall中通过命令将一个 markdown 文件转换成 HTML 和 PDF 文件(之所以称为简易是因为只支持部分语法,支持的语法将会在之后列出)。其原理是借助 docopt 进行命令行解析,随后逐行借助 re 匹配特定的 markdown 语法,向目标文件中添加入对应的 HTML 标签,最后借助 wkhtmltopdf 工具将 HTML 文件转换成 PDF 文件。

    1. 使用前的准备

    由于本解释器使用了 wkhtmltopdf 工具,以及第三方库 docopt,所以在使用前,你需要安装它们,在此给出它们的官方网站。

    2. 支持的语法

    1. 标题

    标题前使用 # ,例如下述代码块:

    # 一级标题
    ## 二级标题
    ### 三级标题
    #### 四级标题
    

    2. 列表

    有序列表前使用数字 1. , 2. 等,例如:

    1. item1
    2. item2
    3. item3
    
    1. item1
    2. item2
    3. item3

    无序列表前面加 -,*或者+ ,例如:

    * itemx
    + itemy
    - itemz
    
    • itemx
    • itemy
    • itemz

    3. 代码块

    在代码的上下分别用```包围:

    1
    2
    a = b + c
    print (a)

    4. 链接和图片

    1
    \[现实文本](网址)

    插入链接的样式为显示文本

    5. 引用

    使用 > 表示后续文字为引用,例如

    1
    > 引用文字

    引用文字

    6. 粗体和斜体

    用两个 包含一段文本就是粗体的语法,用一个 包含一段文本就是斜体

    1
    2
    3
    **粗体文字**

    *斜体文字*

    粗体文字

    斜体文字

    7. 分割线

    大于等于三个连续的 -

    1
    2
    3
    abc
    ---

    345

    3. 使用说明

    1
    $ python3 md2pdf.py 原文件名 参数 目标文件名
    1
    2
    3
    4
    5
    -h --help     显示帮助文档
    -v --version 显示版本
    -o --output 只生成 HTML 文件
    -p --print 分别生成 HTML 文件和 PDF 文件
    -P --Print 只生成 PDF 文件

    例:

    1
    $ python3 md2pdf.py test1.md -p test2.pdf

    生成一个 test2.pdf 文件和一个 translation_result.html 文件

    二、代码分析

    1. docopt文档

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    """md2pdf

    translates markdwon file into html or pdf, and support picture insertion.

    Usage:
    md2pdf <sourcefile> <outputfile> [options]

    Options:
    -h --help show help document.
    -v --version show version information.
    -o --output translate sourcefile into html file.
    -p --print translate sourcefile into pdf file and html file respectively.
    -P --Print translate sourcefile into pdf file only.
    """

    这部分在接下来会作为 __doc__ 来初始化 docopt

    docopt 会自动对其做命令行解释,这个过程不用我们来操心,十分方便

    2. 需要导入的库和全局状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import os,re
    import sys,getopt
    from enum import Enum
    from subprocess import call
    from functools import reduce

    from docopt import docopt

    __version__ = '1.0'

    # 有序序列状态
    class ORDERLIST(Enum):
    Init = 1
    List = 2

    # 块状态
    class BLOCK(Enum):
    Init = 1
    Block = 2
    CodeBlock = 3

    # 定义全局状态,并初始化状态
    # table_state = TABLE.Init
    orderList_state = ORDERLIST.Init
    block_state = BLOCK.Init
    is_code = False
    is_normal = True

    这些全局变量帮程序来表示当前文本的状态,比如是否为代码块的开头、中间、结尾,以此来加入合适的 HTML 标签

    3. 主函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    def main():
    dest_file = "translation_result.html"
    dest_pdf_file = "translation_result.pdf"

    only_pdf = False

    args = docopt(__doc__, version=__version__)

    #只要HTML,如果参数是-P,即HTML和PDF都要生成,HTML采用默认文件名
    dest_file = args['<outputfile>'] if args['--output'] else dest_file

    #需要生成PDF
    dest_pdf_file = args['<outputfile>'] if args['--print'] or args['--Print'] else ""


    run(args['<sourcefile>'], dest_file, dest_pdf_file, args['--Print'])


    if __name__=="__main__":
    main()

    其中args是docopt返回的一个字典,其key和value是根据我们在开头写的注释来生成的,如args[‘\<sourcefile>’]就会返回用户在命令行输入的原文件名。

    4. 执行函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    def run(source_file, dest_file, dest_pdf_file, only_pdf):
    # 获取文件名
    file_name = source_file
    # 转换后的 HTML 文件名
    dest_name = dest_file
    # 转换后的 PDF 文件名
    dest_pdf_name = dest_pdf_file

    # 获取文件后缀
    _, suffix = os.path.splitext(file_name)
    if suffix not in [".md",".markdown",".mdown","mkd"]:
    print('Error: the file should be in markdown format')
    sys.exit(1)

    if only_pdf:
    dest_name = ".~temp~.html"


    f = open(file_name, "r")
    f_r = open(dest_name, "w")

    # 往文件中填写 HTML 的一些属性
    f_r.write("""<style type="text/css">div {display: block;font-family: "Times New Roman",Georgia,Serif}\
    #wrapper { width: 100%;height:100%; margin: 0; padding: 0;}#left { float:left; \
    width: 10%; height: 100%; }#second { float:left; width: 80%;height: 100%; \
    }#right {float:left; width: 10%; height: 100%; \
    }
    </style><div id="wrapper"> <div id="left"></div><div id="second">""")

    f_r.write("""<meta charset="utf-8"/>""")

    # 逐行解析 markdwon 文件
    for eachline in f:
    result = parse(eachline)
    if result != "":
    f_r.write(result)

    f_r.write("""<br /><br /></div><div id="right"></div></div>""")

    f_r.close()
    f.close()

    # 调用扩展 wkhtmltopdf 将 HTML 文件转换成 PDF
    if dest_pdf_name != "" or only_pdf:
    call(["wkhtmltopdf", dest_name, dest_pdf_name])
    # 如果有必要,删除中间过程生成的 HTML 文件
    if only_pdf:
    call(["rm", dest_name])

    5. 转换函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    def parse(input):
    global block_state, is_normal
    is_normal = True
    result = input

    # 检测当前 input 解析状态
    result = test_state(input)

    #如果是代码块,那再test_state中已经处理完了,直接返回
    if block_state == BLOCK.Block:
    return result

    # 分析标题标记
    title_rank = 0
    for i in range(6, 0, -1):
    if input[:i] == '#'*i:
    title_rank = i
    break
    if title_rank != 0:
    # 处理标题,转化为相应的 HTML 文本
    result = handleTitle(input, title_rank)
    return result

    # 分析分割线标记 --
    if len(input) > 2 and all_same(input[:-1], '-') and input[-1] == '\n':
    result = "<hr>"
    return result

    # 解析无序列表
    unorderd = ['+', '-']
    if result != "" and result[0] in unorderd :
    result = handleUnorderd(result)
    is_normal = False

    f = input[0]
    count = 0
    sys_q = False
    while f == '>':
    count += 1
    f = input[count]
    sys_q = True
    #<b>是粗体
    if sys_q:
    result = "<blockquote style=\"color:#8fbc8f\"> "*count + "<b>" + input[count:] + "</b>" + "</blockquote>"*count
    is_normal = False

    # 处理特殊标记,比如 **, *
    result = tokenHandler(result)

    # 解析链接
    result = link_image(result)

    #空行换行
    pa = re.compile(r'^(\s)*$')
    a = pa.match(input)
    if input[-1] == "\n" and is_normal == True and not a :
    result+="<br />"

    return result

    通俗易懂的说,这个函数输入一行文本,通过 python 中的 re 库进行正则匹配 markdown语法,然后在文本中添加对应的 HTML 标签,然后返回处理好的行

    下面将列出转换函数的辅助函数。

    5.1 处理文本块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    def test_state(input):
    global orderList_state, block_state, is_code
    Code_List = ["python\n", "c++\n", "c\n"]

    result = input

    # 构建正则表达式规则
    # 匹配块标识
    pattern = re.compile(r'```(\s)*\n')
    a = pattern.match(input)

    # 普通块的开始
    if a and block_state == BLOCK.Init:
    result = "<blockquote>"
    block_state = BLOCK.Block
    is_normal = False
    # 特殊代码块
    elif len(input) > 4 and input[0:3] == '```' and (input[3:9] == "python" or input[3:6] == "c++" or input[3:4]== "c") and block_state == BLOCK.Init:
    block_state = BLOCK.Block
    result = "<code><br />"
    is_code = True
    is_normal = False
    # 块结束
    elif block_state == BLOCK.Block and input == '```\n':
    if is_code:
    result = "</code>"
    else:
    result = "</blockquote>"
    block_state = BLOCK.Init
    is_code = False
    is_normal = False
    elif block_state == BLOCK.Block:
    pattern = re.compile(r'[\n\r\v\f\ ]')
    result = pattern.sub("&nbsp", result)
    pattern = re.compile(r'\t')
    result = pattern.sub("&nbsp" * 4, result)
    result = "<span>" + result + "</span><br />"
    is_normal = False

    # 解析有序序列

    #列表开头
    if len(input) > 2 and input[0].isdigit() and input[1] == '.' and orderList_state == ORDERLIST.Init:
    orderList_state = ORDERLIST.List
    result = "<ol><li>" + input[2:] + "</li>"
    is_normal = False
    #列表中间
    elif len(input) > 2 and input[0].isdigit() and input[1] == '.' and orderList_state == ORDERLIST.List:
    result = "<li>" + input[2:] + "</li>"
    is_normal = False
    #列表结束
    elif orderList_state == ORDERLIST.List and (len(input) <= 2 or input[0].isdigit() == False or input[1] != '.'):
    result = "</ol>" + input
    orderList_state = ORDERLIST.Init

    return result

    这个函数能处理代码块和特殊代码块(三个点后加语言),和有序列表和无序列表

    然而在特殊代码块周围加上\\后好像显示并没有什么区别,所以没有算在支持的语法中。

    5.2 链接处理函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def link_image(s):
    # 超链接
    # pattern = re.compile(r'\\\[(.*)\]\((.*)\)')
    pattern = re.compile(r'\[(.*)\]\((.*)\)')
    match = pattern.finditer(s)
    for a in match:
    if a:
    text, url = a.group(1,2)
    #左闭右开的区间
    x, y = a.span()
    s = s[:x] + "<a href=" + url + " target=\"_blank\">" + text + "</a>" + s[y:]
    break
    return s

    为了处理方便(偏移量没处理好),这个函数只能匹配一行中的第一个链接

    5.3 处理加粗、斜体标识

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    def tokenHandler(s):
    l = ['b', 'i']
    j = 0
    for i in ['**', '*']:
    pattern = re.compile(tokenTemplate(s,i))
    match = pattern.finditer(s)
    k = 0
    for a in match:
    if a:
    content = a.group(1)
    x,y = a.span()
    # <b></b>为7个字符 当*时,添加一次字符穿边长(7-2=5)个单位,当**时,增加(7-4=3)个单位
    c = 3
    if i == '*':
    c = 5
    s = s[:x+c*k] + "<" + l[j] + ">" + content + "</" + l[j] + ">" + s[y+c*k:]
    k += 1
    j += 1
    return s

    这个处理比较简单,偏移量能算出来,一行能匹配多个加粗、斜体。

    5.4 加粗、斜体辅助函数

    1
    2
    3
    4
    5
    6
    7
    def tokenTemplate(s, match):
    pattern = ""
    if match == '*':
    pattern = "\*([^\*]*)\*"
    if match == '**':
    pattern = "\*\*([^\*\*]*)\*\*"
    return pattern

    判断具体是加粗还是斜体,返回正则匹配式

    5.5 两个处理函数和一个简单的判断函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 判断 lst 是否全由字符 sym 构成 
    def all_same(lst, sym):
    return not lst or sym * len(lst) == lst

    # 处理标题
    def handleTitle(s, n):
    temp = "<h" + repr(n) + ">" + s[n:] + "</h" + repr(n) + ">"
    return temp

    # 处理无序列表
    def handleUnorderd(s):
    s = "<ul><li>" + s[1:]
    s += "</li></ul>"
    return s

    三. 其他说明

    我将会在压缩包内放入示例文件(其实就是本文件支持的语法那一部分),以及其生成的 HTML 文件 和 PDF 文件.

    四. 自评分数

    优秀

    五. 评价理由以及心得体会

    这次大作业的代码框架来自实验楼的一个课程,我在半个月前就开始看这份代码,虽然曾经使用过 markdown (大一上时搭建过Hexo,一个轻量的静态博客,文章用 markdown来写,顺便贴上自己的博客地址 cinvx.com),知道其常用的一些语法,然而并看不懂这个解释器,主要是没有学习过正则表达式以及HTML语言,所以也无法理解代码的大思路。

    为此我参加了计蒜课的python程序设计基础,并学习了其中的正则表达式专题,对我快速理解正则表达式和 re 库的使用提供了很大的帮助。以及普及了python语言的一些规范,包括命名规范和编程风格。

    在学习的正则表达式后,整个代码就比较容易看懂了,大思路就是按行处理文本,通过正则匹配 markdown 语法,添加对应的 HTML 标签,写到目标文件里。至于如何在命令行运行和生成 PDF 文件,这些都可以通过库来帮助我实现,比如 wkhtmltopdf 能将 HTML 转成 PDF,以及 docopt 能方便地实现命令行程序。这个理解的过程大概花了我断断续续的一周和元旦完整的一天,然后花了一天时间对于需要的功能重写该代码。

    整个大作业过程其实Python的语法是最简单的部分,难点是正则表达式的使用和学习一些 HTML 标签,以及库的使用。得意于这些难点,我也入门了这些难点,这些每一个第一步都是进步。这次 python 公选课给我的帮助还是非常大的。

    根据我学习这门课程所花的精力和时间,以及学习的成果,我给自己的评价为 优秀

    文章目錄
    1. 1. Python3实现简易的markdown解释器
  • 2016-2017学年第1学期
    1. 实验报告
  • Python3实现简易的markdown解释器
    1. 一、介绍(说明文档)
      1. 1. 使用前的准备
      2. 2. 支持的语法
        1. 1. 1. 标题
        2. 2. 2. 列表
        3. 3. 3. 代码块
        4. 4. 4. 链接和图片
        5. 5. 5. 引用
        6. 6. 6. 粗体和斜体
        7. 7. 7. 分割线
      3. 3. 使用说明
    2. 二、代码分析
      1. 1. docopt文档
      2. 2. 需要导入的库和全局状态
      3. 3. 主函数
      4. 4. 执行函数
      5. 5. 转换函数
        1. 1. 5.1 处理文本块
        2. 2. 5.2 链接处理函数
        3. 3. 5.3 处理加粗、斜体标识
        4. 4. 5.4 加粗、斜体辅助函数
        5. 5. 5.5 两个处理函数和一个简单的判断函数
    3. 三. 其他说明
    4. 四. 自评分数
    5. 五. 评价理由以及心得体会