一种识别对话中日期的方法

中文对话中的日期表达方式有很多

大概总结了一下大概右下边几类

// 昨天 今天 明天
// 前天 大前天 后天 大后天 大大前天 大大后天。。。
// 2号 15日 五月50号
// 下个月五号 上个月今天
// 这周末 这周五 下周六 上周日
// 10天后 五天前

那么找规律,将有共同特性的结构进行统一处理

总结一下共同特性

1. 汉字正整数转化成阿拉伯数字

// 1. 10天后 五十天前 十二月十五日 十二月13号 这些可以提取出将汉字数字转化成阿拉伯数字的方法

对话中很少出现“二十万天前”这种比较离谱的日子,所以在这里我只实现了一个万以内正整数转换的方法

在整个处理过程中考虑了很多,比如说“两万二”这种数字

private static final HashMap<Character, Integer> NUMBER_MAPPER = new HashMap<Character, Integer>() {{
        put('一', 1); put('二', 2); put('三', 3); put('四', 4); put('五', 5);
        put('六', 6); put('七', 7); put('八', 8); put('九', 9); put('零', 0);
        put('壹', 1); put('贰', 2); put('叁', 3); put('肆', 4); put('伍', 5);
        put('陆', 6); put('柒', 7); put('捌', 8); put('玖', 9); put('〇', 0);
        put('两', 2); put('俩', 2); put('倆', 2); put('仨', 3);

        put('1', 1); put('2', 2); put('3', 3); put('4', 4); put('5', 5);
        put('6', 6); put('7', 7); put('8', 8); put('9', 9); put('0', 0);
    }};

    private static final String NUMBER_PATTERN = "(一|二|三|四|五|六|七|八|九|壹|贰|叁|肆|五|陆|柒|捌|玖|两|倆|俩|仨)";

    //口语数字转化为标准数字的正则表达式
    private static final HashMap<String, String> NUMBER_SPOKEN_PATTERN = new HashMap<String, String>() {{
        put(NUMBER_PATTERN + "{1}(百|佰)" + NUMBER_PATTERN + "{1}$", "十");
        put(NUMBER_PATTERN + "{1}(千|仟)" + NUMBER_PATTERN + "{1}$", "百");
        put(NUMBER_PATTERN + "{1}(万|萬)" + NUMBER_PATTERN + "{1}$", "千");
    }};

    private NumberUtils() {}

    /**
     * 字符串数字转阿拉伯数字,支持万以内正整数汉语数字转换
     * @param input
     * @return
     */
    public static Integer parseInteger(String input) {
        try {
            return Integer.parseInt(input);
        } catch (NumberFormatException e) {
            return hanNumber2Arabic(input);
        }
    }

    /**
     * 万以内中国数字转阿拉伯数字(正整数)
     * 5百 -> 500
     * 六十一 -> 61
     * @param input
     * @return
     */
    private static Integer hanNumber2Arabic(String input) {

        if (isEmpty(input)) {
            return null;
        }
        input = regular(input);
        Queue<Integer> stash = new LinkedList<>();
        Integer result = 0;
        for (char c : input.toCharArray()) {
            switch (c) {
                case '零':
                case '〇':
                    break;
                case '十':
                case '拾':
                    result += stash.poll() * 10;
                    break;
                case '百':
                case '佰':
                    result += stash.poll() * 100;
                    break;
                case '千':
                case '仟':
                    result += stash.poll() * 1000;
                    break;
                case '万':
                case '萬':
                    result += stash.poll() * 10000;
                    break;
                default:
                    stash.offer(NUMBER_MAPPER.get(c));
                    break;
            }
        }

        if (stash.size() > 0) {
            result += stash.poll();
        }

        return result;
    }

    /**
     * 万以内口语数字转化为标准数字
     * 两千二 -> 两千二百
     * 两万五 -> 两万五千
     *
     * @param input
     */
    private static String regular(String input) {
        for (Map.Entry<String, String> entry : NUMBER_SPOKEN_PATTERN.entrySet()) {
            Pattern pat = Pattern.compile(entry.getKey());
            Matcher matcher = pat.matcher(input);
            if (matcher.find()) {
                return input + entry.getValue();
            }
        }

        return input;
    }

2. 中文的月表述和日表述

