如何在Python(或另一种语言)中从文本块中解析多个日期

19

我有一个字符串中包含多个日期值,我想将它们全部解析出来。字符串是自然语言,到目前为止,我发现最好的方法是使用dateutil

不幸的是,如果一个字符串中有多个日期值,dateutil会抛出一个错误:

>>> s = "I like peas on 2011-04-23, and I also like them on easter and my birthday, the 29th of July, 1928"
>>> parse(s, fuzzy=True)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/pymodules/python2.7/dateutil/parser.py", line 697, in parse
    return DEFAULTPARSER.parse(timestr, **kwargs)
  File "/usr/lib/pymodules/python2.7/dateutil/parser.py", line 303, in parse
    raise ValueError, "unknown string format"
ValueError: unknown string format

有没有想法如何从一个长字符串中解析出所有的日期?理想情况下,可以创建一个列表,但如果需要的话,我自己也可以处理。

我正在使用Python,但是其他语言在此时也可能可行,只要能完成任务。

PS-我猜我可以递归地将输入文件一分为二并不断尝试,直到成功为止,但这是一种可怕的hack方法。


你在样本字符串中是否将“on easter”视为你想要解析的日期? - MattH
不,我只是在测试它是否有效,但无论如何我都不太在意。 - mlissner
使用DateUtil 1.5版本,当然可以正常工作,我的错。但我仍然希望奖励那些比MattH和Shawn Chin更清晰/更快的方法。 - Dieter
5个回答

19

看起来,最不破坏的方法是修改dateutil parser,增加模糊多选项。

parser._parse接受您的字符串,使用_timelex对其进行标记化,然后将标记与parserinfo中定义的数据进行比较。

在这里, 如果一个标记与parserinfo中的任何内容都不匹配,则解析将失败,除非fuzzy为True。

我建议您在没有处理过的时间标记时允许非匹配,然后当您遇到非匹配时,在该点处理解析数据并重新开始查找时间标记。

不应该需要太多的努力。


更新

在等待您的补丁被合并时...

这是一个有点hacky的方法,使用了库中非公开的函数,但不需要修改库文件,也不是试错法。如果存在可以转换为浮点数的单独标记,则可能会导致误报。您可能需要进一步过滤结果。

from dateutil.parser import _timelex, parser

a = "I like peas on 2011-04-23, and I also like them on easter and my birthday, the 29th of July, 1928"

p = parser()
info = p.info

def timetoken(token):
  try:
    float(token)
    return True
  except ValueError:
    pass
  return any(f(token) for f in (info.jump,info.weekday,info.month,info.hms,info.ampm,info.pertain,info.utczone,info.tzoffset))

def timesplit(input_string):
  batch = []
  for token in _timelex(input_string):
    if timetoken(token):
      if info.jump(token):
        continue
      batch.append(token)
    else:
      if batch:
        yield " ".join(batch)
        batch = []
  if batch:
    yield " ".join(batch)

for item in timesplit(a):
  print "Found:", item
  print "Parsed:", p.parse(item)

产出:

发现:2011年04月23日
解析:2011-04-23 00:00:00
发现:1928年7月29日
解析:1928-07-29 00:00:00

Dieter的更新

Dateutil 2.1似乎是为了与python3兼容而编写的,并使用名为six的"兼容性"库。它对str对象的处理不正确,不能将其视为文本。

如果您将字符串作为Unicode或类文件对象传递,则此解决方案可与dateutil 2.1一起使用:

from cStringIO import StringIO
for item in timesplit(StringIO(a)):
  print "Found:", item
  print "Parsed:", p.parse(StringIO(item))

如果您想在解析器信息中设置选项,请实例化一个解析器信息并将其传递给解析器对象。例如:
from dateutil.parser import _timelex, parser, parserinfo
info = parserinfo(dayfirst=True)
p = parser(info)

