بستهی 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*`)
با استفاده از این تابع، نیازی به مدیریت خطا نیست.
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 نباشند و فاصلهی ابتدایی و انتهایی نداشته باشند.
باید تصوری کلی داشته باشید از اینکه چند خط لاگ در بایگانی شما با استانداردهای کنونی مطابقت ندارد. به نظر شما یک آزمون ساده مشخص میکند که یک خط لاگ معتبر است یا نه. برای اینکه خطی معتبر شمرده شود، باید با یکی از رشتههای زیر آغاز شود:
تابع IsValidLine را پیادهسازی کنید تا اگر رشتهای معتبر نبود false و در غیر این صورت true برگرداند.
IsValidLine("[ERR] A good error here")
// => true
IsValidLine("Any old [ERR] text")
// => false
IsValidLine("[BOB] Any old text")
// => false
تیم تازهای به سازمان پیوسته است و میبینید که فایلهای لاگشان از جداکنندهای عجیب برای «فیلدها» استفاده میکند. بهجای چیزی معقول مانند دونقطهی «:»، از رشتهای مانند «<--->» یا «<=>» استفاده میکنند (چون قشنگتر است). در واقع هر رشتهای که نخستین نویسهاش «<» و آخرین نویسهاش «>» باشد و در فاصلهی میانشان هر ترکیبی از نویسههای «~»، «*»، «=» و «-» بیاید.
تابع SplitLogLine را پیادهسازی کنید که یک خط میگیرد و آرایهای از رشتهها برمیگرداند که هر کدام شامل یک فیلد است.
SplitLogLine("section 1<*>section 2<~~~>section 3")
// => []string{"section 1", "section 2", "section 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
متوجه شدهاید که بخشی از پردازشهای بالادستی لاگها متن 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 "
متوجه شدهاید که برخی از خطوط لاگ جملههایی دارند که به کاربران اشاره میکنند.
این جملهها همیشه رشتهی "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 " آمده است.