资讯动态

华为OD机试日志解析题:Python与JavaScript双版本实现与避坑指南

发布时间:2026/10/8 4:11:17 来源:尧图企业网站定制
华为OD机试的双机位C卷里字符串处理题几乎是绕不开的坎而日志解析就是这类题目里最典型的一种。我准备这块内容的时候一开始觉得简单——无非是把每行日志拆开、找找ERROR、统计一下结果真正上手写了Python和JavaScript两个版本之后居然各自踩了一个不小的坑一个栽在字段解析上一个栽在排序上。这篇文章不打算只贴代码交差而是把题目本身、解题思路的取舍、两版代码的细节以及机试现场的时间管理一起讲透给正在准备双机位C卷或者想系统练一练字符串处理题的同学做个参考。1. 这道日志解析题到底考什么1.1 双机位C卷的题型分布先说说我了解到的整体情况。OD机试一般有三道题分值按梯度分布第一道偏基础后面两道拉差距。C卷被很多人叫做“复试题型卷”考察范围更广字符串处理、数组矩阵、栈、队列、哈希表、排序都出现过。日志解析这种题在C卷里算不上最难的但它不是靠背模板就能拿满分的类型因为它把好几块基本功揉在了一起。从题目形式上看日志解析属于“文本结构化”类问题给你若干行半结构化的日志要求提取关键字段做统计再按指定规则输出。这背后其实是一套非常通用的能力我在实际刷题过程中把它拆成了四块第一字符串拆解能力。日志每一行包含时间戳、级别、模块名、内容这些字段之间是空格、方括号、冒号混在一起。能不能准确把模块名从这串混合文本里抠出来直接决定后面统计结果的质量。第二哈希统计能力。按模块聚合统计ERROR数量本质上就是不停往一个Map或者Counter里累加。这道题还要求只统计ERROR意味着解析的同时要过滤级别不是所有日志都进统计。第三自定义排序能力。按ERROR次数降序次数相同再按模块名字典序升序这种双关键字排序是机试高频考点。比较函数写反了统计做得再对也是零分。第四边界输入处理。空行、多余空格、日志内容里出现伪装成级别的文本都会干扰你的解析。这些边界恰恰是很多人丢分的地方。1.2 为什么这类题目值得单独练有人觉得日志解析是运维场景才用的东西跟算法题不太搭。但实际上机试里考察的就是“非结构化文本处理”这个底层能力。它跟JSON解析、CSV处理、URL参数拆分属于同一类问题。日常开发里分析访问日志、处理异常堆栈、从接口返回里抽字段全都是同一套思维先找稳定分隔符再抽取目标字段最后做聚合或过滤。我在练习时发现一个普遍误区很多同学包括最开始的我一看到日志题就想着上正则觉得一行正则匹配万事大吉。这里有个很大的误解正则适合格式高度规则、干扰极少的场景但日志内容往往不受控。你看到的样例可能很干净测试用例里却可能出现日志内容包含方括号、伪ERROR字样的情况正则容易误匹配。后面第3章我会专门对比正则和字符串切分两种思路这里先留个钩子。另外这道题的规模一般不会很大n通常不超过10000行长度也就是几百字符。O(n)的解析加上O(m log m)的排序m是模块数量完全够用。机试对性能要求不会苛刻到让你手写快排重点是把规则全部写对把边界处理干净。清楚这一点之后解题思路就可以不用考虑什么复杂算法老老实实解析、统计、排序。2. 题目全貌与样例推演2.1 完整题目描述我参考了能见到的真题回忆结合自己的复现把题目整理成下面这个版本。机试时文字表述可能略有省略但核心规则是一致的你按这个版本练习就够了。【日志解析】某系统运行日志每一行的格式如下YYYY-MM-DD HH:mm:ss [级别] 模块名: 日志内容其中YYYY-MM-DD HH:mm:ss是时间戳格式固定[级别]只能是[INFO]、[WARN]、[ERROR]三种模块名由字母、数字、下划线组成不含空格后面紧跟一个英文冒号日志内容是任意文本可能包含空格、英文标点甚至可能包含类似[ERROR]的文本。现在需要你统计每个模块出现的ERROR日志数量并按以下规则输出只统计级别为[ERROR]的日志按ERROR次数从大到小排序若ERROR次数相同按模块名字典序升序排序每行输出一个模块格式为“模块名 次数”中间用空格分隔。输入格式第一行是一个整数n表示日志行数1 ≤ n ≤ 10000。接下来n行每行是一条完整的日志。输出格式按规则排序后的若干行结果。若没有任何ERROR日志则不输出任何内容。2.2 样例手算与两个隐藏规则样例输入6 2026-03-15 10:20:01 [INFO] user_service: login success 2026-03-15 10:20:05 [ERROR] order_service: timeout 2026-03-15 10:20:11 [WARN] user_service: slow query 2026-03-15 10:21:02 [ERROR] user_service: db connection lost 2026-03-15 10:21:30 [ERROR] pay_service: amount mismatch 2026-03-16 09:00:00 [ERROR] order_service: retry failed拿到输入先别急着写代码手算一遍user_service 有三条日志分别是INFO、WARN、ERROR其中ERROR统计为1order_service 有两条日志都是ERROR统计为2pay_service 有一条ERROR统计为1。于是输出应该是order_service 2 pay_service 1 user_service 1因为 order_service 的次数2最大排第一pay_service 和 user_service 次数同为1按字典序 pay 的 p 在 user 的 u 前面所以 pay_service 在前。这个样例里有两个隐藏规则值得特别注意。第一个隐藏规则是“日志内容不可信”。样例里第三行日志内容是 slow query看起来正常但测试用例里完全可能出现类似something [ERROR] appears的内容。如果你用line.split()之后找包含ERROR的片段就可能把日志内容里的[ERROR]统计进去。所以解析时不能只看“有没有ERROR子串”而是要定位级别位置上的[ERROR]token用全等判断。第二个隐藏规则是“时间戳里的冒号”。日志时间戳是10:20:05里面全是冒号。如果你习惯用split(:)去切字符串提取模块名时间戳会把你直接带沟里。正确做法是先按空格切分或者找到[级别]的位置再往后取下一个token。输出规则里“若没有任何ERROR日志则不输出”这个边界也容易被漏掉。我最初写代码时没有处理空统计结果某次自测发现多输出了一行换行符在机试评分里这种格式问题会造成不必要的扣分后面章节我会给出具体处理办法。3. Python实现能用字符串就不用正则3.1 思路的取舍拿到题我最先想到的方案是用正则import re pattern re.compile(r\[ERROR\]\s(\w):)一行正则就能把级别和模块名都抓出来代码非常短小。但真正在本地跑测试时我发现了两个问题。第一\w默认匹配字母、数字、下划线模块名正好是这几个字符匹配没问题。但如果日志内容里出现[ERROR] fake_module:这样的文本比如某条日志正在记录一条错误信息的内容正则search会匹配到日志内容里的假冒ERROR导致误统计。第二正则编译虽然只做一次但逐行search的开销在马甲规模下不算大机试环境里Python本身启动和IO就比C慢能省则省。这道题完全可以用更朴素的字符串方法解决那就没必要给自己加复杂度。所以我最终的方案是先用split()按空白切分整行然后遍历切分后的token找到级别token恰好等于[ERROR]的那一个再把它的下一个token去掉尾部冒号作为模块名。这样做的正确性依据是题目格式里[级别]和模块名:之间是空格切分后这两个字段必然是独立token。判断时直接用token [ERROR]全等比较而不是ERROR in token从根上杜绝误伤。如果有同学就是想用正则我建议改成行首锚定形式来降低误匹配风险pattern re.compile(r^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[ERROR\] (\w):)锚定在行首只匹配开头是时间戳加ERROR的那一条这样日志内容里就算出现[ERROR]也不会被误抓。但这样写对时间格式做了强假设一旦题目描述里时间戳格式有细微差异比如没年份就要调整。所以我认为考场上首选还是字符串切分逻辑直观还不容易出幺蛾子。3.2 完整代码与关键行解释下面这份代码可以直接提交基于Python 3.8不依赖任何第三方库import sys from collections import Counter def main(): data sys.stdin.read().splitlines() if not data: return n int(data[0].strip()) cnt Counter() for i in range(1, n 1): if i len(data): break line data[i].strip() if not line: continue tokens line.split() # 遍历token找级别位置上的 [ERROR] for j, token in enumerate(tokens): if token [ERROR]: # 下一个token形如 order_service:去掉尾部冒号 module tokens[j 1] if module.endswith(:): module module[:-1] cnt[module] 1 break results sorted(cnt.items(), keylambda item: (-item[1], item[0])) out_lines [f{mod} {num} for mod, num in results] sys.stdout.write(\n.join(out_lines)) if __name__ __main__: main()逐段说明一下。sys.stdin.read().splitlines()一次性读取所有输入并按行拆分。相比循环调用input()这种方式在机试环境里更少踩到输入缓冲的坑也不会因为最后一行没有换行符而漏读。读完以后用索引i从1遍历到n。int(data[0].strip())第一行是日志行数n。strip()是为了去掉行尾可能残留的\r尤其Windows环境生成的输入文件不加这个很容易出问题。if not line: continue跳过空行。机试的测试用例偶尔会夹空行尤其是从文本文件复制样例的时候这行代码能保住你的统计不被打乱。内层循环为什么用break每行日志的级别只有一个找到ERROR后没必要继续往后找同时也避免日志内容里的token被误算。如果这行是INFO或WARN遍历完整个tokens也不会触发[ERROR]分支正好实现“只统计ERROR”。排序key写成(-item[1], item[0])第一个维度是次数反方向排Python的sorted默认升序取负就实现降序第二个维度是模块名正序。这个元组key是自定义排序最简洁的写法。要是图省事先按(item[1], item[0])排完再reverseTrue会把模块名也逆序结果完全错。if i len(data): break防止输入给的行数n大于实际行数时越界。正常不会发生但加上它更稳尤其是自测时手滑复制少了行。3.3 三条边界用例自测写完代码不是直接交我习惯先在本地跑几组边界用例。这里分享三个我实际测试过的用例。第一个没有任何ERROR日志3 2026-03-15 10:20:01 [INFO] user_service: login success 2026-03-15 10:20:11 [WARN] user_service: slow query 2026-03-15 10:20:13 [INFO] pay_service: payment done运行结果应该是没有任何输出退出码0。如果代码最后用print()而不是sys.stdout.write就会多输出一个空行机试里可能被判格式错误。所以我最终用sys.stdout.write(\n.join(out_lines))没有额外换行。这个细节过不过在这一下子就能体现出来。第二个模块名和日志内容里都有冒号2 2026-03-15 10:20:05 [ERROR] order_service: timeout: please retry 2026-03-15 10:21:02 [ERROR] user_service: db connection lost关键在于order_service这行日志内容里又出现了一个冒号。因为我们是先按空格切分再对tokens[j1]只做一次尾部冒号去除所以timeout:并不会被误认为模块名tokens[j1]依然是order_service:去掉冒号得到order_service。统计正确。第三个日志内容里出现伪ERROR2 2026-03-15 10:20:05 [ERROR] order_service: timeout, reference [ERROR] in docs 2026-03-15 10:21:02 [INFO] user_service: see [ERROR] example第一行是真正的ERROR统计 order_service 1第二行虽然内容里写了[ERROR]但因为token [ERROR]只匹配级别token而第二行级别token是[INFO]所以不会误统计。运行结果只输出order_service 1。这三组用例跑完我才比较放心地提交。说实话边界用例比主流程更能体现一个人对题目的理解程度。只拿样例验证过就交很多隐藏问题根本发现不了。4. JavaScript实现排序和Map的细节4.1 readline的输入读取姿势JavaScript在机试环境里通常是Node.js运行标准输入需要走readline模块。这里有个常见的写法误区有人直接在line事件的回调里就开始解析但这样很难处理“先读n再读接下来n行”的两段式输入因为回调是逐行触发的你没法确定当前到底读到了哪一行。我个人的习惯是先把所有行收集到数组再统一处理const readline require(readline); const rl readline.createInterface({ input: process.stdin, output: process.stdout }); const lines []; rl.on(line, (line) { lines.push(line); }); rl.on(close, () { // 在这里统一处理 });收集完成后lines数组和Python里sys.stdin.read().splitlines()得到的结果是一样的。内存方面n最大10000每行几百字节总共几MB完全不用操心。读取之后要注意\r的处理。如果判题平台在Windows上生成输入某些line末尾会带\r解析前统一做一次trim()把首尾空白去掉时间戳和模块名都不受影响。我在代码里就是const line lines[i].trim()这样最省心。4.2 完整代码与关键行解释const readline require(readline); const rl readline.createInterface({ input: process.stdin, output: process.stdout }); const lines []; rl.on(line, (line) { lines.push(line); }); rl.on(close, () { if (lines.length 0) { return; } const n parseInt(lines[0].trim(), 10); const cnt new Map(); for (let i 1; i n; i) { if (i lines.length) { break; } const line lines[i].trim(); if (line ) { continue; } const tokens line.trim().split(/\s/); for (let j 0; j tokens.length; j) { if (tokens[j] [ERROR]) { let module tokens[j 1]; if (module.endsWith(:)) { module module.slice(0, -1); } cnt.set(module, (cnt.get(module) || 0) 1); break; } } } const arr Array.from(cnt.entries()); arr.sort((a, b) { if (a[1] ! b[1]) { return b[1] - a[1]; } if (a[0] b[0]) { return -1; } if (a[0] b[0]) { return 1; } return 0; }); const output arr.map(([mod, num]) ${mod} ${num}).join(\n); process.stdout.write(output); });几个关键点单独拆开说。parseInt(lines[0].trim(), 10)第二个参数10必须写。不写的话如果输入是06这种字符串老版本Node可能按八进制解析。现在的环境普遍是新的不写也能得到6但养成写10的习惯总没错。用Map而不是普通对象{}根本原因有两点。第一模块名如果是__proto__、constructor这类特殊名字直接给对象赋值会污染原型链统计就乱了。第二Map的get和set是标准哈希语义对任何字符串key都安全。虽然这道题的模块名由字母数字下划线组成理论上不会出现特殊名字但在其他哈希统计题里普通对象的这个坑非常常见不如一开始就统一用Map。切分统一用line.trim().split(/\s/)这里用正则\s匹配一个或多个空白字符效果跟Python的line.split()一致。如果你用split( )严格按单个空格切分一旦日志里时间戳和级别之间出现多个连续空格token数组里就会混进空字符串后续tokens[j1]取到的可能就不是模块名了。字数不多但属于很容易忽略的细节。排序比较函数里次数相同时按模块名升序。直接用和比较字符串在模块名是纯ASCII字符的前提下安全且高效。如果用localeCompare在中文环境和部分Node版本下可能有不必要的开销而且这里字典序就是ASCII序用最贴题。最后process.stdout.write(output)如果output为空字符串也就是根本没有ERROR日志那就什么都不输出。这里跟Python版一样要避免用console.log多打一个空行。4.3 和Python版本的差异总结两版代码逻辑完全对齐但实现细节有不少差异。我整理了一张对照表方便复习对比点Python版JavaScript版输入读取sys.stdin.read().splitlines()readline收集line事件统计容器collections.CounterMap去尾冒号module.endswith(:) module[:-1]module.endsWith(:) module.slice(0, -1)按空白切分line.split()line.trim().split(/\s/)排序sorted(cnt.items(), keylambda x: (-x[1], x[0]))arr.sort((a, b) b[1] - a[1] || (a[0] b[0] ? -1 : 1))空结果输出sys.stdout.write()process.stdout.write()这张表里最容易被忽略的是“按空白切分”这一行。Python的split()不带参数时会自动处理连续多个空格和首尾空白JS的split( )是严格按单个空格切分。虽然题目格式约定是单空格但考场上样例数据不一定那么规整所以JS这边一定要用split(/\s/)。双语言实现有个额外的好处你在大脑里维护的是同一套“解析统计排序”的逻辑只是翻译成不同语法。机试现场往往只能用一种语言但平时用两种语言各写一遍对逻辑本身的理解会深刻很多遇到相似题也能更快想清楚方案。5. 复盘我踩过的坑和后续备考方向5.1 日志题最容易踩的五个坑第一用split(:)提取字段。时间戳里的冒号会直接导致字段错位这是日志类题目最经典的陷阱。正确做法是先切空格再定位级别token。我第一版Python代码就是这么写错的跑了样例才发现输出全乱排查了半天才意识到是时间戳里的冒号惹的祸。第二排序规则写反。次数降序、模块名升序方向不一样。Python里如果图省事写成sorted(..., reverseTrue)会把模块名也降序排列同次数模块的输出顺序正好跟题目要求相反。JS里则容易忘写第二个排序条件只按次数排次数相同的情况下结果不稳定可能被测试用例卡住。第三统计时忘了过滤级别。有人把逻辑写成“遇到模块名就加1”结果把INFO和WARN也算进去了。题目要求只统计ERROR就必须在解析时判断级别token而不是遇到模块就计数。这个错误在样例里不太明显因为样例的ERROR和非ERROR分布比较均衡但一旦出现大量非ERROR日志统计结果就完全跑偏。第四没处理空结果。没有任何ERROR日志时很多代码会输出一个空行。机试判分机制里多输出空行大概率被判格式错误。处理方式很简单统一用write或stdout.write而不是print或console.log。第五内容里的伪ERROR。日志内容出现[ERROR]字样时如果匹配用了includes(ERROR)或indexOf(ERROR) -1就会误统计。必须用全等判断token [ERROR]或者严格定位级别位置才能避免这个问题。这些坑单个看都不大但在考场上叠加起来就是失分隐患。尤其是机试评分在这种题上往往不是“部分得分”模式一个样例不过可能整道题就拿不到分所以格式和规则的细节必须抠到极致。5.2 从日志解析延伸的同类题型日志解析练完之后我建议往几个方向扩展这些方向在机试里会以不同的包装反复出现。第一个是日志关键词统计。给定一批检索关键词统计每个关键词在日志内容里出现的次数。做法跟本题几乎一样只是把“按模块统计ERROR数量”换成“按关键词统计出现次数”解析时不再提取模块名而是在日志内容里检查关键词是否存在。这考察的是字符串匹配和哈希计数。第二个是日志时间区间聚合。把日志按小时或按天聚合统计每个时间段的ERROR总数。这就需要从时间戳中提取日期时间字段然后按区间分组。本质上还是字符串截取和哈希聚合只是多了一层时间处理。第三个是日志行去重加TOP-K输出。先解析出某个字段比如错误码按出现次数排序后取前K个。排序逻辑和本题几乎一样就是多了一个K的限制直接用堆或者排序都行。第四个是多日志文件合并排序。模拟多个日志源按时间戳归并这个更接近归并排序加双指针。解析日志格式的功底是共通的先把单文件解析练熟这种变体自然也能驾驭。5.3 一道题之外的训练建议最后说一点我个人的备考体会。机试刷题不能只看数量更要看短时间内能不能把一道常规题完整拿住。日志解析这种题我给自己定的标准是读题30秒、写代码10分钟、边界用例验证5分钟全程控制在15分钟以内。如果超时说明字符串切分和排序的基本功还不扎实需要专项补强。补强方法也很简单把机试高频的字符串处理题分门别类练一遍比如IP地址合法性校验、URL参数解析、版本号排序、简易分词统计。练到“看到格式描述立刻能说出用哪种切分方式”的程度日志解析这类题基本就是送分题了。日志解析本质上是这些基础能力的一个综合载体把它练透同类题都能顺手拿下。我自己在完整跑通Python和JS两个版本之后等于把正则和字符串切分的边界差异又复盘了一遍收获很大。如果你也在准备华为OD机试建议不要只背答案而是亲手把两个版本的代码各写一遍遇到排序或者解析卡住的地方再回来对照这篇文章效果一定比直接抄代码好得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