Python自然语言处理实战:日期实体提取

浏览: 5991

本文来自《Python自然语言处理实战》章节内容,机械工业出版社华章授权发布,未经允许,禁止转载!

今天我要介绍的案例是自然语言处理中最为常见的:关键实体识别- 日期识别!

在工程项目中,我们会经常面临日期识别的任务。当针对结构化数据时,日期设置一般有良好的规范,在数据入库时予以类型约束,在需要时能够通过解析还原读取到对应的日期。然而在一些非结构化数据应用场景下,日期和文本混杂在一起,此时日期的识别就变得艰难许多。

非结构数据下的日期识别多是与具体需求有关,本节实战的背景如下:现有一个基于语音问答的酒店预订系统,其根据用户的每句语音进行解析,识别出用户的酒店预订需求,如房间型号、入住时间等;用户的语音在发送给后台进行请求时已经转换成中文文本,然而由于语音转换工具的识别问题,许多日期类的数据并不是严格的数字,会出现诸如“六月12”“2016年八月”“20160812”“后天下午”等形式。这里我们不关注问答系统的具体实现过程,主要目的是识别出每个请求文本中可能的日期信息,并将其转换成统一的格式进行输出。例如“我要今天住到明天”(假设今天为2017年10月1号),那么通过日期解析后,应该输出为“2017-10-01”和“2017-10-02”。

       接下来开始实战,我们主要通过正则表达式和Jieba分词来完成该任务,主要引入以下库:

l  import re
l  from datetime import datetime, timedelta
l  from dateutil.parser import parse
l  import jieba.posseg as psg

首先通过Jieba分词将带有时间信息的词进行切分,然后记录连续时间信息的词。这里面就用到Jieba词性标注的功能,提取其中“m”(数字)t”(时间)词性的词。

def time_extract(text):
    time_res = []
    word = ''
    keyDate = {'今天': 0, '明天':1, '后天': 2}
    for k, v in psg.cut(text):
        if k in keyDate:
            if word != '':
                time_res.append(word)
            word = (datetime.today() + timedelta(days=keyDate.get(k, 0))).strftime('%Y年%m月%d日')
        elif word != '':
            if v in ['m', 't']:
                word = word + k
            else:
                time_res.append(word)
                word = ''
        elif v in ['m', 't']:
            word = k
    if word != '':
        time_res.append(word)
    result = list(filter(lambda x: x is not None, [check_time_valid(w) for w in time_res]))
    final_res = [parse_datetime(w) for w in result]
    return [x for x in final_res if x is not None]

time_extract实现了这样的规则约束:对句子进行解析,提取其中所有能表示日期时间的词,并进行上下文拼接,如词性标注完后出现“今天/t 住/v 到/v 明天/t 下午/t 3/m点/m”,那么需要将“今天”和“明天下午3点”提取出来。代码里面定义了几个关键日期——“今天”“明天”和“后天”,当解析遇到这些词时进行日期格式转换,以方便后面的解析。关键日期可根据实际场景覆盖情况进行添加,这里由于是酒店入住,基本不会出现“前天”“昨天”等情况,因此未予添加。

       time_extract中有个check_time_valid函数,用来对提取的拼接日期串进行进一步处理,以进行有效性判断的。

def check_time_valid(word):
    m = re.match("\d+$", word)
    if m:
        if len(word) <= 6:
            return None
    word1 = re.sub('[号|日]\d+$', '日', word)
    if word1 != word:
        return check_time_valid(word1)
    else:
        return word1

在time_extract中最后还有个parse_datetime函数,用以将每个提取到的文本日期串进行时间转换。其主要通过正则表达式将日期串进行切割,分为“年”“月”“日”“时”“分”“秒”等具体维度,然后针对每个子维度单独再进行识别。

