{ }codelper
读取一行常量连接输出P0165

过滤并归并服务器日志

从服务器日志中挑出重要的行,并整理得便于阅读。
日志级别从低到高有 DEBUG、INFO、WARN、ERROR 四种,程序中要把这个顺序定义为常量。
第一行给出要保留的最低级别名称和记录数 n,以一个空格分隔。
接下来 n 行每行一条记录,形如 "时刻 级别 消息",三部分之间各有一个空格。
消息中可能含有空格,也可能有两个以上连续的空格。
消息不以空格开头或结尾。
只保留级别不低于要保留的最低级别的记录。
按顺序查看保留下来的记录,级别和消息都完全相同的记录连续出现时算作一组。
消息连空格个数也相同才算相同。
被过滤掉的记录不会打断分组:两条保留的记录之间如果只有被过滤掉的记录,就算它们是连续出现的。
每组输出一行 "时刻 [级别] 消息",时刻取该组第一条记录的时刻。
如果该组有 k 条记录(k ≥ 2),在行尾加一个空格和 "(xk)"(k 处写数字:3 条就写 "(x3)")。
只有一条记录的组不加。
消息要与输入一字不差地照原样输出。
最后一行按 "DEBUG=a, INFO=b, WARN=c, ERROR=d" 的形式输出全部 n 条记录(包括被过滤的)中各级别的条数。
如果没有保留任何记录,只输出最后一行。

