简介本资源是一份面向大数据初学者与Hadoop实践者的完整日志分析项目聚焦于使用MapReduce实现网站访问日志的按日期统计功能解决真实场景中PB级日志数据的分布式聚合需求。压缩包共33个文件含4个核心Java源码含Mapper/Reducer主逻辑、10个编译后class文件、3个XML配置Hadoop环境与项目依赖、3个properties参数文件及1个测试用txt日志数据user_login.txt辅以Maven工程结构pom.xml、.project、.classpath和IDE调试配置整体3.5MB开箱即用。目前已有2050人学习下载。读者可直接导入Eclipse/IDEA运行获得从日志解析、MapReduce编程、本地/伪分布式调试到结果验证的全流程实践材料代码结构清晰关键注释完备并隐含Combiner优化与日期字段提取等进阶设计点是理解Hadoop批处理核心机制的典型教学范例。1. Hadoop按日期统计访问次数不是写个MapReduce就完事而是要让日志时间戳对齐、分区可复用、结果能直接进BI看板你手头有一堆Nginx或App服务器的原始访问日志每行类似192.168.1.100 - - [15/Jul/2023:14:22:03 0800] GET /api/v1/user?id123 HTTP/1.1 200 1245现在领导说“明天早会前把最近7天每天的总访问量、独立IP数、4xx错误率拉出来”你第一反应是不是立刻翻出《Hadoop权威指南》第5章抄个WordCount改个正则别急——我去年在某高校大数据实验室带学生做模拟项目X时就栽在这类“简单需求”上MapReduce跑通了但日期字段解析错位导致7月15日的数据全算进7月14日分区路径写死导致第二天要手动改脚本输出文件名带下划线不被BI工具识别最后硬是用Python脚本二次清洗才救回来。这份资源不是教你怎么写Mapper/Reducer而是提供一套可复用、可调度、可验证的Hadoop日期统计流水线含真实日志生成器支持自定义QPS、时间偏移、错误率、预编译的Java MR Jar包已适配Hadoop 3.3.6、Shell调度模板自动创建按天分区的HDFS路径、以及关键的日期校验模块——它会在Reduce阶段主动比对系统时间与日志时间戳偏差超阈值直接告警并跳过该条记录。适合正在搭建日志分析基线的运维工程师、需要交付稳定报表的数据开发以及想避开时间处理玄学的新手。2. 日志数据构造与HDFS准备用可控噪声模拟真实场景避免“本地测试全绿集群跑就丢数据”真实生产环境的日志从不规整时区混用、毫秒缺失、时间格式不统一[15/Jul/2023:14:22:03 0800]vs2023-07-15T14:22:03.1230800、甚至存在乱码时间字段。如果直接拿线上日志测试你会发现MR任务在map阶段就因ParseException大量失败而错误日志里只显示java.lang.RuntimeException: java.text.ParseException根本看不出哪一行、哪个字段崩了。所以必须先构造带缺陷但可控的测试数据——这不是为了偷懒而是把问题显性化、前置化。2.1 用Python生成带时区偏移和错误率的日志文件# generate_logs.py import random import time from datetime import datetime, timedelta import pytz def generate_log_line(timestamp, ip, status_code): # 模拟真实日志80%正常请求15%4xx5%5xx if random.random() 0.15: status_code random.choice([400, 401, 403, 404]) elif random.random() 0.05: status_code random.choice([500, 502, 503]) # 构造Apache格式时间戳[15/Jul/2023:14:22:03 0800] tz pytz.timezone(Asia/Shanghai) dt datetime.fromtimestamp(timestamp, tz) time_str dt.strftime([%d/%b/%Y:%H:%M:%S %z]) # 随机插入1%的时区错误如写成0000 if random.random() 0.01: time_str time_str.replace(0800, 0000) # 随机插入0.5%的格式错误如少冒号 if random.random() 0.005: parts time_str.split(:) time_str :.join(parts[:2]) parts[2] # 故意破坏 return f{ip} - - {time_str} GET /api/v1/{random.choice([user, order, product])}?id{random.randint(1,1000)} HTTP/1.1 {status_code} {random.randint(100, 5000)} if __name__ __main__: start_ts int(time.mktime(datetime(2023, 7, 15).timetuple())) end_ts int(time.mktime(datetime(2023, 7, 21).timetuple())) with open(access_logs_20230715_20230721.log, w) as f: for ts in range(start_ts, end_ts 1, 2): # 每2秒1条基础流量 for _ in range(random.randint(1, 5)): # 每秒1~5次波动 ip f192.168.{random.randint(1,255)}.{random.randint(1,255)} f.write(generate_log_line(ts, ip, 200) \n)提示运行前需安装pytzpip install pytz。此脚本生成7天日志关键控制点有三① 时间戳基于datetime对象生成确保时区一致性② 主动注入1%时区错误和0.5%格式错误模拟真实脏数据③ QPS按秒级波动1~5次避免均匀流量导致统计失真。生成的文件约12MB足够MR任务压测。2.2 上传到HDFS并创建按天分区路径Hadoop生态中“按日期统计”的本质是利用HDFS路径天然分区能力。不要把所有日志塞进一个目录然后靠MR解析时间字段来分组——那会强制全量扫描且无法利用Hive/Spark的分区裁剪。正确做法是上传时就按日期拆分目录让MR输入路径本身成为分区键。# 1. 创建根目录若不存在 hdfs dfs -mkdir -p /logs/access/raw # 2. 将生成的日志按天切分并上传示例2023-07-15 # 先用sed提取当天日志注意此处依赖日志中时间格式为[15/Jul/2023:...] sed -n /\[15\/Jul\/2023:/p access_logs_20230715_20230721.log logs_20230715.log hdfs dfs -mkdir -p /logs/access/raw/date2023-07-15 hdfs dfs -put logs_20230715.log /logs/access/raw/date2023-07-15/ # 3. 重复步骤2为2023-07-16至2023-07-21创建对应目录 # 实际项目中应写Shell循环见第4章参数说明/logs/access/raw/date2023-07-15是标准Hive分区路径格式。date是分区列名2023-07-15是分区值。MR程序后续可通过FileInputFormat.setInputPaths(job, new Path(/logs/access/raw/date2023-07-15))精准读取单日数据避免全表扫描。注意sed命令中的日期格式必须与日志中完全一致15/Jul/2023而非2023-07-15这是第一个易错点。2.3 验证HDFS数据完整性与可读性上传后别急着跑MR先确认三件事① 文件是否真正落盘② 行数是否与本地一致③ 是否存在不可见字符导致MR解析失败。# 1. 查看HDFS文件大小和块数 hdfs dfs -ls -h /logs/access/raw/date2023-07-15/ # 输出应类似-rw-r--r-- 3 hdfs supergroup 1.8 M 2023-07-22 10:22 /logs/access/raw/date2023-07-15/logs_20230715.log # 2. 统计HDFS文件行数对比本地 hdfs dfs -cat /logs/access/raw/date2023-07-15/logs_20230715.log | wc -l # 本地执行wc -l logs_20230715.log # 3. 抽样检查前10行重点看时间戳格式 hdfs dfs -cat /logs/access/raw/date2023-07-15/logs_20230715.log | head -10 | sed s/[^[:print:]]//g # sed命令过滤不可见字符避免\x00等导致MR崩溃逻辑说明hdfs dfs -cat直接流式读取HDFS文件wc -l统计行数是最准的校验方式。sed s/[^[:print:]]//g是血泪经验——某次线上日志因编码问题混入\x00MR在split时直接抛IOException: Invalid UTF-8 byte排查3小时才发现是数据源问题。这步花2分钟省去后续2小时debug。3. MapReduce核心实现日期解析不是String.split而是用DateTimeFormatter防时区陷阱很多教程教新手用line.split( )[3]取[15/Jul/2023:14:22:03再substring(1)去括号——这在单机测试时没问题但一旦集群节点时区不一致比如DataNode设为UTC而日志是CSTSimpleDateFormat就会解析出错。更糟的是SimpleDateFormat非线程安全多个Mapper共用同一个实例会导致日期错乱。本方案采用Java 8的DateTimeFormatter它线程安全、支持严格模式、可绑定时区且解析失败时抛明确异常便于定位。3.1 Mapper精准提取日期并输出日期, 1键值对// DateCountMapper.java import org.apache.hadoop.io.*; import org.apache.hadoop.mapreduce.Mapper; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.ZoneId; import java.time.ZonedDateTime; public class DateCountMapper extends MapperLongWritable, Text, Text, IntWritable { // 定义Apache日志时间格式[15/Jul/2023:14:22:03 0800] private static final DateTimeFormatter APACHE_FORMAT DateTimeFormatter.ofPattern([dd/MMM/yyyy:HH:mm:ss Z]); private final Text outputKey new Text(); private final IntWritable outputValue new IntWritable(1); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String line value.toString().trim(); if (line.isEmpty()) return; try { // 1. 提取时间戳部分位置固定第4个空格分隔字段 String[] parts line.split( , -1); // -1保留末尾空字段 if (parts.length 4) { context.getCounter(DateCount, PARSE_ERROR_EMPTY_PARTS).increment(1); return; } String timestampStr parts[3]; // [15/Jul/2023:14:22:03 if (!timestampStr.startsWith([)) { context.getCounter(DateCount, PARSE_ERROR_NO_BRACKET).increment(1); return; } // 2. 解析为ZonedDateTime带时区 ZonedDateTime zdt ZonedDateTime.parse(timestampStr, APACHE_FORMAT); // 3. 转换为标准日期字符串yyyy-MM-dd用于分区聚合 String dateStr zdt.withZoneSameInstant(ZoneId.of(Asia/Shanghai)) .toLocalDate().toString(); // 2023-07-15 outputKey.set(dateStr); context.write(outputKey, outputValue); } catch (DateTimeParseException e) { // 记录解析失败详情便于调试 context.getCounter(DateCount, PARSE_EXCEPTION).increment(1); System.err.println(Parse failed for line: line | Error: e.getMessage()); } catch (Exception e) { context.getCounter(DateCount, UNEXPECTED_ERROR).increment(1); } } }参数说明DateTimeFormatter.ofPattern([dd/MMM/yyyy:HH:mm:ss Z])中[ ]是字面量MMM匹配英文月份缩写JulZ匹配时区偏移0800。ZonedDateTime.parse()自动处理时区转换withZoneSameInstant()确保所有时间统一转为东八区再取日期彻底规避“服务器时区不同导致同一条日志算到不同天”的经典翻车。计数器context.getCounter是关键——它不写日志但可在JobTracker UI中实时查看各类错误数量比System.err更可靠。3.2 Reducer聚合计数并添加质量校验Reducer不只是累加还要做两件事① 校验输入日期是否在合理范围防止日志时间戳写错成2003年② 输出结构化结果含日期、总请求数、独立IP数——后者需Mapper额外输出IP。// DateCountReducer.java import org.apache.hadoop.io.*; import org.apache.hadoop.mapreduce.Reducer; import java.util.HashSet; import java.util.Set; public class DateCountReducer extends ReducerText, IntWritable, Text, Text { private final Text outputValue new Text(); Override protected void reduce(Text key, IterableIntWritable values, Context context) throws IOException, InterruptedException { String dateStr key.toString(); int totalCount 0; SetString uniqueIPs new HashSet(); // 1. 解析日期字符串校验是否在合理范围内2020-2030 try { int year Integer.parseInt(dateStr.substring(0, 4)); if (year 2020 || year 2030) { context.getCounter(DateCount, DATE_OUT_OF_RANGE).increment(1); return; // 跳过异常年份 } } catch (NumberFormatException e) { context.getCounter(DateCount, DATE_PARSE_FAIL).increment(1); return; } // 2. 遍历所有value此处简化实际需Mapper同时输出IP // 完整版见GitHub仓库含IP提取逻辑 for (IntWritable val : values) { totalCount val.get(); } // 3. 构造输出日期\t总请求数\t独立IP数示例2023-07-15\t12450\t2341 outputValue.set(totalCount \t uniqueIPs.size()); context.write(key, outputValue); } }逻辑说明SetString uniqueIPs在Reducer中维护独立IP集合但注意——这要求Mapper必须同时输出日期, IP键值对然后在Reducer中用combine或二次排序实现。本节为聚焦核心暂用简化版完整实现见资源包中的AdvancedDateCountMapper.java。关键点在于year 2020 || year 2030校验某次某公司日志系统故障时间戳回滚到1970年若无此校验MR会把所有数据归到1970-01-01报表直接崩盘。3.3 Driver配置Job参数与输入输出路径Driver是MR的“指挥中心”必须显式设置序列化类、Combiner减少网络传输、以及最重要的——输入路径的通配符支持。// DateCountDriver.java import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.*; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; public class DateCountDriver { public static void main(String[] args) throws Exception { Configuration conf new Configuration(); // 启用对压缩文件的支持如日志是.gz conf.setBoolean(mapreduce.input.fileinputformat.input.dir.recursive, true); Job job Job.getInstance(conf, Date Count); job.setJarByClass(DateCountDriver.class); // 设置Mapper和Reducer job.setMapperClass(DateCountMapper.class); job.setReducerClass(DateCountReducer.class); // 设置输出类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); // 设置Combiner本地聚合减少Shuffle数据量 job.setCombinerClass(DateCountReducer.class); // 输入路径支持通配符一次读多天 FileInputFormat.setInputPaths(job, new Path(/logs/access/raw/date2023-07-*)); // 输出路径必须不存在MR会自动创建 Path outputPath new Path(/logs/access/output/date_count_ System.currentTimeMillis()); FileOutputFormat.setOutputPath(job, outputPath); System.exit(job.waitForCompletion(true) ? 0 : 1); } }参数说明FileInputFormat.setInputPaths(job, new Path(/logs/access/raw/date2023-07-*))是核心技巧。*通配符让MR自动匹配date2023-07-15到date2023-07-31所有目录无需硬编码。job.setCombinerClass()启用本地聚合假设某天有10万请求Mapper输出10万次2023-07-15,1Combiner会在每个Mapper节点上先聚合成2023-07-15,10000再发给Reducer网络传输量直降90%。outputPath用时间戳命名避免重复执行冲突。4. Shell调度与自动化把MR包装成“日期参数化”命令告别手动改路径写完MR代码只是开始真正的生产力在于一键调度。你不可能每次统计都打开IDEA改DateCountDriver.java里的路径再mvn package再hadoop jar。本方案提供一个健壮的Shell脚本支持① 指定起止日期② 自动创建HDFS分区路径③ 并行提交多个MR任务按天④ 失败自动重试。4.1 核心调度脚本date_count_scheduler.sh#!/bin/bash # date_count_scheduler.sh - Hadoop日期统计调度器 # 用法./date_count_scheduler.sh 2023-07-15 2023-07-21 set -e # 任何命令失败立即退出 START_DATE$1 END_DATE$2 if [[ -z $START_DATE || -z $END_DATE ]]; then echo 用法$0 开始日期 结束日期格式YYYY-MM-DD exit 1 fi # 1. 验证日期格式正则匹配YYYY-MM-DD if ! [[ $START_DATE ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || \ ! [[ $END_DATE ~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then echo 错误日期格式必须为 YYYY-MM-DD当前$START_DATE, $END_DATE exit 1 fi # 2. 计算日期差天数 start_sec$(date -d $START_DATE %s 2/dev/null) end_sec$(date -d $END_DATE %s 2/dev/null) if [[ $? -ne 0 ]]; then echo 错误无法解析日期请检查系统date命令支持 exit 1 fi days$(( (end_sec - start_sec) / 86400 )) echo 将统计 $START_DATE 至 $END_DATE 共 $((days1)) 天数据... # 3. 循环提交每日MR任务并行度3避免集群过载 for ((i0; idays; i)); do current_date$(date -d $START_DATE $i days %Y-%m-%d) input_path/logs/access/raw/date$current_date output_path/logs/access/output/date_count_${current_date}_$(date %s) # 检查输入路径是否存在 if ! hdfs dfs -test -d $input_path; then echo 警告输入路径不存在 $input_path跳过 continue fi # 提交MR任务后台运行日志重定向 echo 启动 $current_date 统计... hadoop jar /opt/hadoop-jars/date-count-1.0.jar \ com.example.DateCountDriver \ $input_path $output_path \ /var/log/hadoop/date_count_${current_date}.log 21 # 控制并行数每3个任务sleep 1秒 if (( (i1) % 3 0 )); then sleep 1 fi done # 等待所有后台任务完成 wait echo 所有任务提交完毕请检查日志/var/log/hadoop/date_count_*.log逻辑说明set -e是安全底线确保任意步骤失败立即终止避免“一半成功一半失败”的脏状态。date -d $START_DATE $i days是跨平台日期计算Linux/macOS通用比用awk或perl更简洁。hdfs dfs -test -d检查路径存在性避免MR因输入路径不存在而报FileNotFoundException。后台运行配合wait实现伪并行比单纯更可控——某次某实验室集群负载高同时启20个MR直接拖垮NameNode加了sleep 1后稳定运行。4.2 集成Cron定时任务每天凌晨2点跑昨日数据# 编辑crontabcrontab -e # 添加以下行每天凌晨2点执行统计昨日数据 0 2 * * * /home/hadoop/scripts/date_count_scheduler.sh $(date -d yesterday \%Y-\%m-\%d) $(date -d yesterday \%Y-\%m-\%d) # 验证cron语法推荐用https://crontab.guru/ # 0 2 * * * 每天2:00执行参数说明$(date -d yesterday \%Y-\%m-\%d)中的\%是转义确保cron中正确解析。crontab -e必须以hadoop用户身份运行否则无HDFS权限。首次部署后用systemctl status cron确认服务运行并手动执行一次脚本验证路径权限。4.3 输出结果整合用Hive建外部表让BI工具直接查询MR输出是纯文本BI工具如Tableau、Superset无法直接连接。必须通过Hive暴露为表-- 在Hive中执行 CREATE EXTERNAL TABLE IF NOT EXISTS access_date_stats ( date_str STRING, total_requests INT, unique_ips INT ) ROW FORMAT DELIMITED FIELDS TERMINATED BY \t LOCATION /logs/access/output/; -- 指向MR输出根目录 -- 刷新分区若MR输出按天分目录 MSCK REPAIR TABLE access_date_stats;提示EXTERNAL TABLE不删除HDFS数据安全MSCK REPAIR TABLE自动发现新分区如date_count_2023-07-15_1690000000无需手动ALTER TABLE ... ADD PARTITION。BI工具连接HiveServer2后即可用SQLSELECT * FROM access_date_stats WHERE date_str 2023-07-15实时取数。5. 避坑日期统计的五个经典翻车现场与血泪解法Hadoop日期统计看似简单实则遍布“时间陷阱”。以下是我在某跨平台系统项目中踩过的坑每一条都附带真实现象、根因分析和可落地的解法拒绝空泛“注意时区”。5.1 现象MR任务成功但2023-07-15的数据全出现在2023-07-14分区原因日志时间戳为[15/Jul/2023:00:00:00 0000]UTC而DateTimeFormatter默认按JVM时区CST解析导致15/Jul/2023 00:00:00 UTC被当成15/Jul/2023 08:00:00 CST再取日期时仍是2023-07-15但若日志是[14/Jul/2023:16:00:00 0000]即CST的15/Jul/2023 00:00:00解析后日期变成2023-07-14。解决在DateCountMapper.java中强制指定时区解析// 错误ZonedDateTime.parse(timestampStr, APACHE_FORMAT); // 正确ZonedDateTime.parse(timestampStr, APACHE_FORMAT) // .withZoneSameInstant(ZoneId.of(UTC)); // 先转UTC再统一转CST并在Driver中添加配置conf.set(hadoop.date.timezone, UTC)确保所有节点行为一致。5.2 现象统计结果比预期少30%且PARSE_EXCEPTION计数器飙升原因日志中存在[15/Jul/2023:14:22:03 0800]和[15/Jul/2023:14:22:03 0000]混用而DateTimeFormatter的Z模式符只能解析0800对0000抛DateTimeParseException。解决预处理日志标准化时区。在generate_logs.py中增加修复逻辑# 修复时区将0000替换为0800或根据业务统一 if 0000 in time_str: time_str time_str.replace(0000, 0800)或在Mapper中用正则预处理timestampStr timestampStr.replaceAll(\\0000, \\0800);5.3 现象HDFS上date2023-07-15目录下有多个小文件128MBMR任务慢且失败率高原因日志生成脚本按秒切分每秒生成一个文件上传导致HDFS小文件爆炸。Hadoop小文件过多会撑爆NameNode内存每个文件占150字节元数据且MR需启动大量Mapper。解决上传前合并文件。修改调度脚本# 替换原sed命令 sed -n /\[15\/Jul\/2023:/p access_logs_20230715_20230721.log | \ split -l 10000 - logs_20230715_part_ # 每10000行一个文件 hdfs dfs -mkdir -p /logs/access/raw/date2023-07-15 hdfs dfs -put logs_20230715_part_* /logs/access/raw/date2023-07-15/或用Hadoop自带的hadoop archiveHAR归档但需修改MR输入为HAR路径。5.4 现象hadoop jar命令报ClassNotFoundException: com.example.DateCountDriver原因mvn package生成的jar包未包含依赖如hadoop-client而集群节点没有这些jar。java -cp能跑但hadoop jar会忽略-cp。解决构建Uber JARfat jar。在pom.xml中添加plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.DateCountDriver/mainClass /transformer /transformers /configuration /execution /executions /plugin执行mvn clean package后使用target/date-count-1.0-jar-with-dependencies.jar。5.5 现象BI工具连Hive查不到最新数据SELECT COUNT(*)返回0原因MR输出路径为/logs/access/output/date_count_2023-07-15_1690000000但Hive外部表LOCATION指向/logs/access/output/而MSCK REPAIR TABLE只识别标准分区格式date2023-07-15不识别时间戳后缀。解决MR输出路径必须符合Hive分区规范。修改Driver// 错误new Path(/logs/access/output/date_count_ System.currentTimeMillis()); // 正确new Path(/logs/access/output/date dateStr); // dateStr来自输入路径或在调度脚本中MR完成后用hdfs dfs -mv重命名输出目录hdfs dfs -mv /logs/access/output/date_count_2023-07-15_* /logs/access/output/date2023-07-15 hive -e MSCK REPAIR TABLE access_date_stats;6. 进阶验证用Python脚本交叉校验MR结果建立可信度闭环MR跑出结果只是第一步如何证明“这个数字真的可信”靠人工抽样不我从某图像处理Demo项目中学到一招用轻量级Python脚本对同一份HDFS数据做独立统计与MR结果比对。这不是为了替代MR而是建立双重校验机制——当两者差异0.5%时自动告警逼你去查是数据问题还是代码问题。6.1 编写HDFS日志校验脚本hdfs_log_validator.py#!/usr/bin/env python3 # hdfs_log_validator.py - 从HDFS读取日志用Python独立统计日期分布 import subprocess import re import sys from collections import defaultdict from datetime import datetime def parse_apache_date(date_str): 安全解析Apache时间戳返回日期字符串 try: # 匹配 [15/Jul/2023:14:22:03 0800] match re.search(r\[(\d{2})/(\w{3})/(\d{4}):, date_str) if not match: return None day, month_abbr, year match.groups() # 英文月份转数字 month_map {Jan: 01, Feb: 02, Mar: 03, Apr: 04, May: 05, Jun: 06, Jul: 07, Aug: 08, Sep: 09, Oct: 10, Nov: 11, Dec: 12} month month_map.get(month_abbr, 00) return f{year}-{month}-{day} except Exception as e: return None def validate_hdfs_path(hdfs_path): 从HDFS路径读取日志并统计 # 使用hadoop fs -cat 流式读取避免下载到本地 cmd [hadoop, fs, -cat, hdfs_path] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: print(fERROR: HDFS读取失败 {hdfs_path}: {result.stderr}) return {} date_count defaultdict(int) line_count 0 for line in result.stdout.splitlines(): line_count 1 if line_count % 10000 0: print(f已处理 {line_count} 行...) date_str parse_apache_date(line) if date_str: date_count[date_str] 1 else: # 记录解析失败行用于调试 if line_count 10: print(f解析失败示例: {line[:100]}) return dict(date_count) if __name__ __main__: if len(sys.argv p a hrefhttps://download.csdn.net/download/qq_32168087/11102159 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p