// 在中文中表示月的说法:这个月 上个月 五月 5月 大上个月。。。
// 同样在中文中表示日的说法:今天 昨天 大前天 大大后天 5号 18日。。。
// 可以提取出一个识别这些表述的方法

在这里我没有实现“X天前”,“上周末”,“星期二”这些表述;只实现了针对于上边表述的识别

不论是日表述还是月表述都可以分为两类

    1. 针对于当前时间的表述 比如说:这个月 昨天

    2. 针对于具体月份或日的表示 比如说:3月 六号

所以代码如下

private static final HashMap<String, Integer> CHINESE_DAY_EXPRESSION_MAPPER = new HashMap<String, Integer>() {{
        put("今天", 0); put("明天", 1); put("后天", 2); put("昨天", -1); put("前天", -2);
    }};

    private static final HashMap<String, Integer> CHINESE_MONTH_EXPRESSION_MAPPER = new HashMap<String, Integer>() {{
        put("这个月", 0); put("下个月", 1); put("上个月", -1);
    }};

    private DateUtils() {}

    /**
     * 根据月表述和日表述 计算日期
     * @param monthExpression 月表述:"这个月" "上个月" "五月" "5月"
     * @param dayExpression 日表述:"今天" "前天" "5号" "5日"
     * @return 计算后的日期 这个月五号 -> 2017-11-05
     */
    public static LocalDate dateExcursion(String monthExpression, String dayExpression) {
        monthExpression = Utils.isNull(monthExpression, "");
        dayExpression = Utils.isNull(dayExpression, "");
        LocalDate result = LocalDate.now(ZoneId.systemDefault());

        int monthOffset = getDateAdjunct(monthExpression);
        if (monthOffset != 0) {
            monthExpression = monthExpression.substring(monthOffset);
        }

        // 对于不同的表述 不同处理
        if (CHINESE_MONTH_EXPRESSION_MAPPER.get(monthExpression) != null) {
            int baseOffset = CHINESE_MONTH_EXPRESSION_MAPPER.get(monthExpression);
            if (baseOffset > 0) { // 针对基于当前日期的表述 进行加或减
                result = result.plusMonths(baseOffset + monthOffset);
            } else if (baseOffset != 0) {
                result = result.plusMonths(baseOffset - monthOffset);
            }
        } else if (!Utils.isEmpty(monthExpression)) { // 针对指定日期的表述 进行指定
            Integer month = NumberUtils.parseInteger(monthExpression.substring(0, monthExpression.length() - 1));
            result = result.withMonth(month);
        }

        int dayOffset = getDateAdjunct(dayExpression);
        if (dayOffset != 0) {
            dayExpression = dayExpression.substring(dayOffset);
        }

        if (CHINESE_DAY_EXPRESSION_MAPPER.get(dayExpression) != null) {
            int baseOffset = CHINESE_DAY_EXPRESSION_MAPPER.get(dayExpression);
            if (baseOffset > 0) {
                result = result.plusDays(baseOffset + dayOffset);
            } else if (baseOffset != 0) {
                result = result.plusDays(baseOffset - dayOffset);
            }
        } else if (!Utils.isEmpty(dayExpression)) {
            Integer dayOfMonth = NumberUtils.parseInteger(dayExpression.substring(0, dayExpression.length() - 1));
            result = result.withDayOfMonth(dayOfMonth);
        }
        return result;
    }

    // 在这里处理用‘大’字修饰的表述 处理的有点笨拙
    private static Integer getDateAdjunct (String dateExpression) {
        int result = 0;
        for (char c : dateExpression.toCharArray()) {
            if (c == '大') {
                result++;
            } else {
                break;
            }
        }
        return result;
    }

3. 最后就是需要在分词的时候把这些表述准确的分出来,在这里我用的是HanLP分词,屏蔽了数量词识别,加入了自己的专门用于日期识别的方法,同时为了不污染HanLP优秀的词库,加入了自己定义的专门用于日期识别的词库

今天 t_day 1024
昨天 t_day 1024
明天 t_day 1024
上个月 t_month 1024
下个月 t_month 1024
这个月 t_month 1024
一月 t_month 1024
二月 t_month 1024
三月 t_month 1024
四月 t_month 1024
五月 t_month 1024

那么有了自己的时间词和词性,就可以方便的识别这些时间表述了

