法定节假日爬虫(百度版)

前因

这两天受朋友所托,要写一个工具类判断哪些是工作日哪些是假期。在项目开发中,一般情况下都会做个节假日维护模块让用户来维护数据,那么有没有简便些的方式,自动根据国家放假安排自动维护节假日数据呢?

以前我查放假安排都是直接百度的,类似下面这样:

突然奇想,干脆写个爬虫爬一下百度的这个日历把。


接口分析

第一步,还是要分析下百度的接口才行,敲个"F12"(打开浏览器的开发人员工具),开始我们的旅程。

  • 百度一下,你就知道
  • 【假期安排接口】看到一条调用记录,看看返回值:
    在这里插入图片描述
    复制出来分析一下结构吧(太长了,我已经去掉一下无用的参数。值得注意的是“holiday”包括了1900年到2050年的假期安排):
    {
        "status":"0",
        "data":[
            {
                "holiday":[
                    {
                        "list":[
                            {
                                "date":"1900-1-30",	//节假日当天日期
                                "name":"除夕"		//节假日名称
                            },
                            {
                                "date":"1900-1-31",
                                "name":"春节"
                            },
                            {
                                "date":"1900-5-1",
                                "name":"劳动节"
                            }
                        ],
                        "list#num#baidu":3,		//节假日个数
                        "year":"1900"			//年份
                    }, 											//1900年开始............
                    {
                        "list":[
                            {
                                "date":"2021-1-1",
                                "name":"元旦节"
                            },
                            {
                                "date":"2021-2-11",
                                "name":"除夕"
                            },
                            {
                                "date":"2021-2-12",
                                "name":"春节"
                            },
                            {
                                "date":"2021-4-4",
                                "name":"清明节"
                            },
                            {
                                "date":"2021-5-1",
                                "name":"劳动节"
                            },
                            {
                                "date":"2021-6-14",
                                "name":"端午节"
                            },
                            {
                                "date":"2021-9-21",
                                "name":"中秋节"
                            },
                            {
                                "date":"2021-10-1",
                                "name":"国庆节"
                            }
                        ],
                        "list#num#baidu":8,
                        "year":"2021"
                    },
                    {
                        "list":[
                            {
                                "date":"2050-1-1",
                                "name":"元旦节"
                            },
                            {
                                "date":"2050-1-22",
                                "name":"除夕"
                            },
                            {
                                "date":"2050-1-23",
                                "name":"春节"
                            },
                            {
                                "date":"2050-4-4",
                                "name":"清明节"
                            },
                            {
                                "date":"2050-5-1",
                                "name":"劳动节"
                            },
                            {
                                "date":"2050-6-23",
                                "name":"端午节"
                            },
                            {
                                "date":"2050-10-1",
                                "name":"国庆节"
                            }
                        ],
                        "list#num#baidu":7,
                        "year":"2050"
                    } 											//2050年结束............
                ]
            }
        ]
    }
  • 【假期安排接口】接口入参分析
    地址:https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=%E6%B3%95%E5%AE%9A%E8%8A%82%E5%81%87%E6%97%A5&co=&resource_id=39042&t=1623582604347&ie=utf8&oe=gbk&cb=op_aladdin_callback&format=json&tn=wisetpl&cb=jQuery110209272902710856101_1623582471867&_=1623582471872

query: %E6%B3%95%E5%AE%9A%E8%8A%82%E5%81%87%E6%97%A5 //“法定节假日”的UrlEncode的结果,保留参数
co: //不知道是啥,去除参数
resource_id: 39042 //应该是资源id,保留参数
t: 1623582604347 //调用接口的时间戳,保留参数
ie: utf8 //ie的编码吧?无所谓了
oe: gbk //outlook express的编码吧?无所谓了
cb: op_aladdin_callback //回调函数,去除参数
format: json //格式化类型,保留参数
tn: wisetpl //不能缺少的未知参数,保留参数
cb: jQuery110209272902710856101_1623582471867 //jsonp,去除参数
_: 1623582471872 //随机数,防缓存吧?传时间戳,保留参数


