Iterando a través de un rango de fechas en Python


369

Tengo el siguiente código para hacer esto, pero ¿cómo puedo hacerlo mejor? En este momento creo que es mejor que los bucles anidados, pero comienza a obtener Perl-one-linerish cuando tienes un generador en una lista de comprensión.

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())

Notas

  • En realidad no estoy usando esto para imprimir. Eso es solo para fines de demostración.
  • Las variables start_datey end_dateson datetime.dateobjetos porque no necesito las marcas de tiempo. (Se usarán para generar un informe).

Salida de muestra

Para una fecha de inicio 2009-05-30y una fecha de finalización de 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
Solo para señalar: no creo que haya ninguna diferencia entre 'time.strftime ("% Y-% m-% d", single_date.timetuple ())' y el más corto 'single_date.strftime ("% Y-% Maryland")'. La mayoría de las respuestas parecen estar copiando el estilo más largo.
Mu Mind

8
Wow, estas respuestas son demasiado complicadas. Pruebe esto: stackoverflow.com/questions/7274267/…
Gringo Suave

@GringoSuave: lo complicado de la respuesta de Sean Cavanagh ?
jfs


1
Duplicado o no, obtendrá una respuesta más simple en la otra página.
Gringo Suave

Respuestas:


554

¿Por qué hay dos iteraciones anidadas? Para mí, produce la misma lista de datos con solo una iteración:

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

Y no se almacena ninguna lista, solo se repite un generador. También el "si" en el generador parece ser innecesario.

Después de todo, una secuencia lineal solo debería requerir un iterador, no dos.

Actualización después de la discusión con John Machin:

Quizás la solución más elegante es usar una función de generador para ocultar / abstraer completamente la iteración en el rango de fechas:

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"))

Nota: para mantener la coherencia con la range()función integrada, esta iteración se detiene antes de llegar a end_date. Entonces, para una iteración inclusiva, use al día siguiente, como lo haría con range().


44
-1 ... tener un cálculo preliminar de day_count y usar el rango no es asombroso cuando un simple ciclo while será suficiente.
John Machin

77
@ John Machin: De acuerdo. Sin embargo, preveré una iteración mientras se repiten los bucles con un incremento explícito de algún contador o valor. El patrón de interacción es más pitónico (al menos en mi opinión personal) y también más general, ya que permite expresar una iteración mientras oculta los detalles de cómo se realiza esa iteración.
Ber

10
@Ber: no me gusta en absoluto; Es DOBLEMENTE malo. ¡YA tuviste una iteración! Al envolver las construcciones sobre las quejas en un generador, ha agregado aún más sobrecarga de ejecución además de desviar la atención del usuario a otro lugar para leer el código y / o los documentos de su línea. -2
John Machin

8
@ John Machin: No estoy de acuerdo. El punto no es reducir el número de líneas al mínimo absoluto. Después de todo, no estamos hablando de Perl aquí. Además, mi código solo realiza una iteración (así es como funciona el generador, pero supongo que lo sabes). *** Mi punto es sobre abstraer conceptos para la reutilización y el código autoexplicativo. Sostengo que esto vale mucho más la pena que tener el código más corto posible.
Ber

99
Si va por la brevedad, puede usar una expresión generadora:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Mark Ransom el

219

Esto podría ser más claro:

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
Muy claro y breve, pero no funciona bien si desea continuar
rslite

funciona muy bien para mi caso de uso
doomdaam

169

Usa la dateutilbiblioteca:

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")

Esta biblioteca de Python tiene muchas características más avanzadas, algunas muy útiles, como relative deltas, y se implementa como un solo archivo (módulo) que se incluye fácilmente en un proyecto.


3
Tenga en cuenta que la fecha final en el bucle aquí es incluyente de untilque la fecha final del daterangemétodo en la respuesta de Ber es exclusiva de end_date.
Ninjakannon


77

Pandas es ideal para series temporales en general y tiene soporte directo para rangos de fechas.

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

Luego puede recorrer el rango de fechas para imprimir la fecha:

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

También tiene muchas opciones para hacer la vida más fácil. Por ejemplo, si solo quisiera entre semana, simplemente cambiaría en bdate_range. Ver http://pandas.pydata.org/pandas-docs/stable/timeseries.html#generating-ranges-of-timestamps

El poder de Pandas es realmente sus marcos de datos, que admiten operaciones vectorizadas (al igual que numpy) que hacen que las operaciones en grandes cantidades de datos sean muy rápidas y fáciles.

EDITAR: También puede omitir por completo el bucle for y simplemente imprimirlo directamente, lo que es más fácil y más eficiente:

print(daterange)

"como numpy" - Pandas se basa en 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())

Esta función hace más de lo que estrictamente requiere, al admitir pasos negativos, etc. Siempre y cuando tenga en cuenta su lógica de rango, entonces no necesita day_countel código separado y, lo más importante, el código se vuelve más fácil de leer cuando llama a la función desde múltiples lugares.


Gracias, renombrado para que coincida más estrechamente con los parámetros del rango, olvidé cambiar en el cuerpo.

