日志分析与活跃用户统计
问题背景
我们需要分析一份接口访问日志,以统计特定条件下的“日活跃用户数 (DAU)”和“月活跃用户数 (MAU)”。
日志格式
日志记录在一个字符串数组 logs 中,每条日志的格式为 yyyy-mm-dd|client_ip|url|result,字段之间由竖线 | 分隔。
-
yyyy-mm-dd: 日志记录的日期。- 注意: 题目约定,所有月份都按 31 天计算。所有输入日志都在同一个月内,但日志本身不保证按日期排序。
-
client_ip: 客户端的IPv4地址。- 注意: 需要对IP地址进行标准化处理。点分十进制的每个部分中的前导零应被忽略。例如,
1.01.001.1和1.1.1.1应被视为同一个地址。
- 注意: 需要对IP地址进行标准化处理。点分十进制的每个部分中的前导零应被忽略。例如,
-
url: 访问的接口地址。- 格式示例:
/login.do,/query.html。仅包含字母、.、/和_。
- 格式示例:
-
result: 访问结果。- 只有两种可能的值:
success或fail。
- 只有两种可能的值:
统计规则
我们需要根据以下精确的规则来统计活跃用户数:
-
日活跃用户数 (Daily Active Users - DAU):
对于指定的某一天,其“日活数”是指,在当天所有满足以下全部条件的日志中,不重复的 client_ip 的数量。
url必须精确匹配/login.do。result必须是success。
-
月活跃用户数 (Monthly Active Users - MAU):
其“月活数”是指,在当月所有天的日志中,所有满足上述相同条件的日志里,不重复的 client_ip 的数量。
- 特别注意: 月活数不是每日日活数的简单相加,而是对整个月去重后的IP总数。
任务要求
给定一份日志 logs,请计算其对应的月活数和该月每一天的日活数。最终返回一个包含 32 个整数的序列:
- 序列的第一个成员 (
[0]) 是月活数。 - 后续的 31 个成员 (
[1]到[31]) 依次是当月第 1 天到第 31 天的日活数。
输入格式
-
logs: 一个字符串数组,代表日志列表。0 < logs.length <= 50000- 每条日志的长度
logs[i].length <= 150
输出格式
- 一个包含 32 个整数的序列(数组或列表)。
样例说明
样例 1
-
输入:
["2020-02-01|192.168.218.218|/login.do|success", "2020-02-01|192.168.218.218|/login.do|success", "2020-02-01|192.168.210.210|/login.do|fail", "2020-02-02|192.168.210.210|/login.do|success", "2020-02-02|192.168.218.218|/login.do|success"] -
输出:
[2, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -
解释:
-
分析第1天 (
2020-02-01) :- 有两条日志符合
url和result条件,但它们的client_ip都是192.168.218.218。 - 去重后,当天活跃的独立IP只有 1 个。因此,第1天的日活数为 1。
- 有两条日志符合
-
分析第2天 (
2020-02-02) :- 有两条日志符合条件,它们的
client_ip分别是192.168.210.210和192.168.218.218。 - 去重后,当天活跃的独立IP有 2 个。因此,第2天的日活数为 2。
- 有两条日志符合条件,它们的
-
分析整月:
- 所有天中,满足条件的独立IP共有
192.168.218.218和192.168.210.210这两个。 - 因此,月活数为 2。
- 所有天中,满足条件的独立IP共有
-
组合输出:
[月活, 日活1, 日活2, ...]->[2, 1, 2, 0, ...]
-
样例 2
-
输入:
["2020-12-01|192.168.218.001|/login.do|success", "2020-12-01|192.168.218.1|/login.do|success", "2020-12-01|192.168.218.2|/to_login.do|success"] -
输出:
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -
解释:
-
分析第1天 (
2020-12-01) :- 日志1:
192.168.218.001标准化后为192.168.218.1,URL和结果符合。 - 日志2:
192.168.218.1,URL和结果符合。 - 日志3:
url是/to_login.do,不符合== /login.do的条件,被忽略。 - 符合条件的独立IP只有
192.168.218.1这一个。第1天的日活数为 1。
- 日志1:
-
分析整月:
- 整个月中,满足条件的独立IP也只有
192.168.218.1这一个。月活数为 1。
- 整个月中,满足条件的独立IP也只有
-
组合输出:
[1, 1, 0, ...]
-
import java.util.*;
import java.util.stream.Collectors;
public class Solution {
/**
* 根据日志记录统计该月的日活数和月活数。
*
* 算法思想:
* 1. **IP 规范化**: 由于 "1.1.1.1" 和 "1.01.001.1" 应视为同一地址,我们需要一个函数
* 将 IP 地址转换为唯一的、规范的格式。方法是按 '.' 分割,将每部分转为整数再转回字符串,
* 最后重新用 '.' 连接。例如 "1.01.1" -> [1, 1, 1] -> "1.1.1"。
*
* 2. **数据结构选择**:
* - **日活 (DAU)**: 我们需要记录每一天有哪些**不同**的 IP 成功登录过。使用一个
* `Map<Integer, Set<String>>` 非常合适。Map 的 Key 是天 (1-31),
* Value 是一个 `Set`,存储当天所有成功登录的、规范化的 IP 地址。Set 自动处理了当天内的 IP 去重。
* - **月活 (MAU)**: 我们需要记录整个月有哪些**不同**的 IP 成功登录过。使用一个
* `Set<String>` 就可以存储所有成功登录的、规范化的 IP 地址,自动完成月度去重。
*
* 3. **处理流程**:
* a. 遍历每一条日志。
* b. 对每一条日志,检查是否满足活跃条件 (`url` 是 "/login.do" 且 `result` 是 "success")。
* c. 如果满足条件:
* i. 解析出日期中的 "天"。
* ii. 解析并规范化 IP 地址。
* iii. 将规范化的 IP 加入对应日期的 `Set` (更新日活统计)。
* iv. 将规范化的 IP 加入代表月活的 `Set` (更新月活统计)。
* d. 遍历结束后,`月活Set` 的大小就是 MAU,每个 `日活Set` 的大小就是对应那一天的 DAU。
*
* 4. **构建输出**: 创建一个长度为 32 的数组,将 MAU 和 DAU 填入相应位置。
*
* @param logs 日志记录的字符串数组。
* @return 一个长度为 32 的整数数组,`result[0]` 为月活数,`result[i]` (i > 0) 为第 i 天的日活数。
*/
public int[] analyzeLog(String[] logs) {
// --- 1. 初始化数据结构 ---
// 用于统计日活:Key=天(1-31), Value=当天活跃的唯一IP集合
Map<Integer, Set<String>> dailyActiveUsers = new HashMap<>();
// 用于统计月活:存储本月所有活跃过的唯一IP
Set<String> monthlyActiveUsers = new HashSet<>();
// --- 2. 遍历并处理日志 ---
for (String log : logs) {
// a. 分割日志字符串,注意|是特殊字符
String[] parts = log.split("\|");
// 确保日志格式正确,有4个部分
if (parts.length != 4) {
continue; // 格式不符,跳过
}
// b. 提取字段
String dateStr = parts[0];
String ipStr = parts[1];
String url = parts[2];
String result = parts[3];
// c. 检查是否为成功的登录事件
if ("/login.do".equals(url) && "success".equals(result)) {
// d. 如果是,则提取信息并更新统计
try {
// 提取日期中的“天”
// lastIndexOf('-') 找到最后一个 '-',其后就是天的部分
int day = Integer.parseInt(dateStr.substring(dateStr.lastIndexOf('-') + 1));
// 规范化 IP 地址
String normalizedIp = normalizeIp(ipStr);
// 更新日活统计:
// computeIfAbsent: 如果 day 不在 map 中,则创建一个新的 HashSet,
// 否则返回现有的 Set。然后将 IP 加入该 Set。
dailyActiveUsers.computeIfAbsent(day, k -> new HashSet<>()).add(normalizedIp);
// 更新月活统计
monthlyActiveUsers.add(normalizedIp);
} catch (NumberFormatException | StringIndexOutOfBoundsException e) {
// 如果日期或IP格式解析错误,打印错误信息并跳过此条日志
System.err.println("跳过格式错误的日志: " + log + " | 错误: " + e.getMessage());
}
}
}
// --- 3. 构建输出数组 ---
int[] output = new int[32]; // 索引 0 用于月活,1-31 用于日活
// a. 填入月活数
output[0] = monthlyActiveUsers.size();
// b. 填入日活数
// 遍历 1 到 31 天
for (int day = 1; day <= 31; day++) {
// 从 Map 中获取当天的活跃用户集合
Set<String> dailySet = dailyActiveUsers.get(day);
if (dailySet != null) {
// 如果当天有活跃记录,则集合的大小即为日活数
output[day] = dailySet.size();
}
// 如果当天没有活跃记录,数组对应位置默认为 0,无需操作
}
return output;
}
/**
* 辅助方法:将一个点分十进制的 IP 字符串规范化。
* 例如,"1.01.001.1" 会被转换为 "1.1.1.1"。
*
* @param ipStr 原始 IP 字符串
* @return 规范化后的 IP 字符串
*/
private String normalizeIp(String ipStr) {
// 使用 Stream API 简洁地实现
// 1. 按 '.' 分割
// 2. 将每个部分转换为 int (这会自动去除前导0,例如 "01" -> 1)
// 3. 将 int 转回 String
// 4. 用 '.' 重新连接
return Arrays.stream(ipStr.split("\."))
.map(s -> String.valueOf(Integer.parseInt(s)))
.collect(Collectors.joining("."));
}
}