精简一下接口地址:https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=%E6%B3%95%E5%AE%9A%E8%8A%82%E5%81%87%E6%97%A5&resource_id=39042&t=1623582604347&ie=utf8&oe=gbk&format=json&tn=wisetpl&_=1623582471872

  • 【日历信息接口】没看到日历信息啊?别急,切换一下月份试试。


    复制出来分析一下结构吧(太长了,我已经去掉一下无用的参数。值得注意的是“almanac”包括了选定月份前后总共3个月(90天那样子)的日历信息,这里暂且不谈他们的接口设计,也方便我们到时候算跨月补班、休假那些):
    {
        "status":"0",
        "data":[
            {
                "almanac":[
                    {
                        "animal":"牛",
                        "avoid":"搬家.装修.动土.安床.出行.安葬.上梁.旅游.破土.修造.移徙.求医.词讼.出师.打官司",
                        "cnDay":"三",
                        "day":"1",
                        "gzDate":"壬子",
                        "gzMonth":"丙申",
                        "gzYear":"辛丑",
                        "isBigMonth":"1",
                        "lDate":"廿五",
                        "lMonth":"七",
                        "lunarDate":"25",
                        "lunarMonth":"7",
                        "lunarYear":"2021",
                        "month":"9",
                        "oDate":"2021-08-31T16:00:00.000Z",
                        "suit":"开业.结婚.入宅.领证.开工.订婚.开张.作灶.入学.求嗣.赴任.祈福.祭祀.解除.开市.纳财.纳畜.启钻.裁衣.除服.嫁娶.纳采.盖屋.冠笄.竖柱.栽种.斋醮.求财.招赘.纳婿",
                        "term":"",
                        "year":"2021"
                    }, 											//9月1日开始(前一个月第一天)............
                    {
                        "animal":"牛",
                        "avoid":"搬家.装修.开业.结婚.入宅.领证.开工.动土.安床.出行.安葬.上梁.开张.旅游.修坟.破土.修造.开市.纳财.纳畜.启钻.嫁娶.移徙.伐木.盖屋.经络.立券.分居.造桥.筑堤",
                        "cnDay":"五",			//星期几
                        "day":"1",				//日份
                        "desc":"国庆节",
                        "gzDate":"壬午",
                        "gzMonth":"丁酉",
                        "gzYear":"辛丑",
                        "isBigMonth":"",
                        "lDate":"廿五",
                        "lMonth":"八",
                        "lunarDate":"25",
                        "lunarMonth":"8",
                        "lunarYear":"2021",
                        "month":"10",			//月份
                        "oDate":"2021-09-30T16:00:00.000Z",
                        "status":"1",			//状态(分析接口已知的有: 1表示假期; 2表示补班)
                        "suit":"订婚.入学.求嗣.赴任.祈福.祭祀.解除.捕捉.纳采.竖柱.栽种.斋醮.求财.招赘.纳婿",
                        "term":"国庆节",
                        "type":"h",
                        "value":"国际音乐日",
                        "year":"2021"			//年份
                    }, 											//10月9日要补班............
                    {
                        "animal":"牛",
                        "avoid":"装修.开业.入宅.开工.动土.安床.出行.开张.旅游.修造.开市.纳财.纳畜.立券.求医.栽种.词讼.分居.置产.出师.打官司",
                        "cnDay":"六",
                        "day":"9",
                        "desc":"世界邮政日",
                        "gzDate":"庚寅",
                        "gzMonth":"戊戌",
                        "gzYear":"辛丑",
                        "isBigMonth":"1",
                        "lDate":"初四",
                        "lMonth":"九",
                        "lunarDate":"4",
                        "lunarMonth":"9",
                        "lunarYear":"2021",
                        "month":"10",
                        "oDate":"2021-10-08T16:00:00.000Z",
                        "status":"2",
                        "suit":"搬家.结婚.领证.订婚.安葬.上梁.入学.求嗣.赴任.破土.祈福.祭祀.解除.启钻.裁衣.除服.嫁娶.纳采.移徙.盖屋.冠笄.竖柱.求财.和讼",
                        "term":"",
                        "year":"2021"
                    },
                    {
                        "animal":"牛",
                        "avoid":"安床.安葬.破土.纳畜.伐木.造床.行丧.开仓",
                        "cnDay":"二",
                        "day":"30",
                        "gzDate":"壬午",
                        "gzMonth":"己亥",
                        "gzYear":"辛丑",
                        "isBigMonth":"",
                        "lDate":"廿六",
                        "lMonth":"十",
                        "lunarDate":"26",
                        "lunarMonth":"10",
                        "lunarYear":"2021",
                        "month":"11",
                        "oDate":"2021-11-29T16:00:00.000Z",
                        "suit":"搬家.装修.结婚.入宅.领证.动土.出行.订婚.上梁.旅游.求嗣.修坟.赴任.修造.祈福.祭祀.解除.纳财.启钻.捕捉.嫁娶.纳采.移徙.立券.竖柱.栽种.斋醮.招赘.纳婿.取渔",
                        "term":"",
                        "year":"2021"
                    } 											//11月30日结束(后一个月最后一天)............
                ]
            }
        ]
    }
  • 【日历信息接口】接口入参分析
    地址:https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=2021%E5%B9%B410%E6%9C%88&co=&resource_id=39043&t=1623582601343&ie=utf8&oe=gbk&cb=op_aladdin_callback&format=json&tn=wisetpl&cb=jQuery110209272902710856101_1623582471867&_=1623582471870

