C# – Chèn dữ liệu tại vị trí bất kì trong file nhị phân

Một trong những vấn đề thường gặp phải khi bạn thao tác với các file nhị phân là chèn dữ liệu tại một vị trí nào đó. Giả sử bạn tìm được vị trí cần thiết để chèn, tuy nhiên sau khi thực hiện thì phần dữ liệu phía sau sẽ bị ghi đè. Mặt khác bạn không muốn thực hiện điều này bằng cách nạp toàn bộ file vào memory. Có một số cách giải quyết vấn đề này, tuy nhiên chúng cũng chỉ theo một nguyên tắc gồm 3 bước là: tìm vị trí, tạo khoảng trống ghi dữ liệu.

Trong ví dụ sau tôi sẽ sử dụng dữ liệu là văn bản (string), với các kiểu dữ liệu khác bạn cần dùng lớp System.BitConverter để chuyển qua lại giữa dữ liệu và mảng byte.

Tìm vị trí trong file

Để tìm vị trí của dữ liệu ta thực hiện tương tự như tìm kiếm một từ trong một chuỗi. Tất cả đều thực hiện trên mảng byte. Ta sử dụng phương thức GetBytes() của lớp UTF8Encoding để chuyển văn bản sang mảng byte:

UTF8Encoding  _encoding =new UTF8Encoding();

Tìm vị trí bắt đầu một phần dữ liệu:

public long FindOffset(Stream stream, string text)
{

	byte[] bytes=_encoding.GetBytes(text);
	int aByte;
	int index = 0;
	long position = 0;

	while((aByte=stream.ReadByte())!=-1)
	{
		if (bytes[index++] != (byte)aByte) {
			index = 0;
			position = stream.Position;
		}

		if (index == bytes.Length)
			return position;

	}
	return -1;
}

Biến position sẽ lưu vị trí đầu tiên của phần dữ liệu trùng với văn bản cần tìm. Tuy nhiên vì đối tượng stream được đọc cho đến khi hết phần dữ liệu đó nên thuộc tính Position của stream sẽ là vị trí cuối cùng của phần dữ liệu được tìm thấy.

Tìm vị trí của một dòng bất kì:

public long FindOffsetLine(Stream stream,int line)
{
	if(line==0)
		return 0;

	int aByte;
	int counter = 0;
	byte lf=(byte)'\n'; // line feed
	while((aByte=stream.ReadByte())!=-1)
	{
		if((byte)aByte==lf)
			counter++;
		if(counter==line)
			return stream.Position;
	}
	return -1;
}

Chèn dữ liệu

Sau khi đã có vị trí của dữ liệu cần chèn, ta chỉ cần thực hiện công việc là lưu phần dữ liệu phía sau vị trí đó vào buffer, ghi dữ liệu chèn và sau đó là ghi phần dữ liệu từ buffer ra.

Phương thức chèn dữ liệu tại một vị trí bất kì:

public void InsertAt(Stream stream,int offset,string insertObj)
{
	// Lấy phần dữ liệu phía sau vị trí chèn và lưu vào buffer
	stream.Position=offset;
	BinaryReader reader=new BinaryReader(stream);
	byte[] buffer= reader.ReadBytes((int)stream.Length-offset);

	stream.Position=offset;
	BinaryWriter writer=new BinaryWriter(stream);
	// Ghi dữ liệu cần chèn và buffer vào phía sau
	writer.Write(_encoding.GetBytes(insertObj));
	writer.Write(buffer);
	writer.Flush();
	writer.Close();
 }

Nhận xét

Mặc dù thuật toán ta sử dụng chỉ dùng buffer lưu một phần dữ liệu của file, tuy nhiên trong những trường hợp như chèn ở phần đầu, buffer sẽ lưu gần như toàn bộ file. Hướng giải quyết cho vấn đề này là thay vì lưu toàn bộ dữ liệu từ vị trí cần chèn đến cuối, ta sẽ lưu một phần dữ liệu, sau đó lại tiến hành chèn phần dữ liệu đó vào vị trí tiếp theo cho đến hết file.

Minh họa cho ví dụ sử dụng cách chèn liên tục với độ lớn của buffer bằng với dữ liệu cần chèn:

string1 string2 string3
(buffer: <empty>)

string1 string4 string3
(buffer: string2)

string1 string4 string2
(buffer: string3)

string1 string4 string2 string3

Mã nguồn hoàn chỉnh

Y2BinaryIO.cs:

/*
 * Binary File Manipulation
 * User: Yin Yang
 * Date: 4/5/2011
 * Time: 8:38 PM
 *
 * https://yinyangit.wordpress.com
 *
 */
using System;
using System.IO;
using System.Text;