可能是最有效的解决方案。+1。当然,修改库本身会使部署/维护变得更加困难,除非这些更改被吸收到官方源中。 - Shawn Chin
这是一个惊人的答案 - 迄今为止我在SO上得到的最好的答案。我会测试它并让您知道它的效果如何。谢谢! - mlissner
1
此页面的新闻下有一条评论,建议您在Python 2.x中使用DateUtil 1.x。 - MattH
这个输入“查找2013年8月2日和2014年8月5日的所有情况”的操作失败了。 - Praveen
1
@Praveen: 这是因为 and 是一个 info.jump 令牌,用于组合时间令牌,并不一定表示时间令牌序列已经结束。我建议你修改 timesplit 的行为,将 info.jump 视为中断。即,if timetoken(token) and not info.jump(token): batch.append(token) - MattH
显示剩余2条评论

6

在我离线的时候,我被昨天在这里发布的答案所困扰。是的,它完成了工作,但它不必要地复杂且极其低效。

这是一个简化版,应该能够更好地完成任务!

import itertools
from dateutil import parser

jumpwords = set(parser.parserinfo.JUMP)
keywords = set(kw.lower() for kw in itertools.chain(
    parser.parserinfo.UTCZONE,
    parser.parserinfo.PERTAIN,
    (x for s in parser.parserinfo.WEEKDAYS for x in s),
    (x for s in parser.parserinfo.MONTHS for x in s),
    (x for s in parser.parserinfo.HMS for x in s),
    (x for s in parser.parserinfo.AMPM for x in s),
))

def parse_multiple(s):
    def is_valid_kw(s):
        try:  # is it a number?
            float(s)
            return True
        except ValueError:
            return s.lower() in keywords

    def _split(s):
        kw_found = False
        tokens = parser._timelex.split(s)
        for i in xrange(len(tokens)):
            if tokens[i] in jumpwords:
                continue 
            if not kw_found and is_valid_kw(tokens[i]):
                kw_found = True
                start = i
            elif kw_found and not is_valid_kw(tokens[i]):
                kw_found = False
                yield "".join(tokens[start:i])
        # handle date at end of input str
        if kw_found:
            yield "".join(tokens[start:])

    return [parser.parse(x) for x in _split(s)]

使用示例:

>>> parse_multiple("I like peas on 2011-04-23, and I also like them on easter and my birthday, the 29th of July, 1928")
[datetime.datetime(2011, 4, 23, 0, 0), datetime.datetime(1928, 7, 29, 0, 0)]

值得注意的是,当处理空/未知字符串时,dateutil.parser.parse的行为略有偏差。Dateutil将返回当前日期,而parse_multiple会返回一个空列表,我认为这才是人们所期望的结果。

>>> from dateutil import parser
>>> parser.parse("")
datetime.datetime(2011, 8, 12, 0, 0)
>>> parse_multiple("")
[]

附注:刚刚发现MattH的更新答案,其做法非常相似。


这个初始看起来比MattH的建议更可靠,但在更大的测试中性能非常糟糕(并不令人惊讶)。还是感谢你的帮助! - mlissner
@mlissner 不用谢,解决这个问题很有趣。我昨晚思考了很长时间并想出了一种更好的解决方案。请查看更新后的答案。 - Shawn Chin

0

我看到已经有一些不错的答案了,但是我要加上这个答案,因为在我的使用情况下它表现得更好,而上面的答案则没有。

使用这个库:https://datefinder.readthedocs.io/en/latest/index.html#module-datefinder


import datefinder

def DatesToList(x):
    
    dates = datefinder.find_dates(x)
    
    lists = []
    
    for date in dates:
        
        lists.append(date)
        
    return (lists)


dates = DateToList(s)


输出:

[datetime.datetime(2011, 4, 23, 0, 0), datetime.datetime(1928, 7, 29, 0, 0)]


0

我认为如果你把这些“单词”放在一个数组中,它应该可以解决问题。这样你就可以验证它是否是一个日期,并将其放入一个变量中。

一旦你有了日期,你应该使用日期时间库库。


0
为什么不编写一个覆盖日期可能出现的所有形式的正则表达式模式,然后启动正则表达式来探索文本呢? 我认为字符串中表达日期的方式并不是十几种。唯一的问题就是收集最大数量的日期表达式。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接