Tối ưu hóa xử lý XML dung lượng lớn trong C# bằng kỹ thuật Streaming

Khi làm việc với các tệp XML có kích thước lên đến hàng trăm MB hoặc vài GB, việc sử dụng các phương pháp truyền thống như XDocument.Load() hoặc XmlDocument.Load() thường dẫn đến lỗi tràn bộ nhớ (OutOfMemoryException). Nguyên nhân là do các phương thức này cố gắng tải toàn bộ cấu trúc cây DOM vào RAM.

Để giải quyết vấn đề này, giải pháp tối ưu nhất là sử dụng XmlReader để đọc dữ liệu theo dạng luồng (streaming). Tuy nhiên, XmlReader thuần túy lại khá khó sử dụng do cơ chế đọc tuần tự mức thấp. Bài viết này sẽ hướng dẫn bạn xây dựng một bộ công cụ kết hợp giữa XmlReaderLINQ to XML để truy xuất dữ liệu theo đường dẫn cụ thể mà vẫn đảm bảo hiệu năng cao.

Cơ chế hoạt động

  1. Đọc theo luồng: Sử dụng XmlReader để duyệt qua từng node mà không nạp toàn bộ tệp vào bộ nhớ.
  2. Điều hướng theo đường dẫn: Phân tách chuỗi path (ví dụ: "root/parent/child") và điều hướng con trỏ của reader đến đúng vị trí mong muốn.
  3. Chuyển đổi cục bộ: Chỉ khi tìm thấy node mục tiêu, chúng ta mới sử dụng XNode.ReadFrom để chuyển đổi node đó thành một đối tượng XElement hoàn chỉnh, giúp việc thao tác với dữ liệu trở nên dễ dàng hơn.

Triển khai mã nguồn

Dưới đây là lớp tiện ích XmlStreamingNavigator được thiết kế để xử lý việc lọc các node theo đường dẫn phân cấp:

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Linq;

public static class XmlStreamingNavigator
{
    /// <summary>
    /// Truy xuất các phần tử XML dựa trên đường dẫn chỉ định dưới dạng streaming
    /// </summary>
    /// <param name="xmlSource">Nguồn XML (string hoặc file path)</param>
    /// <param name="elementPath">Đường dẫn logic, ví dụ: "catalog/products/item"</param>
    public static IEnumerable<XElement> EnumerateNodes(string xmlSource, string elementPath)
    {
        if (string.IsNullOrEmpty(xmlSource) || string.IsNullOrEmpty(elementPath))
            yield break;

        string[] segments = elementPath.Split('/');
        int targetDepth = segments.Length - 1;

        using var stringReader = new StringReader(xmlSource);
        using var xmlReader = XmlReader.Create(stringReader, new XmlReaderSettings 
        { 
            IgnoreWhitespace = true, 
            IgnoreComments = true 
        });

        // Di chuyển đến node đầu tiên
        if (!xmlReader.Read()) yield break;

        while (!xmlReader.EOF)
        {
            if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.Name == segments[0])
            {
                // Nếu đường dẫn chỉ có 1 cấp
                if (segments.Length == 1)
                {
                    yield return (XElement)XNode.ReadFrom(xmlReader);
                }
                else
                {
                    // Xử lý các cấp độ sâu hơn thông qua đệ quy hoặc duyệt cây
                    foreach (var node in ExtractSubElements(xmlReader, segments, 1))
                    {
                        yield return node;
                    }
                }
            }
            else
            {
                xmlReader.Read();
            }
        }
    }

    private static IEnumerable<XElement> ExtractSubElements(XmlReader reader, string[] segments, int currentIndex)
    {
        string targetName = segments[currentIndex];
        bool isFinalSegment = currentIndex == segments.Length - 1;

        using var subReader = reader.ReadSubtree();
        while (subReader.Read())
        {
            if (subReader.NodeType == XmlNodeType.Element && subReader.Name == targetName)
            {
                if (isFinalSegment)
                {
                    yield return (XElement)XNode.ReadFrom(subReader);
                }
                else
                {
                    foreach (var child in ExtractSubElements(subReader, segments, currentIndex + 1))
                    {
                        yield return child;
                    }
                }
            }
        }
    }
}

Cách sử dụng thực tế

Giả sử bạn có một tệp XML lưu trữ danh sách sản phẩm như sau:

<store>
  <inventory>
    <product>
      <id>P001</id>
      <name>Bàn phím cơ</name>
    </product>
    <product>
      <id>P002</id>
      <name>Chuột không dây</name>
    </product>
  </inventory>
</store>

Để lấy danh sách các product mà không làm quá tải bộ nhớ, bạn thực hiện như sau:

string xmlData = File.ReadAllText("data.xml");
var path = "store/inventory/product";

var products = XmlStreamingNavigator.EnumerateNodes(xmlData, path);

foreach (var p in products)
{
    string id = p.Element("id")?.Value;
    string name = p.Element("name")?.Value;
    Console.WriteLine($"Sản phẩm: {name} (Mã: {id})");
}

Ưu điểm của phương pháp

  • Tiết kiệm tài nguyên: Chỉ lưu trữ node hiện tại đang xử lý thay vì toàn bộ cấu trúc file.
  • Tốc độ xử lý: Phù hợp cho các hệ thống cần xử lý dữ liệu theo thời gian thực hoặc các tiến trình chạy ngầm (Background Jobs).
  • Linh hoạt: Cho phép truy xuất đến bất kỳ độ sâu nào của cây XML thông qua mảng phân tách đường dẫn.
  • Lazy Evaluation: Sử dụng yield return giúp dữ liệu chỉ được xử lý khi thực sự cần thiết trong vòng lặp foreach.

Thẻ: C# XML XmlReader LINQ-to-XML memory-management

Đăng vào ngày 31 tháng 7 lúc 01:42