Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> การเขียนโปรแกรม Bash

เชี่ยวชาญ Regular Expressions: คู่มือปฏิบัติจริงพร้อมตัวอย่าง Real-World

Regular Expressions (Regex) คืออะไร?

Regular expressions หรือ regex ทำงานโดยการกำหนด pattern ที่ใช้ค้นหาตัวอักษรหรือคำเฉพาะภายใน string

เมื่อกำหนด pattern แล้ว คุณสามารถแก้ไข ลบตัวอักษร/คำ แทนที่ สกัดข้อมูลที่เกี่ยวข้องจากไฟล์หรือ string ที่มี pattern นั้นๆ ได้

ทำไมควรเรียน Regex?

Regex ช่วยประมวลผลข้อความแบบประหยัดเวลา และเพิ่มความสนุกในกระบวนการ ทำให้การค้นหาข้อมูลง่ายขึ้น และสามารถ batch edit/replace/delete หรือประมวลผลได้ทีละหลายรายการ

ตัวอย่างการใช้งานจริง: batch file renaming, parsing logs, validating forms, mass edits ใน codebase, recursive search

Regex Basics

Regular expression คือลำดับตัวอักษรที่ match pattern นอกจาก literal characters (เช่น 'abc') ยังมี meta characters (*,+,? ฯลฯ) ที่มีวัตถุประสงค์พิเศษ และ features เช่น character classes ที่ช่วยลดความซับซ้อน

ก่อนเขียน regex ต้องเข้าใจ edge cases ทั้งหมดของ pattern ที่ต้องการ เช่น ต้องการ match 'Hello World' - บรรทัดต้องเริ่มด้วย 'Hello' หรือไม่? เว้นวรรค 1 ช่องหรือมากกว่า? มีตัวอักษรหลัง 'World' ได้หรือไม่? Case sensitive หรือไม่?

Exact Match

รูปแบบพื้นฐานที่สุด: match ลำดับตัวอักษรคล้าย Ctrl-F ใน text editor

Character Set

Character sets ให้ match ตัวอักษรตัวใดตัวหนึ่งจากกลุ่ม โดยล้อมด้วย square brackets []

ตัวอย่าง: t[ah]i match "tai" และ "thi" - 't' และ 'i' คงที่ ระหว่างกลางเป็น 'a' หรือ 'h'

Match Ranges ใน Regex

ใช้ - เป็น range operator สำหรับกลุ่มตัวอักษรที่เรียงลำดับ:

RangeMatches
[A-Z]ตัวพิมพ์ใหญ่ A-Z
[a-z]ตัวพิมพ์เล็ก a-z
[0-9]ตัวเลข 0-9

ระบุ partial ranges ได้ เช่น [b-e] match 'bcde' หรือ [3-6] match '3456'

รวมหลาย ranges และตัวอักษรอื่นๆ ได้ เช่น [3-6u-w;] match '3456uvw;'

Match ตัวอักษรที่ไม่อยู่ใน Set

ขึ้นต้น set ด้วย ^ เพื่อ inverse operation: [^A-Z0-9] match ทุกอย่างยกเว้นตัวพิมพ์ใหญ่และตัวเลข

Character Classes

Class สำหรับกลุ่มตัวอักษรที่ใช้บ่อย ทำให้ expression สะอาดขึ้น:

ClassMatchesEquivalent
.อะไรก็ได้ยกเว้น newline[^\n\r]
\wword character[a-zA-Z0-9_]
\Wnon-word character[^\w]
\ddigits[0-9]
\Dnon-digits[^\d]
\sspace, tab, newlines[ \t\r\n\f]
\Snon-whitespace[^\s]

Class . ช่วยไม่ต้องเขียนทุกตัวอักษรใน set เช่น t.. match เริ่มด้วย t ตามด้วยตัวอักษร 2 ตัว

Quantifiers

Quantifiers ใช้สำหรับ repetition แทนการเขียนซ้ำ:

QuantifierMatches
*0 หรือมากกว่า
?0 หรือ 1
+1 หรือมากกว่า
{n}ตรง n ครั้ง
{n,}n ครั้งขึ้นไป
{n,m}n ถึง m ครั้ง

ตัวอย่าง: can\s+write match 'can' + 1+ whitespace + 'write' (ไม่ match 'canwrite' เพราะ \s+ ต้องมีอย่างน้อย 1 whitespace)

Capture Groups

Capture groups คือ sub-expressions ในวงเล็บ () สร้างได้ไม่จำกัด และ nested ได้

(The ){2} match 'The ' 2 ครั้ง แต่ The {2} จะ match 'The' + 2 spaces (quantifier ใช้กับ space ไม่ใช่ group)

Logical OR ใน Regex

ใช้ | match หลาย pattern: This is (good|bad|sweet) match 'This is ' + 'good' หรือ 'bad' หรือ 'sweet'

ความสำคัญของ capture group: This is good|bad|sweet (ไม่มี group) จะ match 'This is good' หรือ 'bad' หรือ 'sweet' แยกกัน

Reference Capture Groups

