مسیرها
/
Go
Go
/
تمرین‌ها
/
تجزیه‌ی فایل‌های لاگ
تجزیه‌ی فایل‌های لاگ

تجزیه‌ی فایل‌های لاگ

تمرین یادگیری

مقدمه

بسته‌ی regexp از عبارت‌های باقاعده در Go پشتیبانی می‌کند.

نحوه‌ی نگارش

نحوه‌ی نگارش عبارت‌های باقاعده‌ای که پذیرفته می‌شوند، همان نحوه‌ی نگارش عمومی است که Perl، Python و زبان‌های دیگر به کار می‌برند.

هم الگوهای جست‌وجو و هم متن‌های ورودی به‌صورت UTF-8 تفسیر می‌شوند.

وقتی از بک‌تیک‌ها (`) برای ساختن رشته‌ها استفاده می‌کنید، بک‌اسلش‌ها (\) هیچ معنای خاصی ندارند و آغاز کاراکترهای خاصی مانند تب‌های \t یا خطوط جدید \n را علامت نمی‌زنند:

"\t\n" // regular string literal with 2 characters: a tab and a newline
`\t\n`// raw string literal with 4 characters: two backslashes, a 't', and an 'n'

به همین دلیل، استفاده از بک‌تیک‌ها برای ساختن عبارت‌های باقاعده مطلوب است، چون یعنی لازم نیست بک‌اسلش‌ها را اسکیپ کنیم:

"\\" // string with a single backslash
`\\` // string with 2 backslashes

کامپایل الگوها: نوع RegExp

برای استفاده از یک عبارت باقاعده، ابتدا باید الگوی رشته‌ای را کامپایل کنیم. کامپایل در اینجا یعنی برداشتن الگوی رشته‌ای عبارت باقاعده و تبدیل آن به یک نمایش داخلی که کار کردن با آن آسان‌تر است. فقط باید هر الگو را یک بار کامپایل کنیم؛ بعد از آن می‌توانیم نسخه‌ی کامپایل‌شده‌ی عبارت باقاعده را چندین بار به کار ببریم. نوع regexp.Regexp یک عبارت باقاعده‌ی کامپایل‌شده را نمایش می‌دهد. می‌توانیم یک الگوی رشته‌ای را با استفاده از تابع regexp.Compile به یک regexp.Regexp کامپایل کنیم. این تابع در صورت شکست کامپایل، nil و یک خطا برمی‌گرداند:

re, err := regexp.Compile(`(a|b)+`)
fmt.Println(re, err) // => (a|b)+ <nil>
re, err = regexp.Compile(`a|b)+`)
fmt.Println(re, err) // => <nil> error parsing regexp: unexpected ): `a|b)+`

تابع MustCompile جایگزین راحت‌تری برای Compile است:

re = regexp.MustCompile(`[a-z]+\d*`)

با استفاده از این تابع، نیازی به مدیریت خطا نیست.

Caution

MustCompile را فقط زمانی باید استفاده کرد که مطمئن باشیم الگو کامپایل می‌شود، چون در غیر این صورت برنامه دچار panic می‌شود.

متدهای عبارت باقاعده

۱۶ متد از Regexp وجود دارد که با یک عبارت باقاعده تطبیق می‌دهند و متن تطبیق‌شده را مشخص می‌کنند. نام‌هایشان با این عبارت باقاعده تطبیق داده می‌شود:

Find(All)?(String)?(Submatch)?(Index)?
  • اگر All وجود داشته باشد، متد تطبیق‌های پیاپی و بدون هم‌پوشانی کل عبارت را تطبیق می‌دهد.
  • اگر String وجود داشته باشد، آرگومان یک رشته است؛ در غیر این صورت برشی از بایت‌ها است؛ مقادیر بازگشتی به‌تناسب تنظیم می‌شوند.
  • اگر Submatch وجود داشته باشد، مقدار بازگشتی برشی است که زیرتطبیق‌های پیاپی عبارت را مشخص می‌کند.
  • اگر Index وجود داشته باشد، تطبیق‌ها و زیرتطبیق‌ها با جفت‌های اندیس بایتی درون رشته‌ی ورودی مشخص می‌شوند.

همچنین متدهایی برای این موارد وجود دارد:

  • جایگزین کردن تطبیق‌های عبارت‌های باقاعده با رشته‌های جایگزین و
  • تقسیم رشته‌هایی که با عبارت‌های باقاعده از هم جدا شده‌اند.

در مجموع، بسته‌ی regexp بیش از ۴۰ تابع و متد تعریف می‌کند. در ادامه استفاده از چند متد را نشان می‌دهیم. برای جزئیات این توابع و توابع دیگر، لطفاً مستندات API را ببینید.

مثال‌های MatchString

متد MatchString مشخص می‌کند که آیا یک رشته شامل تطبیقی از یک عبارت باقاعده است یا نه.

re = regexp.MustCompile(`[a-z]+\d*`)
b = re.MatchString("[a12]")       // => true
b = re.MatchString("12abc34(ef)") // => true
b = re.MatchString(" abc!")       // => true
b = re.MatchString("123 456")     // => false

مثال‌های FindString

متد FindString رشته‌ای برمی‌گرداند که متن چپ‌ترین تطبیق عبارت باقاعده را در خود دارد.

re = regexp.MustCompile(`[a-z]+\d*`)
s = re.FindString("[a12]")       // => "a12"
s = re.FindString("12abc34(ef)") // => "abc34"
s = re.FindString(" abc!")       // => "abc"
s = re.FindString("123 456")     // => ""

مثال‌های FindStringSubmatch

متد FindStringSubmatch برشی از رشته‌ها را برمی‌گرداند که متن چپ‌ترین تطبیق عبارت باقاعده و در صورت وجود، تطبیق‌های زیرعبارت‌های آن را در خود دارد. از این می‌توان برای شناسایی رشته‌های منطبق با گروه‌های ضبط استفاده کرد. مقدار بازگشتی nil نشان‌دهنده‌ی عدم تطبیق است.

re = regexp.MustCompile(`[a-z]+(\d*)`)
sl = re.FindStringSubmatch("[a12]")       // => []string{"a12","12"}
sl = re.FindStringSubmatch("12abc34(ef)") // => []string{"abc34","34"}
sl = re.FindStringSubmatch(" abc!")       // => []string{"abc",""}
sl = re.FindStringSubmatch("123 456")     // => <nil>

مثال‌های ReplaceAllString

متد re.ReplaceAllString(src,repl) نسخه‌ای از src را برمی‌گرداند و تطبیق‌های عبارت باقاعده‌ی re را با رشته‌ی جایگزین repl جایگزین می‌کند.

re = regexp.MustCompile(`[a-z]+\d*`)
s = re.ReplaceAllString("[a12]", "X")       // => "[X]"
s = re.ReplaceAllString("12abc34(ef)", "X") // => "12X(X)"
s = re.ReplaceAllString(" abc!", "X")       // => " X!"
s = re.ReplaceAllString("123 456", "X")     // => "123 456"

مثال‌های Split

متد re.Split(s,n) متن s را به زیررشته‌هایی که با عبارت از هم جدا شده‌اند تقسیم می‌کند و برشی از زیررشته‌های بین آن تطبیق‌های عبارت را برمی‌گرداند. تعداد n حداکثر تعداد زیررشته‌هایی را تعیین می‌کند که باید برگردانده شوند. اگر n<0 باشد، متد همه‌ی زیررشته‌ها را برمی‌گرداند.

re = regexp.MustCompile(`[a-z]+\d*`)
sl = re.Split("[a12]", -1)      // => []string{"[","]"}
sl = re.Split("12abc34(ef)", 2) // => []string{"12","(ef)"}
sl = re.Split(" abc!", -1)      // => []string{" ","!"}
sl = re.Split("123 456", -1)    // => []string{"123 456"}

دستورالعمل‌ها

این تمرین به تجزیه‌ی فایل‌های لاگ می‌پردازد.

پس از یک بازبینی امنیتی اخیر، از شما خواسته شده است که فایل‌های لاگ بایگانی‌شده‌ی سازمان را پاک‌سازی کنید.

تضمین شده است که همه‌ی رشته‌هایی که به توابع داده می‌شوند null نباشند و فاصله‌ی ابتدایی و انتهایی نداشته باشند.

1. خطوط لاگ مخدوش را شناسایی کنید

باید تصوری کلی داشته باشید از اینکه چند خط لاگ در بایگانی شما با استانداردهای کنونی مطابقت ندارد. به نظر شما یک آزمون ساده مشخص می‌کند که یک خط لاگ معتبر است یا نه. برای اینکه خطی معتبر شمرده شود، باید با یکی از رشته‌های زیر آغاز شود:

  • [TRC]
  • [DBG]
  • [INF]
  • [WRN]
  • [ERR]
  • [FTL]

تابع IsValidLine را پیاده‌سازی کنید تا اگر رشته‌ای معتبر نبود false و در غیر این صورت true برگرداند.

IsValidLine("[ERR] A good error here")
// => true
IsValidLine("Any old [ERR] text")
// => false
IsValidLine("[BOB] Any old text")
// => false

2. خط لاگ را تقسیم کنید

تیم تازه‌ای به سازمان پیوسته است و می‌بینید که فایل‌های لاگشان از جداکننده‌ای عجیب برای «فیلدها» استفاده می‌کند. به‌جای چیزی معقول مانند دونقطه‌ی «:»، از رشته‌ای مانند «<--->» یا «<=>» استفاده می‌کنند (چون قشنگ‌تر است). در واقع هر رشته‌ای که نخستین نویسه‌اش «<» و آخرین نویسه‌اش «>» باشد و در فاصله‌ی میانشان هر ترکیبی از نویسه‌های «~»، «*»، «=» و «-» بیاید.

تابع SplitLogLine را پیاده‌سازی کنید که یک خط می‌گیرد و آرایه‌ای از رشته‌ها برمی‌گرداند که هر کدام شامل یک فیلد است.

SplitLogLine("section 1<*>section 2<~~~>section 3")
// => []string{"section 1", "section 2", "section 3"},

3. شمارش خطوطی که password را در متن داخل گیومه دارند

تیم باید از ارجاع‌های موجود به گذرواژه‌ها در متن داخل گیومه باخبر شود تا بتواند آن‌ها را به‌صورت دستی بررسی کند.

تابع CountQuotedPasswords را پیاده‌سازی کنید تا نشانه‌ای از مقیاس احتمالی بررسی دستی به دست دهد.

خطوط لاگی را مشخص کنید که در آن‌ها رشته‌ی password، با هر ترکیبی از حروف بزرگ و کوچک، میان گیومه‌ها قرار گرفته است. باید این احتمال را در نظر بگیرید که میان گیومه‌ها، پیش و پس از password، محتوای دیگری هم باشد. هر خط حداکثر دو گیومه خواهد داشت.

خطوطی که به این تابع داده می‌شوند ممکن است طبق تعریف بخش ۱ معتبر باشند یا نباشند. ما آن‌ها را یکسان پردازش می‌کنیم، چه معتبر باشند و چه نباشند.

lines := []string{
    `[INF] passWord`, // contains 'password' but not surrounded by quotation marks
    `"passWord"`,  // count this one
    `[INF] User saw error message "Unexpected Error" on page load.`, // does not contain 'password'
    `[INF] The message "Please reset your password" was ignored by the user`, // count this one
}
// => 2

4. حذف باقی‌مانده‌ها از لاگ

متوجه شده‌اید که بخشی از پردازش‌های بالادستی لاگ‌ها متن end-of-line را همراه با یک شماره‌ی خط (بدون فاصله‌ی میان آن دو) در سراسر لاگ‌ها پراکنده کرده است.

تابع RemoveEndOfLineText را پیاده‌سازی کنید تا یک رشته بگیرد، متن end-of-line را از آن حذف کند و یک رشته‌ی «پاک» برگرداند.

خطوطی که متن end-of-line ندارند باید بدون تغییر برگردانده شوند.

فقط رشته‌ی end-of-line را حذف کنید. سعی نکنید فاصله‌ها را تنظیم کنید.

RemoveEndOfLineText("[INF] end-of-line23033 Network Failure end-of-line27")
// => "[INF]  Network Failure "

5. برچسب زدن خطوط با اسم کاربران

متوجه شده‌اید که برخی از خطوط لاگ جمله‌هایی دارند که به کاربران اشاره می‌کنند. این جمله‌ها همیشه رشته‌ی "User" را دارند و پس از آن یک یا چند نویسه‌ی فاصله و سپس یک اسم کاربر می‌آید. تصمیم می‌گیرید چنین خطوطی را برچسب بزنید.

تابعی به اسم TagWithUserName پیاده‌سازی کنید که خطوط لاگ را پردازش می‌کند:

  • خطوطی که رشته‌ی "User " را ندارند بدون تغییر می‌مانند.
  • برای خطوطی که رشته‌ی "User " را دارند، [USR] را به‌همراه اسم کاربر به ابتدای خط بیفزایید.

برای مثال:

result := TagWithUserName([]string{
    "[WRN] User James123 has exceeded storage space.",
	"[WRN] Host down. User   Michelle4 lost connection.",
	"[INF] Users can login again after 23:00.",
	"[DBG] We need to check that user names are at least 6 chars long.",
})
// => []string {
//  "[USR] James123 [WRN] User James123 has exceeded storage space.",
//  "[USR] Michelle4 [WRN] Host down. User   Michelle4 lost connection.",
//  "[INF] Users can login again after 23:00.",
//  "[DBG] We need to check that user names are at least 6 chars long."
// }

می‌توانید فرض کنید که:

  • در لاگ، پس از اسم کاربر دست‌کم یک نویسه‌ی فضای خالی می‌آید.
  • در هر خط حداکثر یک بار رشته‌ی "User " آمده است.
  • اسم‌های کاربر رشته‌هایی ناتهی هستند که فضای خالی ندارند.
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
Go Exercism

آماده‌اید تجزیه‌ی فایل‌های لاگ را شروع کنید؟

در Exercism ثبت‌نام کنید تا Go را همراه با 34 مفهوم165 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.