在Python中遍历一系列日期


367

我有以下代码可以做到这一点,但是我该如何做得更好呢?现在,我认为它比嵌套循环更好,但是当列表理解器中包含生成器时,它开始变得Perl-linerish。

day_count = (end_date - start_date).days + 1
for single_date in [d for d in (start_date + timedelta(n) for n in range(day_count)) if d <= end_date]:
    print strftime("%Y-%m-%d", single_date.timetuple())

笔记

  • 我实际上并没有用它来打印。这只是出于演示目的。
  • start_dateend_date变量是datetime.date因为我不需要时间戳对象。(它们将用于生成报告)。

样本输出

开始日期为2009-05-30,结束日期为2009-06-09

2009-05-30
2009-05-31
2009-06-01
2009-06-02
2009-06-03
2009-06-04
2009-06-05
2009-06-06
2009-06-07
2009-06-08
2009-06-09

3
只是指出:我认为'time.strftime(“%Y-%m-%d”,single_date.timetuple())和较短的'single_date.strftime(“%Y-%)之间没有任何区别m-%d“)'。大多数答案似乎是在复制更长的样式。
Mu Mind 2010年

8
哇,这些答案太复杂了。:试试这个stackoverflow.com/questions/7274267/...
外国佬倜傥

@GringoSuave:肖恩·卡瓦纳的答案有何复杂之处
jfs 2015年


1
是否重复,您将在另一页上得到一个简单的答案。
Gringo Suave

Answers:


552

为什么会有两个嵌套的迭代?对我来说,它仅需一次迭代即可生成相同的数据列表:

for single_date in (start_date + timedelta(n) for n in range(day_count)):
    print ...

而且没有存储任何列表,仅迭代了一个生成器。同样,生成器中的“ if”似乎不必要。

毕竟,线性序列只需要一个迭代器,而不是两个。

与John Machin讨论后更新:

也许最优雅的解决方案是使用生成器函数完全隐藏/抽象日期范围内的迭代:

from datetime import timedelta, date

def daterange(start_date, end_date):
    for n in range(int ((end_date - start_date).days)):
        yield start_date + timedelta(n)

start_date = date(2013, 1, 1)
end_date = date(2015, 6, 2)
for single_date in daterange(start_date, end_date):
    print(single_date.strftime("%Y-%m-%d"))

注意:为了与内置range()函数保持一致,此迭代到达之前停止end_date。因此,对于包容迭代,请使用第二天,就像使用一样range()


4
-1 ...如果一个简单的while循环就足够了,那么对day_count的初步计算和使用范围并不理想。
约翰·马钦

7
@John Machin:好的。但是,我确实会在while循环中使用某个计数器或值的显式增量进行迭代。交互模式更Python化(至少在我个人看来),并且也更通用,因为它允许表达迭代,同时隐藏如何进行迭代的细节。
Ber

10
@Ber:我一点都不喜欢。太糟糕了。您已经有一个迭代!通过将抱怨的构造包装在生成器中,您增加了更多的执行开销,并将用户的注意力转移到其他地方来阅读您的3层代码和/或文档。-2
约翰·马钦

8
@约翰·马钦:我不同意。关键不是要把行数减少到绝对最小值。毕竟,我们不是在这里谈论Perl。另外,我的代码仅执行一次迭代(这就是生成器的工作方式,但我想您知道这一点)。***我的观点是关于抽象概念以供重用和自我解释代码。我坚持认为,这比拥有最短的代码更有价值。
小檗碱

9
如果您想简洁,可以使用生成器表达式:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Mark Ransom

219

这可能更清楚:

from datetime import date, timedelta

start_date = date(2019, 1, 1)
end_date = date(2020, 1, 1)
delta = timedelta(days=1)
while start_date <= end_date:
    print (start_date.strftime("%Y-%m-%d"))
    start_date += delta

3
非常清晰且简短,但是如果您要使用continue
不佳

对于我的使用情况,它很可爱
doomdaam

169

使用dateutil库:

from datetime import date
from dateutil.rrule import rrule, DAILY

a = date(2009, 5, 30)
b = date(2009, 6, 9)

for dt in rrule(DAILY, dtstart=a, until=b):
    print dt.strftime("%Y-%m-%d")

该python库具有许多更高级的功能,例如relative deltas 等非常有用的功能,并以单个文件(模块)的形式实现,很容易包含在项目中。


3
请注意,在这里for循环的最后日期是包容性until,而最后日期daterange的方法在误码率的回答独家end_date
Ninjakannon 2015年


77

总体而言,Pandas非常适合用于时间序列,并且直接支持日期范围。

import pandas as pd
daterange = pd.date_range(start_date, end_date)

