ในการค้นหาและแทนที่ข้อความใน Dart หากใช้งานผ่าน String class จะมีคำสั่งที่ช่วยในการทำงานได้แก่
ใช้ค้นหา
ใช้แทนที่
คำสั่งเหล่านี้ หากผู้ใช้ต้องการค้นหาหรือแทนที่ด้วยข้อความที่มีรูปแบบแบบข้อความธรรมดา ก็สามารถใช้ข้อความที่สนใจได้ทันที โดยหากดูใน API จะพบว่าตัวข้อความที่ใช้ค้นหา จะเป็น Pattern class มันเป็น interface ที่ใช้สำหรับการค้นหาข้อความแบบพื้นฐาน
String animal = "cat bat rat";
print(animal.indexOf('bat')); // output → 4
print(animal.replaceRange(4, 7, 'BAT')); // output → cat BAT rat
print(animal.contains('rat')); // output → true
print(animal.startsWith('cat')); // output → true
print(animal.startsWith('Cat')); // output → false
print(animal.endsWith('rat')); // output → true
print(animal.replaceAll('t', 'T')); // output → caT baT raT
print(animal.replaceFirst('t', 'T')); // output → caT bat rat
print(animal.replaceAll('rat', 'mouse')); // output → cat bat mouse
หากผู้ใช้งานจำเป็นต้องการค้นหาข้อความในรูปแบบที่กำหนด ไม่ใช่เป็นข้อความตรง ๆ ตามตัวอย่างข้างต้น Dart จะมี RegExp class ช่วยในการสร้างรูปแบบตามที่ผู้ใช้ต้องการ รูปแบบข้อความที่ใช้ใน RegExp เรียกกว่า regular expression pattern เป็นรูปแบบโดยการใช้ข้อความ ตัวอักษร สัญญาลักษณ์ต่าง ๆ แทนรูปแบบที่ต้องการ โดยรูปแบบสามารถเขียนได้ตั้งแต่ง่าย ๆ จนถึงซับซ้อน
แนวคิดของ regular expressions เริ่มต้นขึ้นในปี ค.ศ.1950 เมื่อนักคณิตศาสตร์ชาวอเมริกัน Stephen Cole Kleene ได้กำหนดแนวคิดของ regular language ซึ่งเป็นภาษาที่มีแบบแผนในการกำหนดวิธีการสะกดหรือลำดับตัวอักษร เพื่ออธิบายรูปแบบข้อความที่ต้องการ ใน Dart จะใช้รูปแบบเดียวกับมาตรฐานของ JavaScript regular expressions
การใช้ RegExp จะช่วยให้งานบางอย่างทำเสร็จเร็วขึ้น แต่อาจต้องแลกกับเวลาที่ใช้ในการประมวลผลรูปแบบของ RegExp ที่เพิ่มเข้ามา โดยหากเป็นรูปแบบที่ซับซ้อน การใช้ RegExp จะช่วยประหยัดเวลาในการทำงานมาก เพราะไม่ต้องเขียนคำสั่งเพื่อจัดการข้อความที่อาจต้องใช้เวลาเขียนเยอะ ตัวอย่าง ลองแปลงคำในข้อความเป็น proper case ด้วย RegExp
String animal = "cat bat rat";
RegExp properCase = RegExp(r'(\w)(\w+)');
String result = animal.replaceAllMapped(
properCase,
(Match m) => "${m[1]!.toUpperCase()}${m[2]}"
);
print(result); // output → Cat Bat Rat
สิ่งที่จำเป็นในการสร้าง RegExp ก็คือข้อความ source
ที่เป็น regular expressions และเงื่อนไขในการค้นหา ดังนี้
RegExp(
String source, {
bool multiLine = false,
bool caseSensitive = true,
bool unicode = false,
bool dotAll = false,
})
ในกรณีที่ผู้ใช้งานระบุ source
ที่ไม่ใช่ regular expressions จะทำให้เกิด FormatException error ขึ้น
RegExp test = RegExp('moon');
print(test.hasMatch("The moon so big")); // output → true
print(test.hasMatch("The sun is bigger")); // output → false
RegExp notValid = RegExp('?'); // FormatException: Nothing to repeat ?
ในชีวิตจริง การเขียน regular expressions ไม่ใช่เรื่องง่าย เนื่องจากเป็นภาษาที่อธิบายรูปแบบของข้อความที่ต้องการ แม้ว่าตัว VSCode จะช่วยเตือนตั้งแต่ก่อน compile แล้วว่าตัว regular expressions ไม่ถูกต้อง ดังนั้นการใช้เครื่องมือช่วยในการเขียน regular expressions น่าจะเป็นทางออกที่ดีกว่า
การแจ้งเตือน regular expressions ไม่ถูกต้องใน VSCode
เครื่องมือช่วยในการเขียน regular expressions แบบ Online ที่ใช้งานง่าย มีคำอธิบาย พร้อมทั้งยั้งสามารถทดสอบการทำงานได้ ส่วนตัวใช้ https://regex101.com ตัว engine ใช้ JavaScript ซึ่ง Dart ก็ใช้งานตัวนี้ด้วยเช่นกัน
https://regex101.com
ก่อนที่จะไปพูดถึง RegExp กันต่อ จะขออธิบายในส่วนของ regular expressions เบื่องต้น ที่ใช้กันบ่อย ๆ
รูปแบบข้อความธรรมดาที่เป็นตัวอักษร สามารถระบุได้โดยตรงได้เลย เช่น moon
sun
หมูเด้ง
หากใช้ข้อความตรงไปตรงมา จะมีค่าไม่ต่างจากการเปรียบเทียบข้อความแบบปกติที่ใช้ใน String class
String hippo = 'หมูเด้ง';
String message = "หลายคนที่ติดตาม เพจขาหมูแอนด์เดอะแก๊ง ได้ติดตามพัฒนาการของหมูเด้ง"
" ลูกฮิปโปแคระตัวตึง ซุปตาร์ดวงใหม่ของสวนสัตว์เปิดเขาเขียว";
// test use RegExp
RegExp testReg = RegExp(hippo);
print(testReg.hasMatch(message)); // output → true
// test use simple contains() method
print(message.contains(hippo)); // output → true
เมื่อทำใน regex101.com จะมีคำอธิบาย และผลของการจับคู่คำที่ตรงกับ regular expressions
ผลการทดสอบใน regex101.com
.
เพื่อแทนตัวอักษรใด ๆใน regular expressions จะใช้ .
แทนตัวอักษรใด ๆ ก็ได้จำนวน 1 ตัวอักษร รวมถึงช่องว่าง และอักษรพิเศษต่าง ๆ
RegExp anyChar = RegExp('.');
anyChar.allMatches("abc 123 ").forEach((RegExpMatch m) {
print(m.group(0));
});
ผลที่ได้
a
b
c
1
2
3
\s
\S
ตัว .
จะแทนตัวอักษรทุกตัว หากต้องการแยกเป็นตัวอักษรปกติ กับ ตัวอักษรที่เป็นช่องว่าง (white space) สามารถใช้
\s
แทนตัวอักษรที่เป็นช่องว่าง ได้แก่ space, tab, line feed (newline), carriage return, form feed, vertical tab\S
แทนตัวอักษรปกติที่ไม่ใช่ตัวอักษรว่างตัวอย่างข้างล่าง จะเป็นการเลือกเฉพาะตัวอักษร ไม่รวมช่องว่าง ผลที่ได้ตัว space จะไม่ถูกนำมารวมในผลการจับคู่
RegExp anyChar = RegExp(r'\S'); // match a none white space
anyChar.allMatches("abc 123 ").forEach((RegExpMatch m) {
print(m.group(0));
});
ผลที่ได้
a
b
c
1
2
3
\d
\D
\d
แทนตัวเลขใด ๆ 0 ถึง 9\D
แทนตัวอักษรที่ไม่ใช่ตัวเลข 0 ถึง 9RegExp anyChar = RegExp(r'\d'); // match a digit
anyChar.allMatches("abc 123 ").forEach((RegExpMatch m) {
print(m.group(0));
});
ผลที่ได้
1
2
3
[...]
สามารถกำหนดกลุ่มตัวอักษรที่ต้องการได้ดังนี้
[abcz]
เฉพาะตัวอักษร a b c z จำนวน 1 ตัว[ก-ฮ]
เฉพาะตัวอักษร ก ถึง ฮ จำนวน 1 ตัว[A-Z0-9]
เฉพาะตัวอักษร A ถึง Z หรือ 0 ถึง 9 จำนวน 1 ตัวหากต้องการกำหนดกลุ่มตัวอักษรที่ไม่ต้องการ ก็สามารถทำได้โดยการใช้ ^
[^abcz]
ตัวอักษรที่ไม่ใช่ a b c z จำนวน 1 ตัว[^ก-ฮ]
ตัวอักษรที่ไม่ใช่ ก ถึง ฮ จำนวน 1 ตัว[^A-Z0-9]
ตัวอักษรที่ไม่ใช่ A ถึง Z และ 0 ถึง 9 จำนวน 1 ตัวตัวอักษรพิเศษต่าง ๆ เช่น +
*
.
?
เมื่ออยู่ใน [ ]
จะถูกตีความหมายว่าเป็นอักษรนั้น ๆ ไม่ใช่อักษรพิเศษอีกต่อไป
สามารถใช้รูปแบบด้านล่างต่อท้ายเพื่อใช้ระบุจำนวนที่ต้องการ
?
0 ตัว หรือ 1 ตัว*
0 ตัว หรือมากกว่า+
1 ตัว หรือมากกว่า{3}
3 ตัว{3,}
3 ตัวหรือมากกว่า{3,6}
3 ถึง 6 ตัวRegExp anyChar = RegExp(r'\d{3}'); // match 3 digits
anyChar.allMatches("abc 123 ").forEach((RegExpMatch m) {
print(m.group(0));
});
ผลที่ได้
123
|
รูปแบบทางเลือกใช้สำหรับจับคู่โดยหากรูปแบบแรกไม่ตรง ให้ดูรูปแบบที่สอง เช่น ant|cat
คือเลือกจับคู่ ant หรือ cat
RegExp regChoose = RegExp('ant|cat');
regChoose.allMatches("ant bat cat").forEach((RegExpMatch m) {
print(m.group(0));
});
ผลที่ได้
ant
cat
หากต้องการกำหนดรูปแบบกับตัวอักษรพิเศษที่ใช้มาข้างต้น ได้แก่ \
[
]
{
}
.
?
*
|
<
>
ให้ใส่ \
ไว้ข้างหน้าเหมือน escape string ใน Dart ตัวอย่างต่อไปนี้จะเป็นการเขียนเพื่อจับคู่ ***
และ [abc]
String test = " *** [abc] ";
RegExp star = RegExp(r'\*{3}'); // match ***
star.allMatches(test).forEach((RegExpMatch m) {
print(m.group(0));
});
RegExp special = RegExp(r'\[.+\]'); // match [....]
special.allMatches(test).forEach((RegExpMatch m) {
print(m.group(0));
});
ผลที่ได้
***
[abc]
อีกความสามารถที่ใช้บ่อยคือ การทำ sub pattern เพื่ออ้างถึงส่วนของข้อความที่สนใจ การกำหนด sub pattern ทำได้โดยการใช้ ( )
ครอบส่วนที่ต้องการเอาไว้ เช่น การแยกส่วนของอีเมลแบบง่าย ๆ เป็นชื่อ กับ โดเมน โดยใช้ ([^@]+)@(.+)
@
ไม่ได้@
ทั้งหมดทดสอบใน regex101.com
RegExp simpleEmail = RegExp(r"([^@]+)@(.+)");
Iterable<RegExpMatch> matchs = simpleEmail.allMatches("info@google.com");
print(matchs.length); // output → 1
RegExpMatch match = matchs.first;
print(match.groupCount); // output → 2
for (var i = 0; i <= match.groupCount; i++) {
print("Group: $i → ${match.group(i)}");
}
ผลที่ได้
1
2
Group: 0 → info@google.com
Group: 1 → info
Group: 2 → google.com
จะเห็นว่าผลที่ได้จากการจับคู่ของกลุ่ม 1 และ 2 เหมือนกับที่แสดงใน regex101.com
^
และ $
.
ที่เปลี่ยนไป\n
\r
\t
\0
(?: )
\1
\2
.hasMatch()
.hasMatch()
คำสั่งนี้จะคล้ายกับ .contains()
ของ String แต่เปลี่ยนไปทดสอบตาม RegExp ที่กำหนดแทน ตัวอย่างทดสอบว่ามี รหัสไปรณีย์ หรือไม่
RegExp postCode = RegExp(r'[1-9]\d{4}');
bool result = postCode.hasMatch("ที่อยู่ 12/13 หาดใหญ่ สงขลา 90110");
print(result); // output → true
อธิบายรูปแบบ [1-9]\d{4}
[1-9]
ตัวเลข 1 ถึง 9 จำนวน 1 ตัว\d{4}
ตัวเลข 0-9 จำนวน 4 ตัวสามารถเลือกใช้ได้ 2 แบบคือ
.firstMatch()
ค้นหาการจับคู่แรกของ regular expression กับข้อความที่กำหนด ผลที่ได้เป็น RegExpMatch?
.allMatches()
ค้นหาการจับคู่ทั้งหมดของ regular expression กับข้อความที่กำหนด ผลที่ได้เป็น Iterable<RegExpMatch>
การใช้งานก็ตรงไปตรงมา หากต้องการจับคู่เพื่อเอาข้อมูลชุดแรกที่เจอ หรือข้อมูลที่จะนำไปจับคู่มีแค่คู่เดียวหรือไม่มี การใช้ .firstMatch()
จะสมเหตุสมผลมากกว่า และมีประสิทธิภาพในการทำงานมากกว่า และหากไม่เจอข้อความที่จับคู่ได้ก็จะได้ค่ากลับมาเป็น null
แต่หากต้องการข้อมูลทุกตัวที่จับคู่ได้ก็ใช้ .allMatches()
RegExpMatch class เป็น class ที่จะเก็บผลการจับคู่ สามารถประยุกต์ใช้งานได้หลายแบบ ตามข้อมูลที่ได้จากข้อความที่จับคู่ได้ โดยตัว RexExpMatch เป็นการ implement Match class อีกที
RegExpMatch class
.start
.end
คืนค่าตำแหน่งที่พบตรงกับ regular expression ที่กำหนด สามารถนำค่านี้ไปใช้เพื่อแทนที่ข้อความดังกล่าวได้
String animal = "cat bat rat";
RegExp bat = RegExp('bat');
RegExpMatch? match = bat.firstMatch(animal);
if (match != null) {
print(animal.replaceRange(match.start, match.end, 'BAT')); // output → cat BAT rat
} else {
print('not found "bat"');
}
ใน regular expression ที่มีการกำหนด sub pattern หรือ group สามารถเข้าถึงข้อมูลของ group ที่พบได้ดังนี้
.groupCount
จำนวน group ที่พบ.group()
คืนค่าข้อความที่ตรงกับ sub pattern แต่ละตำแหน่ง โดย .group(0)
จะเป็นข้อความทั้งหมดที่ตรงกับ regular expressionString allEmail = "contact@simple.zzz info@simple.zzz";
RegExp email = RegExp(r'([^@]+)@(\S+)'); // match email address with 2 sub patterns
RegExpMatch? match = email.firstMatch(allEmail);
if (match != null) {
print(match.groupCount); // output → 2
print(match.group(0)); // output → contact@simple.zzz
print(match.group(1)); // output → contact
print(match.group(2)); // output → simple.zzz
}
วิธีการนับ index ของ .group()
String allEmail = "contact@simple.zzz info@simple.zzz";
RegExp email = RegExp(r'[^@]+@\S+'); // remove sub pattern
RegExpMatch? match = email.firstMatch(allEmail);
if (match != null) {
print(match.groupCount); // output → 0
print(match.group(0)); // output → contact@simple.zzz
print(match.group(1)); // RangeError: Value not in range: 1
print(match.group(2)); // RangeError: Value not in range: 2
}
[]
ในกรณีที่ต้องการเข้าถึง group ด้วยการเขียนที่สั้นกว่า สามารถใช้ []
แทนได้
String allEmail = "contact@simple.zzz info@simple.zzz";
RegExp email = RegExp(r'([^@]+)@(\S+)');
RegExpMatch? match = email.firstMatch(allEmail);
if (match != null) {
print(match[0]); // output → contact@simple.zzz
print(match[1]); // output → contact
print(match[2]); // output → simple.zzz
}
.groups()
หากต้องการดึงข้อมูล group ที่จับคู่ได้ออกมาเป็น List โดยกำหนดตัวหมายเลข group ที่สนใจ สามารถใช้คำสั่ง .groups()
มีรูปแบบการใช้งานดังนี้
List<String?> groups(
List<int> groupIndices
)
ให้ผู้ใช้ส่ง List ของเลข group ที่ต้องการเข้าไป ก็จะได้ List ของข้อความที่อยู่ใน group นั้นออกมา โดยเลข group ก็คือเลขเดียวกันที่ใช้กับคำสั่ง .group()
String allEmail = "contact@simple.zzz info@simple.zzz";
RegExp email = RegExp(r'([^@]+)@(\S+)');
RegExpMatch? match = email.firstMatch(allEmail);
if (match != null) {
List<String?> result = match.groups([1, 2]);
print(result); // output → [contact, simple.zzz]
print(result.join('@')); // output → contact@simple.zzz
}
นอกจากการใช้เลขลำดับเพื่อเข้าถึงข้อความที่ตรงกับ sub pattern ที่กำหนดแล้ว ยังสามารถใส่ชื่อเพื่ออ้างถึง group ได้ผ่านคำสั่ง .namedGroup()
วิธีการกำหนดชื่อใน sub pattern (?<name>)
การกำหนดชื่อให้กับ sub pattern
String allEmail = "contact@simple.zzz info@simple.zzz";
RegExp email = RegExp(r'(?<account>[^@]+)@(?<domain>\S+)');
RegExpMatch? match = email.firstMatch(allEmail);
if (match != null) {
print(match.groupCount); // output → 2
// access via group name
print(match.namedGroup('account')); // output → contact
print(match.namedGroup('domain')); // output → simple.zzz
// you can access via group number too.
print(match.group(1)); // output → contact
print(match.group(2)); // output → simple.zzz
}
หากต้องการตรวจสอบว่ามี group name อะไรบ้างที่จับคู่ได้ สามารถใช้คำสั่ง .groupNames()
เพื่อคืนค่า Iterable ของ group name ทั้งหมดมาให้
String allEmail = "contact@simple.zzz info@simple.zzz";
RegExp email = RegExp(r'(?<account>[^@]+)@(?<domain>\S+)');
RegExpMatch? match = email.firstMatch(allEmail);
if (match != null) {
print(match.groupCount); // output → 2
print(match.groupNames); // output → (account, domain)
}