query: %E6%B3%95%E5%AE%9A%E8%8A%82%E5%81%87%E6%97%A5 //“法定节假日”的UrlEncode的结果,保留参数
co: //不知道是啥,去除参数
resource_id: 39042 //应该是资源id,保留参数
t: 1623582601343 //调用接口的时间戳,保留参数
ie: utf8 //ie的编码吧?无所谓了
oe: gbk //outlook express的编码吧?无所谓了
cb: op_aladdin_callback //回调函数,去除参数
format: json //格式化类型,保留参数
tn: wisetpl //不能缺少的未知参数,保留参数
cb: jQuery110209272902710856101_1623582471867 //jsonp,去除参数
_: 1623582471870 //随机数,防缓存吧?传时间戳,保留参数


精简一下接口地址:https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=2021%E5%B9%B410%E6%9C%88&resource_id=39043&t=1623582601343&ie=utf8&oe=gbk&format=json&tn=wisetpl&_=1623582471870

后果

有前因,那么就有后果。上面的分析差不多了,开始码代码。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.4</version>
        <relativePath/>
    </parent>

    <groupId>com.zze0</groupId>
    <artifactId>zze0-crawler</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-core</artifactId>
            <version>5.6.4</version>
        </dependency>

    </dependencies>

</project>

Holiday节假日类

package org.zze0.crawler.holiday;

import lombok.Data;

import java.util.Date;
import java.util.List;

/**
 * 节假日
 *
 * @author Zze0
 * @since 2021/6/13
 */
@Data
public class Holiday {

    /**
     * 年份
     */
    private Integer year;

    /**
     * 日期
     */
    private Date date;

    /**
     * 名称
     */
    private String name;

    /**
     * 补班日期列表
     */
    private List<Date> addWorkDateList;

    /**
     * 假期日期列表
     */
    private List<Date> holidayDateList;
}

BaiDuHolidayCrawler爬虫类

package org.zze0.crawler.holiday.baidu;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import org.zze0.crawler.holiday.Holiday;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 法定节假日爬虫(百度版)
 * //TODO 理论上应该定时一天同步一次,并持久化到数据库,爬虫接口不能强依赖,需考虑planB方案(业务系统可自己修改配置)
 *
 * @author Zze0
 * @since 2021/6/13
 */
