{ }codelper
1行読み込み定数つなげて出力P0165

サーバーログの絞り込みとまとめ

サーバーの記録(ログ)から重要な行だけを選び、見やすく短くする。
記録のレベルは低いほうから DEBUG、INFO、WARN、ERROR の四つで、この順番はプログラムで定数として置く。
一行目に残す最も低いレベルの名前と記録の数 n が空白一つで区切って与えられる。
続く n 行は記録一行ずつで、"時刻 レベル メッセージ" の形である。
三つの部分の間は空白一つである。
メッセージの中には空白が入っていることがあり、空白が二つ以上続くこともある。
メッセージは空白で始まったり終わったりしない。
レベルが残す最も低いレベル以上の記録だけを残す。
残した記録を順に見たとき、レベルとメッセージがどちらもまったく同じ記録が続けて出たら一つのまとまりとする。
メッセージは空白の数まで同じでなければ同じではない。
取り除かれた記録はまとまりを切らない。
残した二つの記録の間に取り除かれた記録しかなければ、その二つは続けて出たものとする。
まとまりごとに一行に "時刻 [レベル] メッセージ" を出力せよ。
時刻はまとまりの最初の記録の時刻である。
まとまりに記録が k 個(k ≥ 2)あるなら、行の終わりに空白一つと "(xk)" を付ける(k のところに数を書く:3 個なら "(x3)")。
記録が一つだけのまとまりには付けない。
メッセージは入力と一文字も違わずそのまま書く。
最後の行には、n 個の記録全体(取り除いたものも含む)をレベルごとに数えた数を "DEBUG=a, INFO=b, WARN=c, ERROR=d" の形で出力せよ。
残した記録がなければ最後の行だけを出力する。

入力
一行目 "レベル 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]);}コンソール.書く(조각들.つなぐ(", "));