📄 افزودن پشتیبانی DOCX و Excel به FileContentExtractor

تکمیل زنجیره استخراج محتوا برای فرمت‌های آفیس در معماری xAiApi
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
🏢 شرکت: فن آوران ساحر علم
📅 تاریخ تهیه مستند: چهارشنبه ۸ مهر ۱۴۰۵
📦 پروژه: xAiApi / xAiService

📦 گام ۱: نصب پکیج‌های NuGet مورد نیاز

برای استخراج متن از فایل‌های Office بدون نیاز به نصب نرم‌افزار Microsoft Office روی سرور، از کتابخانه‌های سبک و قدرتمند زیر استفاده می‌کنیم:

۱ دستورات نصب در Package Manager Console (پروژه xAiService یا xAiApi):
# برای پردازش فایل‌های Word (DOCX)
Install-Package DocumentFormat.OpenXml

# برای پردازش فایل‌های Excel (XLSX و XLS)
Install-Package ExcelDataReader
Install-Package ExcelDataReader.DataSet
💡 چرا این پکیج‌ها؟
DocumentFormat.OpenXml استاندارد رسمی مایکروسافت است و بسیار پایدار می‌باشد. ExcelDataReader فوق‌العاده سریع است و نیاز به COM Interop ندارد، که آن را برای محیط‌های سروری (Server-side) ایده‌آل می‌کند.

📝 گام ۲: پیاده‌سازی DocxContentExtractor

این کلاس مسئول استخراج متن از فایل‌های .docx است. نکته مهم در اینجا این است که استریم ورودی را به MemoryStream تبدیل می‌کنیم، زیرا OpenXml به یک استریم قابل جستجو (Seekable) نیاز دارد.

NEW xAiService/Providers/DocxContentExtractor.cs
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using xAiService.Interfaces;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;

namespace xAiService.Providers
{
    /// <summary>
    /// Extracts text content from DOCX files using DocumentFormat.OpenXml ...
    /// </summary>
    public class DocxContentExtractor : IFileContentExtractor
    {
        private const string DocxMimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";

        public bool CanExtract(string mimeType)
        {
            return string.Equals(mimeType, DocxMimeType, StringComparison.OrdinalIgnoreCase);
        }

        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            // نکته حیاتی: OpenXml به استریم قابل جستجو (Seekable) نیاز دارد.
            // استریم IFormFile ممکن است Seekable نباشد، بنابراین آن را در حافظه کپی می‌کنیم.
            using var memoryStream = new MemoryStream();
            await fileStream.CopyToAsync(memoryStream, cancellationToken);
            memoryStream.Position = 0;

            var stringBuilder = new StringBuilder();

            // باز کردن سند Word به حالت فقط خواندنی
            using (var wordDocument = WordprocessingDocument.Open(memoryStream, false))
            {
                var body = wordDocument.MainDocumentPart?.Document?.Body;
                if (body != null)
                {
                    // استخراج پاراگراف‌ها و joining آنها با خط جدید برای حفظ ساختار برای LLM
                    var paragraphs = body.Elements<Paragraph>();
                    foreach (var paragraph in paragraphs)
                    {
                        var text = paragraph.InnerText?.Trim();
                        if (!string.IsNullOrEmpty(text))
                        {
                            stringBuilder.AppendLine(text);
                        }
                    }
                }
            }

            return stringBuilder.ToString();
        }
    }
}

📊 گام ۳: پیاده‌سازی ExcelContentExtractor

این کلاس فایل‌های .xlsx و .xls را خوانده و محتوای آن را به فرمت متنی ساختاریافته (شبیه CSV) تبدیل می‌کند تا مدل زبانی (LLM) بتواند به راحتی روابط سطرها و ستون‌ها را درک کند.

NEW xAiService/Providers/ExcelContentExtractor.cs
using System;
using System.Data;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using xAiService.Interfaces;
using ExcelDataReader;

namespace xAiService.Providers
{
    /// <summary>
    /// Extracts tabular content from Excel files (XLSX, XLS) using ExcelDataReader ...
    /// </summary>
    public class ExcelContentExtractor : IFileContentExtractor
    {
        private static readonly string[] SupportedMimeTypes = new[]
        {
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", // .xlsx
            "application/vnd.ms-excel" // .xls
        };

        public bool CanExtract(string mimeType)
        {
            return Array.Exists(SupportedMimeTypes, type => 
                string.Equals(type, mimeType, StringComparison.OrdinalIgnoreCase)
            );
        }

        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            // ثبت Encoding Provider برای پشتیبانی از فرمت‌های قدیمی‌تر Excel
            System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);