输入
第一行 "级别 n"(1 ≤ n ≤ 2000)。
时刻为 HH:MM:SS(各两位)。
级别是 DEBUG、INFO、WARN、ERROR 之一。
消息为 1~50 个字符,由英文字母、数字、空格和标点(.,:;!?'()=_/#-)组成
输出
每组一行 "时刻 [级别] 消息"(两条以上加 " (xk)"),最后一行 "DEBUG=a, INFO=b, WARN=c, ERROR=d"
样例 1 · stdin
INFO 7
08:00:01 DEBUG boot  sequence start
08:00:02 INFO disk ok
08:00:03 WARN fan  slow
08:00:04 DEBUG fan check
08:00:05 WARN fan  slow
08:00:09 WARN fan slow
08:00:10 ERROR disk ok
stdout
08:00:02 [INFO] disk ok
08:00:03 [WARN] fan  slow (x2)
08:00:09 [WARN] fan slow
08:00:10 [ERROR] disk ok
DEBUG=2, INFO=1, WARN=3, ERROR=1
main.py
输入(stdin)

运行时这些行会依次作为 input()/标准输入传入(运行中不会等待输入)。

$ 就绪

请在电脑上打开以编写和运行代码。在手机上可以查看题目和题解。

其他语言怎么写?查看各语言题解 · 8 种语言题解

① 读一行并拆分:要保留消息里的空格,就整行读入,只在前两个空格处拆开。
Python line.split(" ", 2)、Java split(" ", 3)、C# Split(' ', 3) 可以指定拆成几段。
JS・TS 的 split(" ", 3) 会丢掉后面的部分,所以用 indexOf 找位置再用 slice 截取。
C++ 用 find + substr,C 用 strchr 把空格改成 '\0' 得到三段。
Craykon 也用 查找位置 和 截取 来切(分割() 会在每个空格处拆分)。
用 Python split() 全部拆开再用 " ".join 拼回去,fan slow 会变成 fan slow,和另一条消息分到一组。
Java 在 Scanner.next()・nextInt() 之后调用 nextLine(),会先读到第一行剩下的部分(空字符串),所有记录都错开一条。
用 BufferedReader.readLine() 逐行读取才安全。
② 常量顺序:级别高低取决于它在常量列表中的位置,而不是名字。
把名字当字符串比较用的是字母顺序,"WARN" > "ERROR",于是本该只留 ERROR 时 WARN 也留了下来。
常量列表可以写成 Python 元组、JS Object.freeze([...])、TS readonly string[]、Java List.of(...)(static final String[] 只固定名字,元素仍可改)、C# static readonly string[](数组不能是 const,元素仍可改)、C static const char *const LEVELS[]、C++ constexpr std::array<std::string_view, 4>。
Craykon 没有常量,就用约定不改的列表。
③ 连接输出:Python ", ".join(...),JS・TS join(", "),Java StringJoiner・String.join,C# string.Join,Craykon 连接(", ")。
C・C++ 只在第一个计数以外的前面加 ", "。
每个计数后面都加的话,行尾会剩下一个 ,。
分组要同时比较级别和消息,并且不被过滤掉的行打断。
计数行要统计全部记录,包括被过滤掉的。

Pythonmain.py · 24 行
import sys # 차례가 곧 단계의 높이다. 튜플이라 칸을 바꿀 수 없다LEVELS = ("DEBUG", "INFO", "WARN", "ERROR") min_name, n = sys.stdin.readline().split()min_rank = LEVELS.index(min_name)counts = [0] * len(LEVELS)runs = []  # [시각, 단계, 메시지, 개수]for _ in range(int(n)):    line = sys.stdin.readline().rstrip("\n")    # 앞의 두 빈칸에서만 나눈다: 메시지 안의 빈칸(두 칸 이상도)은 그대로 남는다    time, level, message = line.split(" ", 2)    rank = LEVELS.index(level)    counts[rank] += 1    if rank < min_rank:        continue  # 걸러진 줄은 묶음을 끊지 않는다    if runs and runs[-1][1] == level and runs[-1][2] == message:        runs[-1][3] += 1    else:        runs.append([time, level, message, 1])for time, level, message, k in runs:    print(f"{time} [{level}] {message}" + (f" (x{k})" if k > 1 else ""))print(", ".join(f"{name}={c}" for name, c in zip(LEVELS, counts)))
JavaScriptmain.js · 26 行
// const 배열: 이름은 다시 넣을 수 없고, Object.freeze로 칸도 못 바꾸게 한다const LEVELS = Object.freeze(["DEBUG", "INFO", "WARN", "ERROR"]); const lines = require("fs").readFileSync(0, "utf8").split("\n");const [minName, n] = lines[0].trim().split(/\s+/);const minRank = LEVELS.indexOf(minName);const counts = [0, 0, 0, 0];const runs = []; // { time, level, message, k }for (let i = 1; i <= Number(n); i++) {  const line = lines[i];  // 앞의 두 빈칸 자리로 자른다: split(" ")은 메시지 안의 빈칸까지 나눈다  const a = line.indexOf(" ");  const b = line.indexOf(" ", a + 1);  const time = line.slice(0, a);  const level = line.slice(a + 1, b);  const message = line.slice(b + 1);  const rank = LEVELS.indexOf(level);  counts[rank]++;  if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다  const last = runs[runs.length - 1];  if (last && last.level === level && last.message === message) last.k++;  else runs.push({ time, level, message, k: 1 });}const out = runs.map((r) => `${r.time} [${r.level}] ${r.message}` + (r.k > 1 ? ` (x${r.k})` : ""));out.push(LEVELS.map((name, i) => `${name}=${counts[i]}`).join(", "));console.log(out.join("\n"));
TypeScriptmain.ts · 29 行
import { readFileSync } from "node:fs";// readonly 배열: 타입 검사기가 칸을 바꾸지 못하게 막는다const LEVELS: readonly string[] = ["DEBUG", "INFO", "WARN", "ERROR"]; interface Run { time: string; level: string; message: string; k: number } const lines: string[] = readFileSync(0, "utf8").split("\n");const [minName, n] = lines[0].trim().split(/\s+/);const minRank: number = LEVELS.indexOf(minName);const counts: number[] = [0, 0, 0, 0];const runs: Run[] = [];for (let i = 1; i <= Number(n); i++) {  const line: string = lines[i];  // 앞의 두 빈칸 자리로 자른다: 메시지 안의 빈칸은 그대로  const a: number = line.indexOf(" ");  const b: number = line.indexOf(" ", a + 1);  const time = line.slice(0, a);  const level = line.slice(a + 1, b);  const message = line.slice(b + 1);  const rank: number = LEVELS.indexOf(level);  counts[rank]++;  if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다  const last: Run | undefined = runs[runs.length - 1];  if (last !== undefined && last.level === level && last.message === message) last.k++;  else runs.push({ time, level, message, k: 1 });}const out: string[] = runs.map((r) => `${r.time} [${r.level}] ${r.message}` + (r.k > 1 ? ` (x${r.k})` : ""));out.push(LEVELS.map((name, i) => `${name}=${counts[i]}`).join(", "));console.log(out.join("\n"));
JavaMain.java · 46 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.List;import java.util.StringJoiner; public class Main {    // final은 이름만 묶는다: LEVELS[0] = "X"는 막지 않으니 List.of로 바꿀 수 없는 목록을 쓴다    static final List<String> LEVELS = List.of("DEBUG", "INFO", "WARN", "ERROR");     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        String[] first = in.readLine().trim().split("\\s+");        int minRank = LEVELS.indexOf(first[0]);        int n = Integer.parseInt(first[1]);        int[] counts = new int[LEVELS.size()];        List<String[]> runs = new ArrayList<>(); // {시각, 단계, 메시지}        List<Integer> sizes = new ArrayList<>();        for (int i = 0; i < n; i++) {            // split(" ", 3): 앞의 두 빈칸에서만 나눠 메시지 안의 빈칸은 그대로 남는다            String[] p = in.readLine().split(" ", 3);            int rank = LEVELS.indexOf(p[1]);            counts[rank]++;            if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다            int last = runs.size() - 1;            if (last >= 0 && runs.get(last)[1].equals(p[1]) && runs.get(last)[2].equals(p[2]))                sizes.set(last, sizes.get(last) + 1);            else {                runs.add(p);                sizes.add(1);            }        }        StringBuilder out = new StringBuilder();        for (int i = 0; i < runs.size(); i++) {            String[] r = runs.get(i);            out.append(r[0]).append(" [").append(r[1]).append("] ").append(r[2]);            if (sizes.get(i) > 1) out.append(" (x").append(sizes.get(i)).append(')');            out.append('\n');        }        StringJoiner sj = new StringJoiner(", ");        for (int i = 0; i < LEVELS.size(); i++) sj.add(LEVELS.get(i) + "=" + counts[i]);        out.append(sj).append('\n');        System.out.print(out);    }}
C#Main.cs · 37 行
using System;using System.Collections.Generic;using System.Linq;using System.Text; class Program{    // const는 배열이 될 수 없다: static readonly는 이름만 묶고, 칸은 바꿀 수 있다    static readonly string[] Levels = { "DEBUG", "INFO", "WARN", "ERROR" };     static void Main()    {        string[] first = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);        int minRank = Array.IndexOf(Levels, first[0]);        int n = int.Parse(first[1]);        int[] counts = new int[Levels.Length];        var runs = new List<(string Time, string Level, string Message, int K)>();        for (int i = 0; i < n; i++)        {            // Split(' ', 3): 앞의 두 빈칸에서만 나눠 메시지 안의 빈칸은 그대로            string[] p = Console.ReadLine().Split(' ', 3);            int rank = Array.IndexOf(Levels, p[1]);            counts[rank]++;            if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다            int last = runs.Count - 1;            if (last >= 0 && runs[last].Level == p[1] && runs[last].Message == p[2])                runs[last] = runs[last] with { K = runs[last].K + 1 };            else                runs.Add((p[0], p[1], p[2], 1));        }        var sb = new StringBuilder();        foreach (var r in runs)            sb.Append($"{r.Time} [{r.Level}] {r.Message}").Append(r.K > 1 ? $" (x{r.K})" : "").Append('\n');        sb.Append(string.Join(", ", Levels.Select((name, i) => $"{name}={counts[i]}"))).Append('\n');        Console.Write(sb);    }}
C++main.cpp · 55 行
#include <algorithm>#include <array>#include <iostream>#include <sstream>#include <string>#include <string_view>#include <vector> // constexpr 배열: 컴파일할 때 정해지고 칸도 바꿀 수 없다constexpr std::array<std::string_view, 4> LEVELS{"DEBUG", "INFO", "WARN", "ERROR"}; int rankOf(const std::string& name) {    return static_cast<int>(std::find(LEVELS.begin(), LEVELS.end(), name) - LEVELS.begin());} struct Run {    std::string time, level, message;    int k;}; int main() {    std::string line;    std::getline(std::cin, line);    std::istringstream head(line);    std::string minName;    int n;    head >> minName >> n;    int minRank = rankOf(minName);    std::array<int, 4> counts{};    std::vector<Run> runs;    for (int i = 0; i < n; i++) {        std::getline(std::cin, line); // 빈칸이 든 한 줄을 통째로        // 앞의 두 빈칸 자리로 자른다: 메시지 안의 빈칸은 그대로        std::size_t a = line.find(' '), b = line.find(' ', a + 1);        std::string time = line.substr(0, a), level = line.substr(a + 1, b - a - 1), message = line.substr(b + 1);        int rank = rankOf(level);        counts[rank]++;        if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다        if (!runs.empty() && runs.back().level == level && runs.back().message == message)            runs.back().k++;        else            runs.push_back({time, level, message, 1});    }    std::string out;    for (const Run& r : runs) {        out += r.time + " [" + r.level + "] " + r.message;        if (r.k > 1) out += " (x" + std::to_string(r.k) + ")";        out += '\n';    }    for (std::size_t i = 0; i < LEVELS.size(); i++) {        if (i > 0) out += ", ";        out += std::string(LEVELS[i]) + "=" + std::to_string(counts[i]);    }    std::cout << out << '\n';}
Cmain.c · 55 行
#include <stdio.h>#include <string.h> #define LEVEL_COUNT 4static const char *const LEVELS[LEVEL_COUNT] = {"DEBUG", "INFO", "WARN", "ERROR"}; static int rank_of(const char *name) {    for (int i = 0; i < LEVEL_COUNT; i++)        if (strcmp(LEVELS[i], name) == 0) return i;    return -1;} int main(void) {    char line[128], min_name[8];    int n;    fgets(line, sizeof line, stdin);    sscanf(line, "%7s %d", min_name, &n);    int min_rank = rank_of(min_name);    int counts[LEVEL_COUNT] = {0};    char run_time[9] = "", run_msg[64] = "";    int run_rank = -1, run_k = 0;    for (int i = 0; i < n; i++) {        fgets(line, sizeof line, stdin); /* 빈칸이 든 한 줄을 통째로 */        line[strcspn(line, "\n")] = '\0';        /* 앞의 두 빈칸을 '\0'으로 바꿔 세 조각으로: 메시지 안의 빈칸은 그대로 */        char *level = strchr(line, ' ');        *level++ = '\0';        char *msg = strchr(level, ' ');        *msg++ = '\0';        int rank = rank_of(level);        counts[rank]++;        if (rank < min_rank) continue; /* 걸러진 줄은 묶음을 끊지 않는다 */        if (run_k > 0 && rank == run_rank && strcmp(msg, run_msg) == 0) {            run_k++;            continue;        }        if (run_k > 0) {            printf("%s [%s] %s", run_time, LEVELS[run_rank], run_msg);            if (run_k > 1) printf(" (x%d)", run_k);            putchar('\n');        }        strcpy(run_time, line);        strcpy(run_msg, msg);        run_rank = rank;        run_k = 1;    }    if (run_k > 0) {        printf("%s [%s] %s", run_time, LEVELS[run_rank], run_msg);        if (run_k > 1) printf(" (x%d)", run_k);        putchar('\n');    }    for (int i = 0; i < LEVEL_COUNT; i++) printf("%s%s=%d", i ? ", " : "", LEVELS[i], counts[i]);    putchar('\n');    return 0;}
Craykonmain.cray · 55 行
// 크레이콘에는 상수가 없다: 바꾸지 않기로 약속한 목록으로 둔다(차례가 곧 높이)단계들 = ["DEBUG", "INFO", "WARN", "ERROR"]; 첫줄 = 控制台.读行().分割();최소 = 단계들.查找位置(첫줄[0]);n = 첫줄[1].转数字();개수들 = [0, 0, 0, 0];묶음시각 = [];묶음단계 = [];묶음메시지 = [];묶음개수 = [];循环 (i = 0; i < n; i++){    줄 = 控制台.读行();    // 앞의 두 빈칸 자리로 자른다: 나누기()는 메시지 안의 빈칸까지 나눈다    첫빈칸 = 줄.查找位置(" ");    시각 = 줄.截取(0, 첫빈칸);    나머지 = 줄.截取(첫빈칸 + 1);    둘째빈칸 = 나머지.查找位置(" ");    단계 = 나머지.截取(0, 둘째빈칸);    메시지 = 나머지.截取(둘째빈칸 + 1);    높이 = 단계들.查找位置(단계);    개수들[높이] += 1;    // 걸러진 줄은 묶음을 끊지 않는다    如果 (높이 >= 최소)    {        끝 = 묶음단계.数量 - 1;        如果 (끝 >= 0 && 묶음단계[끝] == 단계 && 묶음메시지[끝] == 메시지)        {            묶음개수[끝] += 1;        }        否则        {            묶음시각.添加(시각);            묶음단계.添加(단계);            묶음메시지.添加(메시지);            묶음개수.添加(1);        }    }}循环 (j = 0; j < 묶음단계.数量; j++){    출력줄 = 묶음시각[j] + " [" + 묶음단계[j] + "] " + 묶음메시지[j];    如果 (묶음개수[j] > 1)    {        출력줄 = 출력줄 + " (x" + 묶음개수[j] + ")";    }    控制台.输出(출력줄);}조각들 = [];循环 (j = 0; j < 4; j++){    조각들.添加(단계들[j] + "=" + 개수들[j]);}控制台.输出(조각들.连接(", "));