/**
     * 从中文输入中提取具体日期信息
     * @param input "上个月今天" "这个月6号" "明天" "5月6号"
     * @return 2017-05-06
     */
    public static String getAppointDay(String input) {
        List<Term> terms = NLPSegment.seg(input);
        dateMerge(terms);
        String monthExpression = "";
        String dayExpression = "";
        for (int i = 0; i < terms.size(); i++) {
            Term term = terms.get(i);
            if ("t_month".equals(term.nature.toString())) {
                monthExpression = term.word;
            } else if ("t_day".equals(term.nature.toString())) {
                dayExpression = term.word;
                return DateUtils.dateExcursion(monthExpression, dayExpression).toString();
            }
        }
        return DateUtils.dateExcursion(monthExpression, dayExpression).toString();
    }

    /**
     * 合并数量词和日期单位
     * 5/m 月/q -> 5月/mq
     * 大/a 前天/t_day -> 大前天/t_day
     */
    public static void dateMerge(List<Term> terms) {
        Term prev = null;
        Iterator<Term> iterator = terms.iterator();
        while (iterator.hasNext()) {
            Term current = iterator.next();
            if ("月".equals(current.word) && prev != null && "m".equals(prev.nature.toString())) {
                prev.word += current.word;
                prev.nature = Nature.create("t_month");
                iterator.remove();
            } else if (("日".equals(current.word) || "号".equals(current.word))
                    && prev != null && "m".equals(prev.nature.toString())) {
                prev.word += current.word;
                prev.nature = Nature.create("t_day");
                iterator.remove();
            } else if (("大".equals(current.word) || "大大".equals(current.word))
                    && prev != null && prev.word != null
                    && (prev.word.startsWith("大") && prev.word.endsWith("大"))) {
                prev.word += current.word;
                prev.nature = Nature.a;
                iterator.remove();
            } else if (("t_month".equals(current.nature.toString()) || "t_day".equals(current.nature.toString()))
                    && prev != null && prev.word != null
                    && (prev.word.startsWith("大") && prev.word.endsWith("大"))) {
                prev.word += current.word;
                prev.nature = Nature.create("t_month".equals(current.nature.toString()) ? "t_month" : "t_day");
                iterator.remove();
            } else {
                prev = current;
            }
        }
    }

dateMerge方法有两个作用

    1. 用于合并数字和时间单位

    2. 用于合并用‘大’修饰的时间表述

最后调用getAppointDay()方法进行分析即可

测试一下

public void testGetAppointDay() {
        LocalDate now = LocalDate.now(ZoneId.systemDefault());
        assertEquals(HanLPUtils.getAppointDay("昨天天气怎么样"), now.plusDays(-1).toString());
        assertEquals(HanLPUtils.getAppointDay("前天天气怎么样"), now.plusDays(-2).toString());
        assertEquals(HanLPUtils.getAppointDay("今天天气怎么样"), now.toString());
        assertEquals(HanLPUtils.getAppointDay("明天天气怎么样"), now.plusDays(1).toString());
        assertEquals(HanLPUtils.getAppointDay("后天天气怎么样"), now.plusDays(2).toString());

        assertEquals(HanLPUtils.getAppointDay("大后天天气怎么样"), now.plusDays(3).toString());
        assertEquals(HanLPUtils.getAppointDay("大大后天天气怎么样"), now.plusDays(4).toString());
        assertEquals(HanLPUtils.getAppointDay("大大大后天天气怎么样"), now.plusDays(5).toString());
        assertEquals(HanLPUtils.getAppointDay("大大大大后天天气怎么样"), now.plusDays(6).toString());
        assertEquals(HanLPUtils.getAppointDay("大大前天天气怎么样"), now.plusDays(-4).toString());
        assertEquals(HanLPUtils.getAppointDay("大前天天气怎么样"), now.plusDays(-3).toString());

        assertEquals(HanLPUtils.getAppointDay("大下个月后天天气怎么样"), now.plusMonths(2).plusDays(2).toString());
        assertEquals(HanLPUtils.getAppointDay("大上个月后天天气怎么样"), now.plusMonths(-2).plusDays(2).toString());

        assertEquals(HanLPUtils.getAppointDay("下个月后天天气怎么样"), now.plusMonths(1).plusDays(2).toString());
        assertEquals(HanLPUtils.getAppointDay("上个月今天天气怎么样"), now.plusMonths(-1).toString());
        assertEquals(HanLPUtils.getAppointDay("下个月前天天气怎么样"), now.plusMonths(1).plusDays(-2).toString());
        assertEquals(HanLPUtils.getAppointDay("上个月昨天天气怎么样"), now.plusMonths(-1).plusDays(-1).toString());
        assertEquals(HanLPUtils.getAppointDay("这个月今天天气怎么样"), now.toString());

        assertEquals(HanLPUtils.getAppointDay("五月二十日天气怎么样"), now.withMonth(5).withDayOfMonth(20).toString());
        assertEquals(HanLPUtils.getAppointDay("5月二十日天气怎么样"), now.withMonth(5).withDayOfMonth(20).toString());
        assertEquals(HanLPUtils.getAppointDay("五月20日天气怎么样"), now.withMonth(5).withDayOfMonth(20).toString());
        assertEquals(HanLPUtils.getAppointDay("5月20日天气怎么样"), now.withMonth(5).withDayOfMonth(20).toString());

        assertEquals(HanLPUtils.getAppointDay("20号天气怎么样"), now.withDayOfMonth(20).toString());
        assertEquals(HanLPUtils.getAppointDay("二十号天气怎么样"), now.withDayOfMonth(20).toString());

        assertEquals(HanLPUtils.getAppointDay("大上个月大大大大大前天天气怎么样"), now.plusMonths(-2).plusDays(-7).toString());
        assertEquals(HanLPUtils.getAppointDay("天气怎么样"), now.toString());
    }