อ้างอิง nth captured group ด้วย \n (ใน replacement บาง tool ใช้ $n)

(This) is \1 power capture 'This' อ้างอิงด้วย \1 match 'This is This power'

Named Capture Groups

ตั้งชื่อ group: (?<name>pattern) backreference: \k<name> replacement: $<name> (syntax เปลี่ยนได้ตามภาษา)

ใช้ Regex กับ Command Line Tools

Recursive Regex Search ด้วย grep

grep ค้นหา pattern ในไฟล์ หรือ recursive search

Windows: winget install -e --id GnuWin32.Grep

ตัวอย่าง CTF challenge: zip file หลายระดับ ไฟล์เยอะ Flag format: coderush{flag is here} ค้นหา pattern coderush{.*}

unzip ripG.zip && cd ripG
grep --color -R "coderush{.*}"

-R enable recursive search

Substitution ด้วย sed

sed ใช้ insertion, deletion, substitution บน text files ด้วย regex

sed 's/pattern/replacement/g' filename
echo "${text}" | sed 's/pattern/replacement/g'

g = replace all occurrences -n suppress default print, ใช้ p แทน g เพื่อ print เฉพาะบรรทัดที่ถูกแทนที่

ตัวอย่าง: แทนที่ Henlo number ด้วย Hello number เฉพาะบรรทัดที่มี "GREP"

grep "GREP" texts.txt | sed -En 's/Henlo ([0-9]+)/Hello \1/p'
# หรือ
sed -En '/GREP/ s/Henlo ([0-9]+)/Hello \1/p' texts.txt

Advanced Regex: Lookarounds

Lookaheads และ Lookbehinds (รวมเรียกว่า lookarounds) ตรวจสอบการมีอยู่ของ pattern โดยไม่รวมใน match - เรียกว่า zero-width assertions

Lookbehinds

Match 'linux' ก็ต่อเมื่อ 'GNU' อยู่ก่อนหน้า และ match แค่ 'linux' เท่านั้น

Positive lookbehind: (?<=GNU.*)linux - match 'linux' ก็ต่อเมื่อ 'GNU.*' อยู่ก่อนหน้า

Negative lookbehind: (?<!GNU.*)linux - match 'linux' ก็ต่อเมื่อ 'GNU.*' ไม่อยู่ก่อนหน้า

Lookaheads

Match 'Hello' ก็ต่อเมื่อ 'World' อยู่หลัง และ match แค่ 'Hello'

Positive lookahead: Hello(?=.*World)

Negative lookahead: Hello(?!.*World) - match 'Hello' ก็ต่อเมื่อ 'World' ไม่อยู่หลัง

OperationRegEx
Positive lookahead(?=C)X
Negative lookahead(?!C)X
Positive lookbehind(?<=C)X
Negative lookbehind(?<!C)X

Practical Examples

Logs Parsing

Log lines:

[1/10000] Train loss: 11.30368, Valid loss: 8.95446, Elapsed_time: 7.58941
[500/10000] Train loss: 0.96180, Valid loss: 0.20098, Elapsed_time: 82.48651

Extract training/validation loss: Train loss: (\d+\.\d+) และ Valid loss: (\d+\.\d+)

Python example:

import re
f = open("log_train.txt", "r").read()
train_loss = re.findall(r'Train loss: (\d+\.\d+)', f)
valid_loss = re.findall(r'Valid loss: (\d+\.\d+)', f)
train_loss = [float(i) for i in train_loss]
valid_loss = [float(i) for i in valid_loss]

Bulk File Renaming

ไฟล์ชื่อ Random_Episode_1.mp4 ต้องการ rename เป็น 1.mp4, 2.mp4...

Regex: .*Episode_([0-9]+)\.mp4 capture หมายเลข replacement: \1.mp4

for i in *.mp4; do
    newname=$(echo $i | sed -En 's/.*Episode_([0-9]+)\.mp4/\1.mp4/p')
    mv $i $newname
done; ls

Email Validation

Simple regex: ^[^@ ]+@[^@.]+\.\w+$ match format A@B.C

Password Constraints

ใช้ positive lookaheads สำหรับ constraints:

  • Min 8, Max 16 chars: .{8,16}
  • อย่างน้อย 1 lowercase: (?=.*[a-z])
  • อย่างน้อย 1 uppercase: (?=.*[A-Z])
  • อย่างน้อย 1 digit: (?=.*\d)

Regex รวม: ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,16}$

ต้องการอย่างน้อย 5 uppercase: ^(?=.*[a-z])(?=(.*[A-Z]){5,})(?=.*\d).{8,16}$

Reject ถ้ามี 'pass' หรือ '1234': ^(?!.*(pass|1234)).*$ (negative lookahead)

Final Words

เข้าใจ core concepts และรู้ว่า regex ทำได้อะไรสำคัญกว่าจำ syntax ลืมก็ google หรือ cheatsheet ได้ ฝึกมากขึ้น จะเขียน regex ซับซ้อนได้ offline

ไม่บังคับใช้ regex ถ้า string.find() เพียงพอ แต่ถ้าอยู่ใน terminal บ่อย regex จะช่วยมาก