然后,您可以遍历日期范围以打印日期:

for single_date in daterange:
    print (single_date.strftime("%Y-%m-%d"))

它还有很多选择可以使生活更轻松。例如,如果您只想要工作日,则只需交换bdate_range。请参阅http://pandas.pydata.org/pandas-docs/stable/timeseries.html#generating-ranges-of-timestamps

Pandas的强大功能实际上就是其数据帧,它支持矢量化操作(非常类似于numpy),使跨大量数据的操作变得非常快速和容易。

编辑:您也可以完全跳过for循环,而直接直接打印它,这更容易,更有效:

print(daterange)

“非常像numpy”-熊猫建立在numpy之上:P
Zach Saucier

15
import datetime

def daterange(start, stop, step=datetime.timedelta(days=1), inclusive=False):
  # inclusive=False to behave like range by default
  if step.days > 0:
    while start < stop:
      yield start
      start = start + step
      # not +=! don't modify object passed in if it's mutable
      # since this function is not restricted to
      # only types from datetime module
  elif step.days < 0:
    while start > stop:
      yield start
      start = start + step
  if inclusive and start == stop:
    yield start

# ...

for date in daterange(start_date, end_date, inclusive=True):
  print strftime("%Y-%m-%d", date.timetuple())

通过支持负步等,此函数可以完成超出您严格要求的范围。只要您考虑范围逻辑,就不需要单独使用day_count,最重要的是,当您从多个函数中调用函数时,代码变得更易于阅读的地方。


谢谢,重命名为更紧密匹配范围的参数,忘记了在体内进行更改。

+1 ...但由于您允许该步骤成为timedelta,因此您应该(a)将其命名为dateTIMErange(),并使timedelta(hours = 12)和timedelta(hours = 36)之类的步骤正常工作,或( b)捕获不是整数天的步骤,或者(c)为调用者省去麻烦,并将该步骤表示为天数而不是时间增量。
约翰·马钦

任何timedelta都应该已经可以工作,但是由于(a),我在写完此代码后确实将datetime_range和date_range添加到了我的个人剪贴簿中。不确定(c)是否值得使用另一个函数,最常见的情况是days = 1,并且必须经过明确的时间增量以避免混淆。也许最好将其上传到某处:bitbucket.org/kniht/scraps/src/tip/python/gen_range.py

为了使这项工作可以按天而不是增量进行,您应该对照step.total_seconds()而不是step.days进行检查
amohr '16

12

这是我能想到的最易读的解决方案。

import datetime

def daterange(start, end, step=datetime.timedelta(1)):
    curr = start
    while curr < end:
        yield curr
        curr += step

11

为什么不尝试:

import datetime as dt

start_date = dt.datetime(2012, 12,1)
end_date = dt.datetime(2012, 12,5)

total_days = (end_date - start_date).days + 1 #inclusive 5 days

for day_number in range(total_days):
    current_date = (start_date + dt.timedelta(days = day_number)).date()
    print current_date

7

Numpy arange函数可以应用于日期:

import numpy as np
from datetime import datetime, timedelta
d0 = datetime(2009, 1,1)
d1 = datetime(2010, 1,1)
dt = timedelta(days = 1)
dates = np.arange(d0, d1, dt).astype(datetime)

的用途astype是将转换numpy.datetime64datetime.datetime对象数组。


超级精益施工!最后一行对我dates = np.arange(d0, d1, dt).astype(datetime.datetime)
有用-pyano

+1用于发布允许任何时间变化的通用单线解决方案,而不是固定的四舍五入步骤,例如小时/分钟/...。
F.Raab

7

显示从今天开始的最近n天:

import datetime
for i in range(0, 100):
    print((datetime.date.today() + datetime.timedelta(i)).isoformat())

输出:

2016-06-29
2016-06-30
2016-07-01
2016-07-02
2016-07-03
2016-07-04

请添加圆括号,例如print((datetime.date.today() + datetime.timedelta(i)).isoformat())
TitanFighter

@TitanFighter,请随时进行编辑,我会接受的。
user1767754

2
我试过了。编辑至少需要6个字符,但在这种情况下,仅需添加2个字符“(”和“)”
TitanFighter

print((datetime.date.today() + datetime.timedelta(i)))不带.isoformat()的输出完全相同。我需要我的脚本来打印YYMMDD。有人知道该怎么做吗?
zog先生,

只需在for循环中执行此操作,而不要在print语句中进行操作d = datetime.date.today() + datetime.timedelta(i); d.strftime("%Y%m%d")
user1767754

5
import datetime

