Python计算两日期之间排除节假日与非上班时间的工作时间

Python计算两日期之间排除节假日与非上班时间的工作时间


前言

工作中遇见需要写UDF计算事项办理时间的需求,事项申请和办结由于在线上,可能不在办理时间内,因此要求排除节假日与工作日的非工作时间(午休时间、上班前与下班后的时间),在次做下记录。


一、基本思路

首先需要获取法定节假日,这里参考了另一篇从万年历爬取全年法定节假日时间的文章:
Python获取全年法定节假日时间
文章中已经很详细地叙述了从万年历爬取节假日日期的方法,逻辑也比较简明,有具体节假日爬取需求可以参考一下。
代码示例:

# -*- coding: utf-8 -*-
import requests
from lxml import etree

def get_holiday(year):
    """Params:year 四位数年份字符串"""
    """页面解析"""
    url = 'https://wannianrili.51240.com/ajax/'
    headers = {
        'Host': 'wannianrili.51240.com',
        'Connection': 'keep-alive',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
        'Accept': '*/*',
        'Sec-Fetch-Site': 'same-origin',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Dest': 'empty',
        'Referer': 'https://wannianrili.51240.com/',
        'Accept-Encoding': 'gzip, deflate, br',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    }
    list_holiday = []
    # 生成月份列表
    dateList = [year + '-' + '%02d' % i for i in range(1, 13)]
    for year_month in dateList:
        s = requests.session()
        url = 'https://wannianrili.51240.com/ajax/'
        payload = {'q': year_month}
        response = s.get(url, headers=headers, params=payload)
        element = etree.HTML(response.text)
        html = element.xpath('//div[@class="wnrl_riqi"]')
        for _element in html:
            # 获取节点属性
            item = _element.xpath('./a')[0].attrib
            if 'class' in item:
                if item['class'] == 'wnrl_riqi_xiu' or item['class'] == 'wnrl_riqi_mo':
                    _span = _element.xpath('.//text()')
                    list_holiday.append(year_month + '-' + _span[0])
    return list_holiday

然而因为这个网站时不时会崩,因此可以考虑稳妥的方式,就是使用chinesecalendar这个库,其中is_holiday方法可以判断法定节假日。
代码示例:

# -*- coding: utf-8 -*-

from chinese_calendar import is_holiday
from datetime import date

def get_holiday(year):
    list_holiday=[]
    year=int(year)
    for month in range(1,13):
        for day in range(1,32):
            try:
                dt=date(year,month,day)
            except:
                break
            if is_holiday(dt):
                list_holiday.append('{}-{:02d}-{:02d}'.format(year,month,day))
    return list_holiday

在获得了节假日期后,就需要对开始时间与办结时间做对应的精准化判断,判断逻辑上略显繁琐但也比较简单,计算主要是使用datetime库中的timedelta,其实如果常用的话可以模块化一下流程中的日期处理方法,我这里写的比较糙。
我这边的案例口径是只将工作日中08:30-12:00,14:00-18:00的时间纳入计算,时间参数的格式需要是“YYYY-MM-DD HH24:MI:SS”。

二、代码示例

总体代码示例:

# -*- coding: utf-8 -*-

from chinese_calendar import is_holiday
from datetime import date
from datetime import datetime
from datetime import timedelta


def get_holiday(year):
    list_holiday=[]
    year=int(year)
    for month in range(1,13):
        for day in range(1,32):
            try:
                dt=date(year,month,day)
            except:
                break
            if is_holiday(dt):
                list_holiday.append('{}-{:02d}-{:02d}'.format(year,month,day))
    return list_holiday

def datediff_no_holiday(start,end):
    """Params:
    start:开始时间
    end:结束时间
    'yyyy-mm-dd hh24:mi:ss'格式字符串"""
    if start>=end or not start or not end:return 0
    if start[:4]==end[:4]:
        list_holiday=get_holiday(end[:4])
    else:
        list_holiday=[]
        for year in range(int(start[:4]),int(end[:4])+1):
            list_holiday=list_holiday+get_holiday(str(year))
    list_holiday=list( map(lambda x : datetime.strptime(x,'%Y-%m-%d'), list_holiday) )
    result=0
    list_start=start.split(' ')
    start=datetime.strptime(start, '%Y-%m-%d %H:%M:%S')
    start_d=datetime.strptime(list_start[0],'%Y-%m-%d')
    list_end=end.split(' ')
    end=datetime.strptime(end, '%Y-%m-%d %H:%M:%S')
    end_d=datetime.strptime(list_end[0],'%Y-%m-%d')
    #首先判断结束时间是否在工作时间内,如果早于当天工作时间则转换为上班时间,如果大于则转换为后一天上班时间
    if list_end[1]>'18:00:00':
        end=end_d+timedelta(hours=18)
    if list_end[1]<'08:30:00':
        end=end_d+timedelta(hours=8, minutes=30)
        list_end[1]='08:30:00'
    #同理判断开始时间,如果大于则转换为后一天上班时间
    if list_start[1]>'18:00:00':
        start_d+=timedelta(days=1)
        start=start_d+timedelta(hours=8, minutes=30)
        list_start[1]='08:30:00'
    #判断是否在节假日(包括不补班的周末)中,如果是则转入下一天进行循环判断
    if start_d in list_holiday:
        while start_d in list_holiday:
            start_d+=timedelta(days=1)
        start=start_d+timedelta(hours=8, minutes=30)
        list_start[1]='08:30:00'
    if end_d in list_holiday:
        while end_d in list_holiday:
            end_d+=timedelta(days=1)
        end=end_d+timedelta(hours=8, minutes=30)
        list_end[1]='08:30:00'
    #判断开始时间,如果早于当天工作时间则转换为当天上班时间
    if list_start[1]<'08:30:00':
        start=start_d+timedelta(hours=8, minutes=30)
    #剔除12点到14点的午休时间
    if '12:00:00'<list_start[1]<'14:00:00':
        start=start_d+timedelta(hours=14)
        list_start[1]='14:00:00'
    if start>=end:return 0
    #如果开始与结束在同一日期里
    if start_d==end_d:
        result=(end-start).seconds
        if list_start[1]<='12:00:00':  #如果开始时间在上午
            if list_end[1]>='14:00:00':
                result-=7200
            if '12:00:00'<list_end[1]<'14:00:00':
                result-=(end-(end_d+timedelta(hours=12))).seconds
    #如果开始与结束不在同一日期里,排除中间可能存在的节假日
    else:
        if list_start[1]<='12:00:00':
            result-=7200
        result+=(start_d+timedelta(hours=18)-start).seconds
        start_d+=timedelta(days=1) #
        while start_d<end_d:
            if start_d not in list_holiday:
                result+=27000  #每日工时7.5*3600秒
            start_d+=timedelta(days=1)
        result+=(end-(end_d+timedelta(hours=8, minutes=30))).seconds
        if list_end[1]>='14:00:00':
            result-=7200
        elif '12:00:00'<list_end[1]<'14:00:00':
            result-=(end-(end_d+timedelta(hours=12))).seconds

    return result if result>0 else 0
    """返回相差的秒数"""

if __name__ == '__main__':
    start='2020-09-30 13:00:00'
    end='2020-10-09 15:00:00'
    hours=datediff_no_holiday(start,end)
    print(hours/3600,'小时')

结果为 8.5 小时

总结

呱。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值