@Slf4j
public class BaiDuHolidayCrawler {

    /**
     * 年度法定节假日查询接口地址
     */
    private static final String YEAR_HOLIDAY_URL = "https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=法定节假日&resource_id=39042&t=%s&ie=utf8&oe=gbk&format=json&tn=wisetpl&_=%s";

    /**
     * 日历查询接口地址
     */
    private static final String CALENDAR_URL = "https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=%s&resource_id=39043&t=%s&ie=utf8&oe=gbk&format=json&tn=wisetpl&_=%s";

    /**
     * http请求工具类
     */
    private static final RestTemplate REST_TEMPLATE = new RestTemplate();

    /**
     * 年度法定节假日(key是年份,value是节假日列表)
     */
    private static final Map<Integer, List<Holiday>> HOLIDAYS = new LinkedHashMap<>();


    /**
     * 初始化年度法定节假日
     *
     * @param startYear 开始年度
     * @param endYear   结束年度
     */
    public static void initHolidays(int startYear, int endYear) {

        Assert.isTrue(startYear <= endYear, "年度入参有误,请检查!开始年度=" + startYear + "; 结束年度=" + endYear);

        long ts = System.currentTimeMillis();

        /*
        查询年度法定节假日列表(查到2050年),格式如下:
        {
            "status":"0",
            "t":"1623495008826",
            "data":[
                {
                    "holiday":[
                        {
                            "list":[
                                {
                                    "date":"2021-1-1",
                                    "name":"元旦节"
                                },
                                {
                                    "date":"2021-2-11",
                                    "name":"除夕"
                                },
                                {
                                    "date":"2021-2-12",
                                    "name":"春节"
                                },
                                {
                                    "date":"2021-4-4",
                                    "name":"清明节"
                                },
                                {
                                    "date":"2021-5-1",
                                    "name":"劳动节"
                                },
                                {
                                    "date":"2021-6-14",
                                    "name":"端午节"
                                },
                                {
                                    "date":"2021-9-21",
                                    "name":"中秋节"
                                },
                                {
                                    "date":"2021-10-1",
                                    "name":"国庆节"
                                }
                            ],
                            "list#num#baidu":8,
                            "year":"2021"
                        }
                    ]
                }
            ]
        }
         */
        String result = REST_TEMPLATE.getForObject(String.format(YEAR_HOLIDAY_URL, ts, ts), String.class);

        //获取年度节假日列表
        JSONArray yearHolidayArr =
                Optional.ofNullable(result)
                        .filter(StrUtil::isNotBlank)
                        .map(JSON::parseObject)
                        .filter(MapUtil::isNotEmpty)
                        .map(json -> json.getJSONArray("data"))
                        .filter(CollUtil::isNotEmpty)
                        .map(dataJsonArr -> dataJsonArr.getJSONObject(0))
                        .filter(MapUtil::isNotEmpty)
                        .map(dataJson -> dataJson.getJSONArray("holiday"))
                        .filter(CollUtil::isNotEmpty)
                        .orElseThrow(() -> new IllegalArgumentException("年度法定节假日查询失败"));

        //----------------下面定位本节假日在日历中的下标位位置--------------------

        //年份获取函数
        Function<JSONObject, Integer> yearGetter = yearHoliday -> yearHoliday.getInteger("year");

        //年度法定节假日列表中第一个年份
        int firstYear =
                Optional.of(yearHolidayArr)
                        .map(arr -> arr.getJSONObject(0))
                        .map(yearGetter)
                        .orElseThrow(() -> new IllegalArgumentException("年度法定节假日列表中第一个年份获取失败"));

        if (startYear < firstYear) {
            startYear = firstYear;
        }

        for (int index = startYear - firstYear; index < yearHolidayArr.size() && firstYear + index <= endYear; index++) {

            JSONObject yearHoliday = yearHolidayArr.getJSONObject(index);
            if (MapUtil.isEmpty(yearHoliday)) {
                continue;
            }

            Integer year = yearGetter.apply(yearHoliday);
            if (null == year) {
                log.warn("年份获取失败:{}", yearHoliday);
                continue;
            }

            //本年度的节假日列表
            JSONArray holidayInfoArr = yearHoliday.getJSONArray("list");
            if (CollUtil.isEmpty(holidayInfoArr)) {
                log.warn("{}年没有节假日:{}", year, yearHoliday);
                continue;
            }

            //本年度的节假日个数(其实根据"list#num#baidu"key也可取到)
            //long holidayCount = holidayInfoArr.size();

            for (int holidayIndex = 0; holidayIndex < holidayInfoArr.size(); holidayIndex++) {

                //节假日详情
                JSONObject holidayInfo = holidayInfoArr.getJSONObject(holidayIndex);
                if (MapUtil.isEmpty(holidayInfo)) {
                    log.warn("{}年的第{}个节假日是空的?{}", year, holidayIndex, yearHoliday);
                    continue;
                }

                //节假日日期
                Date date = DateUtil.parse(holidayInfo.getString("date"));
                if (null == date) {
                    log.warn("{}年的第{}个节假日日期是空的?{}", year, holidayIndex, holidayInfo);
                    continue;
                }

                //节假日名称
                String name = holidayInfo.getString("name");
                if (StrUtil.isBlank(name)) {
                    log.warn("{}年的第{}个节假日名称是空的?{}", year, holidayIndex, holidayInfo);
                    continue;
                }
                if ("除夕".equals(StrUtil.trim(name))) {
                    //忽略“除夕”节假日,因为会和“春节”重复
                    continue;
                }

                Holiday holiday = new Holiday();
                holiday.setDate(date);
                holiday.setYear(year);
                holiday.setName(name);

                //补全节假日详情(如 假期、补班日 等等)
                initHolidayDetails(holiday);

                //TODO 持久化到数据库
                HOLIDAYS.computeIfAbsent(year, y -> new ArrayList<>())
                        .add(holiday);
            }
        }
    }

