Iterando um intervalo de datas no Python


369

Eu tenho o seguinte código para fazer isso, mas como posso fazer isso melhor? No momento, acho que é melhor do que loops aninhados, mas ele começa a ficar com o Perl-one-linerish quando você tem um gerador em uma lista de compreensão.

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

  • Na verdade, não estou usando isso para imprimir. Isso é apenas para fins de demonstração.
  • As variáveis start_datee end_datesão datetime.dateobjetos porque não preciso dos carimbos de data e hora. (Eles serão usados ​​para gerar um relatório).

Saída de amostra

Para uma data de início 2009-05-30e uma data final 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
Apenas para salientar: não acho que exista diferença entre 'time.strftime ("% Y-% m-% d", single_date.timetuple ())' e o menor 'single_date.strftime ("% Y-% m-% d ") '. A maioria das respostas parece estar copiando o estilo mais longo.
Mu mente

8
Uau, essas respostas são muito complicadas. Tente isto: stackoverflow.com/questions/7274267/…
Gringo Suave

@GringoSuave: o que é complicado na resposta de Sean Cavanagh ?
JFS

Aplicação: trapaceie nas faixas do GitHub: stackoverflow.com/questions/20099235/…
Ciro Santilli

11
Duplicado ou não, você receberá uma resposta mais simples na outra página.
Gringo Suave

Respostas:


553

Por que existem duas iterações aninhadas? Para mim, ele produz a mesma lista de dados com apenas uma iteração:

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

E nenhuma lista é armazenada, apenas um gerador é iterado. Também o "se" no gerador parece ser desnecessário.

Afinal, uma sequência linear deve exigir apenas um iterador, não dois.

Atualização após discussão com John Machin:

Talvez a solução mais elegante seja usar uma função de gerador para ocultar / abstrair completamente a iteração no intervalo de datas:

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 consistência com a range()função incorporada, esta iteração pára antes de atingir o end_date. Portanto, para iteração inclusiva, use no dia seguinte, como faria com range().


4
-1 ... ter um cálculo preliminar de day_count e usar range não é impressionante quando um loop while simples é suficiente.
31711 John Machin

7
@ John Machin: Tudo bem. Entretanto, eu prevejo uma iteração over loops com incrementos explícitos de algum contador ou valor. O padrão de interação é mais pitônico (pelo menos na minha opinião pessoal) e também mais geral, pois permite expressar uma iteração enquanto oculta os detalhes de como essa iteração é feita.
Ber

10
@ Ber: Eu não gosto nada disso; É DUPLO ruim. Você JÁ teve uma iteração! Ao agrupar as construções de reclamação em um gerador, você adicionou ainda mais sobrecarga de execução e desviou a atenção do usuário para outro lugar para ler o código e / ou documentos do seu 3-liner. -2
John Machin

8
@ John Machin: Eu discordo. A questão não é reduzir o número de linhas ao mínimo absoluto. Afinal, não estamos falando de Perl aqui. Além disso, meu código faz apenas uma iteração (é assim que o gerador funciona, mas acho que você sabe disso). *** Meu argumento é sobre abstrair conceitos para reutilização e código auto-explicativo. Eu mantenho que isso vale muito mais a pena do que ter o código mais curto possível.
Ber

9
Se você estiver indo para terseness você pode usar um gerador de expressão:(start_date + datetime.timedelta(n) for n in range((end_date - start_date).days))
Mark Ransom

219

Isso pode ser mais 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
Muito claro e curto, mas não funciona bem se você quiser usar continuar
rslite

funciona adorável para o meu caso de uso
doomdaam 11/02

169

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

Essa biblioteca python possui muitos recursos mais avançados, alguns muito úteis, como relative deltas - e é implementada como um único arquivo (módulo) que é facilmente incluído em um projeto.


3
Note-se que a data final no loop para aqui é inclusive de untilque a data final do daterangemétodo na resposta de Ber é exclusivo de end_date.
Ninjakannon 23/05


77

O Pandas é ótimo para séries temporais em geral e tem suporte direto para períodos.

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

Em seguida, você pode percorrer a faixa de data para imprimir a data:

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

Ele também tem muitas opções para facilitar a vida. Por exemplo, se você quisesse apenas dias da semana, trocaria bdate_range. Consulte http://pandas.pydata.org/pandas-docs/stable/timeseries.html#generating-ranges-of-timestamps

O poder do Pandas é realmente seus quadros de dados, que suportam operações vetorizadas (como numpy) que tornam as operações em grandes quantidades de dados muito rápidas e fáceis.

EDIT: Você também pode pular completamente o loop for e imprimi-lo diretamente, o que é mais fácil e mais eficiente:

print(daterange)

"parecido com numpy" - o Pandas é construído com o 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())

Essa função faz mais do que você exige estritamente, suportando etapas negativas, etc. Desde que você considere sua lógica de intervalo, você não precisa do separado day_counte, o mais importante, o código se torna mais fácil de ler quando você chama a função de vários locais.


Obrigado, renomeado para corresponder melhor aos parâmetros do intervalo, esqueceu de mudar no corpo.

