عنوان:

‫مدیریت هوشمندانه خطوط جدید در regular expressions دات‌نت ۱۱


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۴/۲۵ ۰۹:۲۵
آدرس: www.dntips.ir
یکی از چالش‌های همیشگی و آزاردهنده برای توسعه‌دهندگان در هنگام پردازش متون، مدیریت کاراکترهای پایان خط یا همان خط جدید (Newline) بوده است. تفاوت در نحوه ذخیره‌سازی خطوط جدید در سیستم‌عامل‌های مختلف و همچنین استانداردهای گوناگون انکودینگ، همواره باعث بروز باگ‌های پنهان در پردازش متن و عبارات باقاعده (Regex) می‌شود.
خوشبختانه مایکروسافت در .NET 11 با معرفی یک قابلیت جدید و کاربردی به نام RegexOptions.AnyNewLine، این گره کور را باز کرده‌است. در این مقاله، به بررسی این چالش قدیمی، مشکلات راهکارهای سنتی و نحوه حل اصولی آن در دات‌نت ۱۱ می‌پردازیم.

چالش تاریخی: تنوع استانداردهای خط جدید
از گذشته تا به امروز، سیستم‌عامل‌های مختلف روش‌های متفاوتی برای تعریف پایان یک خط داشته‌اند. به طور سنتی، سه فرمت اصلی وجود داشت:
  • ویندوز (Windows): استفاده از ترکیب \r\n (CRLF)
  • یونیکس و لینوکس (Unix & Linux): استفاده از \n (LF)
  • مک‌اواس قدیمی (Older MacOS): استفاده از \r (CR)
با ظهور استاندارد یونیکد (Unicode)، این تنوع باز هم فراتر رفت و سه کاراکتر دیگر نیز به این جمع اضافه شدند:
  • Next Line (\u0085 - NEL)
  • Line Separator (\u2028 - LS)
  • Paragraph Separator (\u2029 - PS)
حال تصور کنید متنی ترکیبی دارید که از منابع مختلف (وب‌سرویس‌ها، فایل‌های آپلود شده توسط کاربران با سیستم‌عامل‌های گوناگون و دیتابیس‌ها) جمع‌آوری شده و شامل انواع این کاراکترهاست:
var text = "The Quick\r\nBrown Fox\u0085Jumped Over\u2028The Lazy Dog\nBigly";
هدف ما این است که خطوط این متن چندخطی (Multiline) را به صورت تفکیک‌شده استخراج کنیم.

بررسی عملکرد در نسخه‌های قدیمی دات‌نت
در دات‌نت ۱۰ و نسخه‌های پیش از آن، برای استخراج خطوط معمولاً از الگوی زیر استفاده می‌شد:
using System;
using System.Linq;
using System.Text.RegularExpressions;

var oldLines = Regex.Matches(text, @"^.*$", RegexOptions.Multiline)
                    .Select(r => r.Value)
                    .ToArray();
در این عبارت باقاعده:
  • علامت ^ نشان‌دهنده ابتدای خط است.
  • الگوی .* هر کاراکتری به جز \n را به تعداد دلخواه جستجو می‌کند.
  • علامت $ نشان‌دهنده انتهای خط است.
  • گزینه RegexOptions.Multiline به موتور پردازش اعلام می‌کند که متن ورودی ساختار چندخطی دارد.
مشکل کجاست؟
موتور Regex دات‌نت به طور سنتی فقط کاراکتر \n (LF) را به عنوان جداکننده خط در حالت Multiline می‌شناسد. در نتیجه اجرای کد بالا روی متن نمونه، خروجی مطلوبی نخواهد داشت:
  • کاراکترهای یونیکد مثل \u0085 یا \u2028 اصلاً شناسایی نمی‌شوند و خطوط متصل به آن‌ها جدا نخواهند شد.
  • در مورد ویندوز (\r\n)، از آنجا که فقط \n به عنوان پایان خط در نظر گرفته می‌شود، کاراکتر \r (Carriage Return) به عنوان بخشی از متن خط اول (پایان آن) استخراج می‌شود که نیاز به پاکسازی مجدد (مانند Trim کردن) دارد.

راهکار دات‌نت ۱۱: پرچمRegexOptions.AnyNewLine
میکروسافت در نسخه .NET 11، پرچم جدیدی به نام RegexOptions.AnyNewLine را به کلاس Regex اضافه کرده است. با فعال‌سازی این گزینه، لنگرهای سر خط (^)، ته خط ($)، پایان رشته (\Z) و همچنین کاراکتر نقطه (.) به صورت هوشمند تمامی استانداردهای خط جدید (اعم از ویندوز، لینوکس و انواع یونیکدها) را به رسمیت می‌شناسند.
کد بهینه‌شده در دات‌نت ۱۱ به شکل زیر تغییر می‌کند:
using System;
using System.Linq;
using System.Text.RegularExpressions;

var text = "The Quick\r\nBrown Fox\u0085Jumped Over\u2028The Lazy Dog\nBigly";

// استفاده همزمان از حالت چندخطی و پرچم جدید AnyNewLine
var newLines = Regex.Matches(text, @"^.*$", RegexOptions.Multiline | RegexOptions.AnyNewLine)
                    .Select(r => r.Value)
                    .ToArray();

foreach (var line in newLines)
{
    Console.WriteLine($"Line: {line}");
}
چرا این راهکار بسیار کارآمدتر است؟
  • شناسایی کامل یونیکد: تمام کاراکترهای پایان خط یونیکد بدون نیاز به نوشتن پترن‌های پیچیده و طولانی به طور خودکار پردازش می‌شوند.
  • یکپارچه‌سازی اتمیک \r\n: برخلاف ترفندهای قدیمی (مانند استفاده از \r?$) که کاراکتر \r را به عنوان بخشی از تطابق (Match) برمی‌گرداندند، گزینه AnyNewLine با ترکیب \r\n به عنوان یک موجودیت واحد و اتمیک برخورد کرده و از ورود کاراکتر مزاحم \r به خروجی نهایی جلوگیری می‌کند.

جمع‌بندی
معرفی قابلیت RegexOptions.AnyNewLine در دات‌نت ۱۱ گام مهمی در جهت بهبود پایداری و سادگی پردازش متون چندزبانه و بین‌المللی است. با بکارگیری این پرچم، دیگر نیازی به نوشتن متدهای کمکیِ سنگین برای نرمال‌سازی خطوط پیش از اعمال Regex یا استفاده از پترن‌های پیچیده و غیراستاندارد نخواهید داشت. کد شما خواناتر، سریع‌تر و به شدت در برابر انواع فرمت‌های متنی مقاوم‌تر خواهد بود.