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 XmlReader và LINQ 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
- Đọ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ớ. - Đ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. - 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ượngXElementhoà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 returngiúp dữ liệu chỉ được xử lý khi thực sự cần thiết trong vòng lặpforeach.