def daterange(start, stop, step_days=1):
    current = start
    step = datetime.timedelta(step_days)
    if step_days > 0:
        while current < stop:
            yield current
            current += step
    elif step_days < 0:
        while current > stop:
            yield current
            current += step
    else:
        raise ValueError("daterange() step_days argument must not be zero")

if __name__ == "__main__":
    from pprint import pprint as pp
    lo = datetime.date(2008, 12, 27)
    hi = datetime.date(2009, 1, 5)
    pp(list(daterange(lo, hi)))
    pp(list(daterange(hi, lo, -1)))
    pp(list(daterange(lo, hi, 7)))
    pp(list(daterange(hi, lo, -7))) 
    assert not list(daterange(lo, hi, -1))
    assert not list(daterange(hi, lo))
    assert not list(daterange(lo, hi, -7))
    assert not list(daterange(hi, lo, 7)) 

4
for i in range(16):
    print datetime.date.today() + datetime.timedelta(days=i)

4

为了完整起见,Pandas还提供了一个period_range超出范围的时间戳功能:

import pandas as pd

pd.period_range(start='1/1/1626', end='1/08/1627', freq='D')

3

我有一个类似的问题,但是我需要每月而不是每天进行迭代。

这是我的解决方案

import calendar
from datetime import datetime, timedelta

def days_in_month(dt):
    return calendar.monthrange(dt.year, dt.month)[1]

def monthly_range(dt_start, dt_end):
    forward = dt_end >= dt_start
    finish = False
    dt = dt_start

    while not finish:
        yield dt.date()
        if forward:
            days = days_in_month(dt)
            dt = dt + timedelta(days=days)            
            finish = dt > dt_end
        else:
            _tmp_dt = dt.replace(day=1) - timedelta(days=1)
            dt = (_tmp_dt.replace(day=dt.day))
            finish = dt < dt_end

例子1

date_start = datetime(2016, 6, 1)
date_end = datetime(2017, 1, 1)

for p in monthly_range(date_start, date_end):
    print(p)

输出量

2016-06-01
2016-07-01
2016-08-01
2016-09-01
2016-10-01
2016-11-01
2016-12-01
2017-01-01

范例#2

date_start = datetime(2017, 1, 1)
date_end = datetime(2016, 6, 1)

for p in monthly_range(date_start, date_end):
    print(p)

输出量

2017-01-01
2016-12-01
2016-11-01
2016-10-01
2016-09-01
2016-08-01
2016-07-01
2016-06-01

3

可以“T *相信这个问题已经存在了9年,没有任何人暗示一个简单的递归函数:

from datetime import datetime, timedelta

def walk_days(start_date, end_date):
    if start_date <= end_date:
        print(start_date.strftime("%Y-%m-%d"))
        next_date = start_date + timedelta(days=1)
        walk_days(next_date, end_date)

#demo
start_date = datetime(2009, 5, 30)
end_date   = datetime(2009, 6, 9)

walk_days(start_date, end_date)

输出:

2009-05-30
2009-05-31
2009-06-01
2009-06-02
2009-06-03
2009-06-04
2009-06-05
2009-06-06
2009-06-07
2009-06-08
2009-06-09

编辑: *现在我可以相信-请参阅Python是否优化了尾递归?。谢谢你


3
为什么用递归替换一个简单的循环?超过大约两年半的时间范围会中断。
蒂姆·埃文

@ Tim-Erwin老实说,我不知道CPython不能优化尾递归,所以您的评论很有价值。
Pocketsand,

2

您可以使用pandas库简单可靠地生成两个日期之间的一系列日期

import pandas as pd

print pd.date_range(start='1/1/2010', end='1/08/2018', freq='M')

您可以通过将频率设置为D,M,Q,Y(每天,每月,每季度,每年)来更改生成日期的频率。



2
> pip install DateTimeRange

from datetimerange import DateTimeRange

def dateRange(start, end, step):
        rangeList = []
        time_range = DateTimeRange(start, end)
        for value in time_range.range(datetime.timedelta(days=step)):
            rangeList.append(value.strftime('%m/%d/%Y'))
        return rangeList

    dateRange("2018-09-07", "2018-12-25", 7)  

    Out[92]: 
    ['09/07/2018',
     '09/14/2018',
     '09/21/2018',
     '09/28/2018',
     '10/05/2018',
     '10/12/2018',
     '10/19/2018',
     '10/26/2018',
     '11/02/2018',
     '11/09/2018',
     '11/16/2018',
     '11/23/2018',
     '11/30/2018',
     '12/07/2018',
     '12/14/2018',
     '12/21/2018']

1

