From 53306f0eac1b5a156872b17febed8d7ca12ca717 Mon Sep 17 00:00:00 2001 From: jcwang812 Date: Mon, 1 Jul 2024 01:07:51 +0000 Subject: [PATCH] =?UTF-8?q?!3=20mapReduce=E6=89=8B=E6=9C=BA=E5=8F=B7?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=A7=A3=E6=9E=90Demo=E6=8F=90=E4=BA=A4=20*?= =?UTF-8?q?=20=E6=89=8B=E6=9C=BA=E5=8F=B7excel=E6=96=87=E4=BB=B6=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E5=B9=B6=E5=85=A5=E5=BA=93--map=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=20*=20=E6=89=8B=E6=9C=BA=E5=8F=B7excel=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C--map=20reduce=20=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/demo.sql | 10 +- pom.xml | 10 ++ .../example/snailjob/bo/PhoneNumberBo.java | 18 +++ .../snailjob/bo/PhoneNumberCheckBo.java | 40 ++++++ .../snailjob/dao/PhoneNumberBaseMapper.java | 15 ++ .../example/snailjob/dao/PhoneNumberDao.java | 34 +++++ .../job/TestExcelAnalyseMapJobExecutor.java | 97 +++++++++++++ .../TestExcelAnalyseMapReduceJobExecutor.java | 132 ++++++++++++++++++ .../listener/PhoneNumberExcelListener.java | 78 +++++++++++ .../example/snailjob/po/PhoneNumberPo.java | 39 ++++++ src/main/resources/doc/number.xlsx | Bin 0 -> 11457 bytes 11 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/snailjob/bo/PhoneNumberBo.java create mode 100644 src/main/java/com/example/snailjob/bo/PhoneNumberCheckBo.java create mode 100644 src/main/java/com/example/snailjob/dao/PhoneNumberBaseMapper.java create mode 100644 src/main/java/com/example/snailjob/dao/PhoneNumberDao.java create mode 100644 src/main/java/com/example/snailjob/job/TestExcelAnalyseMapJobExecutor.java create mode 100644 src/main/java/com/example/snailjob/job/TestExcelAnalyseMapReduceJobExecutor.java create mode 100644 src/main/java/com/example/snailjob/listener/PhoneNumberExcelListener.java create mode 100644 src/main/java/com/example/snailjob/po/PhoneNumberPo.java create mode 100644 src/main/resources/doc/number.xlsx diff --git a/docs/demo.sql b/docs/demo.sql index 1498ad0..2a38e66 100644 --- a/docs/demo.sql +++ b/docs/demo.sql @@ -12,4 +12,12 @@ CREATE TABLE fail_order `create_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`) -); \ No newline at end of file +); + +-- 手机号表 +CREATE TABLE `phone_number` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `phone_number` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='手机号表' diff --git a/pom.xml b/pom.xml index 716b2d1..51efe03 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,16 @@ okhttp 4.2.0 + + com.alibaba + easyexcel + 3.1.3 + + + com.alibaba + fastjson + 1.2.83 + diff --git a/src/main/java/com/example/snailjob/bo/PhoneNumberBo.java b/src/main/java/com/example/snailjob/bo/PhoneNumberBo.java new file mode 100644 index 0000000..2b79e6d --- /dev/null +++ b/src/main/java/com/example/snailjob/bo/PhoneNumberBo.java @@ -0,0 +1,18 @@ +package com.example.snailjob.bo; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * excel表格手机号BO + * + * @author JiChenWang + * @since 2024/6/27 20:28 + */ +@Data +public class PhoneNumberBo { + + @ExcelProperty(value = "手机号码", index = 0) + private String phoneNumber; + +} diff --git a/src/main/java/com/example/snailjob/bo/PhoneNumberCheckBo.java b/src/main/java/com/example/snailjob/bo/PhoneNumberCheckBo.java new file mode 100644 index 0000000..caa8adb --- /dev/null +++ b/src/main/java/com/example/snailjob/bo/PhoneNumberCheckBo.java @@ -0,0 +1,40 @@ +package com.example.snailjob.bo; + +import com.example.snailjob.po.PhoneNumberPo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * 手机号检测BO + * + * @author JiChenWang + * @since 2024/6/27 20:50 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PhoneNumberCheckBo { + + @Schema(description = "检测总条数", accessMode = Schema.AccessMode.READ_WRITE) + private Long checkTotalNum = 0L; + + @Schema(description = "检测失败条数", accessMode = Schema.AccessMode.READ_WRITE) + private Long checkErrorNum = 0L; + + @Schema(description = "检测成功条数", accessMode = Schema.AccessMode.READ_WRITE) + private Long checkSuccessNum = 0L; + + @Schema(description = "检测失败临时的数据", accessMode = Schema.AccessMode.READ_WRITE) + private List checkErrorPhoneNumberList = new ArrayList<>(); + + @Schema(description = "检测成功临时的数据", accessMode = Schema.AccessMode.READ_WRITE) + private List checkSuccessPhoneNumberList = new ArrayList<>(); + +} diff --git a/src/main/java/com/example/snailjob/dao/PhoneNumberBaseMapper.java b/src/main/java/com/example/snailjob/dao/PhoneNumberBaseMapper.java new file mode 100644 index 0000000..fe36b1d --- /dev/null +++ b/src/main/java/com/example/snailjob/dao/PhoneNumberBaseMapper.java @@ -0,0 +1,15 @@ +package com.example.snailjob.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.snailjob.po.PhoneNumberPo; +import org.springframework.stereotype.Repository; + +/** + * 手机号mapper + * + * @author JiChenWang + * @since 2024/6/30 11:55 + */ +@Repository +public interface PhoneNumberBaseMapper extends BaseMapper { +} diff --git a/src/main/java/com/example/snailjob/dao/PhoneNumberDao.java b/src/main/java/com/example/snailjob/dao/PhoneNumberDao.java new file mode 100644 index 0000000..18d71dd --- /dev/null +++ b/src/main/java/com/example/snailjob/dao/PhoneNumberDao.java @@ -0,0 +1,34 @@ +package com.example.snailjob.dao; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.snailjob.po.PhoneNumberPo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * TODO + * + * @author JiChenWang + * @since 2024/6/30 11:58 + */ +@Service +public class PhoneNumberDao extends ServiceImpl { + + @Autowired + private PhoneNumberBaseMapper phoneNumberBaseMapper; + + /** + * 批量保存手机号信息 + * + * @param phoneNumberPoList 手机号po列表 + * @return Boolean 保存成功标识:true-成功、false-失败 + * @author JichenWang + * @since 2024/6/30 12:03 + */ + public Boolean insertBatch (List phoneNumberPoList) { + return this.saveBatch(phoneNumberPoList); + } + +} diff --git a/src/main/java/com/example/snailjob/job/TestExcelAnalyseMapJobExecutor.java b/src/main/java/com/example/snailjob/job/TestExcelAnalyseMapJobExecutor.java new file mode 100644 index 0000000..3b577dd --- /dev/null +++ b/src/main/java/com/example/snailjob/job/TestExcelAnalyseMapJobExecutor.java @@ -0,0 +1,97 @@ +package com.example.snailjob.job; + +import cn.hutool.core.util.ObjectUtil; +import com.aizuda.snailjob.client.job.core.MapHandler; +import com.aizuda.snailjob.client.job.core.annotation.JobExecutor; +import com.aizuda.snailjob.client.job.core.annotation.MapExecutor; +import com.aizuda.snailjob.client.job.core.dto.MapArgs; +import com.aizuda.snailjob.client.model.ExecuteResult; +import com.alibaba.excel.EasyExcel; +import com.example.snailjob.bo.PhoneNumberBo; +import com.example.snailjob.bo.PhoneNumberCheckBo; +import com.example.snailjob.dao.PhoneNumberDao; +import com.example.snailjob.listener.PhoneNumberExcelListener; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.List; + +/** + * 解析手机号excel文件,并将正确的手机号分片入库 + * + * @author JiChenWang + * @since 2024/6/30 10:37 + */ +@Slf4j +@Component +@JobExecutor(name = "testExcelAnalyseMapJobExecutor") +public class TestExcelAnalyseMapJobExecutor { + + private final Integer BATCH_SIZE = 100; + + @Autowired + private PhoneNumberDao phoneNumberDao; + + /** + * 读取手机号文件总行数,并进行分组 + * 比如文档中的手机号总量为307条,每100条一个分组,分组结果为[{0,99}, {100, 199}, {200,299}, {300, 307}] + * + * @return ExecuteResult + * @author JichenWang + * @since 2024/6/30 10:48 + */ + @MapExecutor + public ExecuteResult rootMapExecute(MapArgs mapArgs, MapHandler mapHandler) { + List> ranges = null; + // 先获取文件总行数,便于分组 + try { + @Cleanup InputStream numberInputStream = getClass().getClassLoader().getResourceAsStream("doc/number.xlsx"); + final PhoneNumberCheckBo phoneNumberCheckBo = new PhoneNumberCheckBo(); + PhoneNumberExcelListener phoneNumberExcelListener = new PhoneNumberExcelListener(phoneNumberCheckBo, true, BATCH_SIZE); + EasyExcel.read(numberInputStream, PhoneNumberBo.class, phoneNumberExcelListener).sheet().headRowNumber(1).doReadSync(); + + // 设置区间范围 + ranges = TestMapReduceJobExecutor.doSharding(0L, phoneNumberCheckBo.getCheckTotalNum(), BATCH_SIZE); + } catch (Exception e) { + log.error("文件读取异常", e.getMessage()); + } + return mapHandler.doMap(ranges, "MONTH_MAP"); + + } + + /** + * + * + * @param mapArgs + * @return ExecuteResult + * @author JichenWang + * @since 2024/6/30 11:05 + */ + @MapExecutor(taskName = "MONTH_MAP") + public ExecuteResult monthMapExecute(MapArgs mapArgs) { + // 获取本次要处理的区间 + final List mapResult = (List) mapArgs.getMapResult(); + log.info("本次要处理的区间为:{}", mapResult); + + // 按照处理区间,去读取数据 + final PhoneNumberCheckBo phoneNumberCheckBo = new PhoneNumberCheckBo(); + try { + @Cleanup InputStream numberInputStream = getClass().getClassLoader().getResourceAsStream("doc/number.xlsx"); + PhoneNumberExcelListener phoneNumberExcelListener = new PhoneNumberExcelListener(phoneNumberCheckBo, false, BATCH_SIZE); + EasyExcel.read(numberInputStream, PhoneNumberBo.class, phoneNumberExcelListener).sheet().headRowNumber(mapResult.get(0) + 1).doReadSync(); + } catch (Exception e) { + log.error("文件读取异常:", e.getMessage()); + } + + // 如果正确手机号不为空,则入库 + if (ObjectUtil.isNotEmpty(phoneNumberCheckBo.getCheckSuccessPhoneNumberList())) { + phoneNumberDao.insertBatch(phoneNumberCheckBo.getCheckSuccessPhoneNumberList()); + } + + return ExecuteResult.success(phoneNumberCheckBo.getCheckSuccessNum()); + } + +} diff --git a/src/main/java/com/example/snailjob/job/TestExcelAnalyseMapReduceJobExecutor.java b/src/main/java/com/example/snailjob/job/TestExcelAnalyseMapReduceJobExecutor.java new file mode 100644 index 0000000..aabf428 --- /dev/null +++ b/src/main/java/com/example/snailjob/job/TestExcelAnalyseMapReduceJobExecutor.java @@ -0,0 +1,132 @@ +package com.example.snailjob.job; + +import com.aizuda.snailjob.client.job.core.MapHandler; +import com.aizuda.snailjob.client.job.core.annotation.JobExecutor; +import com.aizuda.snailjob.client.job.core.annotation.MapExecutor; +import com.aizuda.snailjob.client.job.core.annotation.MergeReduceExecutor; +import com.aizuda.snailjob.client.job.core.annotation.ReduceExecutor; +import com.aizuda.snailjob.client.job.core.dto.MapArgs; +import com.aizuda.snailjob.client.job.core.dto.MergeReduceArgs; +import com.aizuda.snailjob.client.job.core.dto.ReduceArgs; +import com.aizuda.snailjob.client.model.ExecuteResult; +import com.alibaba.excel.EasyExcel; +import com.alibaba.fastjson.JSONArray; +import com.example.snailjob.bo.PhoneNumberBo; +import com.example.snailjob.bo.PhoneNumberCheckBo; +import com.example.snailjob.listener.PhoneNumberExcelListener; +import lombok.Cleanup; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * 解析校验Excel中的手机号,统计出错手机号数量,并返回错误手机号详情 + * + * @author JichenWang + * @since 2024/6/27 19:52 + */ +@Slf4j +@Component +@JobExecutor(name = "TestExcelAnalyseMapReduceJobExecutor") +public class TestExcelAnalyseMapReduceJobExecutor { + + private final Integer BATCH_SIZE = 100; + + /** + * 处理手机号文件信息,将文档中的手机号进行分组 + * 比如文档中的手机号总量为307条,每100条一个分组,分组结果为[{0,99}, {100, 199}, {200,299}, {300, 307}] + * + * @return ExecuteResult + * @author JichenWang + * @since 2024/6/29 14:03 + */ + @MapExecutor + public ExecuteResult rootMapExecute(MapArgs mapArgs, MapHandler mapHandler) { + List> ranges = null; + // 先获取文件总行数,便于分组 + try { + @Cleanup InputStream numberInputStream = getClass().getClassLoader().getResourceAsStream("doc/number.xlsx"); + final PhoneNumberCheckBo phoneNumberCheckBo = new PhoneNumberCheckBo(); + PhoneNumberExcelListener phoneNumberExcelListener = new PhoneNumberExcelListener(phoneNumberCheckBo, true, BATCH_SIZE); + EasyExcel.read(numberInputStream, PhoneNumberBo.class, phoneNumberExcelListener).sheet().headRowNumber(1).doReadSync(); + + // 设置区间范围 + ranges = TestMapReduceJobExecutor.doSharding(0L, phoneNumberCheckBo.getCheckTotalNum(), BATCH_SIZE); + } catch (Exception e) { + log.error("文件读取异常", e.getMessage()); + } + return mapHandler.doMap(ranges, "MONTH_MAP"); + } + + /** + * 处理每个分组内容,如读取{0,99}区间的手机号,并解析 + * + * @return ExecuteResult + * @author JichenWang + * @since 2024/6/29 14:04 + */ + @MapExecutor(taskName = "MONTH_MAP") + public ExecuteResult monthMapExecute(MapArgs mapArgs) { + // 获取本次要处理的区间 + final List mapResult = (List) mapArgs.getMapResult(); + log.info("本次要处理的区间为:{}", mapResult); + + // 按照处理区间,去读取数据 + final PhoneNumberCheckBo phoneNumberCheckBo = new PhoneNumberCheckBo(); + try { + @Cleanup InputStream numberInputStream = getClass().getClassLoader().getResourceAsStream("doc/number.xlsx"); + PhoneNumberExcelListener phoneNumberExcelListener = new PhoneNumberExcelListener(phoneNumberCheckBo, false, BATCH_SIZE); + EasyExcel.read(numberInputStream, PhoneNumberBo.class, phoneNumberExcelListener).sheet().headRowNumber(mapResult.get(0) + 1).doReadSync(); + } catch (Exception e) { + log.error("文件读取异常:", e.getMessage()); + } + + return ExecuteResult.success(phoneNumberCheckBo); + } + + + @ReduceExecutor + public ExecuteResult reduceExecute(ReduceArgs mapReduceArgs) { + log.info("WJC Test reduceExecute, 参数为:{}", mapReduceArgs.getMapResult()); + final PhoneNumberCheckBo phoneNumberCheckBo = this.buildGatherPhoneNumberCheckBo(mapReduceArgs.getMapResult().toString()); + return ExecuteResult.success(phoneNumberCheckBo); + } + + /** + * 当只有一个reduce任务时无此执行器 + */ + @MergeReduceExecutor + public ExecuteResult mergeReduceExecute(MergeReduceArgs mergeReduceArgs) { + final PhoneNumberCheckBo phoneNumberCheckBo = this.buildGatherPhoneNumberCheckBo(mergeReduceArgs.getReduces().toString()); + log.info("WJC 最终检测结果为:{}", phoneNumberCheckBo); + return ExecuteResult.success(phoneNumberCheckBo); + } + + /** + * 构造汇总手机号校验结果BO + * + * @param phoneNumberCheckBoStr 手机号校验BO字符串 + * @return PhoneNumberCheckBo 汇总手机号校验结果BO + * @author JichenWang + * @since 2024/6/29 14:24 + */ + private PhoneNumberCheckBo buildGatherPhoneNumberCheckBo(String phoneNumberCheckBoStr) { + final List phoneNumberCheckBoList = JSONArray.parseArray(phoneNumberCheckBoStr, PhoneNumberCheckBo.class); + // 获取校验总数 + final long checkTotalNum = phoneNumberCheckBoList.get(0).getCheckTotalNum(); + // 汇总校验失败数量 + final long checkErrorNum = phoneNumberCheckBoList.stream().mapToLong(PhoneNumberCheckBo::getCheckErrorNum).sum(); + // 汇总校验成功数量 + final long checkSuccessNum = phoneNumberCheckBoList.stream().mapToLong(PhoneNumberCheckBo::getCheckSuccessNum).sum(); + // 汇总错误手机号 + final List errorPhoneNumberList = new ArrayList<>(); + phoneNumberCheckBoList.forEach(item -> errorPhoneNumberList.addAll(item.getCheckErrorPhoneNumberList())); + + // 汇总手机号校验结果 + return PhoneNumberCheckBo.builder().checkTotalNum(checkTotalNum).checkErrorNum(checkErrorNum).checkSuccessNum(checkSuccessNum).checkErrorPhoneNumberList(errorPhoneNumberList).build(); + } + +} diff --git a/src/main/java/com/example/snailjob/listener/PhoneNumberExcelListener.java b/src/main/java/com/example/snailjob/listener/PhoneNumberExcelListener.java new file mode 100644 index 0000000..62bf08f --- /dev/null +++ b/src/main/java/com/example/snailjob/listener/PhoneNumberExcelListener.java @@ -0,0 +1,78 @@ +package com.example.snailjob.listener; + +import cn.hutool.core.lang.Validator; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.example.snailjob.bo.PhoneNumberBo; +import com.example.snailjob.bo.PhoneNumberCheckBo; +import com.example.snailjob.po.PhoneNumberPo; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 手机号excel解析 Listener + * + * @author JiChenWang + * @since 2024/6/27 20:38 + */ +@Slf4j +public class PhoneNumberExcelListener extends AnalysisEventListener { + + /** 手机号校验BO **/ + private PhoneNumberCheckBo phoneNumberCheckBo; + + /** 是否第一次读取Excel **/ + private Boolean firstReadStatus = false; + + /** 读取批次大小 **/ + private Integer batchSize = 100; + + /** 已读取的数据数量 **/ + private Integer cacheSize = 0; + + @Override + public void invokeHeadMap(Map headMap, AnalysisContext context) { + this.phoneNumberCheckBo.setCheckTotalNum(Long.parseLong(String.valueOf(context.readSheetHolder().getApproximateTotalRowNumber() - 1))); + } + + @Override + public void invoke(PhoneNumberBo phoneNumberBo, AnalysisContext context) { + // 如果是第一次读该文件,已读取的数量已超过读取批次大小,直接返回 + if (firstReadStatus || cacheSize >= batchSize) { + return; + } + + cacheSize++; + + if (ObjectUtil.isEmpty(phoneNumberBo.getPhoneNumber())) { + return; + } + + // 校验手机号 + log.info("本次校验的手机号为: {}", phoneNumberBo.getPhoneNumber()); + Boolean validateStatus = Validator.isMobile(phoneNumberBo.getPhoneNumber()); + if (validateStatus) { + this.phoneNumberCheckBo.setCheckSuccessNum(this.phoneNumberCheckBo.getCheckSuccessNum() + 1); + final PhoneNumberPo phoneNumberPo = PhoneNumberPo.builder().phoneNumber(phoneNumberBo.getPhoneNumber()).createTime(LocalDateTime.now()).build(); + this.phoneNumberCheckBo.getCheckSuccessPhoneNumberList().add(phoneNumberPo); + } else { + this.phoneNumberCheckBo.setCheckErrorNum(this.phoneNumberCheckBo.getCheckErrorNum() + 1); + this.phoneNumberCheckBo.getCheckErrorPhoneNumberList().add(phoneNumberBo.getPhoneNumber()); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + + } + + public PhoneNumberExcelListener(PhoneNumberCheckBo phoneNumberCheckBo, Boolean firstReadStatus, Integer batchSize) { + this.phoneNumberCheckBo = phoneNumberCheckBo; + this.firstReadStatus = firstReadStatus; + this.batchSize = batchSize; + } + +} diff --git a/src/main/java/com/example/snailjob/po/PhoneNumberPo.java b/src/main/java/com/example/snailjob/po/PhoneNumberPo.java new file mode 100644 index 0000000..6372220 --- /dev/null +++ b/src/main/java/com/example/snailjob/po/PhoneNumberPo.java @@ -0,0 +1,39 @@ +package com.example.snailjob.po; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * phone_number + * + * @author JiChenWang + * @since 2024/6/30 11:48 + */ +@TableName("phone_number") +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PhoneNumberPo { + + /** + * 主键 + */ + private Long id; + + /** + * 手机号 + */ + private String phoneNumber; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + +} diff --git a/src/main/resources/doc/number.xlsx b/src/main/resources/doc/number.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4653ed0c4290157a2a9f5ee2cd07e6878be029be GIT binary patch literal 11457 zcmeHtbzD_T_x_=!yFox&q`ON+IFyt$f^>H`NFxf;4H67B;LW$ImV#mziZ| z%Eaz*Z~1rNJbW(lr8TC#;FVUInb~z(=sA`|CcZguDNg^42glD|(=uzTO8=1dS|zk_ zzs@hJx}?0UZ5^AmjfBL}_L>?y8F-{(?8JI{k;ciO1iPvhC9cN{;xdIKruYORzDuc7 z9ctL~?NGD=&3yjO>85Y;YIE3p-5-asmIiT0%*_}*boZB-rQyRLc+3zu>*gp@sVg%Y zC_(Md;=v6L4Aq>YFJHmVu?4-Y$C&M#N33n&y)VZ_QuZiIE0{x!@A$ARTMv4wr97Qa`v2d0hg?Gp46}=E$XeI zJkbNIN8fTf(jF`D^FH+V&g<-|=KG?s^@_~XI(HCh8UD%1HQo{STFw3!39PO!R5V_} zTi-g@1U)tfRWrZpm^#L%}WPz+h1=3Q_-pJB{mF4F8 zzjXXB&cVOjdSSGjLJJ#O(9W}q;LhXG*|%5{U>EUcwNxtJzS1+86_MFAq_d6mWLPRB z0dP`2_1+iV)3ZX6>z!0bi(G}_ICz58l`cg=@n?2UNDt}k1XDsL9~L#nDMlNgOxuA!3CM~LbEZWM$2#K_V@=x(6FA0t#u zCrwX|?(NlG7|Xf!Su|yzp{>A;BrUMS$E1>K2Q6&V8n(~NQiSkg8#bAjC3_o zy6p+Ca~H8Q`B-e$;;j9qcQU^2rhv?FaaQDyi3uSeH$jCwT-`dY)BUTWN%R6^H+J~pQ%E>h`DuqIu8MV1-2*EjosW)BaoRDCzvC~H$| zY>hd@5PeikZ`3pA$kfUWMH{@L+_)z*SPU}gBQ4f@l)nKnvWIR4e%MEBo_jkm`RwG$ zt^ym{)s@^!0e96NFynjmADq5VwT7gA;zry%ee2(AhaoSC^473_W@Pt^%+qv3eDabk zUN(Op(nw!Op&HJ!Ir_n7INtesKE2(bS(}`Vfzfp)E4dt!akKm(w>Iui?b}$D2E8I= zo!+rWbudg#nNd29c5nA19UoR^cx9VVE(;6sJss@CjHJK!YRHsMuwYWfk;Jh&e8Yv? zq%^Mr^jffLb0-aq7>RJ^ude^WNMdz8XnktUh|_tH7ojPedn<09AEFa1tB}>O^whB+ zyY6R0IP^uS8YL~5iThawPW_`qiCBg~LG0nBH>a2!xj9|v{>zqd6U9Dqc&$`1^(r>JG8$Z+0Ha~>1KRoG%@X+;MUED3oW?@?n-F*gc5#*{^mqGK@^}%Iob+wsm0%YZ9&eb)@LNlkb&1 zsPsMWkePZvfNEU!hP~;k)3qeBL20UwQWE3fnJ>Mh5Q&L~VYj)EC{Qw5ZokBp+M-#{bFVn;ombNHS;_uHg39yOh_>wm+v;c4vp~&R$tNsv;hq4DwEC+If?Pt5H|%f}Lvp5o!)c{+uL@ zC&>SnvmMCf$|t}hJ^%>>Lchyd2S+zcBZr&EJ4ChDd|C{vf$$8YXAb{z!rqFSvDFUE z4-Z8;bG!ml7QY>(UKE)!q{ZqLo#u{Ne_$qaMwB`l#f|h`7elM?Jm^@R{ zVBC5%Bk`;2)|QZN6(iYVgNrNUg$VYC=Hi}txmh}v zAw(bCo;W$GP890s4vm(Hp$%4H@_DADa!O5}A>$%$z7fFZpQ9Q3keA+fqd>@n(JNuQ zHMN>Xm=b1h|Lw)Vp+EI@tT)}zz^G$Sp(kIcJ*`}*QpG#Ec5#bmS$hOl$VhH*n{>|$ zs)ss#WfW=VHEFOR{C3xu8<$b(V{y1mC&i^a))rY{#aeL^w5?3djWJcW{5tL$o^ZN& z$sb)l;mW~GRQH!I5(J5&?($X-*2}IcHJ~Ixp{e6uU^_{7{6|;}X%s zH&(G9sSG^T^(J%orFamFzN#Yg;>?+5Yeb0Ze4Ug1P8j}$*rU9ZlWbrhF|Ztpi_veg ze`>IZFn78-*Xr3+SBNC!7$uq#N0wV)-BpSA8MBh;m`CpH&?x|N4ps1f=R$CnR!-#F zz)AM5l=D8igbDUplP6b-PGERRyvNB`=sL7vUe+|(laE_gvClA1HBWIK^ozK5cYgr}Nn7TlE^itmZi@%-L98Ha^j9724>^F(Bsi754%8TdCdLW9nzkR~9 z)PrO5lY82b7g>WoCrVT6h~X14vGiOdvR|}FtVHJ9xH>!;y)#7_RAw#+<|XTF^gvJ4 zk9WqK@5bzllcVEibqJy^QnRxi-iV~~*PQgFCb&x_pf*Rx=`|?s38zF%B+v}U`8e<~ zaCs^`dmn*qglv6^`sl)?a^8F6!BjN%n^L5`&eCVKc)Fu58sw?G-W99bmIJSkk!ZYaJLFmBuLc9gI_AW`Ee+o z`OC+r4M{Ls{v?7TkC+10Ls(Yku(Oyca!JdVs2Zrfq8?B!gvaR(iF(_e8(>=*NvK%A zqo#6O~3kJ@C2u7zYVN1&M*5vo5J z3wke{Mr6kk!+gzQdWhb+Bo!sUeeG(0ofm^CDg?dO;i*zH>e|XMH1EDVJk0a)`RRAO zim7*X_^sS=eb`lhG2QcgFecr{E6H8mZab_a-RF9_V~0ZYVmZp@J|CotUw~~v66e)| zPFu$uIzbC8`Oy;Gl>tu6J!~-SlZ@%i8dhplS0}%G)bXZDR2#dO#Du#;7nMdE-mR{I$PYrah8EG;c9ZX8YueXDfah!Rr8&{~F4*qQWxU)@l!5pQ!`;y9xW zP0}JfSi!3UgWb7s+e_k6?sTNF`=j7;+cfRhr9%i&!X)T z)rQ4(>YuR>n7*)Wm+TBav80C51hb^phOpyKz6zeb*T$LGOA{))S-Xg(gy%M%=-70e zP%k;c*$P8T+Ts=dl4+)&&OceKI9J-uKi}s240aDVVe;Hgoc>;|dT#HlaGmKCzVd9& zbj^?T;p|U3Oe0EN_H}|0$a%^=(@4GeVbvy^id$K3n zp&xBB%VYP$(y}imbLqX#*ZPQ>ml7lMd2lcjxlSenCD7AOU?mAe{?z`M*R1|N)yAe$ ze{o@v$^R&m6}5;UxHhjZhQd~WeHHc5Ddpm1yPRumtWtUgs{9+z&p zO}XS?7!>MHbD`3EUl&(ZCULI>jiPFO`2+kricVBzQsv&4ITU=E3_Qn-;}+&pD1OB! zJ;uLLSHS<-{Nmb6wcLy~eVk*p;;S3;lKZ(kbA_yKgP2i_i<@IV{oKS}B9_eYivOZc zW~~V9oM}58(a09VY>ajV$OPN-D~T+Db@#itpL{Vp*u+~>riewEW!5j{pX9nftRu!9 zU>|C!$qrLk8}rI@tLtbt%PZGb)9D>~4cp~ehgaO538qzcEJ}0PQCoKsbu6eY1jD`G z3@6f;H70%TMD$z_Ljo_&I8b*oQGL)+eNb^dK2!6l-3khu6+XV}%F(wc^((>RQ{q9! zWbapB;pagT>mrI#`VybI$8dg3gi$nL<)n;qRGzG{b2E1`D-f3Yz)T(CSRTnX#O6=9 z@JO&X?p&k2CNX$~Ljh_4CAH^Vj~vIgc=8~x+K-M-d7xF$`n1CQUOW>um&GaW52BV1 zS41fFh!$a%a~!U5qBaYfC3>DFPvN(qTx{$8sZH;GAbtcAJjZg$?n)e?u)0uIQYVV3^j-9a{W#P=Sep zNMSOmDRYBGD4_uEC{|(CgkrSY0nzNcNp9Q2^%2LIwGF{#^iO+|BV`YBtS{x6?Tal~ zpcpu#ZZbkkBVKRdn%J1rzjDZF)XBGmm6t!ChHyT*ho&&Xb7JN6lu!6)8NFy{YM-W) z*rN@Z)RVLCGj*fJNL7A9?zTRBc}hPNn^U=^6c2KE-!fW#A%IeslVFRuIml~X%_>b` z)Wnu(F>p6~Q{^M(NKjk8K;2?1j62tdj%=uGH#I%Tt|rT^I)bZi&IsS!){Xi+JKxcx zm4r%7K$zH&&T8Irch!roN+8jaYHTO`w=brjBbF#<@rCzgY zj`W~LKbyaL)kBJe4Dx9GuTTC>mn`0hypo@%u2P{z(&CYm6OCp)a^g8%824`0jV<#VPo zq>T?|s7YuBG8y=}(ObD0qCSOLDLAWcT6RX73_no$+(oZ1%$(VrH%LGVZwG4pdFEV{ z+fz)%9C+EtA=S#HpP(HdXW7OzzNu1tzy~k6e&vd17RJs!A_)3u!T7ByBsFL(ost0W z_{ThVnS)6ZLDe^|s%afUs6osUIu zwyPlHv&*DmY3Fu#K3SaGdm+PcQ>f;ftUB5FD7UolQD#R`eI~K3D8YpxuWK^G4Qq(n z`Hl89=EWOn$YyHUe|lb`p#&nSNHhG*Jx8fv6=#f)cd%S;Z2w`>BqriHPAtlgDzTfrtLJp z*QMKsH<*=@H?m{Y?wKsn>%kLR&7kQJxz4D|Yg-igr6z`%j(8CSnbtnH?EYonm%>Y; zK-jOZZ!eB1T4CB!SdXg=ATYC@Gx5aeM0MCy@KH|mxukV>2vj=yp5h$=a=c}6xYWVb#7j#yJ$Whx%)19bM>be z&l(%7S#25{8tN{0Pj-HGU+bkEt@Iz9?yaI`_qkqlbx`O%uUt$te{vdDiQlinH1%Th zrrNg6grW6QwqkJ?Kd;C&SJUn+tVc~~n1YFeRnK5@qmPHL;6IT)Xu_`#-coC zCT#2WjW^5i1}*^HuPV?vd*^EHt>T#~s`q$Vqh+et<~CrA9_HVLJzNdwEWx;nyrjU=7Q1Z*05R{Z-@kKB7_biRLT^p`vkLg_aYHl! zc*p7HFNSaV9-wA+%{8JhpB1}p1^^ZA0CfQbAC`~kA@#qUx?}R_hGkl@&0j44ibVWf z1X*uXC8G2f_zOisA`98&MTcb!G+`;H;SWToX<#{8DG{deZn<}1XO!?8Y*(ld^kSGs@@T?dJdzV*^fCLhvXZ#MBnCt|g0UD?+&0SY z=A<(7EFu*R^%rV#cma=4EqV$<4F-(#y)?L=qcuM&WM|?qhUvCk@t>C$M0snhU!<|+ z%Hj8hPCcwE86l)Rb|d*gn0V@ICzA>Qt-pDyd;IJP}G@)U(^_NKV}c9C^0VTEJVn!i0x5WrwJ1;eqCYVgwvKW z-h5Ue5Nd#BsgJIe>Z%m+CXByBZFvHgX1e>3nl4Pd{Pm$FAV)dXoKy+ODO&04Xr(@8 z@2o7TEb6R8_)!_#Bjt(RMmf_=QdJXTplGe1s-*+?UsW(Nf=0BTRGsKpoXr zho4vlI;^z6lm_JIo2PE%2W<2&Z{+V)mmt3Jo}v8KOj2DFs;_9PPp&=N%J!_fgrL~k zQ@JF)}U^J3d28<_HPX}>C`gOkrHMssj*z+0Rn;A>eM#^GAxBL z7WVcqO4ci)jUVR#L-zXh+BN_Y&=TWf-YlXKZFSyp(&uRQEQP5Xkqn5!gk$}OPw@gW zP%VDAx9qPvf@b+^=%ffK*VKw<0N&S{-D~I+2r1XqScc)BqZP0ee%b)&9(!Hk1kn4V zrlg@bfB{jTaBSixN}J6BwHR4MA9U0$#>MdhDp5DK1WcJ5)*ig(bdmec!doM=@Kq-U z3TAU*A`FYMR`!L0Ew+h_O?EIlx&@oUO@^|YMO*lMypw$9hG3Dj9S~2%; zoffyUBX6PL>efp8BC4>anH=sL8%;As+Ijz<)iupzZkNd8sV_Gey}Z*u%#&-U$6Tu- zS*j+dtpLz9cR=8#^#4_c{VJnx%WXUMiSEHzRByxWXqFlchjGL0`@u^WTw^obiM1X2 z)cJOK3`t*gZtl*L$8fvsM_BGHAGf|{k|QM4LcSMw&D^m7$p$&Z-)h1;RN1EnIpe+D z9!jdEbBG6cU>OGP7+`OlWa{}Hd3Uh#l7eBmpW6Fw4E~GS)ht((?v$Kw1=wOMsZhti zLJR860!ap0#E(sJYt+J9RM{s6-L;Pm$hnm`n-%HARDesIna0gb$>-K;>dKwv##oxB z0B$p#hzfNyEyfMEa0+L;ok%o;o2GIq{^-u7vWS1V1;h)25%3pl@{${D>a~!u;;t!L z^_y5jFenZxF#G3dQrg*c?XpwLri(6C&7Aj~k>`NXC27I#S__%8 zrniazrHD4iQdBkFb!!Lkk0785ys`C5{KgKU7kbpbv!GioyvM*m$WOKUpVf==Bw@c`qJeik_i&lV27^;l8mm%BVO=dmhd=?z{)ll?+BT?8 z-6BYl7lZb317f5`fyb_S+EHy<%J&3Kp7%Z;4_pF>)8s;1n$L#cRnn`sR8H4t9w#MO zT>#7B|J>XPPs+m`0KEDE{P2Juu({=6s&8*(sO)HOW^Ho2Ud|Q8E)U0!^~8f_`=y+M zv9ur7p1>2D4~}@YAPbl13dnuIw8@b7jMxZ-8Q%z=K^2<0+3t0J{(OCX?);NXsyzDv zl^yDMIU<@}{Mnaeg0%ZPpRJoh+_x_i5;P}bzBKFTs$+9=_Y5^>B<*13kTSR@=E!QN zIz1e;h&2j`Mm^OD#xcfTX5$4D1B_lL3xuM1BQem)8T}2OHQC zHne%AXm4Zd!1~I@-ssm^rvGgO14APsS`Xa9hSxTa&?wpEk$jct5L5{lE6k6#spyH? zwMAeV6aBc>%VXI2Q!L|2LW+YA|7x@&^fS{AE(BY~m;ps%+0QTx71}$HtA2{QObn*Y zi;aGp2}cC^%F3!VhLy`DWH#zs2^+EcYzS-2(A30Al8wg_iJRNc;Te6L%=k!6_S|An znM(};eYS@~_XK|H2Y~5hIy09g|a`$yYzx}&R-b{$M zR!DMhaE!w{T39`Q)cdHV9W*Ql8!yc9CBxr6%8g+5eE3*ygyn;5Zad;ogi*f?z5!gT z*-W>h>ex3UHWzgF=sY)XIy&4^|DZaQ?EgxDM69hL&HX z^rtrjT>~L@}?%*taTmks~3(Gp1^C1)ctW zhBwLf2y_v$4~&(-?!_22l#zS@lOC%!v8q_j|IlxQGS0z_!H$T_1zu({=V=#!ry1u wKlMSNJ{l0{AM@?+>VJPU{#9Lt{x9mk9+Zk;1R%N~5DM`11s=6b4{vJ!2cXw&761SM literal 0 HcmV?d00001