التعبيرات النمطية في Dart
مقدمة في التعبيرات النمطية
التعبيرات النمطية (regex) هي أنماط قوية تُستخدم لمطابقة النصوص والبحث فيها ومعالجتها. في Dart، توفر فئة RegExp دعماً كاملاً للتعبيرات النمطية بناءً على مواصفات ECMAScript (JavaScript). سواء كنت تحتاج التحقق من المدخلات أو استخراج البيانات أو تحويل النصوص، فإن regex أداة أساسية.
إنشاء RegExp
void main() {
// الطريقة 1: منشئ RegExp
final digitPattern = RegExp(r'\d+');
// الطريقة 2: مع الأعلام
final caseInsensitive = RegExp(r'hello', caseSensitive: false);
final multiLine = RegExp(r'^\w+', multiLine: true);
final dotAll = RegExp(r'start.*end', dotAll: true); // . تطابق أسطر جديدة
// الطريقة 3: وضع يونيكود
final unicode = RegExp(r'\p{L}+', unicode: true); // فئات أحرف يونيكود
}
r'...') لأنماط regex. بدون البادئة r، يفسّر Dart الخطوط المائلة العكسية كأحرف هروب، لذا '\d' يجب كتابتها كـ '\\d'. النصوص الخام تمرر الخط المائل العكسي حرفياً.بناء الأنماط الأساسي
إليك مرجعاً سريعاً لأكثر أنماط regex شيوعاً في Dart:
عناصر الأنماط الشائعة
void main() {
// فئات الأحرف
RegExp(r'\d'); // أي رقم (0-9)
RegExp(r'\D'); // أي غير رقم
RegExp(r'\w'); // حرف كلمة (a-z, A-Z, 0-9, _)
RegExp(r'\W'); // حرف غير كلمة
RegExp(r'\s'); // مسافة بيضاء (مسافة، تبويب، سطر جديد)
RegExp(r'\S'); // غير مسافة بيضاء
RegExp(r'[aeiou]'); // أي حرف علة
RegExp(r'[^aeiou]');// أي غير حرف علة
RegExp(r'[a-zA-Z]');// أي حرف
// المُحددات الكمية
RegExp(r'a*'); // صفر أو أكثر من 'a'
RegExp(r'a+'); // واحد أو أكثر من 'a'
RegExp(r'a?'); // صفر أو واحد 'a'
RegExp(r'a{3}'); // بالضبط 3 'a'
RegExp(r'a{2,5}'); // بين 2 و 5 'a'
RegExp(r'a{2,}'); // 2 أو أكثر 'a'
// المراسي
RegExp(r'^start'); // تطابق في البداية
RegExp(r'end$'); // تطابق في النهاية
RegExp(r'\bhello\b'); // حدود الكلمة
// التناوب
RegExp(r'cat|dog'); // تطابق 'cat' أو 'dog'
}
المطابقة: hasMatch و firstMatch و allMatches
توفر RegExp في Dart ثلاث طرق مطابقة أساسية. كل منها تخدم غرضاً مختلفاً حسب ما إذا كنت تحتاج فحصاً منطقياً أو أول تطابق أو جميع التطابقات.
طرق المطابقة الثلاث
void main() {
final pattern = RegExp(r'\d+');
final text = 'Order 42 has 3 items worth 150 dollars';
// hasMatch — تُرجع true/false
print(pattern.hasMatch(text)); // true
print(pattern.hasMatch('no numbers here')); // false
// firstMatch — تُرجع أول RegExpMatch أو null
final first = pattern.firstMatch(text);
if (first != null) {
print('تطابق: ${first.group(0)}'); // تطابق: 42
print('البداية: ${first.start}'); // البداية: 6
print('النهاية: ${first.end}'); // النهاية: 8
}
// allMatches — تُرجع جميع التطابقات كـ Iterable
final all = pattern.allMatches(text);
print('وُجد ${all.length} تطابقات:');
for (final match in all) {
print(' "${match.group(0)}" في الموضع ${match.start}');
}
// وُجد 3 تطابقات:
// "42" في الموضع 6
// "3" في الموضع 13
// "150" في الموضع 26
}
مجموعات الالتقاط
تسمح لك مجموعات الالتقاط باستخراج أجزاء محددة من التطابق. ضع الجزء الذي تريد التقاطه بين أقواس (...).
استخدام مجموعات الالتقاط
void main() {
// نمط مع مجموعات التقاط لتحليل التاريخ
final datePattern = RegExp(r'(\d{4})-(\d{2})-(\d{2})');
final text = 'تاريخ الحدث: 2024-03-15';
final match = datePattern.firstMatch(text);
if (match != null) {
print('التطابق الكامل: ${match.group(0)}'); // 2024-03-15
print('السنة: ${match.group(1)}'); // 2024
print('الشهر: ${match.group(2)}'); // 03
print('اليوم: ${match.group(3)}'); // 15
}
// مجموعة غير ملتقطة (?:...) — تجمع بدون التقاط
final timePattern = RegExp(r'(\d{1,2}):(\d{2})(?:\s)?(AM|PM)?', caseSensitive: false);
final time = 'الاجتماع في 2:30 PM';
final timeMatch = timePattern.firstMatch(time);
if (timeMatch != null) {
print('الساعة: ${timeMatch.group(1)}'); // 2
print('الدقيقة: ${timeMatch.group(2)}'); // 30
print('الفترة: ${timeMatch.group(3)}'); // PM
}
}
مجموعات الالتقاط المسمّاة
المجموعات المسمّاة تجعل regex أكثر قراءة بإعطاء أسماء ذات معنى للأجزاء الملتقطة، باستخدام الصيغة (?<name>...).
المجموعات المسمّاة للوضوح
void main() {
final urlPattern = RegExp(
r'(?<protocol>https?)://(?<host>[\w.-]+)(?::(?<port>\d+))?(?<path>/[\w/.-]*)?'
);
final url = 'https://api.example.com:8080/v2/users';
final match = urlPattern.firstMatch(url);
if (match != null) {
print('البروتوكول: ${match.namedGroup('protocol')}'); // https
print('المضيف: ${match.namedGroup('host')}'); // api.example.com
print('المنفذ: ${match.namedGroup('port')}'); // 8080
print('المسار: ${match.namedGroup('path')}'); // /v2/users
}
// تحليل عدة روابط
final text = '''
زر http://example.com أو https://secure.site.org:443/login
''';
for (final m in urlPattern.allMatches(text)) {
print('${m.namedGroup('protocol')}://${m.namedGroup('host')}');
}
// http://example.com
// https://secure.site.org
}
match.namedGroup('host') أكثر قراءة بكثير من match.group(2)، ويبقى النمط صحيحاً حتى لو أضفت أو أزلت مجموعات في وقت سابق من التعبير.النظر للأمام والنظر للخلف
تأكيدات النظر للأمام والنظر للخلف تطابق موضعاً دون استهلاك الأحرف. تتحقق مما إذا كان النمط موجوداً قبل أو بعد دون تضمينه في نتيجة التطابق.
النظر للأمام والنظر للخلف
void main() {
// النظر للأمام الإيجابي (?=...) — تطابق فقط إذا تلاها النمط
final beforeDollar = RegExp(r'\d+(?=\s*dollars)');
print(beforeDollar.firstMatch('Price is 50 dollars')?.group(0)); // 50
print(beforeDollar.firstMatch('Count is 50 items')?.group(0)); // null
// النظر للأمام السلبي (?!...) — تطابق فقط إذا لم يتلوها النمط
final notDollar = RegExp(r'\d+(?!\s*dollars)');
final matches = notDollar.allMatches('50 dollars and 30 items');
for (final m in matches) {
print(m.group(0)); // 30 (50 متبوعة بـ dollars لذا استُبعدت)
}
// النظر للخلف الإيجابي (?<=...) — تطابق فقط إذا سبقها النمط
final afterDollar = RegExp(r'(?<=\$)\d+');
print(afterDollar.firstMatch('السعر: \$150')?.group(0)); // 150
// النظر للخلف السلبي (?<!...) — تطابق فقط إذا لم يسبقها النمط
final notAfterHash = RegExp(r'(?<!#)\b\w+\b');
final tags = 'hello #world foo #bar';
for (final m in notAfterHash.allMatches(tags)) {
print(m.group(0)); // hello, foo (كلمات لم يسبقها #)
}
}
(?<=a+) قد لا يعمل في جميع منصات Dart. استخدم النظر للخلف بطول ثابت عند الإمكان، مثل (?<=abc) أو (?<=\d{3}).replaceAll و replaceAllMapped
استبدال النصوص مع regex هو أحد أكثر الاستخدامات العملية. replaceAll يقوم باستبدال بسيط، بينما replaceAllMapped يمنحك الوصول لكل تطابق للاستبدال الديناميكي.
الاستبدال البسيط والمُعيّن
void main() {
// replaceAll بسيط مع regex
final text = 'Hello World Dart';
final cleaned = text.replaceAll(RegExp(r'\s+'), ' ');
print(cleaned); // Hello World Dart
// replaceAllMapped — استبدال ديناميكي باستخدام بيانات التطابق
final template = 'لدي 3 قطط و 12 كلباً';
final doubled = template.replaceAllMapped(
RegExp(r'\d+'),
(match) => '${int.parse(match.group(0)!) * 2}',
);
print(doubled); // لدي 6 قطط و 24 كلباً
// تنسيق أرقام الهاتف
final phone = '5551234567';
final formatted = phone.replaceAllMapped(
RegExp(r'(\d{3})(\d{3})(\d{4})'),
(m) => '(${m.group(1)}) ${m.group(2)}-${m.group(3)}',
);
print(formatted); // (555) 123-4567
// تحويل camelCase إلى snake_case
final camel = 'myVariableName';
final snake = camel.replaceAllMapped(
RegExp(r'[A-Z]'),
(m) => '_${m.group(0)!.toLowerCase()}',
);
print(snake); // my_variable_name
}
التقسيم مع Regex
طريقة String.split() تقبل Pattern، الذي يشمل RegExp. هذا يسمح بمنطق تقسيم معقد يتجاوز نصوص الفاصل البسيطة.
تقسيم النصوص المتقدم
void main() {
// تقسيم على أي مسافة بيضاء (يعالج مسافات متعددة، تبويبات، إلخ)
final messy = 'Hello\t\tWorld Dart\nFlutter';
final words = messy.split(RegExp(r'\s+'));
print(words); // [Hello, World, Dart, Flutter]
// تقسيم على علامات الترقيم
final sentence = 'Hello,World;Dart.Flutter!Rocks';
final parts = sentence.split(RegExp(r'[,;.!]'));
print(parts); // [Hello, World, Dart, Flutter, Rocks]
// تقسيم CSV مع مراعاة الحقول المقتبسة (مبسط)
final csv = 'name,age,"city, state",score';
final fields = RegExp(r'(?:^|,)("(?:[^"]*)"|\w[^,]*)')
.allMatches(csv)
.map((m) => m.group(1)!.replaceAll('"', ''))
.toList();
print(fields); // [name, age, city, state, score]
// تقسيم مع الاحتفاظ بالفواصل
final expr = '3+5*2-1';
final tokens = expr.split(RegExp(r'(?=[+*-])|(?<=[+*-])'));
print(tokens); // [3, +, 5, *, 2, -, 1]
}
مثال عملي: التحقق من البريد الإلكتروني
بينما التحقق المثالي من البريد يتطلب الامتثال لـ RFC 5322، فإن regex عملي يغطي الغالبية العظمى من عناوين البريد في العالم الحقيقي.
مُحقق البريد الإلكتروني
class EmailValidator {
// regex عملي للبريد (يغطي 99%+ من رسائل البريد الحقيقية)
static final _pattern = RegExp(
r'^[a-zA-Z0-9.!#$%&\x27*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$',
);
static bool isValid(String email) => _pattern.hasMatch(email);
static String? validate(String email) {
if (email.isEmpty) return 'البريد مطلوب';
if (!isValid(email)) return 'تنسيق البريد غير صالح';
if (email.length > 254) return 'البريد طويل جداً';
return null; // صالح
}
}
void main() {
final testEmails = [
'user@example.com',
'first.last@company.co.uk',
'invalid@',
'@no-local.com',
'spaces in@email.com',
'valid+tag@gmail.com',
];
for (final email in testEmails) {
final error = EmailValidator.validate(email);
print('$email: ${error ?? "صالح"}');
}
}
مثال عملي: محرك القوالب
Regex مثالي لبناء محرك قوالب بسيط يستبدل العناصر النائبة بالقيم.
محرك قوالب بسيط
/// محرك قوالب بسيط يستخدم regex لاستبدال العناصر النائبة.
class TemplateEngine {
// يطابق {{variableName}} أو {{ variableName }}
static final _placeholder = RegExp(r'\{\{\s*(\w+)\s*\}\}');
// يطابق {{#if condition}}...{{/if}}
static final _conditional = RegExp(
r'\{\{#if\s+(\w+)\}\}(.*?)\{\{/if\}\}',
dotAll: true,
);
static String render(String template, Map<String, dynamic> data) {
// معالجة الشروط أولاً
var result = template.replaceAllMapped(_conditional, (match) {
final condition = match.group(1)!;
final content = match.group(2)!;
final value = data[condition];
if (value == true || (value != null && value != false && value != '')) {
return content;
}
return '';
});
// ثم استبدال العناصر النائبة
result = result.replaceAllMapped(_placeholder, (match) {
final key = match.group(1)!;
return data[key]?.toString() ?? '';
});
return result;
}
}
void main() {
final template = '''
مرحباً {{ name }}!
{{#if isPremium}}مرحباً بعودتك، عضو مميز!{{/if}}
رصيدك \${{ balance }}.
''';
print(TemplateEngine.render(template, {
'name': 'Edrees',
'isPremium': true,
'balance': '250.00',
}));
// مرحباً Edrees!
// مرحباً بعودتك، عضو مميز!
// رصيدك $250.00.
}
مثال عملي: محلل السجلات
تحليل ملفات السجلات المنظمة هو حالة استخدام كلاسيكية لـ regex مع مجموعات الالتقاط المسمّاة.
محلل ملفات السجلات
class LogEntry {
final DateTime timestamp;
final String level;
final String component;
final String message;
LogEntry(this.timestamp, this.level, this.component, this.message);
@override
String toString() => '[$level] $component: $message';
}
class LogParser {
// يطابق: [2024-03-15 14:30:00] ERROR (Auth): Login failed
static final _pattern = RegExp(
r'\[(?<date>\d{4}-\d{2}-\d{2})\s+(?<time>\d{2}:\d{2}:\d{2})\]\s+'
r'(?<level>DEBUG|INFO|WARN|ERROR|FATAL)\s+'
r'\((?<component>\w+)\):\s+(?<message>.+)',
);
static LogEntry? parse(String line) {
final match = _pattern.firstMatch(line);
if (match == null) return null;
final dateStr = match.namedGroup('date')!;
final timeStr = match.namedGroup('time')!;
final timestamp = DateTime.parse('$dateStr $timeStr');
return LogEntry(
timestamp,
match.namedGroup('level')!,
match.namedGroup('component')!,
match.namedGroup('message')!,
);
}
static List<LogEntry> parseAll(String logText) {
return logText
.split('\n')
.map(parse)
.whereType<LogEntry>()
.toList();
}
}
void main() {
final logText = '''
[2024-03-15 14:30:00] INFO (Auth): تسجيل دخول المستخدم
[2024-03-15 14:30:05] ERROR (Database): انتهاء مهلة الاتصال
[2024-03-15 14:30:10] WARN (Cache): فقدان ذاكرة مؤقتة للمفتاح user_123
''';
final entries = LogParser.parseAll(logText);
final errors = entries.where((e) => e.level == 'ERROR');
print('الأخطاء الموجودة: ${errors.length}');
for (final error in errors) {
print(error); // [ERROR] Database: انتهاء مهلة الاتصال
}
}
اعتبارات الأداء
يمكن أن يكون Regex قوياً لكنه بطيء أيضاً إذا أُسيء استخدامه. إليك نصائح أداء رئيسية:
أفضل ممارسات أداء Regex
// سيء: إنشاء RegExp جديد في حلقة
for (var line in lines) {
if (RegExp(r'\d+').hasMatch(line)) { // يترجم regex كل تكرار!
// ...
}
}
// جيد: ترجمة مرة واحدة وإعادة الاستخدام
final digitPattern = RegExp(r'\d+');
for (var line in lines) {
if (digitPattern.hasMatch(line)) { // يعيد استخدام regex المترجم
// ...
}
}
// سيء: التراجع الكارثي
// final bad = RegExp(r'(a+)+b'); // يمكن أن يتوقف على 'aaaaaaaaaaac'
// جيد: إعادة كتابة لتجنب المحددات الكمية المتداخلة
final good = RegExp(r'a+b');
// نصيحة: استخدم المحددات الكمية الممتلكة أو المجموعات الذرية عند التوفر
// لمنع التراجع على الأنماط التي لا يجب أن تعيد المحاولة
(a+)+ والمدخل لا يتطابق. يحاول محرك regex عدداً أسياً من التركيبات قبل الاستسلام. تجنب دائماً أنماط التكرار المتداخلة واختبر regex الخاص بك مع أسوأ المدخلات الممكنة.الملخص
التعبيرات النمطية أداة متعددة الاستخدامات وقوية في Dart. النقاط الرئيسية:
- استخدم النصوص الخام (
r'...') لأنماط regex لتجنب التخليص المزدوج. hasMatchللفحوصات المنطقية،firstMatchللنتيجة الأولى،allMatchesلجميع النتائج.- مجموعات الالتقاط
(...)والمجموعات المسمّاة(?<name>...)تستخرج أجزاء محددة من التطابقات. - النظر للأمام
(?=...)والنظر للخلف(?<=...)يؤكدان الموضع بدون استهلاك الأحرف. replaceAllMappedيمكّن استبدال النصوص الديناميكي والمدرك للسياق.- ترجم regex مرة واحدة وأعد الاستخدام؛ تجنب المحددات الكمية المتداخلة لمنع التراجع الكارثي.