• 高考小强源码阅读


    读别人的代码像破解密码一样,乐趣无穷。
    高考小强是一个基于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包。

  • 相关阅读:
    Requests接口测试(五)
    Requests接口测试(四)
    Requests接口测试(一)
    软件测试杂谈(学习思路、学习方法、面试技巧、后期发展、职业规划等)
    Requests接口测试(三)
    Requests接口测试(二)
    Python基础入门-列表解析式
    Python基础入门-集合
    Jmeter接口测试-完成任务API
    Jmeter接口测试-基于nodejs的to do list项目说明
  • 原文地址:https://www.cnblogs.com/weiyinfu/p/7267225.html
Copyright © 2020-2023  润新知