此功能有一些额外的功能:

  • 可以传递与DATE_FORMAT匹配的字符串作为开始或结束,并将其转换为日期对象
  • 可以传递日期对象作为开始或结束
  • 如果结束比开始更早,则进行错误检查

    import datetime
    from datetime import timedelta
    
    
    DATE_FORMAT = '%Y/%m/%d'
    
    def daterange(start, end):
          def convert(date):
                try:
                      date = datetime.datetime.strptime(date, DATE_FORMAT)
                      return date.date()
                except TypeError:
                      return date
    
          def get_date(n):
                return datetime.datetime.strftime(convert(start) + timedelta(days=n), DATE_FORMAT)
    
          days = (convert(end) - convert(start)).days
          if days <= 0:
                raise ValueError('The start date must be before the end date.')
          for n in range(0, days):
                yield get_date(n)
    
    
    start = '2014/12/1'
    end = '2014/12/31'
    print list(daterange(start, end))
    
    start_ = datetime.date.today()
    end = '2015/12/1'
    print list(daterange(start, end))

1

这是通用日期范围函数的代码,类似于Ber的答案,但更灵活:

def count_timedelta(delta, step, seconds_in_interval):
    """Helper function for iterate.  Finds the number of intervals in the timedelta."""
    return int(delta.total_seconds() / (seconds_in_interval * step))


def range_dt(start, end, step=1, interval='day'):
    """Iterate over datetimes or dates, similar to builtin range."""
    intervals = functools.partial(count_timedelta, (end - start), step)

    if interval == 'week':
        for i in range(intervals(3600 * 24 * 7)):
            yield start + datetime.timedelta(weeks=i) * step

    elif interval == 'day':
        for i in range(intervals(3600 * 24)):
            yield start + datetime.timedelta(days=i) * step

    elif interval == 'hour':
        for i in range(intervals(3600)):
            yield start + datetime.timedelta(hours=i) * step

    elif interval == 'minute':
        for i in range(intervals(60)):
            yield start + datetime.timedelta(minutes=i) * step

    elif interval == 'second':
        for i in range(intervals(1)):
            yield start + datetime.timedelta(seconds=i) * step

    elif interval == 'millisecond':
        for i in range(intervals(1 / 1000)):
            yield start + datetime.timedelta(milliseconds=i) * step

    elif interval == 'microsecond':
        for i in range(intervals(1e-6)):
            yield start + datetime.timedelta(microseconds=i) * step

    else:
        raise AttributeError("Interval must be 'week', 'day', 'hour' 'second', \
            'microsecond' or 'millisecond'.")

0

对于以天为单位递增的范围,以下内容如何处理:

for d in map( lambda x: startDate+datetime.timedelta(days=x), xrange( (stopDate-startDate).days ) ):
  # Do stuff here
  • startDate和stopDate是datetime.date对象

对于通用版本:

for d in map( lambda x: startTime+x*stepTime, xrange( (stopTime-startTime).total_seconds() / stepTime.total_seconds() ) ):
  # Do stuff here
  • startTime和stopTime是datetime.date或datetime.datetime对象(两者应为同一类型)
  • stepTime是一个timedelta对象

请注意,仅在python 2.7之后才支持.total_seconds()。如果您使用的是早期版本,则可以编写自己的函数:

def total_seconds( td ):
  return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6

0

通过将rangeargs 存储在元组中,可逆步骤的方法略有不同。

def date_range(start, stop, step=1, inclusive=False):
    day_count = (stop - start).days
    if inclusive:
        day_count += 1

    if step > 0:
        range_args = (0, day_count, step)
    elif step < 0:
        range_args = (day_count - 1, -1, step)
    else:
        raise ValueError("date_range(): step arg must be non-zero")

    for i in range(*range_args):
        yield start + timedelta(days=i)

0
import datetime
from dateutil.rrule import DAILY,rrule

date=datetime.datetime(2019,1,10)

date1=datetime.datetime(2019,2,2)

for i in rrule(DAILY , dtstart=date,until=date1):
     print(i.strftime('%Y%b%d'),sep='\n')

输出:

2019Jan10
2019Jan11
2019Jan12
2019Jan13
2019Jan14
2019Jan15
2019Jan16
2019Jan17
2019Jan18
2019Jan19
2019Jan20
2019Jan21
2019Jan22
2019Jan23
2019Jan24
2019Jan25
2019Jan26
2019Jan27
2019Jan28
2019Jan29
2019Jan30
2019Jan31
2019Feb01
2019Feb02

欢迎使用Stack Overflow!尽管这段代码可以解决问题,包括解释如何以及为什么解决问题的方法,尤其是对于答案太多的问题,这确实有助于提高您的帖子质量,并可能导致更多的投票。请记住,您将来会为读者回答问题,而不仅仅是现在问的人。请编辑您的答案以添加说明,并指出适用的限制和假设。点评来源
Double-beep
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.