Перебор ряда дат в Python


369

У меня есть следующий код, чтобы сделать это, но как я могу сделать это лучше? Прямо сейчас я думаю, что это лучше, чем вложенные циклы, но он начинает получать Perl-one-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_dateИ end_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 "). Большинство ответов, кажется, копируют более длинный стиль.
Му разум

8
Вау, эти ответы слишком сложны. Попробуйте это: stackoverflow.com/questions/7274267/…
Gringo Suave

@GringoSuave: что сложного в ответе Шона Кавана ?
JFS

Применение: обмануть полоски GitHub: stackoverflow.com/questions/20099235/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

1
Дублировать или нет, вы получите более простой ответ на другой странице.
Gringo Suave

Ответы:


554

Почему есть две вложенные итерации? Для меня это производит тот же список данных только с одной итерацией:

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

И никакой список не сохраняется, только один генератор повторяется. Также «если» в генераторе кажется ненужным.

В конце концов, линейная последовательность должна требовать только одного итератора, а не двух.

Обновление после обсуждения с Джоном Мачином:

Возможно, наиболее элегантное решение - использовать функцию генератора, чтобы полностью скрыть / абстрагировать итерацию в диапазоне дат:

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 ... с предварительным вычислением day_count и использованием диапазона не здорово, когда достаточно простого цикла while.
Джон Мачин

7
@ Джон Мачин: Хорошо. Я, однако, предпочитаю итерацию цикла while с явным приращением некоторого счетчика или значения. Шаблон взаимодействия является более питоническим (по крайней мере, на мой взгляд), а также более общим, поскольку он позволяет выразить итерацию, скрывая детали того, как эта итерация выполняется.
Бер

10
@Ber: мне это совсем не нравится; это вдвойне плохо. У вас уже была итерация! Обернув конструкции с жалобами в генераторе, вы добавили еще больше накладных расходов на выполнение и отвлекли внимание пользователя на что-то другое, чтобы прочитать код и / или документы вашего 3-строчного. -2
Джон Мачин

8
@ Джон Мачин: Я не согласен. Дело не в том, чтобы сократить количество линий до абсолютного минимума. В конце концов, мы не говорим здесь о Perl. Кроме того, мой код выполняет только одну итерацию (так работает генератор, но я думаю, вы это знаете). *** Моя точка зрения касается абстрагирования понятий для повторного использования и самоочевидного кода. Я утверждаю, что это гораздо полезнее, чем иметь самый короткий код.
Бер

9
Если вы стремитесь к краткости, вы можете использовать выражение генератора:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Марк Рэнсом

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
rslite

прекрасно работает для моего случая использования
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
Обратите внимание , что окончательная дата в течение цикла здесь является включена в untilто время как окончательная дата daterangeметоды в ответе Бера является эксклюзивной из end_date.
Ниндзяканнон,

более современные документы dateutil.readthedocs.io/en/stable/rrule.html
qwr

77

Панды отлично подходят для временных рядов в целом и имеют прямую поддержку диапазонов дат.

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
Зак 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) работать должным образом или ( б) перехватить шаги, которые не являются целым числом дней, или (в) избавить вызывающего абонента от хлопот и выразить шаг в виде количества дней вместо временной задержки.
Джон Мачин

Любая timedelta уже должна работать, но я добавил datetime_range и date_range в мою личную коллекцию записок после написания этого из-за (a). Не уверен, что другая функция имеет смысл для (c), наиболее распространенный случай дней = 1 уже решен, и необходимость передавать явную временную дельту позволяет избежать путаницы. Может быть, лучше загрузить его куда-нибудь: bitbucket.org/kniht/scraps/src/tip/python/gen_range.py

чтобы это работало с приращениями, отличными от дней, вы должны проверить по step.total_seconds (), а не по step.days
amohr

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.datetime64в массив datetime.datetimeобъектов.


Супер стройная конструкция! Последняя строка работает для меняdates = np.arange(d0, d1, dt).astype(datetime.datetime)
Пьяно

+1 за публикацию универсального однострочного решения, которое позволяет любую временную разницу вместо фиксированного округленного шага, такого как ежечасно / ежеминутно /….
Ф.Рааб

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. Кто-нибудь знает, как это сделать?
mr.zog

Просто сделайте это в цикле for вместо оператора printd = 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

Для полноты, у Панд также есть 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

Может «т * считаю , этот вопрос существует уже 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

Вы можете создать серию дат между двумя датами, используя библиотеку панд просто и надежно

import pandas as pd

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

Вы можете изменить частоту генерации дат, установив freq как D, M, Q, Y (ежедневно, ежемесячно, ежеквартально, ежегодно).


Уже ответили в этой теме в 2014 году
Алексей Важнов

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

Вот код для общей функции диапазона дат, похожий на ответ Бер, но более гибкий:

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

Обратите внимание, что .total_seconds () поддерживается только после Python 2.7. Если вы застряли в более ранней версии, вы можете написать свою собственную функцию:

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

0

Немного другой подход к обратимым шагам путем хранения rangeаргументов в кортеже.

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

Добро пожаловать в переполнение стека! Хотя этот код может решить вопрос, включая объяснение того, как и почему это решает проблему, особенно по вопросам со слишком большим количеством хороших ответов, действительно поможет улучшить качество вашего сообщения и, вероятно, приведет к большему количеству голосов. Помните, что вы отвечаете на вопрос для читателей в будущем, а не только для того, кто спрашивает сейчас. Пожалуйста, измените свой ответ, чтобы добавить объяснения и указать, какие ограничения и предположения применяются. Из обзора
двойной сигнал
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.