日期范围的通配符表示-附pythonScala代码

Posted BJUT赵亮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了日期范围的通配符表示-附pythonScala代码相关的知识,希望对你有一定的参考价值。

本文记录了将给定的日期范围用通配符表示的相关内容,欢迎与我沟通联系(zhaoliang19960421@outlook.com)
如果在使用中发现程序输入与结果不符,欢迎与我沟通

当前大数据开发中,数据通常以时间date为分区保存在hdfs中,例如 hdfs://abc/date=20220101

在主流的两大大数据开发框架MapReduce、Spark中对于单个日期数据的输入形式一样,都是直接输入对应日期的hdfs路径即可。

在跨分区读取数据时,MapReduce提供了时间的通配符表示法,例如要读取2022年1月份整月的数据,可以表示成202201*
spark相较于MR提供了类似于sql的时间范围写法

sparkSession.read.parquet(s"hdfs://abc")
	.where(s"date between $startDate and $endDate")

但是该方法仍存在不足:在read.parquet(s"hdfs://abc")实际上将该路径下的所有分区都扫描一遍,找到所有满足where条件的分区
这样做虽然并没有实际的将数据读进来,但是仍然需要暴力的扫描整个路径,以便找到满足条件的结果。如果上游数据分区特别多时,采用该方案耗时仍然很高

为了解决该问题,在spark读取路径时可以采用指定输入地址路径列表的方式来避免暴力扫描,也就是将[startDate,endDate]时间范围内的路径直接写出来,不在进行where判断,直接去读对应的时间分区。但是这样实际开发中很不方便,例如要计算2022年1月份的全部数据,需要写31个路径,即使通过写循环语句来解决仍然不够优雅。

在spark中同样支持如MR的通配符表示法,也就是用202201*来表示22年1月份的全部数据,采用这样的方式即避免了暴力扫描的耗时,也避免了写31个路径以及写循环的复杂性。

具体含义以几个case进一步解释

case1 日期范围是:20220101 20221231
解释:需要输入的是2022年一整年的数据,所以采用通配符的表达方式为2022*


case2 日期范围是 : 20210101 20210331
解释:需要输入的2021年,1月、2月、3月的全部日期,采用通配符的表示方式是 '202101*', '202102*', '202103*'

case3 日起范围是:20200104 20200302
解释:需要输入的是,从1月4号到1月底,2月整月、3月1号,3月2号的日期,采用通配符的表示方式是 ['20200104', '20200105', '20200106', '20200107', '20200108', '20200109', '2020011*', '2020012*', '2020013*', '202002*', '20200301', '20200302']
因为1月4号到1月9号,无法继续采用通配符,所以全部枚举;1月10号到1月19号、1月11号到19号、1月21号到1月29号、1月30号到1月31号,均可以用通配符。

将以上问题抽象为数学问题是,给定一个函数,输入的是2个字符串[start,end]的日期范围,输出一个字符串数组,这个字符串数组是给定日期范围的通配符表示式的最简结果

该问题采用递归的方式来解决,具体内容见python、Scala代码

