高考小强源码阅读

读别人的代码像破解密码一样,乐趣无穷。
高考小强是一个基于Python2的、关于高考志愿填报的问答系统,它收集了往年的各个学校、各个专业的分数信息,可以根据省份、分数、文理推荐合适的学校。

一、五大板块

  • CollegeRecommendation
    学校、专业推荐模块,通过SQL查询数据库获得推荐的学校列表、专业列表
  • DialogueManagement
    对话管理模块,将格式化数据转化为具体的句子
  • InformationExtraction
    查询抽取模块
  • InformationRecognition
    查询识别模块
  • 主调用模块
    调用其他模块

不同模块之间以JSON的形式传递数据。

二、DialogueManagement:对话管理模块

本模块是离用户最近的模块,包含两个类:

  • ReadOrWriteWithConsole:控制台读入写出类,涉及到UTF8编码的转换,这个类可有可无,不太重要。
  • Response类,这个类是本模块的核心,下面重点介绍这个类。

Response类有如下几个静态函数,每个函数都表示高考小强能说出的一类话。

  • initial_ask():初始的问候语
  • normal_inquire_response(collegelist):正常询问回答,传入参数为collegelist,即推荐的学校列表
  • normal_major_response(major_list):把推荐的专业列表告知用户,“据小强分析,可以考虑的报考方向或专业有”
  • re_ask_lack_attribute(lack_tag):缺少属性回答,lack_tag表示缺少的属性,用户必须提供省份、文理、分数等信息,如果没有提供全,就要再次询问用户
  • more_restriction():是否有更多限制,比如用户说“我想去北京上大学”,就要限制为学校是北京的大学。
  • could_to_some_college(tag, college, now_type):能否上某某大学。
  • ambiguous_school(base, school_list):模糊学校,比如用户说“我想上东大”,无法判断是东南大学还是东北大学
  • i_donnot_know():小强认怂,“抱歉,小强的数据不够充分,暂时不能预测”
  • what_function():返回功能简介。比如用户问“有什么功能”。
  • too_big_score():“您的分数太高了,吹牛不是好习惯”

下面详细介绍各个函数。

initial_ask()

为了假装自己很灵活,写了5个问候语,随机选一个。这种方法在本系统中大量使用。
打印完问候语之后,就要开始干活了:问问用户的省份、文理、分数。

    def initial_ask():
        seed = random.randint(0, 4)
        re = []
        随机选取5个问候句
        if seed == 0:
            re.append(u'您好,我是人工智能小强,专注于高考志愿填报')
        elif seed == 1:
            re.append(u'您好,我是高考志愿填报助手小强')
        elif seed == 2:
            re.append(u'很高兴见到你,我叫小强')
        elif seed == 3:
            re.append(u'Hello!我是小强!')
        elif seed == 4:
            re.append(u'高考志愿填报助手--小强,竭诚为您服务!')

        re.append(u'小强是根据往年数据,结合分数与排名为您推荐学校,推荐结果仅供参考!')
        re.append(u'请问您有什么和高考志愿填报相关的需求?')
        re.append(u'输入省份、分数和文理即可开始挑选学校啦!')
        return re

normal_inquire_response(collegelist)

collegelist是一个如下结构的JSON

{
    'low':[('东北大学',628,'本科提前批'),('华中科技',630,'本科提前批')],
    'mid':[('北航',650,'本科提前批'),('西安交大',649,'本科提前批')],
    'high':[('清华大学',680,'本科提前批')]
}

此函数的作用就是把这个学校列表转化为一个字符串告知用户。

re_ask_lack_attribute(lack_tag)

lack_tag可能的取值:

  • origin:省份
  • type:文理
  • score:分数

根据缺失的属性,小强会问你“您的[省份|文理|分数]是什么?”,此函数又是写了多个问法模板随机选一个,以避免让人觉得死板。

more_restriction():还有其他要求没有

    def more_restriction():
        seed = random.randint(0, 1)
        re = u''
        if seed == 0:
            re = u'请问还有什么其他要求吗?'
        elif seed == 1:
            re = u'还需要做什么筛选吗?'
        return [re]

could_to_some_college(tag, college, now_type)

此函数用于回答“我能不能上北大”这样的问题。
tag表示小强的观点,分为四类:

  • 完全可以
  • 有把握
  • 很有可能
  • 不可能

college表示用户想要上的学校,now_type表示专业,这两个参数都是在拼接回复的时候用到。

ambiguous_school(base, school_list)

base=‘东大’,school_list=['东北大学','东南大学']

三、CollegeRecommendation模块:数据发生的地方

DialogueManagement只是将结构化数据转化为文本,没做什么大事。
InformatioonExtractor和InformationRecognition只是解析用户输入,也没做什么大事。
CollegeRecommendation模块则是系统的核心,是真正涉及到数据处理的地方。
本系统使用的是MySQL数据库,CollegeRecommendation的作用就是执行SQL语句去数据库里面查询。

下面首先介绍一下数据库设计。

分数名次表

score_rank表的结构

  • origin:省份
  • type:文理
  • year:年份
  • score:分数
  • rank:名次

