在这个案例中,我们将要实现近体诗格律的分析。具体的,我们从如下角度分析近体诗的格律:
- 诗句数量、诗句字数是否符合近体诗的要求,即是否为五绝、七绝、五律、七律中的一种(暂不考虑排律、六言的情况)
- 是否押了平声韵,所押的韵脚是什么平水韵部(暂不考虑首句押韵的情况)
- 诗句是否有拗句,是否存在孤平和拗救的情况
- 诗文是否符合对黏的要求
如果当时该诗不符合第1个或第2个要求,则不再分析;如果符合第1个和第2个要求,则以如下方式分析该诗的格律。
《首春》 李世民
寒随穷律变,春逐鸟声开。
平平平仄仄 平仄仄平平(十灰) —— 仄仄脚正格 平平脚正格
初风飘带柳,晚雪间花梅。
平平平仄仄 仄仄中平平(十灰) —— 【失黏】仄仄脚正格 平平脚正格
碧林青旧竹,绿沼翠新苔。
仄平平仄仄 仄仄仄平平(十灰) —— 【失黏】仄仄脚正格 平平脚正格
芝田初雁去,绮树巧莺来。
平平平仄中 仄中中平平(十灰) —— 【失黏】仄仄脚正格 平平脚正格
下面我们开始实现以上功能。
首先,我们读取案例0201-全唐诗文本整理案例中已经整理好的全唐诗文本进行遍历。
with open("全唐诗.json", encoding="UTF-8") as file:
poem_json = json.loads(file.read())
平水韵表详见“全唐诗.json”
第1个要求
检查诗句数量
近体诗在诗句数量上主要绝句、律诗和排律,绝句(律绝)由四句组成,律诗由八句组成,排律则超过八句,当前只考虑绝句和律诗的情况;在具体实现上,我们可以通过正则表达式,依据标点符号来拆分诗文中的各个句子,并统计诗句的总数是否为4或8。
sentence_list = [sentence for sentence in re.split("[,。?!]", content) if sentence != ""] # 生成句子列表
if len(sentence_list) != 4 and len(sentence_list) != 8:
print("[古体诗]诗句数量:", len(sentence_list))
检查诗句字数
近体诗在诗句字数上通常包括五言和七言两种(六言律诗传世极少,暂不考虑);在具体实现上,我们使用列表生成式和内置函数all判断诗中句子是否均为五言或七言。
if not all([len(sentence) == 5 or len(sentence) == 7 for sentence in sentence_list]):
print("[古体诗]诗句并非五言或七言", [len(sentence) for sentence in sentence_list])
诗句平仄标注
第2个、第3个、第4个要求的分析都需要我们先标注诗句的平仄,即完成类似于如下方式的标注。
寒随穷律变,春逐鸟声开。
平平平仄仄 平仄仄平平
我们使用查平水韵表的方式,来判断诗句中每个字的平仄并标注。其中阴平和阳平均属于平声,标注为“平”;上声、去声、入声均属于仄声,标注为“仄”。若某字不存在于平水韵表中,或同时存在于多个不同平仄的韵部中,均认为该字平仄无法判断,标注为“中”;在后续的分析中,不考虑这些无法判断平仄的字的影响。
在具体实现上,包括如下步骤:
- 导入平水韵表并将其整理为字典;其中key为字,value为所有包含该字的韵部的名称及音调的列表;
- 使用导入平水韵表的字典,实现判断单个字平仄的函数;
- 使用判断单个字平仄的函数,实现诗句平仄的标注。
导入平水韵表
def load_rhythm_list():
"""
载入平水韵表并转换为dict形式
"""
with open("平水韵表.txt", encoding="UTF-8") as file:
rhythm_lines = file.readlines()
rhythm_dict = dict()
for rhythm_line in rhythm_lines:
rhythm_name = re.search(".*(?=[平上去入]声:)", rhythm_line).group() # 读取韵部名称
rhythm_tune = re.search("[平上去入](?=声:)", rhythm_line).group() # 读取韵部声调
rhythm_characters = re.sub(".*[平上去入]声:", "", rhythm_line) # 获取韵部包含的所有文字
for character in rhythm_characters:
if character not in rhythm_dict:
rhythm_dict[character] = list()
rhythm_dict[character].append([rhythm_name, rhythm_tune])
return rhythm_dict
平水韵表详见“平水韵表.txt”
实现计算单个字的平仄
def get_tone(rhythm_list):
"""
依据字所在的韵部,返回当前字的平仄(若当前字为无法确定平仄的多音字,则返回“中”)
:return: <str> 平/仄/中
"""
tone_set = set()
for rhythm_item in rhythm_list:
tone_set.add(rhythm_item[1])
if len(tone_set) == 1: # 若当前字不是多音字或是平仄相同的多音字
return list(tone_set)[0]
else:
return "中"
实现计算诗句中各字的平仄情况
sentence_tone = list()
for sentence in sentence_list:
sentence_tone.append([get_tone(character) for character in sentence])
第2个要求
在近体诗中,我们将奇数句称为出句,将偶数句称为对句,一组出句和对句称为一联。通常来说,近体诗的对句需要押相同的平声韵,首句如押韵可押邻韵“借韵”。因此,我们检查诗句是否押平声韵,只需要检查所有偶数句是否押平声韵即可。在具体的实现上,我们使用列表生成式和内置函数all判断诗文是否押了平声韵。
if not all([sentence_tone[i][-1] in ["平", "中"] for i in range(len(sentence_tone)) if i % 2 == 1]):
print("诗文未押韵或押仄声韵")
第3个要求
“拗句”是指诗句不符合格律。诗句的格律大体上可以用“一三五不论,二四六分明“来概括,包括多种正格、变格,情况十分复杂。在具体的实现上,我们使用的方法,利用正则表达式来匹配所有正格和变格,由此判断诗句是否为拗句。
def inspect_sentence_tone(sentence_tone):
"""
判断诗句是否为拗句,是否存在孤平拗救的情况
:return: <bool> 诗句是否正确, <str> 诗句的正格(用于判断对黏), <str> 诗句情况详细说明
"""
if re.match("[平仄中]?[平中]?[平仄中][仄中][平中][平中][仄中]", sentence_tone): # (仄)仄平平仄
return True, "仄仄平平仄", "平仄脚正格"
elif re.match("[平仄中]?[仄中]?[平仄中][平中][平中][仄中][仄中]", sentence_tone): # (平)平平仄仄
return True, "平平平仄仄", "仄仄脚正格"
elif re.match("[平仄中]?[平中]?[平仄中][仄中][仄中][平中][平中]", sentence_tone): # (仄)仄仄平平
return True, "仄仄仄平平", "平平脚正格"
elif re.match("[平仄中]?[仄中]?[平中][平中][仄中][仄中][平中]", sentence_tone): # 平平仄仄平
return True, "平平仄仄平", "仄平脚正格"
elif re.match("[平仄中]?[平中]?[平仄中][仄中][仄中][平中][仄中]", sentence_tone): # (仄)仄仄平仄
return True, "仄仄平平仄", "平仄脚变格(半拗可救可不救,若救对句五言第第三字、七言第五字用平)"
elif re.match("[平仄中]?[平中]?[平仄中][仄中][平仄中][仄中][仄中]", sentence_tone): # (仄)仄(平)仄仄
return True, "仄仄平平仄", "平仄脚变格(大拗必救,对句五言第第三字、七言第五字用平)"
elif re.match("[平仄中]?[仄中]?[平中][平中][仄中][平仄中][仄中]", sentence_tone): # 平平仄(平)仄
return True, "平平平仄仄", "仄仄脚变格(五言第一字、七言第三字必平)"
elif re.match("[平仄中]?[仄中]?[平仄中][仄中][平中][平中][平中]", sentence_tone): # (仄)仄平平平
return True, "仄仄仄平平", "平平脚变格(极其罕见)"
elif re.match("[平仄中]?[仄中]?[仄中][平中][平中][仄中][平中]", sentence_tone): # 仄平平仄平
return True, "平平仄仄平", "仄平脚变格(孤平拗救,五言第一字、七言第三字必平)"
elif re.match("[平仄中]?[仄中]?[平中][平中][平中][仄中][平中]", sentence_tone): # 平平平仄平
return True, "平平仄仄平", "仄平脚变格(出句拗对句救,五言第三字、七言第五字用平)"
else:
return False, "", "拗句"
第4个要求
诗句的对黏,是指相邻诗句之间的平仄关系。简单来说,同一联中的出句和对句中的第2字、第4字、第6字的平仄应不同,即“对”,若平仄相同则称为“失对”;上一联的对句和下一联的出句的第2字、第4字、第6字的平仄应相同,即“黏”,若平仄不同则称为“失黏”。在判断过程中,如果诗句是变格,则以其原始正格的平仄为准。
判断诗句“对”的情况
def is_tone_differ(tone_1, tone_2):
"""
判断两个字平仄是否不同
"""
if (tone_1 == "仄" or tone_1 == "中") and (tone_2 == "平" or tone_2 == "中"):
return True
elif (tone_1 == "平" or tone_1 == "中") and (tone_2 == "仄" or tone_2 == "中"):
return True
else:
return False
def inspect_corresponding(first_type, second_type):
"""
判断句子的对是否正确
:param first_type: <str> 出句的正格
:param second_type: <str> 对句的正格
:return: <bool>
"""
if len(first_type) != len(second_type):
return False
return is_tone_differ(first_type[-2], second_type[-2]) and is_tone_differ(first_type[-1], second_type[-1])
判断诗句“黏”的情况
def is_tone_same(tone_1, tone_2):
"""
判断两个字平仄是否相同
"""
if (tone_1 == "仄" or tone_1 == "中") and (tone_2 == "仄" or tone_2 == "中"):
return True
elif (tone_1 == "平" or tone_1 == "中") and (tone_2 == "平" or tone_2 == "中"):
return True
else:
return False
def inspect_sticky(last_second_type, this_first_type):
"""
判断句子的黏是否正确
:param last_second_type: <str> 前句对句的正格
:param this_first_type: <str> 当前出句的正格
:return: <bool>
"""
if len(last_second_type) != len(this_first_type):
return False
return is_tone_same(last_second_type[-2], this_first_type[-2])
整理并输出结果
在以上部分,我们已经可以实现所有的需求目标了,下面通过调用以上代码以实现诗文格律分析。
def poem_analyse(title, author, content):
sentences = [sentence for sentence in re.split("[,。?!]", content) if sentence != ""]
punctuations = re.findall("[,。?!]", content)
# 检查诗文的句子数量是否为绝句或律诗
if len(sentences) != 4 and len(sentences) != 8:
print("《" + title + "》", author, "诗句句数不是绝句或律诗")
return False
# 检查诗文中句子的字数是否为五言或七言
if not all([len(sentence) == 5 or len(sentence) == 7 for sentence in sentences]):
print("《" + title + "》", author, "诗文中句子的字数不是五言或七言")
return False
# 计算诗句中每个字的平仄情况
sentence_tone_list = list()
for sentence in sentences:
sentence_tone_list.append("".join([get_tone(character) for character in sentence]))
# 判断是否押平声韵
if not all([sentence_tone_list[i][-1] in ["平", "中"] for i in range(len(sentences)) if i % 2 == 1]):
print("《" + title + "》", author, "诗文没有押韵或押仄声韵")
return False
print("《" + title + "》", author)
last_second_type = ""
for i in range(int(len(sentences) / 2)):
first_sentence = sentences[2 * i + 0] # 出句内容
second_sentence = sentences[2 * i + 1] # 对句内容
first_tone = sentence_tone_list[2 * i + 0] # 出句的平仄
second_tone = sentence_tone_list[2 * i + 1] # 对句的平仄
second_rhythm = "(" + get_rhythm(second_sentence[-1]) + ")" # 对句的韵脚
first_correct, first_type, first_explanation = inspect_sentence_tone(first_tone)
second_correct, second_type, second_explanation = inspect_sentence_tone(second_tone)
other_analysis = ""
if first_correct and second_correct:
if not inspect_corresponding(first_type, second_type): # 判断是否对
other_analysis += "【失对】"
if last_second_type is not None and inspect_sticky(last_second_type, first_type): # 判断是否黏
other_analysis += "【失黏】"
last_second_type = second_type
output_sentence = first_sentence + punctuations[2 * i + 0] + second_sentence + punctuations[2 * i + 1] # 第一行输出
output_analysis = first_tone + " " + second_tone + second_rhythm # 第二行输出
output_analysis += " —— " + other_analysis + first_explanation + " " + second_explanation
print(output_sentence)
print(output_analysis)
return True
if __name__ == "__main__":
with open("全唐诗.json", encoding="UTF-8") as file: # 载入整理完成的全唐诗文本语料
poem_json = json.loads(file.read())
for poem_item in poem_json["data"]:
if poem_analyse(poem_item["title"], poem_item["author"], poem_item["content"].replace("\n", "")):
print("点击回车继续...")
input()
运行结果实现了需求:
......
《春日望海》 李世民 诗句句数不是绝句或律诗
《临洛水》 李世民
春蒐驰骏骨,总辔俯长河。
平平平仄仄 中仄仄中平(五歌) —— 仄仄脚正格 平平脚正格
霞处流萦锦,风前漾卷罗。
平仄平平仄 平平仄中平(五歌) —— 【失黏】平仄脚正格 仄平脚正格
水花翻照树,堤兰倒插波。
仄平平仄仄 平平仄仄平(五歌) —— 【失对】【失黏】仄仄脚正格 仄平脚正格
岂必汾阴曲,秋云发棹歌。
仄仄平平仄 平平仄仄平(五歌) —— 平仄脚正格 仄平脚正格
点击回车继续...
完整源代码详见诗词格律分析(面对过程).py和诗词格律分析(面对对象).py