def timeWindow2timeWildcard(start: str, end: str) -> list:
    """
    将给定的时间范围 [start,end] 转化成 最简的时间通配符表达式
    :param start: 开始时间,左闭
    :param end: 结束时间,右闭
    :return: list[str] 整个list的结果是时间范围的最简单的时间通配符结果
    """

    def _yyyyMM2dd(yyyy, MM):
        """
        输出给定年、月的日期
        """
        if MM in 1, 3, 5, 7, 8, 10, 12:
            return 31
        if MM != 2:
            return 30
        if (yyyy % 4 == 0) and (yyyy % 100 != 0) or (yyyy % 400) == 0:
            return 29
        else:
            return 28

    def _check(yyyyMMdd):
        """
        检查日期是否合理,
        """
        date = _yyyyMM2dd(int(yyyyMMdd[:4]), int(yyyyMMdd[4:6]))
        if 1 <= int(yyyyMMdd[6:8]) <= date:
            return yyyyMMdd
        if 1 > int(yyyyMMdd[6:8]):
            return "01".format(yyyyMMdd[:6])
        if int(yyyyMMdd[6:8]) > date:
            return "".format(yyyyMMdd[:6], date)

    def _count_month(result, start, i, j):
        """
        计算在给定区间中,当前结果包含的月份通配符的个数
        """
        temp = 0
        for k in [":0>2*".format(start[:4], _) for _ in range(i, j)]:
            if k in result:
                temp += 1
        return temp == j - i

    def _delete_month(result, start, i, j):
        """
        当前结果的月份通配符个数满足要求时,删除当前的通配符,聚合成更上一级的通配符结果
        """
        for k in [":0>2*".format(start[:4], _) for _ in range(i, j)]:
            result.remove(k)

    def _count_date(result, start, i, j):
        """
        计算给定区间中的日期个数
        """
        temp = 0
        for k in [":0>2".format(start[:6], _) for _ in range(i, j)]:
            if k in result:
                temp += 1
        return temp == j - i

    def _delete_date(result, start, i, j):
        """
        当日期个数满足时,用通配符表示
        """
        for k in [":0>2".format(start[:6], _) for _ in range(i, j)]:
            result.remove(k)

    start = _check(start)
    end = _check(end)
    if start == end:
        return [start]
    if start > end:
        return []
    result = []
    if start[:4] != end[:4]:
        """
        采用递归的方式来处理,依次判断,年份、月份、日期是否一样

        以 [20200101,20221231]为例,也就是2020 2021 2022 三年全年的数据
        开始和结束的时间年份不同,其中年份中间包含了 2021 。
        2021年全年都在日期范围内,所以中间的部分 2021 可以用通配符表示成 2021*
        那么整个日期范围被拆解成三个部分,
        前面的 start -> 20201231  == 20200101 -> 20201231
        中间的 2021*
        后面的 20220101 -> end    == 20220101 -> end
        前面、后面的部分结构一致,从而递归的进行
        """
        for i in range(int(start[:4]) + 1, int(end[:4])):
            result.append("*".format(i))
        result.extend(timeWindow2timeWildcard(start, f"start[:4]1231"))
        result.extend(timeWindow2timeWildcard(f"end[:4]0101", end))
        return result
    if start[4:6] != end[4:6]:  # 月份不一样
        """
        以上文的[20200101,20201231] 为例
        此时年份相同,月份不同,以刚才同样的思路,将日期范围拆解为三个部分
        前面的 20200101 -> 20200131
        中间的 202002*  
              202003*  
              202004*  
              202005*  
              202006*  
              202007*  
              202008*  
              202009*  
              202010*  
              202011* 
        后面的 20221201 -> 20221231
        依次递归
        """
        for i in range(int(start[4:6]) + 1, int(end[4:6])):
            result.append(":0>2*".format(start[:4], i))
        result.extend(
            timeWindow2timeWildcard(start, "".format(start[:6], _yyyyMM2dd(int(start[:4]), int(start[4:6])))))
        result.extend(timeWindow2timeWildcard("01".format(end[:6]), end))
        """
        合并同类型剪枝
        当获得了年份不同时的结果,可能会出现同类项,此时需要将其合并进一步的以通配符来表示
        其中月份通配符的表示方式有3种 2020*  = 202001 02 03 04 05 06 07 08 09 10 11 12
                                 20200* = 202001 02 03 04 04 05 06 07 08 09
                                 20201* = 202010 11 12
        日期的同通配符表示方式有5种   202001* =  从1号到到当月的所有日期
                                 2020010* = 20200101 02 02 03 04 05 06 07 08 09
                                 2020011* = 20200110 01 02 02 03 04 05 06 07 08 09
                                 2020012* = 20200120 01 02 02 03 04 05 06 07 08 09 (根据每个月日期决定)
                                 2020013* = 20200130 31 (根据每个月日期决定)

        以月份为例:
            当 2020年的12个月份都用通配符表示时,则可以向上合并,合并成年通配符,此时结果种恰有12个结果,且12个通配结果都在
            当 202001 02 03 04 04 05 06 07 08 09 9个月份都用通配符表示时,则可以合并成以第一个月份0*的通配
            当 202010 11 12 3个月份都用通配符表示时,则可以合并成以第二个月份1*的通配
        """
        if _count_month(result, start, 1, 13):
            _delete_month(result, start, 1, 13)
            result.append("*".format(start[:4]))
        if _count_month(result, start, 1, 10):
            _delete_month(result, start, 1, 10)
            result.append("0*".format(start[:4]))
        if _count_month(result, start, 10, 13):
            _delete_month(result, start, 10, 13)
            result.append("1*".format(start[:4]))
        return result

    if start[6:8] != end[6:8]:  # 日期不同
        for i in range(int(start[6:8]), int(end[6:8]) + 1):
            result.append(":0>2".format(start[:6], i))
        if _count_date(result, start, 1, _yyyyMM2dd(int(start[:4]), int(start[4:6])) + 1):
            result = ["*".format(start[:6])]
        if _count_date(result, start, 1, 10):
            _delete_date(result, start, 1, 10)
            result.append("0*".format(start[:6]))
        if _count_date(result, start, 10, 20):
            _delete_date(result, start, 10, 20)
            result.append("1*".format(start[:6]))
        if _count_date(result, start, 20, 30):
            _delete_date(result, start, 20, 30)
            result.append("2*".format(start[:6]))
        if _count_date(result, start, 30, 32):
            _delete_date(result, start, 30, 32)
            result.append("3*".format(start[:6]))

        return result


result = timeWindow2timeWildcard("20200301", "20210231")
result.sort()
print(result)

以上是关于日期范围的通配符表示-附pythonScala代码的主要内容,如果未能解决你的问题,请参考以下文章

日期范围的通配符表示-附python代码

使用具有相对日期范围和标准 SQL 的 Bigquery Table 通配符 [重复]

quartz表达式介绍

quartz表达式介绍 Quartz介绍

模糊查询

Linux -通配符