Python 分析案例|文本整理样例:近体诗格律分析_python如何检测一首古诗里的每一个字是平音还是仄音-程序员宅基地

技术标签: python  格律  近体诗  # Python分析案例  文本分析  Python  案例  

在这个案例中,我们将要实现近体诗格律的分析。具体的,我们从如下角度分析近体诗的格律:

  1. 诗句数量、诗句字数是否符合近体诗的要求,即是否为五绝、七绝、五律、七律中的一种(暂不考虑排律、六言的情况)
  2. 是否押了平声韵,所押的韵脚是什么平水韵部(暂不考虑首句押韵的情况)
  3. 诗句是否有拗句,是否存在孤平和拗救的情况
  4. 诗文是否符合对黏的要求

如果当时该诗不符合第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个要求的分析都需要我们先标注诗句的平仄,即完成类似于如下方式的标注。

寒随穷律变,春逐鸟声开。

平平平仄仄 平仄仄平平

我们使用查平水韵表的方式,来判断诗句中每个字的平仄并标注。其中阴平和阳平均属于平声,标注为“平”;上声、去声、入声均属于仄声,标注为“仄”。若某字不存在于平水韵表中,或同时存在于多个不同平仄的韵部中,均认为该字平仄无法判断,标注为“中”;在后续的分析中,不考虑这些无法判断平仄的字的影响。

在具体实现上,包括如下步骤:

  1. 导入平水韵表并将其整理为字典;其中key为字,value为所有包含该字的韵部的名称及音调的列表;
  2. 使用导入平水韵表的字典,实现判断单个字平仄的函数;
  3. 使用判断单个字平仄的函数,实现诗句平仄的标注。
导入平水韵表
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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Changxing_J/article/details/106362733

智能推荐

Android 6.0 动态申请 音频+拍照+相册 权限-程序员宅基地

文章浏览阅读559次。1.音频的权限(包括录音和播放)1.1.首先要在清单中加上两个权限<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.RECORD_AUDIO"..._android 音频权限

SPON世邦 IP网络对讲广播系统 多处文件上传漏洞复现_世邦通信ip网络对讲广播系统存在任意文件上传漏洞-程序员宅基地

文章浏览阅读1.4k次,点赞19次,收藏9次。SPON世邦IP网络对讲广播系统addscenedata.php、uploadjson.php、my_parser.php等接口处存在任意文件上传漏洞,未经身份验证的攻击者可利用此漏洞上传恶意后门文件,可导致服务器失陷。_世邦通信ip网络对讲广播系统存在任意文件上传漏洞

flutter中App签名_flutter 签名-程序员宅基地

文章浏览阅读2.3k次。app签名_flutter 签名

汇编语言笔记(11)标志寄存器_汇编debug怎么看标志位-程序员宅基地

文章浏览阅读838次。标志寄存器_汇编debug怎么看标志位

Docker Compose快速安装Zookeeper和Kafka_docker-compose zookeeper kafka-程序员宅基地

文章浏览阅读329次。Centos7安装Docker和Docker Compose,并用docker-compose安装kafka!!!_docker-compose zookeeper kafka

Odoo----constraints 使用_api.constraint-程序员宅基地

文章浏览阅读803次。Odoo中的约束一:装饰器约束(字段约束)装饰器参数指定了约束的字段,当涉及的字段中任一发生改变时触发方法执行。如果不满足约束条件,该方法将引发异常。@api.constrains('约束字段')def _check_something(self): for record in self: if record.约束字段 op 值: raise ValidationError("异常信息")Constraints:@api.constrains('instructo_api.constraint

随便推点

线程的使用_如何使用线程-程序员宅基地

文章浏览阅读521次。线程的创建1.继承Thread()方法class MyThread extends Thread{ //继承Thread方法 @Override public void run() { //重写run()方法 System.out.println("hello"); }}public class ThreadTest { public static void main(String[] args) { _如何使用线程

Vue 中 this.$router 与 this.$route 的区别 以及 push() 方法_$route.push谁的方法-程序员宅基地

文章浏览阅读353次。官房文档里是这样说明的:通过注入路由器,我们可以在任何组件内通过this.$router访问路由器,也可以通过this.$route访问当前路由可以理解为:this.$router 相当于一个全局的路由器对象,包含了很多属性和对象(比如 history 对象),任何页面都可以调用其 push(), replace(), go() 等方法。this.$route 表示当前路由对象,每一个路由都会有一个 route 对象,是一个局部的对象,可以获取对应的 name, path, para..._$route.push谁的方法

独立开发者之路(一)_独立开发之路-程序员宅基地

文章浏览阅读634次,点赞3次,收藏2次。起始日:2020年7月10日,独立开发时间,3天。独立开发收益统计:0.00元。这几天主要对独立开发者进行了各种调研,目的是为了给自己做一个初步规划。用心做产品看了很多其他独立开发者成功的案例,其中有成功的有做不下去失败了的,我总结了一下他们的失败,大部分都停留在想法设计这一环节上,其次都是跪在了产品带不来盈利失去耐心。很多失败者抱怨,自己的想法很多大公司都已经做了,我再做根本没有市场,然后开始抱怨自己生不逢时,说什么独立开发者的好时代已经过去了云云。但总的来说,要么没有用心去想,要么就是没_独立开发之路

linux入门---程序地址空间_为什么进程地址空间从40000开始的-程序员宅基地

文章浏览阅读523次。linux中程序地址空间的理解_为什么进程地址空间从40000开始的

微信小程序API 登录-wx.login(OBJECT) + 获取微信用户唯一标识openid | 小程序-程序员宅基地

文章浏览阅读770次。wx.login(OBJECT)调用接口获取登录凭证(code)进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)。用户数据的加解密通讯需要依赖会话密钥完成。https://www.w3cschool.cn/weixinapp/weixinapp-api-login.htmlOBJECT参数说明:参数名..._h5 wx.login(object object)

面向对象设计原则之3-里氏替换原则_public override double area()-程序员宅基地

文章浏览阅读1.6w次。里氏替换原则(Liskov Substitution Principle or LSP)所有引用基类的地方必须透明的使用其子类的对象。LSP:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it...._public override double area()