    /**
     * 补全节假日详情(如 假期、补班日 等等)
     *
     * @param holiday 节假日
     */
    private static void initHolidayDetails(Holiday holiday) {

        long ts = System.currentTimeMillis();

        Date holidayDate = holiday.getDate();

        /*
         查询日历信息(指定月份查询,会查出来前后共90天左右的数据),格式如下:
         {
            "status":"0",
            "t":"1623499626147",
            "data":[
                {
                    "almanac":[
                        {
                            "animal":"牛",
                            "avoid":"搬家.装修.开业.入宅.开工.动土.出行.安葬.上梁.开张.旅游.破土.修造.开市.纳财.移徙.立券.竖柱.放水.分居.行丧.开仓.置产.筑堤.出货",
                            "cnDay":"六",
                            "day":"1",
                            "desc":"劳动节",
                            "gzDate":"己酉",
                            "gzMonth":"壬辰",
                            "gzYear":"辛丑",
                            "isBigMonth":"1",
                            "lDate":"二十",
                            "lMonth":"三",
                            "lunarDate":"20",
                            "lunarMonth":"3",
                            "lunarYear":"2021",
                            "month":"5",
                            "oDate":"2021-04-30T16:00:00.000Z",
                            "status":"1",
                            "suit":"结婚.领证.订婚.求嗣.修坟.赴任.祈福.祭祀.纳畜.启钻.捕捉.嫁娶.纳采.盖屋.栽种.斋醮.招赘.纳婿.藏宝",
                            "term":"",
                            "type":"i",
                            "value":"劳动节",
                            "year":"2021"
                        }
                    ]
                }
            ]
        }
         */
        String yearMonth = new SimpleDateFormat("yyyy年M月").format(holidayDate);
        String result = REST_TEMPLATE.getForObject(String.format(CALENDAR_URL, yearMonth, ts, ts), String.class);


        //获取日历信息列表
        JSONArray almanacArr =
                Optional.ofNullable(result)
                        .filter(StrUtil::isNotBlank)
                        .map(JSON::parseObject)
                        .filter(MapUtil::isNotEmpty)
                        .map(json -> json.getJSONArray("data"))
                        .filter(CollUtil::isNotEmpty)
                        .map(dataJsonArr -> dataJsonArr.getJSONObject(0))
                        .filter(MapUtil::isNotEmpty)
                        .map(dataJson -> dataJson.getJSONArray("almanac"))
                        .filter(CollUtil::isNotEmpty)
                        .orElseThrow(() -> new IllegalArgumentException(yearMonth + "日历信息查询失败"));

        //----------------下面定位本节假日在日历中的下标位位置--------------------

        int almanacCount = almanacArr.size();

        //日历列表第一天日期
        Date firstAlmanacDate = getAlmanac(almanacArr, 0, (almanac, almanacDate) -> almanacDate);

        //本节假日所在日历中的下标位(后面根据这个下标位前后推算哪些是假期或者补班日)
        int holidayIndex = (int) DateUtil.between(firstAlmanacDate, holidayDate, DateUnit.DAY, true);
        Assert.isTrue(
                holidayIndex < almanacCount && DateUtil.isSameDay(getAlmanac(almanacArr, holidayIndex, (almanac, almanacDate) -> almanacDate), holidayDate),
                "未在日历中查找到节假日日期!" + DateUtil.formatDate(holidayDate));

        //----------------下面分析哪些是本节假日的假期、补班日--------------------

        //补班日期列表
        List<Date> addWorkDateList = new ArrayList<>();

        //假期日期列表
        List<Date> holidayDateList = new ArrayList<>();

        //假期是否连续(用来判断是不是同一个假期周期)
        AtomicBoolean holidayContinuous = new AtomicBoolean(true);

        //周末计数(用来分析补班日时截至时间的辅助参数)
        AtomicInteger weekendCount = new AtomicInteger(0);

        //日历分析器,分析哪些是本节假日的假期、补班日。返回true表示需要继续分析,返回false将中断后续分析
        Function<Integer, Boolean> analyzer =
                index ->
                        getAlmanac(almanacArr, index, (almanac, almanacDate) -> {
                            int status = Optional.ofNullable(almanac.getInteger("status")).orElse(-1);

                            if (holidayContinuous.get()) {
                                if (1 == status) {
                                    //记录假期
                                    holidayDateList.add(almanacDate);
                                } else {
                                    //假期不再连续时,设置中断标识
                                    holidayContinuous.set(false);

                                    if (2 == status) {
                                        //记录补班
                                        addWorkDateList.add(almanacDate);
                                    }
                                }
                            } else {
                                if (1 == status) {
                                    //如果是遇到另一个新假期,需要判断补班日的节假日归属问题,然后可以中断继续查找了

                                    Optional.of(holidayDateList)
                                            .map(list -> list.get(list.size() - 1))
                                            .ifPresent(lastHolidayDate -> {

                                                ListIterator<Date> iterator = addWorkDateList.listIterator(addWorkDateList.size());

                                                while (iterator.hasPrevious()) {

                                                    //补班日期
                                                    Date addWorkDate = iterator.previous();

                                                    //补班日和最后一天假期的天数偏移量
                                                    long addWorkDayOffset = DateUtil.between(addWorkDate, lastHolidayDate, DateUnit.DAY, true);

                                                    //补班日和新假期的天数偏移量
                                                    long addWorkDayOffset2NewHoliday = DateUtil.between(addWorkDate, almanacDate, DateUnit.DAY, true);

                                                    if (addWorkDayOffset > addWorkDayOffset2NewHoliday) {

                                                        //另一个新假期离补班日更近,那这个补班日应该是属于它的,而不是属于当前节假日的。
                                                        iterator.remove();

                                                    } else if (addWorkDayOffset == addWorkDayOffset2NewHoliday) {

                                                        //补班日离当前假期、新假期的偏移量一致,就按照优先分配给后面新假期的原则
                                                        if (addWorkDate.before(lastHolidayDate)) {
                                                            break;
                                                        } else {
                                                            iterator.remove();
                                                        }

                                                    } else {

                                                        //补班日离当前假期更近,可以不用再继续判断补班日的节假日归属问题
                                                        break;
                                                    }
                                                }
                                            });

                                    return false;

                                } else if (2 == status) {
                                    //记录补班
                                    addWorkDateList.add(almanacDate);
                                }

                                Week week = DateUtil.dayOfWeekEnum(almanacDate);
                                if (Week.SATURDAY.equals(week) || Week.SUNDAY.equals(week)) {
                                    //过了两个周末了,可以中断继续查找补班日了
                                    return weekendCount.incrementAndGet() < 4;
                                }
                            }
                            return true;
                        });

        //从节假日日期往前查找,看看节假日前面有多少天是假期,多少天是补班日
        for (int index = holidayIndex; index > 0; index--) {
            if (!analyzer.apply(index)) {
                break;
            }
        }

        //往前查找的日期,要把顺序反回来才是按时间先后排序
        holidayDateList.sort(Date::compareTo);
        addWorkDateList.sort(Date::compareTo);

        //重置标识
        holidayContinuous.set(true);
        weekendCount.set(0);

        //从节假日日期往后查找,看看节假日后面有多少天是假期,多少天是补班日
        for (int index = holidayIndex + 1; index < almanacCount; index++) {
            if (!analyzer.apply(index)) {
                break;
            }
        }

        holiday.setHolidayDateList(holidayDateList);
        holiday.setAddWorkDateList(addWorkDateList);

    }