def parse_datetime(msg):
    if msg is None or len(msg) == 0:
        return None

    try:
        dt = parse(msg, fuzzy=True)
        return dt.strftime('%Y-%m-%d %H:%M:%S')
    except Exception as e:
        m = re.match(
            r"([0-9零一二两三四五六七八九十]+年)?([0-9一二两三四五六七八九十]+月)?([0-9一二两三四五六七八九十]+[号日])?([上中下午晚早]+)?([0-9零一二两三四五六七八九十百]+[点:\.时])?([0-9零一二三四五六七八九十百]+分?)?([0-9零一二三四五六七八九十百]+秒)?",
            msg)
        if m.group(0) is not None:
            res = {
                "year": m.group(1),
                "month": m.group(2),
                "day": m.group(3),
                "hour": m.group(5) if m.group(5) is not None else '00',
                "minute": m.group(6) if m.group(6) is not None else '00',
                "second": m.group(7) if m.group(7) is not None else '00',
            }
            params = {}

            for name in res:
                if res[name] is not None and len(res[name]) != 0:
                    tmp = None
                    if name == 'year':
                        tmp = year2dig(res[name][:-1])
                    else:
                        tmp = cn2dig(res[name][:-1])
                    if tmp is not None:
                        params[name] = int(tmp)
            target_date = datetime.today().replace(**params)
            is_pm = m.group(4)
            if is_pm is not None:
                if is_pm == u'下午' or is_pm == u'晚上' or is_pm =='中午':
                    hour = target_date.time().hour
                    if hour < 12:
                        target_date = target_date.replace(hour=hour + 12)
            return target_date.strftime('%Y-%m-%d %H:%M:%S')
        else:
            return None

可以看到,核心是下面的正则表达式:

"([0-9零一二两三四五六七八九十]+年)?([0-9一二两三四五六七八九十]+月)?([0-9一二两三四五六七八九十]+[号日])?([上中下午晚早]+)?([0-9零一二两三四五六七八九十百]+[点:\.时])?([0-9零一二三四五六七八九十百]+分?)?([0-9零一二三四五六七八九十百]+秒)?"

       该正则表达式就是人工制定的一条规则,用以处理阿拉伯数字与汉字混杂的日期串的提取,其还加入了“上中下晚早”的考虑,用以调整最终输出的时间格式。

       parse_datetime在解析具体几个维度时,用了year2dig和cn2dig方法。主要是通过预定义一些模板,将具体的文本转换成相应的数字,以供parse_datetime进行封装。

UTIL_CN_NUM = {
    '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4,
    '五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
    '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
    '5': 5, '6': 6, '7': 7, '8': 8, '9': 9
}
UTIL_CN_UNIT = {'十': 10, '百': 100, '千': 1000, '万': 10000}
def cn2dig(src):
    if src == "":
        return None
    m = re.match("\d+", src)
    if m:
        return int(m.group(0))
    rsl = 0
    unit = 1
    for item in src[::-1]:
        if item in UTIL_CN_UNIT.keys():
            unit = UTIL_CN_UNIT[item]
        elif item in UTIL_CN_NUM.keys():
            num = UTIL_CN_NUM[item]
            rsl += num * unit
        else:
            return None
    if rsl < unit:
        rsl += unit
    return rsl
def year2dig(year):
    res = ''
    for item in year:
        if item in UTIL_CN_NUM.keys():
            res = res + str(UTIL_CN_NUM[item])
        else:
            res = res + item
    m = re.match("\d+", res)
    if m:
        if len(m.group(0)) == 2:
            return int(datetime.datetime.today().year/100)*100 + int(m.group(0))
        else:
            return int(m.group(0))
    else:
        return None

可以看到,预先将常见的中文汉字与对应的阿拉伯数字建立一一对应关系,然后通过匹配,转换成相应的阿拉伯数字。

parse_datetime最后通过解析具体维度(年月日等),然后替换datetime中today的参数,即将“今天”作为默认值,当解析的日期串中未出现表示具体年份或月份等维度的信息时,自动设置为“今天”的属性。下面进行一些测试(假设今天为“2017年10月25号”):

text1 = '我要住到明天下午三点'
print(text1, time_extract(text1), sep=':')

text2 = '预定28号的房间'
print(text2, time_extract(text2), sep=':')

text3 = '我要从26号下午4点住到11月2号'
print(text3, time_extract(text3), sep=':')

输出结果如下:

我要住到明天下午三点:['2017-10-26 15:00:00']

预定28号的房间:['2017-10-28 00:00:00']

我要从26号下午4点住到11月2号:['2017-10-26 16:00:00', '2017-11-02 00:00:00']   

可以看到,结果还是相对较好的。当然采用规则去覆盖所有的语言场景是不太现实的。如果我们测试输入如下语句:

text4 = '我要预订今天到30的房间'
print(text4, time_extract(text4), sep=':')

text5 = '今天30号呵呵'
print(text5, time_extract(text5), sep=':')

输出为:

我要预订今天到30的房间:['2017-10-25 00:00:00']

今天30号呵呵:['2017-10-25 00:03:00']

对于text4和text5这种规则覆盖之外的场景,该方法效果大大降低。但相较于基于统计的方法,规则方法无需在系统建设初期为搜集数据标注训练而苦恼,能够快速见效。

推荐 1
本文由 Kenny 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册