日期范围的通配符表示-附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代码的主要内容,如果未能解决你的问题,请参考以下文章