上一节练习我们实现了文档的转换功能,这一节我们通过另一种形式来实现。
案例分析
当前案例文档的转换实际上包含以下几点:
- 通过原始文档生成内容块
- HTML标签的添加处理
- HTML标签的添加规则
- 主程序进行最终的整合
实现过程
一、通过原始文档生成内容块(上一节中实现的模块util.py)
二、HTML标签的添加处理(handlers.py)
1、处理程序的类(HTMLRenderer)
这个类中,我们需要添加处理的方法:
- 1个方法用于添加HTML文件起始标签
- 1个方法用于添加HTML文件结束标签
- 1个方法用于添加每个内容块开始标签
- 1个方法用于添加内容块中的文档内容
- 1个方法用于添加每个内容块结束标签
- 多个方法用于处理内部标签
此时的代码如下:
class HTMLRenderer(Handler):
def start_html(self): # HTML文件开始标签
print('<html><head><meta charset="gbk"><title>doc.txt</title></head><body>')
def end_html(self): # HTML文件结束标签
print('</body></html>')
def start_tag(self, tag_name): # 内容块开始标签
print('<' + tag_name + '>')
def feed(self, data): # 内容块文档内容
print(data)
def end_tag(self, tag_name): # 内容块结束标签
print('</' + tag_name + '>')
def sub_li(self, tag_name, match): # 内部项目符号标签处理
return '<' + tag_name + '>' + match.group(1) + '</' + tag_name + '>'
def sub_br(self, tag_name, match): # 内部换行标签处理
return match.group(1) + '</' + tag_name + '>\n'
def sub_strong(self, tag_name, match): # 内部加重标签处理
return '<' + tag_name + '>' + match.group(1) + '</' + tag_name + '>'
注意,这个类继承自Handler类。
那么,Handler这个类中都有什么?
class Handler:
'''
处理程序的超类
'''
def callback(self, method_name, tag_name, *args): # 定义回调的方法
method = getattr(self, method_name, None) # 获取指定名称的方法
if callable(method): # 如果方法可调用
return method(tag_name, *args) # 返回方法调用结果
def sub(self, tag_name): # 定义添加子标签的泛型方法
def sub_stitution(match):
result = self.callback('sub_' + tag_name, tag_name, match) # 进行内容处理
if result is None: # 如未能进行处理,输出原始内容。
print(match.group(0))
return result
return sub_stitution
在这个Handler类中,定义了一个方法“callback()”,这个方法用于检查类中的某个方法是否能够被调用,如果能够调用则返回方法调用结果。
而这个类中的另外一个方法“sub()”,是添加内部标签的泛型方法,这个方法是一个闭包,能够作为re.sub()的第2个参数(见下方主程序代码中的add_filter方法),将re.sub()的第1个参数(正则表达式)和内部标签的名称获取后进行处理。
在sub()方法中,调用callback()方法,检查名称为“’sub_’ + tag_name”的方法是否存在于类中,存在则进行调用,返回调用结果,不存在则直接输出原始内容。
三、HTML标签的添加规则(rules)
1、不管符合哪一种外部标签的规则,对需要执行三个动作:添加开始标签、添加文档内容和添加结束标签。所以,这里我们先创建一个类(Rule),定义这些共有的动作。
class Rule:
def action(self, handler, block): # 定义执行处理动作的规则
handler.start_tag(self.tag_name)
handler.feed(block)
handler.end_tag(self.tag_name)
return True # 用于通知调用此方法的程序:当前内容块的处理动作执行结束。
2、定义每一种类型外部标签的规则
class TitleRule(Rule):
tag_name = 'h1' # 标签名称
def condition(self, block): # 定义执行动作的条件
return block[0].isupper() and block[-1].isalpha() # 首字母大写并且末尾为字母
class ListRule(Rule):
tag_name = 'ul' # 标签名称
def condition(self, block): # 定义执行动作的条件
return '<li>' in block # 包含列表项标签
class ParagraphRule(Rule):
tag_name = 'p'
def condition(self, block): # 定义执行动作的条件
return True # 所有的内容块
四、主程序进行最终的整合
完成了以上过程,接下来我们编写主程序,通过主程序整合这些模块协同工作。
首先,先定义一个解析器。
解析器的功能包括:各种外部标签添加规则、各种内部标签的添加处理、整个原始文档的处理过程。
import sys, re
from handlers import *
from rules import *
from util import *
class Parser:
def __init__(self, handler):
self.handler = handler # 初始化处理程序
self.rules = [] # 初始化外部标签处理规则列表
self.filters = [] # 初始化内部标签处理方法列表
def add_rule(self, rule):
self.rules.append(rule) # 添加外部标签处理规则到列表
def add_filter(self, pattern, tag_name):
def doc_filter(block, handler):
return re.sub(pattern, handler.sub(tag_name), block) # 返回处理完成的内容块
self.filters.append(doc_filter) # 添加内部标签处理方法到列表
def parse(self, file): # 定义解析方法,即整个原始文档的处理过程。
self.handler.start_html() # 添加HTML文件开始标签内容
for block in blocks(file):
for f in self.filters: # 添加内部标签的循环处理
block = f(block, self.handler) # 完成添加内部标签处理的内容块
for r in self.rules: # 添加外部标签的循环处理
if r.condition(block): # 进行规则验证
last = r.action(self.handler, block) # 验证通过,执行添加标签的动作。
if last: # 如果动作已结束,跳出当前循环。
break
self.handler.end_html() # 添加HTML文件结束标签内容
然后,我们定义一个子类DocParser,添加外部标签的处理规则和内部标签的处理方法。
class DocParser(Parser): # 继承Parser类
def __init__(self, handler):
Parser.__init__(self, handler) # 调用超类的构造方法进行初始化
self.add_rule(TitleRule()) # 添加标题处理规则
self.add_rule(ListRule()) # 添加列表处理规则
self.add_rule(ParagraphRule()) # 添加段落处理规则
self.add_filter(r'\*(.+)\*', 'strong') # 添加加重文本处理方法
self.add_filter(r' - *(.+)', 'li') # 添加列表项处理方法
self.add_filter(r'([^>:])\n', 'br') # 添加换行处理方法
这里要注意规则添加的顺序,因为段落对所有文本块都有效,所以要放在最后添加。也就是说,如果不是标题和列表,再将内容块添加段落标签。
最后,我们再添加一些代码,让主程序能够执行文档的处理。
handler = HTMLRenderer() # 实例化处理程序类
parser = DocParser(handler) # 将处理程序对象传入解析器类后实例化
parser.parse(sys.stdin) # 调用解析器方法,对系统标准输入中指定的文档进行处理。
完成以上代码后,我们通过命令行终端执行代码。
python main02.py <doc.txt> doc.html
执行完毕之后,大家能够看到程序生成了和练习项目01相同的HTML文件。
很显然,本节练习的实现方法更加复杂。
但是,这样的实现方法结构清晰,并且具有很好扩展性,当需要添加新的功能时,无需修改已有的代码,只要添加新的处理方法、规则并在解析器中调用即可。
假设,我们需要增加一个功能,将原始文档中的邮箱地址都加上链接。
首先,在handlers模块的HTMLRenderer类中新增一个处理方法。
def sub_email(self, tag_name, match): # 内部邮件地址处理
return '<a href="mailto:' + match.group(1) + '">' + match.group(1) + '</a>'
然后,在主程序的DocParser类中也新增一句代码。
self.add_filter(r'([\w\.-]+@[A-Za-z]+\.[A-Za-z]+[\.A-Za-z]*)', 'email') # 添加列表项处理方法
通过添加以上代码,就完成了邮箱地址链接的添加。
另外,大家可以想一想如果把<p>标签改为<div>标签该怎么做?
我们无需删除已有代码,只需要在rules模块中添加一个Div的规则,然后在主程序的DocParser类中,将添加的规则ParagraphRule()改为Div()即可。
这样,无论在什么时候,我们想把<div>标签改回<p>标签,都不需要再添加或修改规则代码,只需要将主程序DocParser类中添加的规则名称改回ParagraphRule()就可以了。