1.项目目的
编写一段能够根据输入的上联对出下联的Python对对联程序。输入上联后,能随机生成下联,最终达到:
(1) 程序的智能性高,功能完善。希望生成的对联满足平仄、韵脚、词性等要求,并具有一定语义。
(2) 在实践项目的过程中提高同学的代码编写能力。具体能力包括:程序设计知识的综合运用能力;自主学习、错误调试的能力;规范书写代码及报告的意识和能力;阅读代码、改写代码的能力等。
2.项目内容
这是一个运用从网上爬取的对联、通过随机的算法生成一段符合押韵、平仄规则的对联的程序。大体过程为:
(1) 运用xPath、Request、Re模块等从网站上爬取对联,并对对联进行文本格式处理;
(2) 运用jieba模块对对联进行词性分析,运用pinyin模块对对联进行平仄校准;
(3) 根据词频随机做出符合押韵、平仄规则的下联。
3.输入输出
输入:任意一副对联的某一联
输出:判断该联为上联或下联并随机生成满足平仄、韵脚的另一联
程序流程图
爬虫分析&数据分析
1.爬虫整体思路
(1)爬取网页
选择想要爬取的网址,并模拟头部浏览器访问该网站。创建一个dict,将头部信息以键值对的形式存入到dict对象中。本次项目爬取的网址为: http://duilian.haoshiwen.org
然后调用urllib.request.Request()函数创建一个request对象,该函数第一个参数传入url,第二个参数可以传入数据,默认是传入0数据,第三个参数是传入头部,该参数也是有默认值的,默认是不传任何头部。将dict对象传入urllib.request.Request()函数第三个参数。
此时,已经成功设置好报头,然后使用urlopen()打开该Request对象即可打开对应的网址。然后使用response.read() 接收 json 数据, 数据格式为UTF-8
(2) 逐一获取需要的标签并解析数据
可以看到在网页中我们需要的对联内容在p标签内,因此从html中获取p标签内容, 再利用正则表达式匹配符合要求的内容, 并将其写入数据库
(3)保存内容
主要涉及文件的读写。
代码如下:
# 取消服务器证书验证功能
ssl._create_default_https_context = ssl._create_unverified_context
database = [] # 列表记录数据
def askUrl(i): # 得到指定一个URL的网页内容
global database
# 爬取对联大全网站
url = "http://duilian.haoshiwen.org/view.php?aid=" + str(i)
head = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54"
} # 模拟头部浏览器
try: # 用try except进行异常处理
request = urllib.request.Request(url, headers=head)
response = urllib.request.urlopen(request)
html = response.read().decode("UTF-8")
selector = etree.HTML(html) # 将源码转化为能被XPath匹配的格式
result = selector.xpath('//p/text()') # 返回所有p标签
for html in result:
findCouplet = re.compile(r'[\u4e00-\u9fff]{7}') # 使用正则表达式查找符合格式的内容
couplet = re.findall(findCouplet, html)
if couplet:
database.append(couplet) # 写入数据库
except urllib.error.URLError as e:
if hasattr(e, "code"):
print(e.code)
if hasattr(e, "reason"):
print(e.reason)
因为该网站的数据格式多样, 我自己爬到的数据比较少,所以我下载了网上的数据库来作为扩充(包含约70万条对联),下载地址如下:
中国对联数据集 - Heywhale.com
train_in.txt(上联)和train_out.txt(下联)两个文件
2. 算法分析
- Lear_data模块是通过学习,记录词频
这部分是逐个分析上联中的词语,如获得一个词语后,在下联中找到这个词语可以匹配的词,并根据该词语匹配的次数用公式计算词频概率(对应概率越大说明该词最有可能与上联中的对应词匹配)。计算公式是多次试验后发现的比较能反映词频的算法,不一定是最优。
代码如下:
# 对联学习
def learns():
global infile, outfile, zishi
size = len(infile) # infile为上联库中的内容
for i in range(size):
cut = jieba.lcut(infile[i]) # 对上联库中对联逐一分词
for j in range(len(cut)):
position = infile[i].find(cut[j]) # 找到该词位置
learn(cut[j], outfile[i][position: position + len(cut[j])]) # 学习该词
for j in range(len(infile[i])):
learn(infile[i][j], outfile[i][j]) # 学习对联中每个字
# 单个的词语学习
def learn(word, nword):
global zishi
zikey = zishi.keys()
if word in zikey: # 如果该词语已被学习过
zishikey = zishi[word].keys() # 获取所有可以匹配该词的词语
if nword in zishikey: # 如果下联对应词在可匹配词语中
for j in zishikey:
if j != '$':
if j != nword: # 可匹配词语中的其他词对应概率减小
zishi[word][j] = zishi[word][j] / (1 + 1 / zishi[word]['$'])
zishi[word][nword] = (zishi[word][nword] + 1 / zishi[word]['$']) / (1 + 1 / zishi[word]['$'])
# 该对应词的概率增大
zishi[word]['$'] = zishi[word]['$'] + 1 # 可匹配词语总数+1
else: # 如果该词语未被学习过(以下步骤同上)
for j in zishikey:
if j != '$':
zishi[word][j] = zishi[word][j] / (1 + 1 / zishi[word]['$'])
zishi[word][nword] = (1 / zishi[word]['$']) / (1 + 1 / zishi[word]['$'])
zishi[word]['$'] = zishi[word]['$'] + 1
else:
zishi[word] = {'$': 1, nword: 1}
这是学习后生成的文件:
意思为:“上联”:{“$”+匹配的总字数,“下联1”:对应概率,“下联2”:对应概率……}
平仄方法区分
看对联的最后一个字,上联最后一个字是三声和四声(仄声),下联的最后一个字是一声和二声(平声),如下联:兴xīng 一声 是上联,旺wàng四声是下联。
def is_down(content): # 判断是否是下联, 如果是返回true
s = list(content)
a = pinyin.get(s[-1], format="numerical")[-1]
return a == '1' or a == '2'
main模块讲解:
首先如果用户输入1, 进行对联自动生成的话,打开学习生成的文件, 用couplet和ran函数实现匹配并生成下联:在couplet函数中,用random随机为上联分配一个权重/概率(达到随机匹配目的),对上联分词后的词语一一使用ran函数,然后再组合成上联。在ran函数中,将上联分配到的概率r与所有能配对的词语一一比较,如果比较到某个词的概率大于r,就返回该词i。否则使r减少后再重复以上步骤。然后进行输出,输出时进行平仄校验,平仄正确率大于80%后直接输出,否则再次随机生成下联,直到平仄符合。
以下介绍verify模块,主要是自定义的jy函数:
方法比较简单,就是获取每个字的音调后一一比对, 如果平仄不对则记录下来, 最后拿正确数除以总的字数获得正确率。
代码如下:
import pinyin
def is_down(content): # 判断是否是下联, 如果是返回true
s = list(content)
a = pinyin.get(s[-1], format="numerical")[-1]
return a == '1' or a == '2'
def jy(s, x):
s = list(s)
x = list(x)
yin1 = []
yin2 = []
for i in s: # 获取对联中每个字的拼音声调
a = pinyin.get(i, format="numerical")
yin1.append(a[-1])
for i in x:
b = pinyin.get(i, format="numerical")
yin2.append(b[-1])
error = 0
length = len(yin1)
for i in range(length): # 比对平仄
if (yin1[i] == '1' or yin1[i] == '2') and (yin2[i] == '1' or yin2[i] == '2'):
error += 1
if (yin1[i] == '3' or yin1[i] == '4') and (yin2[i] == '3' or yin2[i] == '4'):
error += 1
# 平仄正确率输出
return (length - error * 1.0) / length
如果用户输入2, 则调用jy函数输出平仄的评分。
代码如下:
import jieba
import random
import json
from verify import jy, is_down
def couplet(s): # 生成另一联
R = 0
x = []
for i in range(len(s)):
x.append(ran(s[i], random.random()))
# 用random随机生成一个0到1之间的数后,用ran函数进行权重比较、随机输出
return "".join(x)
def ran(w, r):
# 根据随机赋予字词的权重进行比较
global zishi, writemode, R
R = 0
zikey = zishi.keys() ##返回字典中所有键
if w != '' and w in zikey: # 当所输上联在对联库中,根据库得出下联
zishikey = zishi[w].keys()
max = ["", 0.0]
for i in zishikey:
if i != '$':
if r < float(zishi[w][i]): # 当该权重小于匹配到的字词对应概率,返回该字词
R += r
return i
else: # 减小权重,再次比较
r = r - float(zishi[w][i])
return max[0]
else: # 如果该上联不在数据库中,划分字词后逐词匹配
return "".join([ran(i, random.random()) for i in w])
if __name__ == '__main__':
# 引入校验函数
global infile, outfile, zishi, writemode, R
try:
with open("zknow.txt", "r", encoding='utf-8') as zishi_file:
# 打开学习后生成的文件
zishi = json.loads(zishi_file.read().replace("'", '"'))
# 将json格式数据解码为python对象,构建字典
except IOError:
zishi = {}
while True:
choice = input("1. 自动对对联 2. 对联评分")
if choice == "1":
s = input("输入对联:")
s = jieba.lcut(s) # 对联切词
x = couplet(s) # 生成另一联
s = "".join(s)
for i in range(1000):
# 检验平仄,平仄正确率大于百分之80后直接输出,否则再次随机生成下联,直到平仄符合
pz = jy(s, x)
if pz >= 0.8:
print("-----------------------------------")
if is_down(s):
print("上联:" + x)
print("下联:" + s)
else:
print("上联:" + s)
print("下联:" + x)
print("-----------------------------------")
print("评分:")
print("平仄: " + str(round(pz * 100, 2)) + "分")
print("对仗: " + str(round(R * 10000, 2)) + "分")
break
else:
x = couplet(s)
else:
s = input("输入上联:")
x = input("输入下联:")
pz = jy(s, x)
print("评分:")
print("平仄: " + str(round(pz * 100, 2)) + "分")
实验结果&对比分析
1.实验结果
运行结果: