4 个版本 (1 个稳定版)

1.0.0 2023年11月2日
0.0.3 2023年10月31日
0.0.2 2023年10月31日
0.0.1 2023年10月31日

#184 in 日期和时间

GPL-3.0-or-later

365KB
1.5K SLoC

osmanthus丨桂花算法

LANGUAGE CN 中文文档点击这里

从字符串中查找并自动格式化时间文本。

特性

  • 快速
  • 高性能
  • 轻量
  • 自动时区
  • 100+ 语言

可用于新闻情感分析、竞标和数据清洗等场景。

本文档详细介绍了 osmanthus 的使用方法,包括其强大的解析性能、出色的兼容性、全球语言和时区支持、在线体验、对其他编程语言的支持、测试用例和有趣的创意故事。

它支持以下 4+1 种类型的时间文本的解析和自动格式化:

  1. 绝对时间|例如 2013年july18 10:03下午
  2. 相对时间|例如 3小时前2 minutes ago
  3. 时间戳|例如16850253651663025361000
  4. 系列|例如https://example.com/20210315/img/2035.png
  5. 自动模式|默认情况下,它是一个按顺序尝试 时间戳 > 相对时间 > 绝对时间 > 系列 的算法。只要其中之一被识别,就会返回结果。

提示:当您不知道时间文本的类型时,或者如果您希望 osmanthus 自行识别,建议使用自动模式。

快速开始

1. 安装

> cargo add osmanthus

2. 使用

有关更多示例,请参阅 benchesexamples 中的示例代码。

2.1 解析绝对时间文本

use osmanthus::parse_absolute;
use osmanthus::bind::Param;

fn main() {
    let samples = vec![
        "3/08/2023 | 11:51",  // 2023-08-03 11:51:00
        "aug 06 .2023 10h42",  // 2023-08-06 10:42:00"
        "2013年12月8号 pm 3:00",  // 2013-12-08 15:00:00
        "26 ก.ค. 2566 08:00 น.",  // 2023-07-26 08:00:00
        "2014年04月08日11时25分18秒 下午",  // 2014-04-08 23:25:18
        "2023-02-05 10:03:37 pm cst",
        "2023-07-30T14:12:51+02:00",
    ];
    for sample in samples{
        let r =parse_absolute(sample, Some(Param{strict: true, ..Default::default()}));
        let datetime = r.datetime.local.datetime;
        println!("absolute time text parse result: {:?}, status: {}", datetime.format("%Y-%m-%d %H:%M:%S").to_string(), r.status);
    }
}

2.2 解析相对时间文本

use osmanthus::parse_relative;
use osmanthus::bind::Param;

fn main() {
    let samples = vec![
        "发布于 - /n6小時前,",  // 6 hours ago
        "( 시간: 3분 전)", // 3 minute ago
        "- about / 2 minutes ago", // 2 minutes ago
        "30天前 来源:新华网", // 30 days ago
        "publish 5 second ago." // 5 second ago.
    ];
    for sample in samples{
        let r =parse_relative(sample, Some(Param{strict: true, ..Default::default()}));
        let datetime = r.datetime.local.datetime;
        println!("relative time text parse result: {:?}, status: {}", datetime.format("%Y-%m-%d %H:%M:%S").to_string(), r.status);
    }
}

2.3 解析时间戳时间文本

use osmanthus::parse;
use osmanthus::bind::Param;

fn main() {
    let samples = vec![
        "1677380340",  // success
        "1677380340236982058745",  // parse fail
        "16773803abc",   // parse fail
        "你好,中国",   // parse fail
    ];
    for sample in samples{
        let r =parse(sample, Some(Param{strict: true, ..Default::default()}));
        let datetime = r.datetime.local.datetime;
        println!("timestamp time text parse result: {:?}, status: {}", datetime.format("%Y-%m-%d %H:%M:%S").to_string(), r.status);
    }
}

2.4 解析系列时间文本

use osmanthus::parse_series;
use osmanthus::bind::Param;

fn main() {
    let samples = vec![
        "https://www.kingname.info/2022/JULY309/this20350205-is-gnelist/",  // 2022-07-30 00:00:00"
        "H_502_5@2010oct03 @H_502_5@2012/07/26.doc",  // 2010-10-03 00:00:00
        "https://new.qq.com/rain/a/k09381120221126A03W2R00",  // 2022-11-26 00:00:00
        "/202211/W02022110720101102590.jpg", // 2022-11-07 00:00:00
        "http://cjrb.cjn.cn/html/2023-01/16/content_250826.htm" // 2023-01-16 00:00:00
    ];
    for sample in samples{
        let r =parse_series(sample, Some(Param{strict: true, ..Default::default()}));
        let datetime = r.datetime.local.datetime;
        println!("series time text parse result: {:?}, status: {}", datetime.format("%Y-%m-%d %H:%M:%S").to_string(), r.status);
    }
}

2.5 自动,解析任何时间文本

use osmanthus::parse_series;
use osmanthus::bind::Param;

fn main() {
    let samples = vec![
        "https://www.kingname.info/2022/JULY309/this20350205-is-gnelist/",  // series, 2022-07-30 00:00:00"
        "3/08/2023 | 11:51",  // absolute, 2023-08-03 11:51:00
        "发布于 - /n6小時前,",  // relative, 6 hours ago
        "/202211/W02022110720101102590.jpg", // series, 2022-11-07 00:00:00
        "1677380340" // timestamp, 2023-02-26 10:59:00
    ];
    for sample in samples{
        let r =parse_series(sample, Some(Param{strict: true, ..Default::default()}));
        let datetime = r.datetime.local.datetime;
        println!("time text parse result: {:?}, status: {}", datetime.format("%Y-%m-%d %H:%M:%S").to_string(), r.status);
    }
}

3. 参数和结果

使用 osmanthus 时,可以传递多个参数,这些参数将影响最终输出。因此,了解这些参数的细节及其可能产生的潜在影响是必要的。

由于需要支持全球多个时区并满足不同地区的使用需求,解析结果也考虑了兼容性处理。

3.1 参数

在设计桂花时,我强制使用统一的函数签名。

fn parse(text: &str, options: Option<Param>) -> Result

因此,很明显,在调用任何桂花解析函数时,都需要传递textoptions参数。

None

options字段是类型为Option<Param>的,表示它是可选的。如果您不想传递任何内容,可以使用None

use osmanthus::parse;

fn main() {
    let res = parse("july,10,2023 15:02:11 PM", None);
    println!("res: {:?}, status: {}", res.datetime.local.datetime.format("%Y-%m-%d %H:%M:%S").to_string(), res.status)
}
// res: "2023-07-10 15:02:11", status: true 
Param

上面的示例展示了不传递Option<Param>的情况。现在,让我们看看何时需要提供此参数以及其值的具体作用。首先,让我们看看Param结构的签名。

pub struct Param{
    pub timezone: String,  // timezone
    pub strict: bool  // strict mode
}

有两个字段:timezonestrict,它们的意义是:

  • timezone:它是时区,计算结果的最终输出依赖于设置的时区。假设一个时间字符串对应于一个utc时间为2023-08-12 15:00:00,但如果你将时区设置为aest,解析的utc时间将是2023-08-12 05:00:00,因为utc = aest - 36000秒。
  • strict:它代表严格模式,将在下面进一步解释,但在这里,让我们强调一下。在新闻和舆论的背景下,有一个常见的需求是确定新闻发布的时机。一个需要注意的重要点是,新闻的发布时间不能晚于当前本地时间;它必须更早。例如,如果当前时间是2023-10-10 10:00:05,检测到的新闻发布时间必须早于当前时间。它不能晚几个小时或几天。在严格模式下,桂花算法将确定时间文本是否大于当前时间。如果是,算法将跳过当前可疑文本并继续识别下一个可疑文本。

3.2 结果

为了满足不同地区和时区的使用,解析结果考虑了兼容性处理,提供了多个时间输出。首先,让我们看看Result中几个相关结构的签名。

pub struct Result{
    pub status: bool,
    pub method: String,
    pub time: NaiveDateTime,
    pub datetime: DateTime,
    pub timezone: String,
}

pub struct DateTime{
    pub local: Item,
    pub timezone: Item,
}

pub struct Item{
    pub datetime: NaiveDateTime,
    pub timestamp: i64
}

换句话说,当您使用桂花格式化字符串中的时间文本时,您获得的结果不仅是一个字符串或时间戳数字,而是一个包含更多信息的答案

  • 状态:当值为 true 时,表示算法已成功从给定字符串中识别出 有效时间文本 并相应地进行格式化。这里的有效时间文本指的是符合年-月-日格式的文本。正确的例子包括 2023-10-22july,2021,02 15:00,而错误的例子包括 july,2023 15:0015:06:30。换句话说,时间文本字符串 必须 同时满足 年-月-日 格式;否则,状态为 false;
  • 时区:此处可以是调用函数时传递的时区名称,也可以是程序自动检测到的时区名称。它也可以是一个空字符串,以方便在某些场景下的进一步处理;
  • 方法:模式的名称,osmanthus 将返回其识别的模式名称。例如 absoluterelativetimestamp 或者 series
  • time: 直接将输入文本格式化为时间 不附加 任何时区信息;
  • datetime: 附加本地时区和附加 UTC 时区
    • datetime.local
      • datetime.local.datetime: 添加本地时区的时间,获取操作环境的当前时区,并将 time 转换为本地时区对应的时间
      • datetime.local.timestamp: 本地的 timestamp
    • datetime.timezone
      • datetime.timezone.datetime: 添加 UTC 时区的时间,根据提供的时区或运行时识别的时区将时间转换为 UTC 时区对应的 time
      • datetime.timezone.timestamp: UTC 的 timestamp

这可能会让你感到有些困惑,对吧?

你可能想知道为什么不简单地返回一个单一的时间值,而不是提供 timedatetime.local.datetimedatetime.timezone.datetime 选项。

提供这些选项的目的是为了准备未来的处理。如果你只想将字符串格式化为本地时间,你可以简单地从 datetime.local.datetime 获取值,而不必担心其他两种时间类型。

让我们考虑一个特定的场景

  • 假设你为一家全球新闻媒体公司工作。
  • 你位于美国纽约。
  • 然而,处理新闻数据的服务器位于中国上海。
  • 时间文本中显示的时间是澳大利亚东部标准时间(AEST)。
  • 要求记录不同的时间到数据库中。

这种跨时区和跨国场景使得事情变得复杂。当服务器位于与你的位置不同的时区时,仅仅 本地时间 就不足以满足要求。此外,文本中的时区与服务器时区和你的时区都不同。

如果您只有 time 值,您需要自行计算纽约时区、上海时区和澳大利亚东部时区的时间差。每个转换至少需要两个步骤,然后将转换后的时间存储到数据库中。

现在,Osmanthus 为您提供了上海时区和 utc 时区,使您更容易将时间转换为任何所需的时区。只需一步转换,您就可以达到预期的结果。

性能

单次解析仅需 微秒(µs) 甚至 纳秒(ns),并且具有 出色的兼容性

即使输入字符串中存在混乱的噪声符号和不相关的其他文本,它也能准确地识别和格式化正确的时间文本。

基准测试

Osmanthus 的性能测试使用 Criterion,代码位于 benches 目录。

/// Machine Mac Stucio 
/// Chip: Apple M1 Max 
/// Memory:32GB
/// OS: MacOS 14.0

parse_timestamp benchmark result:
                        time:   [302.51 ns 302.98 ns 303.49 ns]
                        change: [+0.3496% +0.6413% +0.9291%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) high mild
  2 (2.00%) high severe

parse_series benchmark result:
                        time:   [24.324 µs 24.363 µs 24.407 µs]
                        change: [-0.3387% +0.1293% +0.5512%] (p = 0.58 > 0.05)
                        No change in performance detected.

parse_relative benchmark result:
                        time:   [525.93 µs 529.13 µs 533.43 µs]
                        change: [+0.4510% +1.0907% +1.8495%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 6 outliers among 100 measurements (6.00%)
  3 (3.00%) high mild
  3 (3.00%) high severe

parse_absolute benchmark result:
                        time:   [45.841 µs 45.966 µs 46.114 µs]
                        change: [+0.6914% +1.0410% +1.4468%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 9 outliers among 100 measurements (9.00%)
  6 (6.00%) high mild
  3 (3.00%) high severe

兼容性

Osmanthus 的解析趋势是尽可能地广泛和准确地识别和解析时间文本,从而清理混有各种噪声的文本。

噪声

Osmanthus 不怕噪声,无论是中文字符、字母、数字、标点符号,甚至是其他语言。

对于特定的兼容性情况,您可以参考 benchesexample 目录中的相关代码。我们也欢迎大家提供更多的测试样本。

时区

由于提供全球支持,因此自然需要考虑时区。

目前,Osmanthus 支持自动计算和 UTC 时间转换,包括 390 个不同的时区,包括常用的 CST、MST、BST、HAST 等时区。

有关详细信息,请参阅文档。

时区列表

在上面的时区列表中,Osmanthus 在处理过程中将自动识别和计算正确的时间,并在解析结果中提供当前操作环境的时区和 UTC 时间,方便大家根据自己的业务和地区进行转换。

时间顺序

世界各地的时间格式各不相同,常见的有年月日、日月年和月日年。算法将根据其内容和出现顺序自动转换时间文本。例如

2013.05/12 -> 2013-05-12 00:00:00  // Correct order, parsed directly
2013.05/july 15:00 -> 2013-07-05 15:00:00 // Month is definite, Adjust order
05,06,2021 13:00 -> 2021-06-05 13:00  // Month is uncertain, but this format is usually day-month-year, Adjust order
05,13,2021 13:00 -> 2021-05-13 13:00  // Month cannot be greater than 12, Order is actually definite

严格模式

在新闻和舆论分析中,通常有一个识别新闻发布时间的要求。需要注意的是,新闻的发布时间不能晚于当前本地时间;它必须更早。例如,如果当前时间是 "2023-10-10 10:00:05",则收集的新闻文章的发布时间将始终早于当前时间。它不能安排在明天。

在严格模式下,Osmanthus 将确定时间文本是否大于当前时间值。如果是,算法将跳过当前可疑文本,继续识别下一个可疑文本。

语言支持 100+

作者是一位企业家,主要负责全球新闻数据的收集和分析。因此,对时间文本的识别和解析必须满足全球化的需求。无论是普通话、俄语、日语、德语、法语、英语、韩语、孟加拉语、越南语,还是世界上其他数十种语言,都已支持。

然而,由于我的语言能力有限,一些更“本地化”的表达可能没有得到充分的覆盖,例如俄语的 недавно 或德语的 gerade eben

如果您能提供更多样本,我们将不胜感激

可能存在的缺陷

过度的兼容性导致准确性下降,使其容易产生误解释。例如,字符串12 批次 2021.05. 13 2023页 应该正确解析为 2021-05-13 00:00:00,但解析结果可能为 2021-12-05 00:00:00

然而,如果使用其他库进行解析,则存在很高的解析失败或无法解析任何有效时间文本的可能性。

支持其他代码语言

待办...

测试

我们已手动收集和组织来自全球五大洲的各个国家或地区的时间文本。结合人工构造的示例,我们获得了70多个变化的时间样本。这些样本涵盖了不同的时区、语言和不同时代的时间表达。以下仅提供了一些示例供参考。

"令和3年12月7日" - Epoch expression in Japan
"26 ก.ค. 2566 08:00 น." - Epoch expression in Thai
"2013-05-06T11:30:22+02:00" - Time zone expression based on UTC time offset
"September 17, 2012 at 10:09am PST" - Clear time zone expression
"29/10/2020 10h38 Pm" - Hour abbreviation
" 4 Αυγούστου 2023, 00:01 " - Different languages
"H_502_5@2010oct03 @H_502_5@2012/07/26.doc" - Long Text and Noise
"发布于 - /n6小時前," - Short Text and Noise
... ...

完整的测试案例仅对开发团队开放。如果您考虑将桂花应用于您的项目,但对其解析能力因缺乏提供的测试样本而感到担忧,您可以选择自行准备足够的样本进行测试,或者联系创建者以获得测试方面的帮助。

创建故事

桂花是从我们团队的商业项目中提取出来的。

我们团队的主要业务是全球新闻数据收集。一方面,我们为情感分析或数据技术公司提供实时数据进行分析。另一方面,我们为自然语言处理领域的AI公司提供长文本训练语料库。

新闻分析中的一个关键元素是发布时间。传统的正则表达式和一些第三方库难以满足全球解析需求。

灵感

在Python中,dateparser可能是最广泛使用的日期文本格式化库。我们也使用过它一段时间,但发现其兼容性不强,在存在某些噪声干扰的情况下,其解析能力显著下降。

后来,我们考虑利用深度学习进行分类和计算。虽然分类能力接近我们的要求,但格式化和兼容性完全不可控,这相当荒谬。

在这种情况下和环境下,我决定设计和开发一个新的时间文本解析程序,这就是桂花的由来。

设计和参考

桂花在设计过程中参考了dateparser的一些文本处理方法和思路,包括预设的正则表达式、文本去噪、时区提取和分类处理。

请不要认为这只是简单的复制。实际上,dateparser的处理方法并不足以满足需求。否则,桂花可能只是dateparser的Rust版本,而没有任何解析能力的提升。

我们设计了几个新的处理逻辑,在保持解析能力的同时,显著提高了兼容性和准确性。自动时区计算、滑动窗口、自动时间顺序切换和严格模式等特性的加入,确保了桂花的解析能力远超其他。

为什么开源

  • 目前,市场上没有可以与其能力和兼容性相匹配的开源工具。创建它是为了解决我们在商业数据中的技术缺陷,开源它是为了避免其他人面临我们当时所面临的相同挑战。
  • 开源可以让桂花树通过收集更多样本、接收反馈和不断优化,以实现更高的性能和准确性。
  • 开源是一种向外界展示我们技术能力的方式

工作愉快...

依赖项

~12-23MB
~298K SLoC