1. Giới thiệu
Bắt đầu với câu hỏi quan trọng — tại sao phải bận tâm về encoding? Nhìn thì có vẻ đơn giản: một tệp, một encoding, mọi chuyện yên ổn. Nhưng thực tế thú vị hơn nhiều. Rất nhanh bạn sẽ thấy tệp có thể đến từ bất cứ đâu và ở một encoding hoàn toàn bất ngờ. Trong khi ứng dụng của bạn tất nhiên lại chờ một encoding khác.
Đôi khi bạn phải tích hợp với một API hoặc hệ thống mà bắt buộc dùng một encoding nhất định. Có khi bạn đào lại một tệp cũ chục năm, mở bằng editor mới — và ra toàn ký tự bị hỏng. Hoặc bạn xuất report CSV và muốn chắc chắn mọi đồng nghiệp có thể mở được — trên Windows, trên Mac, thậm chí trong Excel.
Tóm lại, chuyển mã không phải việc hiếm hay lạ. Ngược lại, đó là một task thực tế và khá thường gặp: khi xuất dữ liệu từ database, làm việc với archive, tích hợp kế toán hay chạy script tự động. Càng hiểu sớm, càng ít bất ngờ sau này.
Từ encoding cũ sang encoding mới
Để chuyển mã tệp, cần làm hai bước:
- Đọc tệp theo encoding nguồn, thu về chuỗi (hoặc ký tự).
- Ghi chuỗi vào tệp mới, chỉ rõ encoding đích.
Tưởng tượng bạn dịch một cuốn sách từ tiếng Pháp sang tiếng Việt. Trước hết bạn phải đọc hiểu tiếng Pháp (đọc văn bản), rồi diễn đạt cùng ý trên tiếng Việt (ghi lại).
Trong C# (và .NET) điều này thực hiện bằng cách truyền đối tượng Encoding tương ứng vào constructor của StreamReader (đọc) và StreamWriter (ghi).
Tổng quan các encoding và lớp Encoding
Tất cả "phép biến" ký tự đều do lớp System.Text.Encoding lo liệu.
Bảng các encoding chính và cách lấy chúng trong C# có thể trình bày như sau:
| Mã hóa | Mô tả | Hằng C# |
|---|---|---|
| UTF-8 | Phổ biến, mặc định không có BOM | |
| UTF-8 với BOM | Như trên nhưng có chữ ký BOM | |
| UTF-16 (LE/BE) | "Wide char", little-endian | Encoding.Unicode (LE), Encoding.BigEndianUnicode (BE) |
| ASCII | Klassic 7-bit | |
| Windows-1251 | Phổ biến cho chữ Cyrillic | |
| ISO-8859-1 | Latin (châu Âu) | |
Lưu ý: Với các encoding cũ bạn sẽ phải dùng Encoding.GetEncoding, đôi khi với số (ví dụ 1251), đôi khi với chuỗi ("windows-1251").
2. Chuyển mã: chiến lược từng bước trong C#
Giờ mình xem cách thực hiện chuyện chuyển mã trên thực tế.
Thuật toán
- Mở tệp nguồn bằng StreamReader, chỉ rõ encoding ban đầu.
- Đọc văn bản (toàn bộ hoặc theo dòng — tuỳ kích thước tệp).
- Mở tệp đích bằng StreamWriter, chỉ rõ encoding mong muốn.
- Ghi văn bản vào tệp đích.
- Nhớ đóng stream — dùng using cho an toàn!
Ví dụ: chuyển mã Windows-1251 → UTF-8
Giả sử ta có tệp "input-1251.txt" ở encoding Windows-1251, và muốn tạo "output-utf8.txt" ở UTF-8 "sạch" (không BOM).
// Chỉ định nguồn với encoding ban đầu
using var reader = new StreamReader("input-1251.txt", Encoding.GetEncoding(1251));
using var writer = new StreamWriter("output-utf8.txt", false, Encoding.UTF8);
string line;
// Đọc theo dòng — tiện cho tệp lớn
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line);
}
Console.WriteLine("Tệp đã được chuyển mã từ Windows-1251 sang UTF-8 thành công!");
Mã này đảm bảo mỗi dòng được chuyển chính xác từ encoding này sang encoding khác.
Sơ đồ hoạt động nhìn như sau:
┌───────────────────────────────┐ ┌───────────────────┐ ┌───────────────────────────────┐
│ Tệp "input-1251.txt" (1251) │ --> │ StreamReader │ --> │ Các chuỗi kiểu string trong bộ nhớ │
└───────────────────────────────┘ │ (Encoding 1251) │ └───────────────────────────────┘
└───────────────────┘
│
▼
┌────────────────────────┐
│ StreamWriter │
│ (Encoding UTF-8) │
└───────────┬────────────┘
│
▼
┌──────────────────────────────┐
│ "output-utf8.txt" (UTF-8) │
└──────────────────────────────┘
3. Ví dụ thực tế: converter mã hóa
Tăng khó độ một chút: viết chương trình converter đơn giản, cho phép user chọn tệp nguồn, tệp đích và các encoding.
Console.WriteLine("Nhập đường dẫn tới tệp nguồn:");
string inputPath = Console.ReadLine();
Console.WriteLine("Encoding của tệp nguồn (ví dụ: 1251, utf-8):");
string sourceEncodingName = Console.ReadLine();
Console.WriteLine("Nhập đường dẫn tới tệp đích:");
string outputPath = Console.ReadLine();
Console.WriteLine("Encoding để lưu (ví dụ: utf-8, 1251):");
string destEncodingName = Console.ReadLine();
Encoding sourceEncoding = Encoding.GetEncoding(sourceEncodingName);
Encoding destEncoding = Encoding.GetEncoding(destEncodingName);
using var reader = new StreamReader(inputPath, sourceEncoding);
using var writer = new StreamWriter(outputPath, false, destEncoding);
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line);
}
Console.WriteLine("Xong! Kiểm tra kết quả.");
Mẹo: Nếu không chắc encoding nào nên dùng, xem tài liệu hoặc thử vài phương án. Đôi khi phải "thí nghiệm" mới biết vì không có README nào.
4. Những điểm cần lưu ý: BOM, ký tự "ẩn" và các trường hợp đặc biệt
Làm sao thêm BOM khi lưu UTF-8?
Mặc định Encoding.UTF8 trong C# tạo tệp không có BOM. Nếu cần tệp có BOM, dùng:
// true — nghĩa là "có BOM"
var utf8WithBom = new UTF8Encoding(true);
using var writer = new StreamWriter("with-bom.txt", false, utf8WithBom);
writer.WriteLine("Văn bản có BOM");
Tệp giờ sẽ bắt đầu bằng ba byte "vô hình": 0xEF, 0xBB, 0xBF.
Khi nào BOM là vấn đề?
- Nếu bạn xuất CSV cho Excel, đa số bản quốc tế của Excel bị rối bởi BOM — xuất hiện ký tự lạ.
- Trên hệ Unix (Linux) BOM đôi khi phá cách xử lý tệp.
- Với JSON, BOM gần như luôn gây vấn đề, nhiều parser không xử lý được!
Mẹo: Nghĩ kĩ trước khi thêm BOM — chỉ dùng khi thực sự cần.
Ký tự ẩn và các "số ma"
Nếu khi mở tệp "đã chuyển mã" ứng dụng báo lỗi định dạng, có thể bạn chưa chú ý ký tự "vô hình" ở đầu tệp (ví dụ BOM), hoặc chọn sai encoding (ví dụ đọc như ASCII trong khi chứa chữ Cyrillic ở 1251). Luôn kiểm tra encoding mà ứng dụng/đối tác/server của bạn dùng.
5. Mẹo hữu ích
Làm việc với tệp lớn: vì sao đọc theo dòng chứ không đọc toàn bộ?
Có thể làm đơn giản như sau:
string allText = File.ReadAllText("input.txt", Encoding.GetEncoding(1251));
File.WriteAllText("output.txt", allText, Encoding.UTF8);
Cách này ổn với tệp nhỏ hoặc vừa (khoảng dưới ~50 MB). Nhưng nếu tệp lớn, toàn bộ văn bản sẽ tải vào RAM — nếu tệp vài GB, sẽ "ôi trời". Vì vậy đọc/ghi theo dòng an toàn hơn cho tính đa dụng.
Chuyển mã giữa các encoding ít gặp hơn
Giả sử bạn có tệp ở ISO-8859-1 (MySQL đôi khi xuất vậy), cần chuyển sang Unicode:
var sourceEnc = Encoding.GetEncoding("iso-8859-1");
var destEnc = Encoding.UTF8;
using var reader = new StreamReader("data-latin.txt", sourceEnc);
using var writer = new StreamWriter("data-unicode.txt", false, destEnc);
string line;
while ((line = reader.ReadLine()) != null)
{
writer.WriteLine(line);
}
Cách này hoạt động với bất kỳ encoding nào nếu .NET hỗ trợ nó.
Đừng chuyển mã tệp nhị phân!
Chương trình mô tả ở trên chỉ dành cho tệp văn bản. Nếu tệp là nhị phân (ảnh, âm thanh, archive), cố đọc như text rồi ghi lại sẽ làm hỏng byte, xuất ra ký tự lạ và file sẽ bị hỏng. Với file nhị phân không có khái niệm "encoding" để chuyển mã.
Bảng đối chiếu: dùng encoding nào cho trường hợp nào
| Mã hóa | Khi nào dùng |
|---|---|
| UTF-8 | Dùng cho file chung, web, ứng dụng hiện đại |
| UTF-8 với BOM | Để tương thích với Notepad, Excel |
| Windows-1251 | Dành cho "lớp cũ", chương trình địa phương dùng tiếng Nga |
| ASCII | File chỉ có tiếng Anh |
| UTF-16 | Cho trường hợp đặc biệt, ứng dụng kỳ lạ |
Mẹo vặt và lời khuyên
Làm sao biết encoding của file?
- Editor chuyên dụng (Notepad++, Visual Studio Code) thường phát hiện và hiển thị encoding.
- Nếu không có BOM và chữ hiện nhưng sai — có khả năng encoding không khớp.
Có tự động xác định encoding được không?
- Trong .NET không có "đũa thần" nhận diện 100% mọi encoding. Thông thường: nếu có BOM thì rõ ràng, nếu không thì phải suy đoán theo nội dung hoặc thử các phương án.
Nếu một phần dòng đọc đúng, phần khác "ký tự bị hỏng" thì sao?
- Có thể file được tạo bằng nhiều chương trình khác nhau hoặc bị hỏng. Kiểm tra xem file có được ghi bằng nhiều encoding không.
6. Lỗi thường gặp và cách tránh
Để rõ ràng, xem các lỗi phổ biến nhất:
Sai encoding nguồn. Nếu bạn nghĩ file là UTF-8 nhưng thực ra là Windows-1251 — bạn sẽ thấy "bậc thang" thay vì chữ Cyrillic. Kiểm tra encoding nguồn, nếu không chắc — mở bằng Notepad++ hoặc editor tương tự để xem encoding thực tế.
BOM đặt sai chỗ. Đôi khi thêm BOM phá parsing, đôi khi thiếu BOM khiến phần mềm khác nhận sai encoding.
Đọc cả file vào bộ nhớ. Với file lớn hãy dùng đọc theo dòng — nếu không chương trình có thể ngốn hết RAM với dữ liệu lớn.
Ghi mà không chỉ rõ encoding. Mặc định StreamWriter dùng UTF-8 không có BOM. Nếu cần khác thì hãy chỉ rõ encoding.
Sai khi dùng GetEncoding. Nếu gõ sai chuỗi, ví dụ "utf8" thay vì "utf-8", sẽ ném exception. Dùng tên chính xác hoặc mã số (ví dụ 1251).
GO TO FULL VERSION