namespace BinaryFileManipulation
{
	/// <summary>
	/// Description of BinaryIO.
	/// </summary>
	public class Y2BinaryIO
	{
		string _fileName;
		UTF8Encoding  _encoding;

		public Y2BinaryIO(string fileName)
		{
			this._fileName=fileName;
			_encoding =new UTF8Encoding();
		}

		public void CreateFile(params string[] contents)
		{
			FileStream fs = new FileStream(_fileName, FileMode.Create);
			BinaryWriter writer = new BinaryWriter(fs);

			foreach(string item in contents)
			{
				writer.Write(_encoding.GetBytes(item));
			}

			writer.Flush();
			writer.Close();
			fs.Close();
			fs.Dispose();
		}
		public void InsertAtLine(int line,string insertObj)
		{
			FileStream fs = new FileStream(_fileName, FileMode.Open);
			int pos=(int)FindOffsetLine(fs,line);
			if(pos==-1)
				throw new Exception("Line not found: "+line);
			InsertAt(fs,pos,insertObj);
		}

		public void InsertBeforeData(string findObj,string insertObj)
		{
			FileStream fs = new FileStream(_fileName, FileMode.Open);

			int pos=(int)FindOffset(fs,findObj);
			if(pos==-1)
				throw new Exception("Data not found: '"+insertObj+"'");
			// if insert after
			// pos=(int)fs.Position;

			InsertAt(fs,pos,insertObj);
		}
		public void InsertAt(Stream stream,int offset,string insertObj)
		{
			// Lấy phần dữ liệu phía sau vị trí chèn và lưu vào buffer
			stream.Position=offset;
			BinaryReader reader=new BinaryReader(stream);
			byte[] buffer= reader.ReadBytes((int)stream.Length-offset);

			stream.Position=offset;
			BinaryWriter writer=new BinaryWriter(stream);
			// Ghi dữ liệu cần chèn và buffer vào phía sau
			writer.Write(_encoding.GetBytes(insertObj));
			writer.Write(buffer);
			writer.Flush();
			writer.Close();
		}
		public string ReadToEnd()
		{
			StreamReader reader=new StreamReader(_fileName);
			string ret= reader.ReadToEnd();
			reader.Close();
			return ret;
		}
		/// <summary>
		/// Tìm offset của 1 dòng
		/// </summary>
		/// <param name="stream"></param>
		/// <param name="line"></param>
		/// <returns></returns>
		public long FindOffsetLine(Stream stream,int line)
		{
			int aByte;
			int counter = 0;
			byte lf=(byte)'\n'; // line feed
			while((aByte=stream.ReadByte())!=-1)
			{
				if((byte)aByte==lf)
					counter++;
				if(counter==line)
					return stream.Position;
			}
			return -1;
		}
		/// <summary>
		/// Sau khi thực hiện thì Position của stream sẽ ở cuối text cần tìm
		/// </summary>
		/// <param name="stream"></param>
		/// <param name="text"></param>
		/// <returns>vị trí bắt đầu text cần tìm, ngược lại là -1</returns>
		public long FindOffset(Stream stream, string text)
		{

			byte[] bytes=_encoding.GetBytes(text);
			int aByte;
			int index = 0;
			long position = 0;

			while((aByte=stream.ReadByte())!=-1)
			{
				if (bytes[index++] != (byte)aByte) {
					index = 0;
					position = stream.Position;
				}

				if (index == bytes.Length)
					return position;

			}
			return -1;
		}
	}
}

Kiểm tra chương trình


using System;
using System.IO;

namespace BinaryFileManipulation
{
	class Program
	{
		public static void Main(string[] args)
		{
			Console.Title="YinYang - Test Binary File Manipulation";
			Y2BinaryIO bin=new Y2BinaryIO("yinyang.dat");
			// create
			bin.CreateFile("string 1\n","string 2\n","string 3\n");
			Console.WriteLine("Before inserting");

			Console.WriteLine(bin.ReadToEnd());

			// insert "string 4" before "string 2"
			bin.InsertBeforeData("string 2\n","string 4\n");
			// insert "string 5" at line 2
			bin.InsertAtLine(2,"string 5\n");
			Console.WriteLine("After inserting");

			Console.WriteLine(bin.ReadToEnd());

			Console.ReadKey(true);
		}
	}
}

Output:

https://yinyangit.wordpress.com

3 thoughts on “C# – Chèn dữ liệu tại vị trí bất kì trong file nhị phân

  1. This problem was written very good by YinYang, I like it because it can be used to patch *exe(s), when we find offsets we can use above code to change the data at those offsets, it’s very good, thank Yin for your information. Have fun!

    Trả lời

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s