Ітерація через різні дати в Python


367

У мене є наступний код для цього, але як я можу це зробити краще? Зараз я думаю, що це краще, ніж вкладені петлі, але він починає отримувати Perl-one-lineerish, коли у вас є генератор у розумінні списку.

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.strearch ("% Y-% m-% d", single_date.timetuple ()) "і коротшим" single_date.strearch "("% Y-% m-% d ") '. Здається, більшість відповідей копіюють довший стиль.
Mu Mind

8
Нічого собі, ці відповіді дуже складні. Спробуйте це: stackoverflow.com/questions/7274267/…
Gringo Suave

@GringoSuave: що складного у відповіді Шона Кавана ?
jfs

Застосування: чит на GitHub прожилками: stackoverflow.com/questions/20099235 / ...
Чіро Сантіллі郝海东冠状病六四事件法轮功

1
Дублікат чи ні, ви отримаєте простішу відповідь на іншій сторінці.
Gringo Suave

Відповіді:


552

Чому є дві вкладені ітерації? Для мене він створює той самий список даних лише з однією ітерацією:

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
@John Machin: Гаразд. Однак я перевіряю ітерацію під час циклів з явним збільшенням деякого лічильника або значення. Схема інтеграції є більш пітонічною (принаймні, на мій особистий погляд), а також більш загальною, оскільки дозволяє виразити ітерацію, приховуючи деталі того, як робиться ця ітерація.
Бер

10
@Ber: Мені це зовсім не подобається; це ДУБЛИВО погано. У вас ВЖЕ було ітерації! Обгортаючи спонсорні конструкції в генератор, ви додали ще більше накладних витрат плюс перенаправили увагу користувача кудись ще, щоб прочитати код вашого 3-лайнера та / або документи. -2
Джон Махін

8
@John Machin: Я не згоден. Справа не в тому, щоб зменшити кількість рядків до абсолютного мінімуму. Зрештою, ми не говоримо тут Perl. Також мій код робить лише одну ітерацію (саме так працює генератор, але я думаю, ви це знаєте). *** Моя думка стосується абстрагування понять для повторного використання та пояснення коду. Я вважаю, що це набагато доцільніше, ніж найкоротший можливий код.
Бер

9
Якщо ви збираєтеся на терміновість, ви можете використовувати генераторний вираз:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Позначити 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
Дуже чітко і коротко, але не працює добре, якщо ви хочете використовувати продовжити
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.
Ninjakannon

більш сучасні документи 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

Потужність Панди насправді є її фреймами даних, які підтримують векторизовані операції (подібно до numpy), які роблять операції над великою кількістю даних дуже швидкими та простими.

EDIT: Ви також можете повністю пропустити цикл для циклу та просто надрукувати його безпосередньо, що простіше та ефективніше:

print(daterange)

"так, як нумі" - Pandas побудований на 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 ... але, оскільки ви дозволяєте кроку бути тиммеделтою, вам слід (а) назвати його dateTIMErange () і зробити кроки, наприклад, timedelta (годин = 12) і timedelta (годин = 36) працювати належним чином або ( b) кроки захоплення, які не є цілісною кількістю днів, або (c) збереження абонента клопоту та вираження кроку як кількість днів замість тиммеделі.
Джон Махін

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

щоб зробити цю роботу з кроком, відмінним від днів, слід перевірити step.total_seconds (), а не step.days
амохр

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 для розміщення загального однолінійного рішення, яке дозволяє будь-яку таймеделю, замість фіксованого закругленого кроку, наприклад, погодинно / хвилинно /….
F.Raab

7

Показати останні п ять днів від сьогодні:

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, будь ласка, не соромтесь редагувати, я прийму їх.
користувач1767754

2
Я намагався. Для редагування потрібно мінімум 6 знаків, але в цьому випадку потрібно додати лише 2 символи, "(" і ")"
TitanFighter

print((datetime.date.today() + datetime.timedelta(i)))без .isoformat () дає точно такий же вихід. Мені потрібен мій сценарій, щоб надрукувати YYMMDD. Хтось знає, як це зробити?
mr.zog

Просто зробіть це в циклі for замість заяви для друку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

Для повноти 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

Може «т * вважаю , це питання існує вже 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
Чому б ви замінили просту петлю на рекурсію? Ця перерва на діапазони, що перевищують приблизно два з половиною роки.
Тім-Ервін

@ Тім-Ервін Чесно кажучи, я не мав уявлення, що CPython не оптимізує рекурсію хвоста, тому ваш коментар є цінним.
Pocketsand

2

Ви можете генерувати серію дат між двома датами, використовуючи бібліотеку панд просто та довірливо

import pandas as pd

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

Ви можете змінити частоту генерації дат, встановивши частоту як 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.