            // کپی در MemoryStream برای اطمینان از Seekable بودن
            using var memoryStream = new MemoryStream();
            await fileStream.CopyToAsync(memoryStream, cancellationToken);
            memoryStream.Position = 0;

            var stringBuilder = new StringBuilder();

            // ایجاد Reader به صورت خودکار بر اساس فرمت فایل
            using (var reader = ExcelReaderFactory.CreateReader(memoryStream))
            {
                var result = reader.AsDataSet();

                foreach (DataTable table in result.Tables)
                {
                    stringBuilder.AppendLine($"--- Sheet: {table.TableName} ---");
                    
                    // افزودن هدر ستون‌ها
                    var headers = new string[table.Columns.Count];
                    for (int i = 0; i < table.Columns.Count; i++)
                    {
                        headers[i] = table.Columns[i].ColumnName;
                    }
                    stringBuilder.AppendLine(string.Join(" | ", headers));
                    stringBuilder.AppendLine(new string('-', 50));

                    // افزودن داده‌های سطرها
                    foreach (DataRow row in table.Rows)
                    {
                        var rowValues = new string[row.ItemArray.Length];
                        for (int i = 0; i < row.ItemArray.Length; i++)
                        {
                            rowValues[i] = row.ItemArray[i]?.ToString()?.Trim() ?? string.Empty;
                        }
                        stringBuilder.AppendLine(string.Join(" | ", rowValues));
                    }
                    stringBuilder.AppendLine();
                }
            }

            return stringBuilder.ToString();
        }
    }
}

🔗 گام ۴: ثبت سرویس‌ها در Dependency Injection

اکنون باید Extractor های جدید را به کانتینر DI اضافه کنیم. CompositeFileContentExtractor که قبلاً طراحی شده بود، به صورت خودکار تمام پیاده‌سازی‌های IFileContentExtractor را از طریق تزریق IEnumerable<IFileContentExtractor> دریافت کرده و مدیریت می‌کند.

MODIFY xAiApi/Startup.cs (متد ConfigureServices)
public void ConfigureServices(IServiceCollection services)
{
    // ... (سایر ثبت‌های سرویس)

    // ✅ ثبت Extractor های فایل (الگوی Composite)
    services.AddSingleton<IFileContentExtractor, PlainTextContentExtractor>();
    services.AddSingleton<IFileContentExtractor, PdfContentExtractor>();
    
    // ✅ افزودن Extractor های جدید آفیس
    services.AddSingleton<IFileContentExtractor, DocxContentExtractor>();
    services.AddSingleton<IFileContentExtractor, ExcelContentExtractor>();
    
    // ✅ ثبت Composite Extractor (این کلاس لیست بالا را در Constructor دریافت می‌کند)
    services.AddSingleton<IFileContentExtractor, CompositeFileContentExtractor>();

    // ... (سایر ثبت‌های سرویس)
}

⚠️ گام ۵: نکات کلیدی و ملاحظات عملکردی

🧠 محدودیت Context Window مدل

فایل‌های Excel بزرگ می‌توانند هزاران خط متن تولید کنند. قبل از ارسال به LLM، حتماً طول رشته استخراج شده را بررسی کنید و در صورت نیاز، آن را خلاصه (Chunk) کنید یا فقط چند سطر اول را ارسال نمایید.

💾 مدیریت حافظه (Memory)

استفاده از MemoryStream برای فایل‌های بسیار بزرگ (مثلاً بالای 50 مگابایت) ممکن است باعث OutOfMemoryException شود. در appsettings.json حداکثر حجم فایل آپلودی را محدود کنید.

🔒 امنیت و اعتبارسنجی

کتابخانه ExcelDataReader در برابر فایل‌های مخرب مقاوم است، اما همیشه قبل از پردازش، نوع فایل (MIME Type) و پسوند آن را در لایه Controller اعتبارسنجی کنید.

🌐 پشتیبانی از Encoding

خط Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) در Extractor اکسل حیاتی است. بدون آن، خواندن فایل‌های .xls قدیمی با کاراکترهای فارسی با خطا مواجه می‌شود.

✅ نتیجه‌گیری:
با افزودن این دو کلاس، معماری FileContentExtractor شما اکنون به طور کامل از فرمت‌های متنی، PDF، Word و Excel پشتیبانی می‌کند. این تغییرات کاملاً با اصل Open/Closed (باز برای توسعه، بسته برای تغییر) در SOLID همخوانی دارد، زیرا برای افزودن فرمت جدید (مثلاً PowerPoint در آینده)، فقط کافی است یک کلاس جدید اضافه و در DI ثبت کنید، بدون اینکه کدهای موجود را تغییر دهید.