如何在Python(或其他语言)中从一段文本中解析多个日期

19 投票
5 回答
7020 浏览
提问于 2025-04-16 23:24

我有一个字符串,其中包含几个日期值,我想把它们全部提取出来。这个字符串是自然语言写的,所以我找到的最好的工具是 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 - 我想我可以试着把输入文件从中间分开,然后不断尝试,直到成功,但这实在是个很糟糕的解决办法。

5 个回答

0

为什么不写一个正则表达式,涵盖日期可能出现的所有形式,然后用这个正则去检查文本呢?我想应该没有很多种方式可以在字符串中表示一个日期。

唯一的问题就是要尽量收集所有日期的表达方式。

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 的更新回答,做的事情和这个很相似。

19

看起来,最简单的方法就是修改 dateutil 的 解析器,让它有一个模糊匹配的选项。

parser._parse 会接收你的字符串,然后用 _timelex 把它分解成小块,再把这些小块和 parserinfo 里定义的数据进行比较。

在这里,如果某个小块在 parserinfo 中找不到匹配项,解析就会失败,除非 fuzzy 这个选项是开启的。

我建议你在没有处理过的时间小块时,允许不匹配的情况,然后当遇到不匹配时,处理当前解析的数据,并重新开始寻找时间小块。

这样做应该不会太费劲。


更新

在你等着你的补丁被合并的时候……

这个方法有点小技巧,使用了库中的非公开函数,但不需要修改库,也不是试错法。如果有一些独立的小块可以被转换成浮点数,你可能会得到错误的结果。你可能需要进一步过滤一下结果。

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)

结果是:

Found: 2011 04 23
Parsed: 2011-04-23 00:00:00
Found: 29 July 1928
Parsed: 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))

如果你想在 parserinfo 上设置选项,可以实例化一个 parserinfo 并把它传递给解析器对象。例如:

from dateutil.parser import _timelex, parser, parserinfo
info = parserinfo(dayfirst=True)
p = parser(info)

撰写回答