    /**
     * 从日历信息列表提取某个日期的信息,并根据mapper转换成特定的类型返回
     *
     * @param almanacArr 日历信息列表
     * @param index      日历日期下标位
     * @param mapper     转换器(入参1:日历信息;入参2:日期)
     * @param <T>        转换器输出值类型
     * @return 转换器输出值
     */
    private static <T> T getAlmanac(JSONArray almanacArr, int index, BiFunction<JSONObject, Date, T> mapper) {
        JSONObject almanac = almanacArr.getJSONObject(index);
        Assert.notEmpty(almanac, "日历中第" + index + "个日期为空?");

        String almanacStr = almanac.toString();

        //年
        Integer year = almanac.getInteger("year");
        Assert.notNull(year, "日历中第" + index + "个日期年份为空?" + almanacStr);

        //月
        Integer month = almanac.getInteger("month");
        Assert.notNull(month, "日历中第" + index + "个日期月份为空?" + almanacStr);

        //日
        Integer day = almanac.getInteger("day");
        Assert.notNull(day, "日历中第" + index + "个日期日份为空?" + almanacStr);

        try {
            Date date = new SimpleDateFormat("yyyy-M-d").parse(year + "-" + month + "-" + day);
            return mapper.apply(almanac, date);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        initHolidays(2020, 2021);
        //System.out.println(JSON.toJSONStringWithDateFormat(HOLIDAYS, "yyyy-MM-dd", SerializerFeature.PrettyFormat));

        HOLIDAYS.forEach((year, holidays) -> {
            System.out.printf("\n【%d年度】----------------------------------------------------------------------------------------------------------------------------------\n", year);
            for (Holiday holiday : holidays) {
                System.out.printf("\n%s 放假 %d 天,补班 %d 天:", holiday.getName(), holiday.getHolidayDateList().size(), holiday.getAddWorkDateList().size());
                System.out.printf("\n       放假:%s", holiday.getHolidayDateList().stream().map(DateUtil::formatDate).collect(Collectors.joining("、")));
                System.out.printf("\n       补班:%s\n", holiday.getAddWorkDateList().stream().map(DateUtil::formatDate).collect(Collectors.joining("、")));
            }
            System.out.println("\n* 国庆节和中秋节有时会合在一起放假,如果发现中秋节和国庆节放假、补班情况一致,习惯就好。");
            System.out.println("-------------------------------------------------------------------------------------------------------------------------------------------");
        });
    }
}

打印结果


【2020年度】----------------------------------------------------------------------------------------------------------------------------------

元旦节 放假 1 天,补班 0 天:
       放假:2020-01-01
       补班:

春节 放假 10 天,补班 1 天:
       放假:2020-01-24、2020-01-25、2020-01-26、2020-01-27、2020-01-28、2020-01-29、2020-01-30、2020-01-31、2020-02-01、2020-02-02
       补班:2020-01-19

清明节 放假 3 天,补班 0 天:
       放假:2020-04-04、2020-04-05、2020-04-06
       补班:

劳动节 放假 5 天,补班 2 天:
       放假:2020-05-01、2020-05-02、2020-05-03、2020-05-04、2020-05-05
       补班:2020-04-26、2020-05-09

端午节 放假 3 天,补班 1 天:
       放假:2020-06-25、2020-06-26、2020-06-27
       补班:2020-06-28

中秋节 放假 8 天,补班 2 天:
       放假:2020-10-01、2020-10-02、2020-10-03、2020-10-04、2020-10-05、2020-10-06、2020-10-07、2020-10-08
       补班:2020-09-27、2020-10-10

国庆节 放假 8 天,补班 2 天:
       放假:2020-10-01、2020-10-02、2020-10-03、2020-10-04、2020-10-05、2020-10-06、2020-10-07、2020-10-08
       补班:2020-09-27、2020-10-10

* 国庆节和中秋节有时会合在一起放假,如果发现中秋节和国庆节放假、补班情况一致,习惯就好。
-------------------------------------------------------------------------------------------------------------------------------------------

【2021年度】----------------------------------------------------------------------------------------------------------------------------------

元旦节 放假 3 天,补班 0 天:
       放假:2021-01-01、2021-01-02、2021-01-03
       补班:

春节 放假 7 天,补班 2 天:
       放假:2021-02-11、2021-02-12、2021-02-13、2021-02-14、2021-02-15、2021-02-16、2021-02-17
       补班:2021-02-07、2021-02-20

清明节 放假 3 天,补班 0 天:
       放假:2021-04-03、2021-04-04、2021-04-05
       补班:

劳动节 放假 5 天,补班 2 天:
       放假:2021-05-01、2021-05-02、2021-05-03、2021-05-04、2021-05-05
       补班:2021-04-25、2021-05-08

端午节 放假 3 天,补班 0 天:
       放假:2021-06-12、2021-06-13、2021-06-14
       补班:

中秋节 放假 3 天,补班 1 天:
       放假:2021-09-19、2021-09-20、2021-09-21
       补班:2021-09-18

国庆节 放假 7 天,补班 2 天:
       放假:2021-10-01、2021-10-02、2021-10-03、2021-10-04、2021-10-05、2021-10-06、2021-10-07
       补班:2021-09-26、2021-10-09

* 国庆节和中秋节有时会合在一起放假,如果发现中秋节和国庆节放假、补班情况一致,习惯就好。
-------------------------------------------------------------------------------------------------------------------------------------------

项目地址

GitHub项目:
https://github.com/Zze0/zze0-crawler

百度网盘下载:
链接:https://pan.baidu.com/s/16Yh93KrF2pa2mFMtvk_k0Q
提取码:Zze0

最后要说的话

可能有读者想问为什么要选择百度的接口呢?那么其实有很多网站都有提供这方面的功能,百度也算是稳定的大公司,接口不会随意变动。考虑再三,还是从百度这边的接口入手较为稳妥。

原创不易,转载请注明出处,谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值