获取 dateutil.parse 中的格式

Get the format in dateutil.parse

有没有办法在 dateutil 中解析日期后获取 "format"。例如:

>>> x = parse("2014-01-01 00:12:12")
datetime.datetime(2014, 1, 1, 0, 12, 12)

x.get_original_string_format()
YYYY-MM-DD HH:MM:SS # %Y-%m-%d %H:%M:%S

# Or, passing the date-string directly
get_original_string_format("2014-01-01 00:12:12")
YYYY-MM-DD HH:MM:SS # %Y-%m-%d %H:%M:%S

更新: 我想为这个问题增加悬赏,看看是否有人可以添加一个答案,该答案与获取常见的字符串格式相同日期字符串已通过。如果需要,它可以使用 dateutil,但不是必须的。希望我们能在这里得到一些有创意的解决方案。

Is there a way to get the "format" after parsing a date in dateutil?

dateutil 不可能。问题是 dateutil 在解析期间的任何时候都不会将格式作为中间结果 ,因为它会分别检测日期时间的单独组件 - 看看这不太容易阅读 source code.

我不知道有什么方法可以 return 从 dateutil(或我知道的任何其他 python 时间戳解析器)解析的格式。

使用 datetime.strptime() 实现您自己的 return 可能格式列表和相关日期时间对象的时间戳解析函数是相当简单的,但是针对广泛有用的可能时间戳格式列表有效地执行它却不是.

以下示例使用了 100 多种格式的列表。它甚至没有触及 dateutil 解析的各种格式的表面。它按顺序测试每种格式,直到用尽列表中的所有格式(可能比 dateutil 独立定位各种日期时间部分的方法效率低得多,如 @alecxe 的答案中所述)。

此外,我还提供了一些包含时区名称(而不是偏移量)的示例时间戳格式。如果您 运行 下面的示例函数针对那些特定的日期时间字符串,您可能会发现它没有 return 预期的匹配,即使我已经使用 %Z 指令包含了匹配格式。关于使用 %Z 处理时区名称的挑战的一些解释可以在 issue 22377 at bugs.python.org 中找到(只是为了强调实现您自己的日期时间解析函数的另一个重要方面)。

考虑到所有这些注意事项,如果您正在处理一组可管理的潜在格式,实施如下所示的简单操作可能会满足您的需求。

尝试将日期时间字符串与格式列表匹配的示例函数和 return 包含原始日期字符串和匹配列表的字典,每个字典都包含日期时间对象以及匹配的格式:

from datetime import datetime

def parse_timestamp(datestring, formats):
    results = {'datestring': datestring, 'matches': []}
    for f in formats:
        try:
            d = datetime.strptime(datestring, f)
        except:
            continue
        results['matches'].append({'datetime': d, 'format': f})
    return results

示例格式和日期时间字符串:

formats = ['%A, %B %d, %Y', '%A, %B %d, %Y %I:%M:%S %p %Z', '%A, %d %B %Y', '%B %d %Y', '%B %d, %Y', '%H:%M:%S', '%H:%M:%S,%f', '%H:%M:%S.%f', '%Y %b %d %H:%M:%S.%f', '%Y %b %d %H:%M:%S.%f %Z', '%Y %b %d %H:%M:%S.%f*%Z', '%Y%m%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S %z', '%Y-%m-%d %H:%M:%S%z', '%Y-%m-%d %H:%M:%S,%f', '%Y-%m-%d %H:%M:%S,%f%z', '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S.%f%z', '%Y-%m-%d %I:%M %p', '%Y-%m-%d %I:%M:%S %p', '%Y-%m-%d*%H:%M:%S', '%Y-%m-%d*%H:%M:%S:%f', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S%Z', '%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S*%f%z', '%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M:%S.%f%z', '%Y/%m/%d', '%Y/%m/%d*%H:%M:%S', '%a %b %d %H:%M:%S %Z %Y', '%a, %d %b %Y %H:%M:%S %z', '%b %d %H:%M:%S', '%b %d %H:%M:%S %Y', '%b %d %H:%M:%S %z', '%b %d %H:%M:%S %z %Y', '%b %d %Y', '%b %d %Y %H:%M:%S', '%b %d, %Y', '%b %d, %Y %I:%M:%S %p', '%b.%d.%Y', '%d %B %Y', '%d %B %Y %H:%M:%S %Z', '%d %b %Y %H:%M:%S', '%d %b %Y %H:%M:%S %z', '%d %b %Y %H:%M:%S*%f', '%d%m_%H:%M:%S', '%d%m_%H:%M:%S.%f', '%d-%b-%Y', '%d-%b-%Y %H:%M:%S', '%d-%b-%Y %H:%M:%S.%f', '%d-%b-%Y %I:%M:%S %p', '%d-%m-%Y', '%d-%m-%Y %I:%M %p', '%d-%m-%Y %I:%M:%S %p', '%d-%m-%y', '%d-%m-%y %I:%M %p', '%d-%m-%y %I:%M:%S %p', '%d/%b %H:%M:%S,%f', '%d/%b/%Y %H:%M:%S', '%d/%b/%Y %I:%M %p', '%d/%b/%Y:%H:%M:%S', '%d/%b/%Y:%H:%M:%S %z', '%d/%m/%Y', '%d/%m/%Y %H:%M:%S %z', '%d/%m/%Y %I:%M %p', '%d/%m/%Y %I:%M:%S %p', '%d/%m/%Y %I:%M:%S %p:%f', '%d/%m/%Y*%H:%M:%S', '%d/%m/%Y*%H:%M:%S*%f', '%d/%m/%y', '%d/%m/%y %H:%M:%S', '%d/%m/%y %H:%M:%S %z', '%d/%m/%y %I:%M %p', '%d/%m/%y %I:%M:%S %p', '%d/%m/%y*%H:%M:%S', '%m%d_%H:%M:%S', '%m%d_%H:%M:%S.%f', '%m-%d-%Y', '%m-%d-%Y %I:%M %p', '%m-%d-%Y %I:%M:%S %p', '%m-%d-%y', '%m-%d-%y %I:%M %p', '%m-%d-%y %I:%M:%S %p', '%m/%d/%Y', '%m/%d/%Y %H:%M:%S %z', '%m/%d/%Y %I:%M %p', '%m/%d/%Y %I:%M:%S %p', '%m/%d/%Y %I:%M:%S %p:%f', '%m/%d/%Y*%H:%M:%S', '%m/%d/%Y*%H:%M:%S*%f', '%m/%d/%y', '%m/%d/%y %H:%M:%S', '%m/%d/%y %H:%M:%S %z', '%m/%d/%y %I:%M %p', '%m/%d/%y %I:%M:%S %p', '%m/%d/%y*%H:%M:%S', '%y%m%d %H:%M:%S', '%y-%m-%d %H:%M:%S', '%y-%m-%d %H:%M:%S,%f', '%y-%m-%d %H:%M:%S,%f %z', '%y/%m/%d %H:%M:%S']

datestrings = ['03-11-1999', '03-12-1999 5:06 AM', '03-12-1999 5:06:07 AM', '03-12-99 5:06 AM', '03-12-99 5:06:07 AM', '03/12/1999', '03/12/1999 5:06 AM', '03/12/1999 5:06:07 AM', '03/12/99 5:06 AM', '03/12/99 5:06:07', '03/12/99 5:06:07 AM', '04/23/17 04:34:22 +0000', '0423_11:42:35', '0423_11:42:35.883', '05/09/2017*08:22:14*612', '06/01/22 04:11:05', '08/10/11*13:33:56', '10-04-19 12:00:17', '10-06-26 02:31:29,573', '10/03/2017 07:29:46 -0700', '11-02-11 16:47:35,985 +0000', '11/22/2017*05:13:11', '11:42:35', '11:42:35,173', '11:42:35.173', '12/03/1999', '12/03/1999 5:06 AM', '12/03/99 5:06 AM', '12/3/1999', '12/3/1999 5:06 AM', '12/3/1999 5:06:07 AM', '150423 11:42:35', '19/Apr/2017:06:36:15 -0700', '1999-03-12 05:06:07.0', '1999-03-12 5:06 AM', '1999-03-12 5:06:07 AM', '1999-03-12+01:00', '1999-3-12 5:06 AM', '1999-3-12 5:06:07 AM', '1999/3/12', '20150423 11:42:35.173', '2017 Mar 03 05:12:41.211 PDT', '2017 Mar 10 01:44:20.392', '2017-02-11T18:31:44', '2017-03-10 14:30:12,655+0000', '2017-03-12 13:11:34.222-0700', '2017-03-12T17:56:22-0700', '2017-06-26 02:31:29,573', '2017-07-01T14:59:55.711+0000', '2017-07-04*13:23:55', '2017-07-22T16:28:55.444', '2017-08-19 12:17:55 -0400', '2017-08-19 12:17:55-0400', '2017-09-08T03:13:10', '2017-10-14T22:11:20+0000', '2017-10-30*02:47:33:899', '2017-11-22T10:10:15.455', '2017/04/12*19:37:50', '2018 Apr 13 22:08:13.211*PDT', '2018-02-27 15:35:20.311', '2018-08-20T13:20:10*633+0000', '22 Mar 1999 05:06:07 +0100', '22 March 1999', '22 March 1999 05:06:07 CET', '22-Mar-1999', '22-Mar-1999 05:06:07', '22-Mar-1999 5:06:07 AM', '22/03/1999 5:06:07 AM', '22/Mar/1999 5:06:07 +0100', '22/Mar/99 5:06 AM', '23 Apr 2017 10:32:35*311', '23 Apr 2017 11:42:35', '23-Apr-2017 11:42:35', '23-Apr-2017 11:42:35.883', '23/Apr 11:42:35,173', '23/Apr/2017 11:42:35', '23/Apr/2017:11:42:35', '3-11-1999', '3-12-1999 5:06 AM', '3-12-99 5:06 AM', '3-12-99 5:06:07 AM', '3-22-1999 5:06:07 AM', '3/12/1999', '3/12/1999 5:06 AM', '3/12/1999 5:06:07 AM', '3/12/99 5:06 AM', '3/12/99 5:06:07', '8/5/2011 3:31:18 AM:234', '9/28/2011 2:23:15 PM', 'Apr 20 00:00:35 2010', 'Dec 2, 2017 2:39:58 AM', 'Jan 21 18:20:11 +0000 2017', 'Jun 09 2018 15:28:14', 'Mar 16 08:12:04', 'Mar 22 1999', 'Mar 22, 1999', 'Mar 22, 1999 5:06:07 AM', 'Mar.22.1999', 'March 22 1999', 'March 22, 1999', 'Mon Mar 22 05:06:07 CET 1999', 'Mon, 22 Mar 1999 05:06:07 +0100', 'Monday, 22 March 1999', 'Monday, March 22, 1999', 'Monday, March 22, 1999 5:06:07 AM CET', 'Sep 28 19:00:00 +0000']

用法示例:

print(parse_timestamp('2018-08-20T13:20:10*633+0000', formats))
# OUTPUT
# {'datestring': '2018-08-20T13:20:10*633+0000', 'matches': [{'datetime': datetime.datetime(2018, 8, 20, 13, 20, 10, 633000, tzinfo=datetime.timezone.utc), 'format': '%Y-%m-%dT%H:%M:%S*%f%z'}]}

我的想法是创建一个 class 像这样的东西,可能不准确

from datetime import datetime
import re
class DateTime(object):
    dateFormat = {"%d": "dd", "%Y": "YYYY", "%a": "Day", "%A": "DAY", "%w": "ww", "%b": "Mon", "%B": "MON", "%m": "mm",
                  "%H": "HH", "%I": "II", "%p": "pp", "%M": "MM", "%S": "SS"}  # wil contain all format equivalent

    def __init__(self, date_str, format):
        self.dateobj = datetime.strptime(date_str, format)
        self.format = format

    def parse_format(self):
        output=None
        reg = re.compile("%[A-Z a-z]")
        fmts = None
        if self.format is not None:
            fmts = re.findall(reg, self.format)
        if fmts is not None:
            output = self.format
            for f in fmts:
                output = output.replace(f, DateTime.dateFormat[f])
        return output


nDate = DateTime("12 January, 2018", "%d %B, %Y")
print(nDate.parse_format())

我的想法是:

  1. 创建一个对象,其中包含您认为 可能 在日期模式中的候选说明符列表(添加的越多,从另一端出来的可能性就越大)
  2. 解析日期字符串
  3. 根据您提供的日期和候选列表,为字符串中的每个元素创建一个可能的说明符列表。
  4. 将它们重新组合以生成 'possibles'.
  5. 的列表

如果你只有一个候选人,你可以很确定这是正确的格式。但是你通常会得到很多可能性(尤其是日期、月份、分钟和小时都在 0-10 范围内)。

示例class:

import re
from itertools import product
from dateutil.parser import parse
from collections import defaultdict, Counter

COMMON_SPECIFIERS = [
    '%a', '%A', '%d', '%b', '%B', '%m',
    '%Y', '%H', '%p', '%M', '%S', '%Z',
]


class FormatFinder:
    def __init__(self,
                 valid_specifiers=COMMON_SPECIFIERS,
                 date_element=r'([\w]+)',
                 delimiter_element=r'([\W]+)',
                 ignore_case=False):
        self.specifiers = valid_specifiers
        joined = (r'' + date_element + r"|" + delimiter_element)
        self.pattern = re.compile(joined)
        self.ignore_case = ignore_case

    def find_candidate_patterns(self, date_string):
        date = parse(date_string)
        tokens = self.pattern.findall(date_string)

        candidate_specifiers = defaultdict(list)

        for specifier in self.specifiers:
            token = date.strftime(specifier)
            candidate_specifiers[token].append(specifier)
            if self.ignore_case:
                candidate_specifiers[token.
                                     upper()] = candidate_specifiers[token]
                candidate_specifiers[token.
                                     lower()] = candidate_specifiers[token]

        options_for_each_element = []
        for (token, delimiter) in tokens:
            if token:
                if token not in candidate_specifiers:
                    options_for_each_element.append(
                        [token])  # just use this verbatim?
                else:
                    options_for_each_element.append(
                        candidate_specifiers[token])
            else:
                options_for_each_element.append([delimiter])

        for parts in product(*options_for_each_element):
            counts = Counter(parts)
            max_count = max(counts[specifier] for specifier in self.specifiers)
            if max_count > 1:
                # this is a candidate with the same item used more than once
                continue
            yield "".join(parts)

以及一些示例测试:

def test_it_returns_value_from_question_1():
    s = "2014-01-01 00:12:12"
    candidates = FormatFinder().find_candidate_patterns(s)
    sut = FormatFinder()
    candidates = sut.find_candidate_patterns(s)
    assert "%Y-%m-%d %H:%M:%S" in candidates


def test_it_returns_value_from_question_2():
    s = 'Jan. 04, 2017'
    sut = FormatFinder()
    candidates = sut.find_candidate_patterns(s)
    candidates = list(candidates)
    assert "%b. %d, %Y" in candidates
    assert len(candidates) == 1


def test_it_can_ignore_case():
    # NB: apparently the 'AM/PM' is meant to be capitalised in my locale! 
    # News to me!
    s = "JANUARY 12, 2018 02:12 am"
    sut = FormatFinder(ignore_case=True)
    candidates = sut.find_candidate_patterns(s)
    assert "%B %d, %Y %H:%M %p" in candidates


def test_it_returns_parts_that_have_no_date_component_verbatim():
    # In this string, the 'at' is considered as a 'date' element, 
    # but there is no specifier that produces a candidate for it
    s = "January 12, 2018 at 02:12 AM"
    sut = FormatFinder()
    candidates = sut.find_candidate_patterns(s)
    assert "%B %d, %Y at %H:%M %p" in candidates

为了更清楚一点,这里有一些在 iPython shell 中使用此代码的示例:

In [2]: ff = FormatFinder()

In [3]: list(ff.find_candidate_patterns("2014-01-01 00:12:12"))
Out[3]:
['%Y-%d-%m %H:%M:%S',
 '%Y-%d-%m %H:%S:%M',
 '%Y-%m-%d %H:%M:%S',
 '%Y-%m-%d %H:%S:%M']

In [4]: list(ff.find_candidate_patterns("Jan. 04, 2017"))
Out[4]: ['%b. %d, %Y']

In [5]: list(ff.find_candidate_patterns("January 12, 2018 at 02:12 AM"))
Out[5]: ['%B %d, %Y at %H:%M %p', '%B %M, %Y at %H:%d %p']

In [6]: ff_without_case = FormatFinder(ignore_case=True)

In [7]: list(ff_without_case.find_candidate_patterns("JANUARY 12, 2018 02:12 am"))
Out[7]: ['%B %d, %Y %H:%M %p', '%B %M, %Y %H:%d %p']

想法:

  1. 检查用户输入的日期字符串,并构建可能的日期格式集
  2. 遍历格式集,使用datetime.strptime解析日期字符串和可能的日期格式。
  3. 使用 datetime.strftime 格式化步骤 2 中的日期,如果结果等于原始日期字符串,则此格式是可能的日期格式。

算法实现

from datetime import datetime
import itertools
import re

FORMAT_CODES = (
    r'%a', r'%A', r'%w', r'%d', r'%b', r'%B', r'%m', r'%y', r'%Y',
    r'%H', r'%I', r'%p', r'%M', r'%S', r'%f', r'%z', r'%Z', r'%j',
    r'%U', r'%W',
)

TWO_LETTERS_FORMATS = (
    r'%p',
)

THREE_LETTERS_FORMATS = (
    r'%a', r'%b'
)

LONG_LETTERS_FORMATS = (
    r'%A', r'%B', r'%z', r'%Z',
)

SINGLE_DIGITS_FORMATS = (
    r'w',
)

TWO_DIGITS_FORMATS = (
    r'%d', r'%m', r'%y', r'%H', r'%I', r'%M', r'%S', r'%U', r'%W',
)

THREE_DIGITS_FORMATS = (
    r'%j',
)

FOUR_DIGITS_FORMATS = (
    r'%Y',
)

LONG_DIGITS_FORMATS = (
    r'%f',
)

# Non format code symbols
SYMBOLS = (
    '-',
    ':',
    '+',
    'Z',
    ',',
    ' ',
)


if __name__ == '__main__':
    date_str = input('Please input a date: ')

    # Split with non format code symbols
    pattern = r'[^{}]+'.format(''.join(SYMBOLS))
    components = re.findall(pattern, date_str)

    # Create a format placeholder, eg. '{}-{}-{} {}:{}:{}+{}'
    placeholder = re.sub(pattern, '{}', date_str)

    formats = []
    for comp in components:
        if re.match(r'^\d{1}$', comp):
            formats.append(SINGLE_DIGITS_FORMATS)
        elif re.match(r'^\d{2}$', comp):
            formats.append(TWO_DIGITS_FORMATS)
        elif re.match(r'^\d{3}$', comp):
            formats.append(THREE_DIGITS_FORMATS)
        elif re.match(r'^\d{4}$', comp):
            formats.append(FOUR_DIGITS_FORMATS)
        elif re.match(r'^\d{5,}$', comp):
            formats.append(LONG_DIGITS_FORMATS)
        elif re.match(r'^[a-zA-Z]{2}$', comp):
            formats.append(TWO_LETTERS_FORMATS)
        elif re.match(r'^[a-zA-Z]{3}$', comp):
            formats.append(THREE_LETTERS_FORMATS)
        elif re.match(r'^[a-zA-Z]{4,}$', comp):
            formats.append(LONG_LETTERS_FORMATS)
        else:
            formats.append(FORMAT_CODES)

    # Create a possible format set
    possible_set = itertools.product(*formats)

    found = 0
    for possible_format in possible_set:
        # Create a format with possible format combination
        dt_format = placeholder.format(*possible_format)
        try:
            dt = datetime.strptime(date_str, dt_format)
            # Use the format to parse the date, and format the 
            # date back to string and compare with the origin one
            if dt.strftime(dt_format) == date_str:
                print('Possible result: {}'.format(dt_format))
                found += 1
        except Exception:
            continue

    if found == 0:
        print('No pattern found')

用法:

$ python3 reverse.py
Please input a date: 2018-12-31 10:26 PM
Possible result: %Y-%d-%M %I:%S %p
Possible result: %Y-%d-%S %I:%M %p
Possible result: %Y-%m-%d %I:%M %p
Possible result: %Y-%m-%d %I:%S %p
Possible result: %Y-%m-%M %I:%d %p
Possible result: %Y-%m-%M %I:%S %p
Possible result: %Y-%m-%S %I:%d %p
Possible result: %Y-%m-%S %I:%M %p
Possible result: %Y-%H-%d %m:%M %p
Possible result: %Y-%H-%d %m:%S %p
Possible result: %Y-%H-%d %M:%S %p
Possible result: %Y-%H-%d %S:%M %p
Possible result: %Y-%H-%M %d:%S %p
Possible result: %Y-%H-%M %m:%d %p
Possible result: %Y-%H-%M %m:%S %p
Possible result: %Y-%H-%M %S:%d %p
Possible result: %Y-%H-%S %d:%M %p
Possible result: %Y-%H-%S %m:%d %p
Possible result: %Y-%H-%S %m:%M %p
Possible result: %Y-%H-%S %M:%d %p
Possible result: %Y-%I-%d %m:%M %p
Possible result: %Y-%I-%d %m:%S %p
Possible result: %Y-%I-%d %M:%S %p
Possible result: %Y-%I-%d %S:%M %p
Possible result: %Y-%I-%M %d:%S %p
Possible result: %Y-%I-%M %m:%d %p
Possible result: %Y-%I-%M %m:%S %p
Possible result: %Y-%I-%M %S:%d %p
Possible result: %Y-%I-%S %d:%M %p
Possible result: %Y-%I-%S %m:%d %p
Possible result: %Y-%I-%S %m:%M %p
Possible result: %Y-%I-%S %M:%d %p
Possible result: %Y-%M-%d %I:%S %p
Possible result: %Y-%M-%S %I:%d %p
Possible result: %Y-%S-%d %I:%M %p
Possible result: %Y-%S-%M %I:%d %p

您可以在调用包装版本时随时包装函数以存储参数和结果:

from dateutil.parser import parse
from functools import wraps

def parse_wrapper(function):
    @wraps(function)
    def wrapper(*args):
        return {'datetime': function(*args), 'args': args}
    return wrapper

wrapped_parse = parse_wrapper(parse)
x = wrapped_parse("2014-01-01 00:12:12")
# {'datetime': datetime.datetime(2014, 1, 1, 0, 12, 12),
#  'args': ('2014-01-01 00:12:12',)}