结果当然是通过了

211248_vObH_3001485.png

转载于:https://my.oschina.net/u/3001485/blog/1563414

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Time-NLP 文语句时间语义识别 author:shinyke 本工具是由复旦NLP时间分析功能修改而来,做了很多细节和功能的优化,具体如下: 泛指时间的支持,如:早上、晚上、午、傍晚等。 时间未来倾向。 如:在周五输入“周一早上开会”,则识别到下周一早上的时间;在下午17点输入:“9点送牛奶给隔壁的汉子”则识别到第二天上午9点。 多个时间识别,及多个时间之间上下文关系处理。如:"下月1号下午3点至5点到图书馆还书",识别到开始时间为下月1号下午三点。同时,结束时间也继承上文时间识别到下月1号下午5点。 可自定义基准时间:指定基准时间为“2016-05-20-09-00-00-00”,则一切分析以此时间为基准。 修复了各种各样的BUG。 简而言之,这是一个输入一句话,能识别出话里的时间的工具。╮(╯▽╰)╭ 示例代码: /**  *   * 测试类  *   * @author kexm  * @version 1.0  * @since 2016年5月4日  *   */ public class TimeAnalyseTest {     @Test     public void test(){         String path = TimeNormalizer.class.getResource("").getPath();         String classPath = path.substring(0, path.indexOf("/com/time"));         System.out.println(classPath "/TimeExp.m");         TimeNormalizer normalizer = new TimeNormalizer(classPath "/TimeExp.m");         normalizer.parse("Hi,all.下周一下午三点开会");// 抽取时间         TimeUnit[] unit = normalizer.getTimeUnit();         System.out.println("Hi,all.下周一下午三点开会");         System.out.println(DateUtil.formatDateDefault(unit[0].getTime())   "-"   unit[0].getIsAllDayTime());          normalizer.parse("早上六点起床");// 注意此处识别到6天在今天已经过去,自动识别为明早六点(未来倾向,可通过开关关闭:new TimeNormalizer(classPath "/TimeExp.m", false))         unit = normalizer.getTimeUnit();         System.out.println("早上六点起床");         System.out.println(DateUtil.formatDateDefault(unit[0].getTime())   "-"   unit[0].getIsAllDayTime());         normalizer.parse("周一开会");// 如果本周已经是周二,识别为下周周一。同理处理各级时间。(未来倾向)         unit = normalizer.getTimeUnit();         System.out.println("周一开会");         System.out.println(DateUtil.formatDateDefault(unit[0].getTime())   "-"   unit[0].getIsAllDayTime());         normalizer.parse("下下周一开会");//对于上/下的识别         unit = normalizer.getTimeUnit();         System.out.println("下下周一开会");         System.out.println(DateUtil.formatDateDefault(unit[0].getTime())   "-"   unit[0].getIsAllDayTime());  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值