分数-学校表

  • origin:省份
  • type:文理
  • year:年份
  • average_score:平均分
  • min_score:最低分
  • min_rank:最低分全省名次
  • batch:批次

分数-专业表

  • origin
  • type
  • year
  • average_score
  • major
  • school
  • batch

饭得一口一口吃,事得一件一件做。先看recommend_school

predict_school(origin, type, score,school)

判断能不能上某学校
参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数, school:学校名
返回值:
{'result':0/1/2/3(基本不可能/可能性较小/有把握/太亏),
 'school_score':学校预测平均分, 
 'school_rank':学校最低分排名, 
 'student_rank':学生排名
 },各项若为-1则是缺少数据,数据不足以做出判断则返回None

根据分数、省份、文理获取省内排名
select rank from score_rank where origin = %s and type = %s and year = 2016 order by abs(score - %s) limit 1 ',(origin,type,score)

根据省份、文理、学校、年份获得学校的平均分、最低分。

select average_score,min_score,
        min_rank from school_score 
        where student_origin = %s 
        and student_type = %s 
        and school = %s 
        and year = 2016',(origin,type,school)

高考小强观点的产生,根据学校的平均分、最低分综合判断

    # 先根据线上分判断
    if sch_ave_score > 0 :
        if score < sch_ave_score - 15 :
            result = 0
        elif score < sch_ave_score - 5 :
            result = 1
        elif score < sch_ave_score + 5 :
            result = 2
        else :
            result = 3

    # 再根据最低分判断,会覆盖线上分结果
    if sch_min_score > 0 :
        if score < sch_min_score :
            result = 0
        elif score < sch_min_score + 10 :
            result = 1
        elif score < sch_min_score + 20:
            result = 2
        else :
            result = 3

recommend_school_rank(origin, type, score)

# 参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数
# 返回值:((保底学校1,保底学校2...),(推荐学校1,推荐学校2...),(冲一冲学校1,冲一冲学校2...)); 学校信息包括(学校名,预测分数,批次)

此函数返回五个学校列表,每个学校是一个三元组(学校名称,平均分,批次)

  • 太亏,预测最低分在考生分数-20以下
  • 保底学校,预测最低分在考生分数-20到-10的学校
  • 推荐学校,预测最低分在考生分数-10到0的学校
  • 冲一冲学校,预测最低分在考生分数+0到+10的学校
  • 不可能,预测最低分在考生分数+10以上

这五种情况的SQL语句都很相似,不同之处在于min_score分数不同。

select school,average_score,batch 
from school_score 
where year = 2016 and student_origin = "%s" and student_type = "%s" and min_score > %d + 10' % (
            origin, type, score)

recommend_school_answer(origin,type,score)

这个函数返回值是str类型的。这个函数主要用来进行单元测试。

再来看recommend_major.py
本模块有一个major.txt,里面是各个专业的名称缩写。

init_major_list()

初始化专业列表,将数据库中各个专业和major.txt中的缩写对应起来。

predict_major_fullname(origin, type, score,school,major)

给定省份、文理、分数、年份,判断能不能上某学校的某专业。
原理就是查询score_major表,得到该学校该专业的分数,根据分数差分为4个等级,来表达小强的态度。


参数:
 origin:省份(不含"省"字,如"山东""新疆""西藏"),
 type:"文科"或 "理科", 
 score:分数, 
 school:学校名, m
 ajor:专业全称
返回值:{'result':0/1/2/3(基本不可能/可能性较小/有把握/太亏), 'major_score':预测平均分},各项若为-1则是缺少数据,数据不足以做出判断则返回None

predict_major(origin, type, score, school, major)

这个函数是上面predict_major_fullname()的包装,它首先获取专业简写对应的全部专业,然后调用predict_major_fullname()函数。

# 判断能不能上某学校专业
# 参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数, school:学校名, major:专业简称
# 返回值:{专业全称:{'result':0/1/2/3(基本不可能/可能性较小/有把握/太亏), 'major_score':预测平均分}},每个全称对应一条结果,若结果为空则该学校无对应专业,各项若为-1则是缺少数据,数据不足以做出判断则返回None
def predict_major(origin, type, score, school, major) :
    r = {}
    if major in majors.keys() :
        major_full = majors[major]
        for m in major_full :
            r[m] = predict_major_fullname(origin,type,score,school,m)
    else :
        return None
    return r

recommend_school_fullname(origin, type, score, major)

根据省份、分数、专业推荐学校。

# 参数: origin:省份(不含"省"字,如"山东""新疆""西藏"), type:"文科"或 "理科", score:分数, major:专业全称
# 返回值:{
 保底学校:学校列表,
 推荐学校:学校列表,
 冲一冲学校:学校列表
}

实现就是SQL语句,三类学校分数差不同。这跟recommend_school.py中直接推荐学校很相似。

select school,average_score,batch from major_score where year = 2016 and student_origin = "%s" and 
        student_type = "%s" and major = "%s" and average_score > %d + 5 and average_score < %d + 15' % (
        origin, type, major, score, score)

recommend_major(origin,type,score,major)