+1 ... mas como você está permitindo que a etapa seja um timedelta, você deve (a) chamá-lo de dateTIMErange () e executar etapas como, por exemplo, timedelta (horas = 12) e timedelta (horas = 36) funcionar corretamente ou ( b) interceptar etapas que não sejam um número inteiro de dias ou (c) salvar o chamador do incômodo e expressar a etapa como um número de dias em vez de um intervalo de tempo.
John Machin

Qualquer timedelta já deve funcionar, mas adicionei datetime_range e date_range à minha coleção de recados pessoais depois de escrever isso, por causa de (a). Não tendo certeza de que outra função valha a pena para (c), o caso mais comum de dias = 1 já está resolvido e a necessidade de passar um tempo explícito evita confusão. Talvez carregá-lo em algum lugar é melhor: bitbucket.org/kniht/scraps/src/tip/python/gen_range.py

para fazer este trabalho em outros incrementos do que dias você deve verificar contra step.total_seconds (), e não step.days
amohr

12

Esta é a solução mais legível para humanos que consigo pensar.

import datetime

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

11

Por que nao tentar:

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

A arangefunção do Numpy pode ser aplicada a datas:

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)

O uso de astypeé converter de numpy.datetime64para uma matriz de datetime.datetimeobjetos.


Construção super enxuta! A última linha funciona para mim comdates = np.arange(d0, d1, dt).astype(datetime.datetime)
pyano

+1 para publicar uma solução genérica de uma linha que permite qualquer intervalo de tempo, em vez de uma etapa arredondada fixa, como por hora / minuto /….
F.Raab

7

Mostrar os últimos n dias a partir de hoje:

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

Resultado:

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

Por favor, adicione colchetes, comoprint((datetime.date.today() + datetime.timedelta(i)).isoformat())
TitanFighter

@TitanFighter, sinta-se livre para fazer edições, eu as aceito.
user1767754

2
Eu tentei. A edição requer no mínimo 6 caracteres, mas neste caso é necessário adicionar apenas 2 caracteres, "(" e ")"
TitanFighter

print((datetime.date.today() + datetime.timedelta(i)))sem o .isoformat () fornece exatamente a mesma saída. Preciso do meu script para imprimir YYMMDD. Alguém sabe como fazer isso?
mr.zog

Basta fazer isso no loop for em vez da instrução de impressãod = 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, o Pandas também possui uma period_rangefunção para carimbos de data / hora que estão fora dos limites:

import pandas as pd

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

3

Eu tenho um problema semelhante, mas preciso iterar mensalmente, e não diariamente.

Esta é a minha solução

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

Exemplo 1

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

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

Resultado

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

Exemplo 2

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

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

Resultado

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

Pode 't * acredito que esta questão já existe há 9 anos, sem que ninguém sugerindo uma função recursiva simples:

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)

Resultado:

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

Edit: * Agora eu posso acreditar - consulte O Python otimiza a recursão da cauda? . Obrigado Tim .


3
Por que você substitui um loop simples por recursão? Isso é interrompido para intervalos superiores a aproximadamente dois anos e meio.
Tim-Erwin

@ Tim-Erwin Honestamente, eu não tinha idéia do CPython não otimiza a recursão da cauda, ​​portanto seu comentário é valioso.
precisa saber é o seguinte

2

Você pode gerar uma série de datas entre duas datas usando a biblioteca do pandas de maneira simples e confiável

import pandas as pd

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

Você pode alterar a frequência da geração de datas definindo a frequência como D, M, Q, Y (diariamente, mensalmente, trimestralmente, anualmente)


Já respondeu a esta discussão em 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 função possui alguns recursos extras:

  • pode transmitir uma sequência correspondente a DATE_FORMAT para início ou fim e é convertida em um objeto de data
  • pode passar um objeto de data para início ou fim
  • verificação de erro no caso de o final ser mais antigo que o início

    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

Aqui está o código para uma função geral do período, semelhante à resposta de Ber, mas mais flexível:

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

Que tal o seguinte para executar um intervalo incrementado por dias:

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

Para uma versão genérica:

for d in map( lambda x: startTime+x*stepTime, xrange( (stopTime-startTime).total_seconds() / stepTime.total_seconds() ) ):
  # Do stuff here
  • startTime e stopTime são o objeto datetime.date ou datetime.datetime (ambos devem ser do mesmo tipo)
  • stepTime é um objeto timedelta

Observe que .total_seconds () é suportado somente após o python 2.7 Se você está preso a uma versão anterior, pode escrever sua própria função:

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

0

Abordagem ligeiramente diferente para etapas reversíveis armazenando rangeargs em uma 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')

RESULTADO:

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

Bem-vindo ao Stack Overflow! Embora esse código possa resolver a questão, incluindo uma explicação de como e por que isso resolve o problema, especialmente em perguntas com muitas respostas boas, realmente ajudaria a melhorar a qualidade da sua postagem e provavelmente resultaria em mais votos. Lembre-se de que você está respondendo à pergunta dos leitores no futuro, não apenas à pessoa que está perguntando agora. Por favor edite sua resposta para adicionar explicações e dar uma indicação do que limitações e premissas se aplicam. Da avaliação
double-beep
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.