前言
Java
中日期、时间相关的类相当的多,并且分不同的版本提供了不同的实现,包括Date
、Calendar
、LocalDateTime
、ZoneDateTime
、OffsetDateTime
等等。针对这些时间类型又通过SimpleDateFormat
和DateTimeFormatter
实现不同的日期与字符串之间的格式化和解析。
为了应对各种各样的日期解析,我们通常会封装类似于DateUtils
的工具类,专门用来处理日期字符串的解析,同时为了兼容不同格式的日期字符串,又需要预先枚举出可能用到的日期格式。这种传统的DateUtils
通常会面临性能与兼容性的两难问题。
而本文要介绍的,是一个截然不同的日期解析工具dateparser
,它可以智能地解析几百上千种任意格式的日期字符串,更为难得的是它的性能同样非常出色。
DateUtils的两难问题
一个比较典型的日期解析函数类似这样(这是commons-lang3
在其DateUtils
中提供的函数):
public static Date parseDate(final String str, final String... parsePatterns) { return parseDate(str, null, parsePatterns); }
这种日期解析函数的内部逻辑,往往是根据一批DATE_FORMAT
轮番尝试,通过异常重试的方式试出来唯一匹配的格式。这种简单粗暴的方式,事实上存在着一个两难问题。
首先,我们很难穷举出全部可能出现的日期格式,年月日的分隔符、排列次序、时分秒、是否有毫秒、时区处理、PM与AM的支持等等,罗列出来的话不计其数。
其次,异常重试的方式存在一些性能损耗,据我粗略测算,在MBP硬件环境中异常中断大概需要消耗2
微秒,而一次日期解析可能消耗0.75
微秒。如果提供的parsePatterns
数量很多,则解析一个日期字符串的循环重试的最终耗时甚至会超过Redis
读写操作。
那么,有没有办法既可以支持无数多个不规则的日期字符串,同时也没有性能问题的技术方案呢?
dateparser
这就是解决日期字符串解析的灵丹妙药,它是一个高性能且非常智能的datetime字符串解析工具。
为了实现高性能与可扩展性,它并没有采用SimpleDateFormat
或DateTimeFormatter
,而是正则表达式。通过预定义的正则表达式来自动地捕捉不同格式的日期片段,它可以自动抽取出字符串中存在的 year
, month
, day
, hour
, minute
, second
, zone
等熟悉。
这些预定义的正则表达式片段包括:
(?<week>%s)\W*
可以将Monday
解析为week
?(?<year>\d{4})$
可以将2019
解析为year
^(?<year>\d{4})(?<month>\d{2})$
可以抽取出201909
内部的year
和month
?(?<hour>\d{1,2}) o'clock\W*
可以将12 o'clock
解析为hour
更多规则参见DateParserBuilder.java
如此多的正则表达式,会不会也存在性能隐患呢?如果使用的是java.util.regex
包来进行循环匹配,随着规则增加,确实会有性能问题。
但是dateparser
使用retree
将预定义的一大批正则表达式合并为一颗树,也就是正则匹配树。它可以非常快速地对一大批正则表达式执行并行匹配,内部结构可以理解为字典树,但是树中的节点并不是字母,而是正则匹配节点。
安装Maven依赖
可以通过此maven
坐标引入依赖
<dependency> <groupId>com.github.sisyphsu</groupId> <artifactId>dateparser</artifactId> <version>1.0.2</version> </dependency>
基础使用
dateparser
提供了一个DateParserUtils
工具类,可以直接使用它将字符串解析为Date
、 Calendar
、 LocalDateTime
、 OffsetDateTime
等:
Date date = DateParserUtils.parseDate("Mon Jan 02 15:04:05 -0700 2006"); // Tue Jan 03 06:04:05 CST 2006 Calendar calendar = DateParserUtils.parseCalendar("Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time)"); // 2015-07-03T17:04:07Z LocalDateTime dateTime = DateParserUtils.parseDateTime("2019-09-20 10:20:30.12345678 +0200"); // 2019-09-20T16:20:30.123456780 OffsetDateTime offsetDateTime = DateParserUtils.parseOffsetDateTime("2015-09-30 18:48:56.35272715 +0000 UTC"); // 2015-09-30T18:48:56.352727150Z
需要注意的是,它会根据字符串中标明的TimeZon
e或ZoneOffset
自动进行偏移量转换。
创建新DateParser实例
由于DateParser
不是线程安全的,同时parse
操作通常非常快速(1us),因此DateParserUtils
内部直接维护了一个DateParser
单例,然后通过synchronized
进行并发控制。
如果你想在多线程中高频率、并发地使用它,就应该为不同的线程创建不同的DateParser
实例:
DateParser parser = DateParser.newBuilder().build(); Date date = parser.parseDate("Mon Jan 02 15:04:05 -0700 2006"); // Tue Jan 03 06:04:05 CST 2006
DateParser
实例相当笨重一些,所以你应该尽量多的复用它以提高性能。
MM/dd
与dd/MM
的优先级
多数情况下,dateparser
可以按照规则自动地识别出字符串内部的month
与day
片段。
但是对于MM/dd/yy
和dd/MM/yy
,有时候它就难以区分了。因为世界上多数国家会使用dd/MM/yy
作为日期的格式,但是也有少数国家会特立独行地使用MM/dd/yy
作为日期格式,最典型的就是美帝国主义。
因此当dateparser
遇到类似于7.8.2019
这样的日期时,它就很难判断到底是7月8日还是8月7日。
为解决这个难题,dateparser
内部增加了一个名为preferMonthFirst
的选项,用于辅助解决这个问题:
DateParserUtils.preferMonthFirst(true); DateParserUtils.parseCalendar("08.03.71"); // 1971-08-03 DateParserUtils.preferMonthFirst(false); DateParserUtils.parseCalendar("08.03.71"); // 1971-03-08
默认情况下,如果无法甄别月与日,则视为月在后。如果你指定了preferMonthFirst
为true
,则试为月在前。
自定义Parser
你可以使用DateParserBuilder
构建自己的日期解析器,通过此builder
,你可以自定义新的解析规则。
例如,如果你想支持【2019】
这样的year
字符串,可以这样做:
DateParser parser = DateParser.newBuilder().addRule("【(?<year>\\d{4})】").build(); Calendar calendar = parser.parseCalendar("【1991】"); assert calendar.get(Calendar.YEAR) == 1991;
需要注意的是,正则表达式【(?<year>\\d{4})】
里面的year
非常重要,它是dateparser
内置的捕捉关键词。
你也可以增加更加灵活的解析规则,就像这样:
DateParser parser = DateParser.newBuilder() .addRule("民国(\\d{3})年", (input, matcher, dt) -> { int offset = matcher.start(1); int i0 = input.charAt(offset) - '0'; int i1 = input.charAt(offset + 1) - '0'; int i2 = input.charAt(offset + 2) - '0'; dt.setYear(i0 * 100 + i1 * 10 + i2 + 1911); }) .build(); Calendar calendar = parser.parseCalendar("民国101年"); assert calendar.get(Calendar.YEAR) == 2012;
这个例子里面,新增了一个捕捉并解析民国xxx年
的日期规则。
性能对比
首先,在单一日期格式下,对比一下dateparser
与SimpleDateFormat
的性能表现:
Benchmark Mode Cnt Score Error Units SingleBenchmark.java avgt 6 921.632 ± 12.299 ns/op SingleBenchmark.parser avgt 6 1553.909 ± 70.664 ns/op
可以看到,在日期格式固定且单一的情况下,dateparser
在性能上处于下风,这也在预料之中。
然后,在单一日期格式下,对比一下dateparser
与DateTimeFormatter
的性能表现:
Benchmark Mode Cnt Score Error Units SingleDateTimeBenchmark.java avgt 6 654.553 ± 16.703 ns/op SingleDateTimeBenchmark.parser avgt 6 1680.690 ± 34.214 ns/op
可以看到,DateTimeFormatter
的性能表现确实比SimpleDateFormat
更加出色一些。同时dateparser的设计初衷是为了应对不规则日期格式,因此在固定格式匹配上存在劣势并不意外。
如果我们将日期格式增加为16种时,性能表现就不一样了:
Benchmark Mode Cnt Score Error Units MultiBenchmark.format avgt 6 47385.021 ± 1083.649 ns/op MultiBenchmark.parser avgt 6 22852.113 ± 310.720 ns/op
如果换算一下,无论日期格式是一种还是16中,dateparser
的性能始终维持在1.5us
,说明它在算法上是非常稳定的,面对不同的场景不会有什么性能损失
支持的日期格式(部分)
以下为dateparser
在单元测试中完成测试解析的日期格式样例,具体可以参考源代码。同时需要注意的是,这个列表只是一个子集:
May 8, 2009 5:57:51 PM oct 7, 1970 oct 7, '70 oct. 7, 1970 oct. 7, 70 Mon Jan 2 15:04:05 2006 Mon Jan 2 15:04:05 MST 2006 Mon Jan 02 15:04:05 -0700 2006 Monday, 02-Jan-06 15:04:05 MST Mon, 02 Jan 2006 15:04:05 MST Tue, 11 Jul 2017 16:28:13 +0200 (CEST) Mon, 02 Jan 2006 15:04:05 -0700 Thu, 4 Jan 2018 17:53:36 +0000 Mon Aug 10 15:44:11 UTC+0100 2015 Fri Jul 03 2015 18:04:07 GMT+0100 (GMT Daylight Time) September 17, 2012 10:09am September 17, 2012 at 10:09am PST-08 September 17, 2012, 10:10:09 October 7, 1970 October 7th, 1970 12 Feb 2006, 19:17 12 Feb 2006 19:17 7 oct 70 7 oct 1970 03 February 2013 1 July 2013 2013-Feb-03 3/31/2014 03/31/2014 08/21/71 8/1/71 4/8/2014 22:05 04/08/2014 22:05 4/8/14 22:05 04/2/2014 03:00:51 8/8/1965 12:00:00 AM 8/8/1965 01:00:01 PM 8/8/1965 01:00 PM 8/8/1965 1:00 PM 8/8/1965 12:00 AM 4/02/2014 03:00:51 03/19/2012 10:11:59 03/19/2012 10:11:59.3186369 2014/3/31 2014/03/31 2014/4/8 22:05 2014/04/08 22:05 2014/04/2 03:00:51 2014/4/02 03:00:51 2012/03/19 10:11:59 2012/03/19 10:11:59.3186369 2014年04月08日 2006-01-02T15:04:05+0000 2009-08-12T22:15:09-07:00 2009-08-12T22:15:09 2009-08-12T22:15:09Z 2014-04-26 17:24:37.3186369 2012-08-03 18:31:59.257000000 2014-04-26 17:24:37.123 2013-04-01 22:43 2013-04-01 22:43:22 2014-12-16 06:20:00 UTC 2014-12-16 06:20:00 GMT 2014-04-26 05:24:37 PM 2014-04-26 13:13:43 +0800 2014-04-26 13:13:43 +0800 +08 2014-04-26 13:13:44 +09:00 2012-08-03 18:31:59.257000000 +0000 UTC 2015-09-30 18:48:56.35272715 +0000 UTC 2015-02-18 00:12:00 +0000 GMT 2015-02-18 00:12:00 +0000 UTC 2015-02-08 03:02:00 +0300 MSK m=+0.000000001 2015-02-08 03:02:00.001 +0300 MSK m=+0.000000001 2017-07-19 03:21:51+00:00 2014-04-26 2014-04 2014 2014-05-11 08:20:13,787 3.31.2014 03.31.2014 08.21.71 2014.03 2014.03.30 20140601 20140722105203 1332151919 1384216367189 1384216367111222 1384216367111222333
原文地址
本文转载至https://sulin.me/2019/38Z4HAT.html
原文转载:http://www.shaoqun.com/a/632730.html
美菜:https://www.ikjzd.com/w/1874
netporter:https://www.ikjzd.com/w/2132
前言Java中日期、时间相关的类相当的多,并且分不同的版本提供了不同的实现,包括Date、Calendar、LocalDateTime、ZoneDateTime、OffsetDateTime等等。针对这些时间类型又通过SimpleDateFormat和DateTimeFormatter实现不同的日期与字符串之间的格式化和解析。为了应对各种各样的日期解析,我们通常会封装类似于DateUtils的工具
墩煌网:https://www.ikjzd.com/w/189
dojo:https://www.ikjzd.com/w/2052
淘粉8:https://www.ikjzd.com/w/1725
G20发布"大阪数字经济宣言":2020年将拿出切实成果!:https://www.ikjzd.com/home/99576
直接上手!亚马逊派来7大成功策略辅佐你,广告也太好做了吧!:https://www.ikjzd.com/home/8223
Shopee运费补贴政策:https://www.ikjzd.com/w/2644