用户查询的major是简写的专业名称,一个专业简写对应多个全名专业。
根据专业推荐学校,分两步:

  • 把简写的专业进行扩展,得到一个专业列表
  • 对于专业列表中的每一个专业推荐学校
def recommend_major(origin,type_in,score,major) :
    r = {}
    if major in majors:
        major_full = majors[major]
        for m in major_full :
            r[m] = recommend_school_fullname(origin,type_in,score,m)
    else :
        return None
    return r

StateTracking模块:用户状态变化

相关文件

  • _type.py:定义了一些枚举
  • state.py:定义了状态变化

先看_type.py中的枚举,需要说明的是Python2实现枚举比较麻烦,Python3中枚举变得非常简单了。


def my_enum(**enums):
    return type('Enum', (), enums)

StateType = my_enum(
    INIT = 0,
    ENOUGH_BASIC_INFO = 1,
    ASK_BACK_FOR_SOMETHING = 2,
    TO_INIT = 3,
)

IntentType = my_enum(
    NORMAL_INQUIRE = 1,
    HOW_ABOUT = 2,
    ASK_FUNCTION = 4,

)

AskType = my_enum(
    NONE = 0,
    ORIGIN = 1,
    TYPE = 2,
    SCORE = 3,
    AMBIGUOUS = 4,
)

这三个枚举非常重要,是理解整个系统的重要入口。
用户状态枚举:开始、足够信息、信息不全
用户意图枚举:正常询问、怎么样(“我能上清华大学吗”)、询问功能(“这个系统怎么用啊”)
用户信息枚举:用来记录用户当前提供了哪些信息,包括什么信息也没有、有了省份信息、有了文理信息、有了分数信息等。

state.py定义的是上下文信息。
首先定义了class Info,它有两个成员tag,info。其实就是键值对。
然后定义了AmbiguousInfo,它有一个TTL类型的成员变量,表示如果问你两次你都没回答就不搭理你了。
最后定义了核心类State,这个类包含了从用户查询中提取出来的全部信息。

State类维护了两个Info列表:
keyInfoList
otherInfoList

还定义了一个tag列表:
keyInfoTags

实际上State类就相当于一个字典,里面存放的就是键值对。可以通过对比keyInfoTags和keyInfoList找出缺少的键值。
State就是用来存储解析出来的:分数、省份、文理等信息的。

InfomationRecognition模块:信息识别模块

这个模块在Dict文件夹中定义了几个同义词列表,每一个文件中的内容都是同义词。

  • agree.txt:好、行、恩、可以、没问题
  • asktone.txt:行不行、能不能、是不是、算不算
  • disagree.txt:不、别、否
  • gongneng.txt:什么功能、能做什么、能干什么
  • howabout.txt:怎么样、介绍
  • normalinquire.txt:能上、能报、可以上、可以报

if_agree.py

if_agree.py定义了一个AgreeJudge类,这个类读取agree.txt和disagree.txt中的词语构建同意和不同意两个字典。
判断同意还是不同意时,直接判断query中是否包含“同意字典”中的词语。同意返回1,不同意返回-1,不确定返回0。

    def judge(self, target):
        # 先判定是否有否定内容,再判断是否有肯定内容
        for pattern in self.__disagree_pattern:
            index = target.find(pattern)
            if index == -1:
                pass
            else:
                return -1
        for pattern in self.__agree_pattern:
            index = target.find(pattern)
            if index == -1:
                pass
            else:
                return 1
        return 0

major.py:识别用户查询中的专业

major.py用来识别出用户查询语句中的专业信息。
首先定义一个专业字典['历史','语言','中医','中药'......],search()函数定义如下,如果用户查询中包含专业,则返回专业名称。如果不包含,返回None

    def search(self, target):
        for word in self.__all_major:
            index = target.find(word)
            if index == -1:
                pass
            else:
                return word
        return None

target.py:识别出用户查询中的省份

此文件定义了10个省份列表:

  • 北方、南方
  • 华东、华北、华中、华南
  • 西北、东北、西南
  • 全国

每一个列表都形如['河北','河南','北京'......]
_label_to_list(label)函数将地区名称映射为省份列表。
determin_area、determin_province函数分别用来确定学校是否属于某个地区、学校是否属于某个省份
search_province(query):返回query中包含的省份名称,如果没有返回None

ac_auto.py

此文件实现了一个AC自动机,它读取Dict目录下的normal_inquire、how_about、ask_tone、gongneng四个词典中的词语构建一棵字典树。

infomation_extraction:信息抽取模块

这一部分代码是我最看不懂的代码,也是离自然语言处理最近的代码。

本模块用到了

  • 哈工大分词器LTP,命名实体识别
  • jieba分词器
  • ahocorasick,即AC自动机,python中有AC自动机的包

在_types.py文件中定义了需要提取到的信息的枚举

from enum import Enum

class Attribute(Enum):
    ORIGIN = 0
    TYPE = 1
    SCORE = 2
    DESTINATION = 3
    SCHOOL = 4

根包下的文件

process.py是最重要的文件,它集中调用上面各个模块。
XQGKFlask是微信接口,本系统调用了wechatpy包。

转载于:https://www.cnblogs.com/weiyinfu/p/7267225.html

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值