Ачааллж байна...
Түвшин 1-ийн төгсгөлд хүрлээ. Арван нэгэн хичээлийн туршид бид for loop-ийн ард нуугдсан iterator протоколоос эхэлж, өөрсдийн түгээгчийг бүтээж, generator-ийн түр зогсолтыг trace-ээр мөрдөж, generator-уудыг конвейер болгон холбож, itertools ба functools-ийн хэрэгслүүдтэй танилцаж, key= функцийг бүрэн эзэмшсэн. Хичээл бүрт нэг санааг жижиг жишээн дээр судалсан. Энэ хичээлд тэр бүх санаа нэг бодит програмд нэгдэнэ.
Бид log файлын боловсруулагч бүтээнэ. Эхлээд сая мөртэй, 70 мегабайт орчим хэмжээтэй log файл үүсгэнэ. Энэ нь жижиг вэб үйлчилгээний нэг өдрийн log-той ойролцоо хэмжээ юм. Дараа нь тэр файлаас тайлан гаргадаг хэрэгсэл бичнэ: түвшин тус бүрийн тоо, хамгийн олон алдаа гаргасан хэрэглэгчид, хамгийн удаан хүсэлтүүд, эхний хэдэн алдааны дэлгэрэнгүй. Хамгийн чухал шаардлага нь файл хэдий том байсан ч санах ойд нэг удаад цөөхөн мөр л байх ёстой. Энэ шаардлагыг бид хэмжилтээр шалгана.
Энэ бол follow-along бүтээл: та алхам бүрийг дагаж бичиж, шалгах цэг бүрт ажиллуулж, үр дүнгээ харьцуулна. Ажлыг Git-ийн тусдаа branch дээр хийж, төгсгөлд нь Course 4-ийн дадлаар жижиг pytest тест нэмж, commit хийнэ. Аль хичээлийн мэдлэг хаана ашиглагдаж байгааг бид явцын дунд тэмдэглэх болно, ингэснээр Түвшин 1 бүхэлдээ хэрхэн нэг дор ажилладгийг харах боломжтой.
Хичээл 2.3-ийн талх нарийн боовны үйлдвэрийг санаарай. Тэнд дөрвөн ажилчинтай жижиг конвейер таван мөртэй файлыг боловсруулсан. Одоо тэр үйлдвэр бүрэн хүчин чадлаараа ажиллана. Агуулахад сая ширхэг бүтээгдэхүүн байна, конвейер ижил зарчмаар буюу төгсгөлөөс татах зарчмаар ажиллана, туузан дээр нэг удаад цөөхөн бүтээгдэхүүн л байна. Шинээр нэмэгдэх зүйл нь конвейерийн төгсгөл дэх хэд хэдэн хэрэглэгч: нэг нь тоолно, нөгөө нь эрэмбэлнэ, гурав дахь нь хамгийн хүндийг сонгоно.
Төслийн бүтэц дөрвөн файлтай байна:
log-analyzer/
generate_log.py log файл үүсгэгч
log_tools.py pipeline-ий шатууд ба дүгнэлтийн функцүүд
analyze.py тайлан хэвлэгч
measure_memory.py санах ойн хэмжилт
test_log_tools.py pytest тестЭнэ сургалтын repository-доо terminal нээж, шинэ branch үүсгэнэ:
git switch -c level1-log-analyzerДараа нь repository дотор log-analyzer нэртэй хавтас үүсгэж, түүн рүү орно. VS Code-ийн file explorer-оор үүсгэж болно. Бүх командыг энэ хавтаснаас ажиллуулна. venv идэвхтэй байгаа эсэхийг шалгаарай. Энэ бүтээл зөвхөн стандарт сан ба pytest ашиглах бөгөөд pytest Course 4-өөс суусан байх ёстой.
generate_log.py файл үүсгэж, дараах кодыг бичнэ:
python
import random
import sys
from datetime import datetime, timedelta
USERS = ["bat", "saraa", "dorj", "anu", "gan", "oyun", "temuulen", "naraa"]
ACTIONS = ["login", "view", "payment", "upload", "logout"]
LEVELS = ["INFO", "WARNING", "ERROR"]
WEIGHTS = [85, 10, 5]
START = datetime(2026, 10, 9)
def log_lines(count, seed=42):
rng = random.Random(seed)
for i in range(count):
if i % 5000 == 4999:
yield "\n"
if i % 20000 == 19999:
yield "### log rotated ###\n"
moment = START + timedelta(seconds=i // 12)
level = rng.choices(LEVELS, weights=WEIGHTS)[0]
user = rng.choice(USERS)
action = rng.choice(ACTIONS)
duration = int(rng.expovariate(1 / 120)) + 5
yield (
f"{moment:%Y-%m-%d %H:%M:%S} {level} "
f"user={user} action={action} duration_ms={duration}\n"
)
if __name__ == "__main__":
count = int(sys.argv[1]) if len(sys.argv) > 1 else 1_000_000
with open("app.log", "w", encoding="utf-8") as f:
f.writelines(log_lines(count))
print(f"app.log үүслээ: {count:,} бичлэг")Энэ файлыг задалъя. Log мөр үүсгэгч өөрөө generator гэдгийг анзаараарай, Хичээл 2.1-ийн машин. Сая мөрийг list-д цуглуулж байгаад бичихийн оронд мөр бүр асуухад л үүсч, шууд файл руу бичигдэнэ. f.writelines() iterable хүлээн авдаг тул generator-ийг шууд өгсөн. Энэ нь Хичээл 1.3-ын мэдлэг: writelines дотроо iter() ба next() ашиглан мөр мөрөөр нь татдаг. Тэгэхээр файлыг бичих тал ч мөн санах ой хэмнэдэг.
random.Random(seed) нь тогтмол эхлэлтэй санамсаргүй тоо үүсгэгч. seed=42 тогтмол тул програм ажиллах бүрт яг ижил файл үүснэ. Энэ нь таны үр дүнг дахин гаргах боломжтой болгодог. rng.choices(LEVELS, weights=WEIGHTS) нь INFO-г 85 хувь, WARNING-ийг 10, ERROR-ийг 5 хувийн магадлалтайгаар сонгоно. rng.expovariate(1 / 120) нь дунджаар 120 орчим, ихэнхдээ бага, хааяа маш их утга гаргадаг. Бодит хүсэлтийн хугацаа яг ийм тархалттай байдаг: ихэнх нь хурдан, цөөхөн нь удаан. Мөр бүрийн цаг i // 12 секундээр нэмэгдэнэ, тэгэхээр секунд тутамд 12 бичлэг орж, сая бичлэг 23 цаг орчмыг хамарна.
Хоёр жижиг "бохирдол" санаатай нэмэгдсэн. 5000 мөр тутамд хоосон мөр, 20000 мөр тутамд “### log rotated ###” гэсэн буруу хэлбэрийн мөр. Бодит log-д ийм зүйл байнга тааралддаг бөгөөд манай pipeline тэдгээрийг тэсвэрлэх ёстой.
Шалгах цэг 1. Эхлээд жижиг файл үүсгэж, нүдээрээ шалгая:
python generate_log.py 20app.log үүслээ: 20 бичлэгapp.log-ийг VS Code-оор нээвэл иймэрхүү мөрүүд харагдана:
2026-10-09 00:00:00 INFO user=dorj action=view duration_ms=38
2026-10-09 00:00:00 INFO user=bat action=upload duration_ms=161
2026-10-09 00:00:00 WARNING user=anu action=login duration_ms=17Нэр, үйлдэл, хугацаа танайд өөр байж болно, хэлбэр нь ижил байх ёстой. Хэлбэр зөв бол бүтэн файлыг үүсгэнэ:
python generate_log.pyapp.log үүслээ: 1,000,000 бичлэгЭнэ хэдэн секунд үргэлжилнэ. Файлын хэмжээ 70 мегабайт орчим байна. Үүнийг File Explorer-оос шалгаж болно.
log_tools.py файл үүсгэж, эхлээд шатуудыг бичнэ:
python
import heapq
from itertools import chain, islice
LEVELS = {"INFO", "WARNING", "ERROR"}
def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.rstrip("\n")
def non_empty(lines):
return (line for line in lines if line.strip())
def parse_line(line):
parts = line.split(" ", 3)
if len(parts) != 4:
return None
date, time, level, rest = parts
if level not in LEVELS:
return None
fields = dict(pair.split("=", 1) for pair in rest.split() if "=" in pair)
return {
"date": date,
"time": time,
"level": level,
"user": fields.get("user"),
"action": fields.get("action"),
"duration_ms": int(fields.get("duration_ms", 0)),
}
def parse(lines):
return filter(None, map(parse_line, lines))
def only_level(records, level):
return (r for r in records if r["level"] == level)
def build_records(paths):
lines = chain.from_iterable(read_lines(path) for path in paths)
return parse(non_empty(lines))Шат бүрийн гарал үүсэл:
read_lines бол Хичээл 2.3-ын эхний шат, үгчлэн. with блок generator дотор байгаа тул файл бүх мөр уншигдах хүртэл нээлттэй байна.
non_empty энэ удаа generator функц биш, generator expression буцаадаг энгийн функц. Хичээл 2.3-ын Жишээ 1-д бид энгийн шатуудыг generator expression-оор бичиж болохыг харсан. Энд түүнийг функцэд ороож нэр өгсөн тул pipeline угсрахад уншигдахуйц болно.
parse_line бол нэг мөрийг dict болгодог энгийн функц, Хичээл 2.3-ын parse_line-ийн илүү бат бөх хувилбар. Мөр дөрвөн хэсэгт хуваагдахгүй эсвэл түвшин нь танигдахгүй бол алдаа шидэхийн оронд None буцаана. Мессежийн хэсэг “user=bat action=login duration_ms=38”-ийг зайгаар хувааж, хэсэг бүрийг “=” тэмдгээр хоёр болгож, dict() руу generator expression өгснөөр fields dict бүтээгдэнэ. if "=" in pair нөхцөл “=” тэмдэггүй хэсгийг алгасна. Тэгэхгүй бол dict() алдаа гаргана.
parse бол энэ бүтээлийн хамгийн товч шат: filter(None, map(parse_line, lines)). Энэ бол Хичээл 3.1-ийн хоёр сургамжийн яг хэрэглээ. map-д бэлэн нэртэй функц parse_line өгсөн тул lambda шаардлагагүй, map цэвэрхэн. filter(None, ...) нь None буюу задлагдаагүй мөрүүдийг хаяна. Бүх dict үнэн гэж тооцогддог тул хүчинтэй бичлэгүүд бүгд үлдэнэ. Нэг мөрөнд хоёр lazy шат.
build_records бол бүхэл pipeline-ийг угсардаг функц. Хичээл 2.4-ийн chain.from_iterable нь хэд хэдэн файлыг нэг урсгал болгоно, гэхдээ бид одоохондоо нэг файл өгнө. Энэ функцийн оршин байх шалтгаан чухал: generator нэг удаагийн хэрэглээтэй учраас тайлангийн хэсэг бүр шинэ pipeline хэрэгтэй. build_records-ийг дуудах бүр шинэ, эхэндээ зогссон машинуудын гинж үүсгэнэ. Энэ бол Хичээл 2.3-ын Түгээмэл алдааны "хоосорсон pipeline-ийг дахин ашиглах" асуудлын шийдэл.
Шалгах цэг 2. Python-ий interactive горимыг log-analyzer хавтаснаас нээгээд, эхний гурван бичлэгийг харъя:
python
>>> from itertools import islice
>>> from log_tools import build_records
>>> records = build_records(["app.log"])
>>> for r in islice(records, 3):
... print(r){'date': '2026-10-09', 'time': '00:00:00', 'level': 'INFO', 'user': 'dorj', 'action': 'view', 'duration_ms': 38}
{'date': '2026-10-09', 'time': '00:00:00', 'level': 'INFO', 'user': 'bat', 'action': 'upload', 'duration_ms': 161}
{'date': '2026-10-09', 'time': '00:00:00', 'level': 'WARNING', 'user': 'anu', 'action': 'login', 'duration_ms': 17}Энэ гурван мөр нүдэнд энгийн харагдах ч ард нь болсон зүйлийг санаарай. 70 мегабайт файлаас зөвхөн эхний гурван мөр уншигдсан. islice гурав дахь бичлэгийн дараа асуухаа больсон тул үлдсэн 999,997 мөр хүрэгдсэнгүй. Хичээл 2.4-ийн trace энд бодит хэмжээгээр ажиллаж байна.
log_tools.py файлын төгсгөлд дөрвөн функц нэмнэ. Тэд бол конвейерийн төгсгөл дэх хэрэглэгчид.
python
def count_levels(records):
counts = {}
for r in records:
counts[r["level"]] = counts.get(r["level"], 0) + 1
return counts
def top_error_users(records, n=5):
counts = {}
for r in only_level(records, "ERROR"):
counts[r["user"]] = counts.get(r["user"], 0) + 1
ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
return ranked[:n]
def slowest(records, n=5):
return heapq.nlargest(n, records, key=lambda r: r["duration_ms"])
def first_errors(records, n=3):
return list(islice(only_level(records, "ERROR"), n))count_levels бичлэгүүдийг нэг нэгээр нь авч, түвшин тус бүрийн тоог dict-д хуримтлуулна. Сая бичлэг дамжих ч санах ойд гурван түлхүүртэй жижиг dict л байна.
top_error_users эхлээд only_level-ээр ERROR бичлэгүүдийг шүүж, хэрэглэгч тус бүрийн алдааг тоолно. Дараа нь Хичээл 3.2-ын tuple key-ээр эрэмбэлнэ: (-item[1], item[0]) нь эхлээд тоогоор буурах, тоо ижил бол нэрээр өсөх дарааллаар. counts.items() нь (нэр, тоо) хосууд өгдөг тул item[1] тоо, item[0] нэр. Энд sorted() бүх элементийг санах ойд авдаг ч, тэр нь сая бичлэг биш, ердөө найман хэрэглэгч тул асуудалгүй. Эрэмбэлэхийн өмнө өгөгдлийг жижигрүүлэх нь их өгөгдөлтэй ажиллах ухаалаг дадал.
slowest нь шинэ хэрэгсэлтэй танилцуулна: стандарт сангийн heapq модулийн nlargest. Хамгийн удаан таван хүсэлтийг олохын тулд сая бичлэгийг бүгдийг нь эрэмбэлэх нь сая dict-ийг санах ойд авахыг шаардана. nlargest өөр аргаар ажилладаг: iterable-ийг нэг нэгээр нь уншиж, тухайн мөч хүртэлх хамгийн том n элементийг л хадгална. Шинэ элемент тэдгээрийн хамгийн жижгээс том бол түүнийг солино, бага бол шууд хаяна. Санах ойд үргэлж таван бичлэг л байна. Хичээл 3.2-ын key= энд ч ажиллаж байгааг анзаараарай: nlargest key= хүлээн авдаг тул түүнд dict-ийн аль талбараар харьцуулахыг хэлж өгсөн. heapq-д ижил ажиллагаатай nsmallest ч бий.
first_errors Хичээл 2.4-ийн islice-ийг only_level-тэй хослуулна. Гурав дахь алдаа олдмогц pipeline зогсож, файлын үлдсэн хэсэг уншигдахгүй.
Шалгах цэг 3. Interactive горимыг дахин нээгээд (log_tools өөрчлөгдсөн тул шинээр импортлох хэрэгтэй), функц бүрийг шинэ pipeline дээр шалгая:
python
>>> from log_tools import build_records, count_levels, first_errors
>>> count_levels(build_records(["app.log"]))
{'INFO': 849822, 'WARNING': 100341, 'ERROR': 49837}
>>> for r in first_errors(build_records(["app.log"])):
... print(r["time"], r["user"], r["action"])00:00:01 oyun payment
00:00:03 gan view
00:00:05 bat uploadТаны тоонууд, нэрс яг эдгээр биш байж болох ч харьцаа нь ойролцоо байх ёстой: INFO 85 хувь орчим, WARNING 10, ERROR 5. Гурван тоог нэмэхэд яг 1,000,000 гарна. Энэ нь хоосон мөр болон “### log rotated ###” мөрүүд бүгд амжилттай хаягдсаныг баталж байна. count_levels хэдэн секунд зарцуулах бол first_errors бараг шууд хариулсныг анзаараарай. Эрт зогсолтын ялгааг та өөрийн нүдээр харж байна.
Тус бүрийн дуудлагад build_records-ийг шинээр дуудсан гэдгийг анзаар. Хэрэв бид records = build_records(["app.log"]) гэж нэг удаа үүсгээд хоёр функцэд өгсөн бол хоёр дахь нь хоосон үр дүн гаргах байсан. Үүнийг Түгээмэл алдаанууд хэсэгт дахин харна.
analyze.py файл үүсгэнэ:
python
import sys
import time
from log_tools import (
build_records,
count_levels,
first_errors,
slowest,
top_error_users,
)
def main(paths):
start = time.perf_counter()
print("=== Түвшин тус бүрийн тоо ===")
levels = count_levels(build_records(paths))
for level, n in sorted(levels.items(), key=lambda item: -item[1]):
print(f"{level:<8} {n:>10,}")
print("\n=== Хамгийн олон алдаатай хэрэглэгчид ===")
for user, n in top_error_users(build_records(paths)):
print(f"{user:<10} {n:>6,}")
print("\n=== Хамгийн удаан 5 хүсэлт ===")
for r in slowest(build_records(paths)):
print(f"{r['time']} {r['user']:<10} {r['action']:<8} {r['duration_ms']:>6} ms")
print("\n=== Эхний 3 алдаа ===")
for r in first_errors(build_records(paths)):
print(f"{r['time']} {r['user']:<10} {r['action']}")
elapsed = time.perf_counter() - start
print(f"\nНийт хугацаа: {elapsed:.2f} секунд")
if __name__ == "__main__":
main(sys.argv[1:] or ["app.log"])main функц тайлангийн хэсэг бүрт build_records-ийг шинээр дуудаж, шинэ pipeline үүсгэнэ. Түвшний тоог хэвлэхдээ key=-ийг дахин ашиглаж, хамгийн олон тоотойг эхэнд гаргасан. f-string доторх :<8 нь текстийг 8 тэмдэгтийн зайд зүүн тийш, :>10, нь тоог 10 тэмдэгтийн зайд баруун тийш, мянгатын таслалтай байрлуулна. Ингэснээр тайлан хүснэгт шиг жигд харагдана. Сүүлийн мөрөнд sys.argv[1:] or ["app.log"] нь command line-аас файлын нэр өгвөл түүнийг, өгөхгүй бол app.log-ийг ашиглана. Хичээл 3.3-ын time.perf_counter() нийт хугацааг хэмжинэ.
Шалгах цэг 4.
python analyze.pyМиний компьютер дээрх нэг ажиллуулалт:
=== Түвшин тус бүрийн тоо ===
INFO 849,822
WARNING 100,341
ERROR 49,837
=== Хамгийн олон алдаатай хэрэглэгчид ===
temuulen 6,312
gan 6,288
bat 6,251
naraa 6,240
oyun 6,203
=== Хамгийн удаан 5 хүсэлт ===
14:32:07 saraa upload 1874 ms
03:11:45 dorj view 1802 ms
19:58:30 anu login 1766 ms
08:04:12 bat payment 1731 ms
22:17:59 gan logout 1719 ms
=== Эхний 3 алдаа ===
00:00:01 oyun payment
00:00:03 gan view
00:00:05 bat upload
Нийт хугацаа: 6.84 секундТаны тоо, нэр, хугацаа өөр байх нь хэвийн. Python-ий хувилбар болон компьютерийн хурдаас хамааран нийт хугацаа гурваас арван хэдэн секундийн хооронд байж болно. Хэрэглэгчдийн алдааны тоо бие биентэйгээ ойролцоо, 6,000 орчим байх ёстой, учир нь генератор хэрэглэгчдийг тэгш магадлалтай сонгодог. Хамгийн удаан хүсэлтүүд 1,500-2,000 ms орчим байх ёстой.
Нэг шударга тэмдэглэл: энэ тайлан файлыг дөрвөн удаа уншдаг, хэсэг бүрт нэг удаа, сүүлийнх нь маш богино. Энэ бол ухамсартай сонголт: хэсэг бүр бие даасан, ойлгоход хялбар, нэг бүрчлэн тестлэгддэг. Үүний үнэ нь хугацаа. Файлыг нэг удаа уншиж бүх дүгнэлтийг зэрэг хийх хувилбар илүү хурдан ч илүү төвөгтэй. Үүнийг сонголтот өргөтгөлд санал болгоно.
Бидний гол шаардлага санах ойд цөөхөн мөр л байх явдал. Хичээл 2.2-ын tracemalloc-оор үүнийг шалгая. Харьцуулахын тулд "бүх файлыг санах ойд уншдаг" хувилбар бичнэ. measure_memory.py:
python
import tracemalloc
from log_tools import build_records, count_levels, parse_line
def streaming():
return count_levels(build_records(["app.log"]))
def whole_file_in_memory():
with open("app.log", encoding="utf-8") as f:
lines = f.readlines()
records = (parse_line(line.rstrip("\n")) for line in lines if line.strip())
return count_levels(r for r in records if r is not None)
def measure(label, func):
tracemalloc.start()
result = func()
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{label}: {result}")
print(f" оргил санах ой: {peak / 1_000_000:,.2f} MB")
measure("Урсгалаар унших ", streaming)
measure("Бүтэн файлыг ачаалах", whole_file_in_memory)whole_file_in_memory нь f.readlines()-аар бүх мөрийг нэг дор list болгон уншина. Үлдсэн хэсэг нь generator хэвээр тул ялгаа зөвхөн мөрүүдийг хадгалах аргад байна.
Шалгах цэг 5.
python measure_memory.pytracemalloc ажиллагааг удаашруулдаг тул энэ нь өмнөхөөс удаан, хагас минут хүртэл үргэлжилж магадгүй. Миний компьютер дээрх нэг ажиллуулалт:
Урсгалаар унших : {'INFO': 849822, 'WARNING': 100341, 'ERROR': 49837}
оргил санах ой: 0.03 MB
Бүтэн файлыг ачаалах: {'INFO': 849822, 'WARNING': 100341, 'ERROR': 49837}
оргил санах ой: 131.48 MBХоёр хувилбар яг ижил хариу өгсөн. Урсгалаар уншдаг pipeline 0.03 мегабайт буюу 30 килобайт орчим оргил санах ой хэрэглэсэн. Бүтэн файлыг ачаалдаг хувилбар 130 мегабайт гаруй, дөрвөн мянга орчим дахин их. Таны тоонууд Python-ий хувилбараас хамааран өөр байж болох ч зэрэглэлийн ялгаа хаана ч ижил. Бүтэн файлын хувилбарын санах ой файлын хэмжээнээс ч их байгааг анхаар: string объект бүр текстээсээ гадна нэмэлт мэдээлэл агуулдаг. Хэрэв log 10 гигабайт байсан бол бүтэн файлын хувилбар ихэнх компьютерийг гацаах бол урсгалын хувилбар ижил 30 килобайттайгаар ажиллана. Энэ бол Түвшин 1-ийн бүх санааны хэмжигдсэн үр дүн.
test_log_tools.py файл үүсгэнэ:
python
from log_tools import parse, parse_line, top_error_users
def test_parse_line_reads_all_fields():
line = "2026-10-09 08:15:40 ERROR user=saraa action=payment duration_ms=230"
assert parse_line(line) == {
"date": "2026-10-09",
"time": "08:15:40",
"level": "ERROR",
"user": "saraa",
"action": "payment",
"duration_ms": 230,
}
def test_bad_lines_skipped_and_users_ranked():
lines = [
"2026-10-09 08:00:00 ERROR user=bat action=login duration_ms=10",
"### log rotated ###",
"2026-10-09 08:00:01 ERROR user=anu action=view duration_ms=20",
"2026-10-09 08:00:02 INFO user=bat action=view duration_ms=30",
"2026-10-09 08:00:03 ERROR user=bat action=payment duration_ms=40",
]
assert top_error_users(parse(lines)) == [("bat", 2), ("anu", 1)]Хоёр дахь тест энэ бүтээлийн дизайны давуу талыг харуулж байна: parse-д файл биш, энгийн list өгсөн. Pipeline-ий шат бүр эх сурвалж нь юу болохыг мэддэггүй тул тестэд файл үүсгэх шаардлагагүй, хэдэн мөрийн list хангалттай. Тест буруу хэлбэрийн мөр алгасагдаж, INFO мөр тооцогдохгүй, хэрэглэгчид зөв эрэмбэлэгдэж байгааг нэг дор шалгана. Ажиллуулах:
pytest -q.. [100%]
2 passed in 0.02sCommit хийхээс өмнө нэг чухал алхам: app.log бол 70 мегабайт үүсгэгдсэн файл, түүнийг repository-д хадгалах ёсгүй. Хүссэн үедээ generate_log.py-аар дахин үүсгэж болно. log-analyzer хавтаст .gitignore файл үүсгэж, дотор нь app.log гэсэн нэг мөр бичнэ. Энэ файлыг VS Code-оор үүсгээрэй, PowerShell-ийн echo командаар биш, учир нь Windows PowerShell зарим хувилбарт Git уншиж чадахгүй кодчилолтой файл үүсгэдэг. Дараа нь:
git status
git add .
git commit -m "Add streaming log analyzer with tests"git status-ийн гаралтад app.log байхгүй, таван Python файл ба .gitignore л байгаа эсэхийг шалгаад commit хийнэ. Энэ branch-ийг main руу merge хийх эсэх нь таны сонголт, Course 4-т сурсан аргаар хийж болно.
Хүсвэл дараах өргөтгөлүүдээс сонгож туршиж болно. Тэдгээр нь заавал биш бөгөөд дараагийн хичээлүүд тэдгээрийг хийсэн гэж огт үзэхгүй.
Нэг удаагийн уншилт. analyze.py файлыг дөрвөн удаа уншдаг. Бүх тоолуурыг нэг loop дотор зэрэг шинэчилдэг single_pass_report(records) функц бичиж, хугацааг perf_counter-оор хэмжиж, одоогийн хувилбартай харьцуулж болно. Хурд хэр нэмэгдэх вэ, код хэр төвөгтэй болох вэ? Энэ хоёрын солилцоог өөрийн хэмжилтээр мэдрэх нь үнэтэй дасгал.
Олон өдрийн log. generate_log.py-д START огноо болон гаралтын файлын нэрийг argument болгон өгдөг болгоод, app-2026-10-09.log ба app-2026-10-10.log гэсэн хоёр файл үүсгэнэ. Дараа нь python analyze.py app-2026-10-09.log app-2026-10-10.log гэж ажиллуулна. build_records аль хэдийн chain.from_iterable ашигладаг тул analyze.py-д нэг ч мөр өөрчлөх шаардлагагүй.
Үйлдэл тус бүрийн дундаж хугацаа. Үйлдэл бүрийн нийт хугацаа ба тоог хуримтлуулж, дунджийг тооцоолоод, хамгийн удаанаас нь эхлэн key=-ээр эрэмбэлж хэвлэдэг функц нэмж болно. Түүнд зориулсан тест бичихэд Алхам 6-ийн list-тэй загвар бэлэн байгаа.
FileNotFoundError: [Errno 2] No such file or directory: ‘app.log’. "Ийм файл эсвэл хавтас байхгүй: ‘app.log’" гэсэн утгатай. Програмыг log-analyzer хавтаснаас биш, өөр газраас ажиллуулсан эсвэл generate_log.py-г хараахан ажиллуулаагүй үед гарна. Python файлын замыг terminal-ийн одоогийн хавтаснаас хайдаг. Terminal-ийн мөрийн эхэнд ямар зам харагдаж байгааг шалгаад, шаардлагатай бол cd log-analyzer хийнэ.
ModuleNotFoundError: No module named ‘log_tools’. “log_tools нэртэй модуль олдсонгүй” гэсэн утгатай. analyze.py эсвэл interactive горимыг log_tools.py байгаа хавтаснаас өөр газраас ажиллуулахад гарна. Python модулийг одоогийн хавтас болон суулгасан сангуудаас хайдаг. Шийдэл нь ижил: log-analyzer хавтаснаас ажиллуулах. Файлын нэрийг санамсаргүй log-tools.py гэж зураасаар бичсэн бол ч энэ алдаа гарна, учир нь Python модулийн нэрэнд зураас зөвшөөрдөггүй.
Тайлангийн нэг хэсэг хоосон гарах. Мессежгүй алдаа, энэ бүтээлийн хамгийн чухал сургамж. main дотор хугацаа хэмнэх гэж records = build_records(paths) гэж нэг удаа үүсгээд дөрвөн функцэд өгвөл count_levels зөв тоолох ч top_error_users хоосон жагсаалт, slowest хоосон, first_errors хоосон буцаана. Python юу ч хэлэхгүй. count_levels pipeline-ийг эцэс хүртэл хоосолсон тул үлдсэн гурав хоосорсон машинуудаас асуусан. Энэ бол Хичээл 1.2-оос хойш бидний олон удаа харсан iterator-ийн нэг удаагийн шинж бодит програмд. Шийдэл нь хэсэг бүрт build_records-ийг шинээр дуудах.
ValueError: invalid literal for int() with base 10: ‘12ms’. “10-тын тооллын систем дэх int()-д хүчингүй утга: ‘12ms’” гэсэн утгатай. Энэ нь манай үүсгэсэн файлд гарахгүй ч хэрэв та энэ хэрэгслийг өөр эх сурвалжийн бодит log дээр туршвал гарч болно: duration_ms=12ms гэх мэт таамаглаагүй хэлбэр. Traceback parse_line-ийн int() мөрийг заана. Шийдэл нь parse_line дотор int() хөрвүүлэлтийг try/except ValueError-ээр хамгаалж, хөрвүүлэгдэхгүй бол None буцаах. Тэгвэл filter(None, ...) тэр мөрийг бусад буруу мөрийн адил хаяна.
git status-д app.log харагдах. .gitignore файлыг зөв хавтаст үүсгээгүй, эсвэл нэр нь .gitignore.txt болсон байж магадгүй. Windows File Explorer зарим үед өргөтгөлийг нуудаг тул VS Code-оор файлын нэрийг шалгаарай. Хэрэв app.log-ийг аль хэдийн git add хийсэн бол git restore --staged app.log командаар бэлтгэлээс гаргаж болно.
Бид 70 мегабайт log файлыг 30 килобайт орчим санах ойгоор боловсруулдаг бүрэн хэрэгсэл бүтээлээ. Файл үүсгэгч хүртэл generator-оор бичигдсэн тул бичих тал ч хэмнэлттэй. Pipeline-ий шатууд Хичээл 2.3-ын загвараар бие даасан бөгөөд parse шат map ба filter(None, ...)-ийг хослуулсан. build_records chain.from_iterable-аар олон файлыг дэмжиж, дуудлага бүрт шинэ pipeline үүсгэдэг нь iterator-ийн нэг удаагийн шинжийн практик шийдэл. Дүгнэлтийн функцүүд tuple key-ээр эрэмбэлж, heapq.nlargest-ээр сая бичлэгээс таван хамгийн удааныг санах ойд таваас илүү бичлэг хадгалалгүй олж, islice-аар эрт зогссон. Хэмжилт урсгалын ба бүтэн файлын хувилбаруудын хооронд дөрвөн мянга орчим дахин санах ойн ялгааг харуулсан. Pipeline-ий шатууд эх сурвалжаа мэддэггүй тул тест энгийн list-ээр бичигдсэн. Ажил branch дээр commit хийгдэж, үүсгэгдсэн файл .gitignore-оор хасагдсан.
Энэ хичээлээр Түвшин 1 дуусч байна. Та for loop-ийн ард юу байдгийг бүрэн мэдэж, түүнийг өөрийн хэрэгцээнд нийцүүлэн бүтээж, том өгөгдөлд хэрэглэж чаддаг боллоо. Дараагийн түвшин Function-ийн эзэн бөгөөд энэ сургалтын хамгийн хэцүү, хамгийн гоё санаануудын нэгээр эхэлнэ: closure. Энэ бүтээлд та функцүүдийг утга мэт дамжуулсан: parse_line-ийг map-д, lambda-г key-д. Бүлэг 4-т бид асуух болно: функцийг өөр функцийн дотор үүсгээд гадагш буцаахад, тэр функц төрсөн орчныхоо хувьсагчдыг хаана хадгалж явдаг вэ? Энэ асуултын хариулт нь Хичээл 3.3-ын global түлхүүр үгийг илүү цэвэр шийдлээр солих, эцэст нь decorator-ыг бүрэн ойлгох замыг нээнэ.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.