+1 ... pero como permite que el paso sea un timedelta, debe (a) llamarlo dateTIMErange () y hacer que los pasos, por ejemplo, timedelta (horas = 12) y timedelta (horas = 36) funcionen correctamente o ( b) atrape los pasos que no son un número integral de días o (c) ahorre a la persona que llama la molestia y exprese el paso como un número de días en lugar de un intervalo de tiempo.
John Machin

Cualquier timedelta ya debería funcionar, pero agregué datetime_range y date_range a mi colección personal de recortes después de escribir esto, debido a (a). No estoy seguro de que valga la pena para otra función para (c), el caso más común de días = 1 ya está solucionado, y tener que pasar un timedelta explícito evita la confusión. Quizás lo mejor sea cargarlo en algún lugar: bitbucket.org/kniht/scraps/src/tip/python/gen_range.py

para que esto funcione en incrementos que no sean días, debe verificar con step.total_seconds (), y no con step.days
amohr

12

Esta es la solución más legible para los humanos que se me ocurre.

import datetime

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

11

¿Por qué no intentarlo?

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

La arangefunción de Numpy se puede aplicar a las fechas:

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)

El uso de astypees convertir de numpy.datetime64a una matriz de datetime.datetimeobjetos.


¡Construcción súper delgada! La última línea me funcionadates = np.arange(d0, d1, dt).astype(datetime.datetime)
pyano

+1 para publicar una solución genérica de una línea que permite cualquier timedelta, en lugar de un paso redondeado fijo como hora / minuto / ...
F.Raab

7

Mostrar los últimos n días a partir de hoy:

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

Salida:

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

Agregue corchetes, comoprint((datetime.date.today() + datetime.timedelta(i)).isoformat())
TitanFighter

@TitanFighter, no dudes en hacer ediciones, las aceptaré.
user1767754

2
Lo intenté. La edición requiere un mínimo de 6 caracteres, pero en este caso es necesario agregar solo 2 caracteres, "(" y ")"
TitanFighter

print((datetime.date.today() + datetime.timedelta(i)))sin .isoformat () da exactamente la misma salida. Necesito mi script para imprimir YYMMDD. ¿Alguien sabe cómo hacer eso?
mr.zog

Simplemente haga esto en el bucle for en lugar de la declaración de impresiónd = 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

Para completar, Pandas también tiene una period_rangefunción para las marcas de tiempo que están fuera de los límites:

import pandas as pd

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

3

Tengo un problema similar, pero necesito iterar mensualmente en lugar de diariamente.

Esta es mi solucion

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

Ejemplo 1

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

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

Salida

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

Ejemplo # 2

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

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

Salida

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

Puede 't * creer que esta pregunta ha existido desde hace 9 años sin que nadie lo que sugiere una función recursiva simple:

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)

Salida:

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

Editar: * Ahora puedo creerlo, vea ¿Python optimiza la recursividad de la cola? . Gracias Tim .


3
¿Por qué reemplazarías un bucle simple con recursividad? Esto se rompe para rangos que son más largos que aproximadamente dos años y medio.
Tim-Erwin

@ Tim-Erwin Honestamente, no tenía idea de que CPython no optimiza la recursión de la cola, por lo que su comentario es valioso.
Bolsillos y el

2

Puede generar una serie de fechas entre dos fechas usando la biblioteca de pandas de manera simple y confiable

import pandas as pd

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

Puede cambiar la frecuencia de generación de fechas configurando la frecuencia como D, M, Q, Y (diaria, mensual, trimestral, anual)


Ya respondí en este hilo en 2014
Alexey Vazhnov

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

Esta función tiene algunas características adicionales:

  • puede pasar una cadena que coincida con DATE_FORMAT para comenzar o finalizar y se convierte en un objeto de fecha
  • puede pasar un objeto de fecha para comenzar o finalizar
  • comprobación de errores en caso de que el final sea anterior al inicio

    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

Aquí hay un código para una función de rango de fecha general, similar a la respuesta de Ber, pero más flexible:

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

¿Qué pasa con lo siguiente para hacer un rango incrementado en días?

for d in map( lambda x: startDate+datetime.timedelta(days=x), xrange( (stopDate-startDate).days ) ):
  # Do stuff here
  • startDate y stopDate son objetos datetime.date

Para una versión genérica:

for d in map( lambda x: startTime+x*stepTime, xrange( (stopTime-startTime).total_seconds() / stepTime.total_seconds() ) ):
  # Do stuff here
  • startTime y stopTime son objeto datetime.date o datetime.datetime (ambos deben ser del mismo tipo)
  • stepTime es un objeto timedelta

Tenga en cuenta que .total_seconds () solo es compatible después de python 2.7. Si tiene una versión anterior, puede escribir su propia función:

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

0

Enfoque ligeramente diferente a los pasos reversibles almacenando rangeargs en una tupla.

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')

SALIDA:

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

¡Bienvenido a Stack Overflow! Si bien este código puede resolver la pregunta, incluida una explicación de cómo y por qué esto resuelve el problema, especialmente en preguntas con demasiadas buenas respuestas, realmente ayudaría a mejorar la calidad de su publicación, y probablemente resultaría en más votos a favor. Recuerde que está respondiendo la pregunta para los lectores en el futuro, no solo la persona que pregunta ahora. Por favor, editar su respuesta para agregar explicaciones y dar una indicación de lo que se aplican limitaciones y supuestos. De la opinión
doble pitido
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.