Ghi và đọc cấu trúc (tệp nhị phân)
Trong phần trước, chúng ta đã học cách thực hiện các thao tác I/O trên mảng các cấu trúc. Khi việc đọc hoặc ghi liên quan đến một cấu trúc riêng lẻ, sẽ thuận tiện hơn khi sử dụng cặp hàm FileWriteStruct
và FileReadStruct
.
uint FileWriteStruct(int handle, const void &data, int size = -1)
Hàm này ghi nội dung của một cấu trúc data
đơn giản vào tệp nhị phân với mô tả handle
. Như chúng ta đã biết, các cấu trúc như vậy chỉ có thể chứa các trường thuộc kiểu không phải chuỗi có sẵn và các cấu trúc đơn giản lồng nhau.
Đặc điểm chính của hàm này là tham số size
. Nó giúp đặt số byte sẽ được ghi, cho phép chúng ta loại bỏ một phần của cấu trúc (phần cuối của nó). Theo mặc định, tham số này là -1, nghĩa là toàn bộ cấu trúc được lưu. Nếu size
lớn hơn kích thước của cấu trúc, phần dư thừa sẽ bị bỏ qua, tức là chỉ có cấu trúc được ghi, với số byte là sizeof(data)
.
Khi thành công, hàm trả về số byte đã ghi, khi lỗi trả về 0.
uint FileReadStruct(int handle, void &data, int size = -1)
Hàm này đọc nội dung từ tệp nhị phân với mô tả handle
vào cấu trúc data
. Tham số size
chỉ định số byte sẽ được đọc. Nếu không được chỉ định hoặc vượt quá kích thước của cấu trúc, thì kích thước chính xác của cấu trúc được chỉ định sẽ được sử dụng.
Khi thành công, hàm trả về số byte đã đọc, khi lỗi trả về 0.
Tùy chọn cắt bỏ phần cuối của cấu trúc chỉ có trong các hàm FileWriteStruct
và FileReadStruct
. Do đó, việc sử dụng chúng trong vòng lặp trở thành lựa chọn phù hợp nhất để lưu và đọc mảng các cấu trúc đã được cắt bớt: các hàm FileWriteArray
và FileReadArray
không có khả năng này, và việc ghi và đọc theo từng trường riêng lẻ có thể tốn tài nguyên hơn (chúng ta sẽ xem xét các hàm tương ứng trong các phần sau).
Cần lưu ý rằng để sử dụng tính năng này, bạn nên thiết kế cấu trúc của mình sao cho tất cả các trường tính toán tạm thời và trung gian không cần lưu được đặt ở cuối cấu trúc.
Hãy xem xét ví dụ sử dụng hai hàm này trong tập lệnh FileStruct.mq5
.
Giả sử chúng ta muốn lưu trữ định kỳ các báo giá mới nhất, để có thể kiểm tra tính không đổi của chúng trong tương lai hoặc so sánh với các khoảng thời gian tương tự từ các nhà cung cấp khác. Về cơ bản, điều này có thể được thực hiện thủ công qua hộp thoại Symbols (trong tab Bars) trong MetaTrader 5. Nhưng điều này sẽ đòi hỏi thêm nỗ lực và tuân thủ lịch trình. Sẽ dễ dàng hơn nhiều khi thực hiện tự động từ chương trình. Ngoài ra, việc xuất báo giá thủ công được thực hiện ở định dạng văn bản CSV, và chúng ta có thể cần gửi tệp đến máy chủ bên ngoài. Do đó, mong muốn lưu chúng ở dạng nhị phân gọn nhẹ. Hơn nữa, giả sử chúng ta không quan tâm đến thông tin về tick, spread và khối lượng thực (luôn trống đối với các biểu tượng Forex).
Trong phần So sánh, sắp xếp và tìm kiếm trong mảng, chúng ta đã xem xét cấu trúc MqlRates
và hàm CopyRates
. Chúng sẽ được mô tả chi tiết sau này, còn bây giờ chúng ta sẽ sử dụng chúng một lần nữa như một môi trường thử nghiệm cho các thao tác tệp.
Sử dụng tham số size
trong FileWriteStruct
, chúng ta có thể chỉ lưu một phần của cấu trúc MqlRates
, không bao gồm các trường cuối cùng.
Ở đầu tập lệnh, chúng ta định nghĩa các macro và tên của tệp thử nghiệm.
#define BARLIMIT 10 // số lượng thanh để ghi
#define HEADSIZE 10 // kích thước tiêu đề của định dạng của chúng ta
const string filename = "MQL5Book/struct.raw";
2
3
Hằng số HEADSIZE đặc biệt đáng chú ý. Như đã đề cập trước đó, các hàm tệp tự nó không chịu trách nhiệm về tính nhất quán của dữ liệu trong tệp và các loại cấu trúc mà dữ liệu này được đọc vào. Lập trình viên phải cung cấp sự kiểm soát như vậy trong mã của họ. Do đó, một tiêu đề nhất định thường được ghi ở đầu tệp, với sự trợ giúp của nó, thứ nhất, có thể đảm bảo rằng đây là tệp ở định dạng cần thiết, thứ hai, lưu trữ thông tin meta cần thiết để đọc đúng cách.
Cụ thể, tiêu đề có thể chỉ ra số lượng mục nhập. Nói một cách nghiêm ngặt, điều này không phải lúc nào cũng cần thiết, vì chúng ta có thể đọc tệp dần dần cho đến khi nó kết thúc. Tuy nhiên, sẽ hiệu quả hơn khi phân bổ bộ nhớ cho tất cả các bản ghi dự kiến cùng một lúc, dựa trên bộ đếm trong tiêu đề.
Cho mục đích của chúng ta, chúng ta đã phát triển một cấu trúc đơn giản FileHeader
.
struct FileHeader
{
uchar signature[HEADSIZE];
int n;
FileHeader(const int size = 0) : n(size)
{
static uchar s[HEADSIZE] = {'C','A','N','D','L','E','S','1','.','0'};
ArrayCopy(signature, s);
}
};
2
3
4
5
6
7
8
9
10
Nó bắt đầu bằng chữ ký văn bản "CANDLES" (trong trường signature
), số phiên bản "1.0" (cùng vị trí), và số lượng mục nhập (trường n
). Vì chúng ta không thể sử dụng trường chuỗi cho chữ ký (nếu không cấu trúc sẽ không còn đơn giản và đáp ứng yêu cầu của các hàm tệp), văn bản thực sự được đóng gói vào mảng uchar
có kích thước cố định HEADSIZE. Việc khởi tạo của nó trong phiên bản được thực hiện bởi hàm tạo dựa trên bản sao tĩnh cục bộ.
Trong hàm OnStart
, chúng ta yêu cầu BARLIMIT của các thanh cuối cùng, mở tệp ở chế độ FILE_WRITE, và ghi tiêu đề theo sau là các báo giá kết quả ở dạng cắt bớt vào tệp.
void OnStart()
{
MqlRates rates[], candles[];
int n = PRTF(CopyRates(_Symbol, _Period, 0, BARLIMIT, rates)); // 10 / ok
if(n < 1) return;
// tạo tệp mới hoặc ghi đè tệp cũ từ đầu
int handle = PRTF(FileOpen(filename, FILE_BIN | FILE_WRITE)); // 1 / ok
FileHeader fh(n); // tiêu đề với số lượng mục nhập thực tế
// ghi tiêu đề trước
PRTF(FileWriteStruct(handle, fh)); // 14 / ok
// sau đó ghi dữ liệu
for(int i = 0; i < n; ++i)
{
FileWriteStruct(handle, rates[i], offsetof(MqlRates, tick_volume));
}
FileClose(handle);
ArrayPrint(rates);
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Giá trị tham số size
trong hàm FileWriteStruct
, chúng ta sử dụng biểu thức với toán tử quen thuộc offsetof
: offsetof(MqlRates, tick_volume)
, tức là tất cả các trường bắt đầu từ tick_volume
bị loại bỏ khi ghi vào tệp.
Để kiểm tra việc đọc dữ liệu, hãy mở cùng tệp ở chế độ FILE_READ và đọc cấu trúc FileHeader
.
handle = PRTF(FileOpen(filename, FILE_BIN | FILE_READ)); // 1 / ok
FileHeader reference, reader;
PRTF(FileReadStruct(handle, reader)); // 14 / ok
// nếu tiêu đề không khớp, đó không phải dữ liệu của chúng ta
if(ArrayCompare(reader.signature, reference.signature))
{
Print("Wrong file format; 'CANDLES' header is missing");
return;
}
2
3
4
5
6
7
8
9
Cấu trúc reference
chứa tiêu đề mặc định không thay đổi (chữ ký). Cấu trúc reader
nhận được 14 byte từ tệp. Nếu hai chữ ký khớp, chúng ta có thể tiếp tục làm việc, vì định dạng tệp hóa ra là đúng, và trường reader.n
chứa số lượng mục nhập đọc từ tệp. Chúng ta phân bổ và đặt về 0 bộ nhớ kích thước cần thiết cho mảng nhận candles
, sau đó đọc tất cả các mục nhập vào nó.
PrintFormat("Reading %d candles...", reader.n);
ArrayResize(candles, reader.n); // phân bổ bộ nhớ cho dữ liệu dự kiến trước
ZeroMemory(candles);
for(int i = 0; i < reader.n; ++i)
{
FileReadStruct(handle, candles[i], offsetof(MqlRates, tick_volume));
}
FileClose(handle);
ArrayPrint(candles);
}
2
3
4
5
6
7
8
9
10
11
Việc đặt về 0 là cần thiết vì các cấu trúc MqlRates
được đọc một phần, và các trường còn lại sẽ chứa rác nếu không được đặt về 0.
Dưới đây là nhật ký hiển thị dữ liệu ban đầu (toàn bộ) cho XAUUSD,H1.
[time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume]
[0] 2021.08.16 03:00:00 1778.86 1780.58 1778.12 1780.56 3049 5 0
[1] 2021.08.16 04:00:00 1780.61 1782.58 1777.10 1777.13 4633 5 0
[2] 2021.08.16 05:00:00 1777.13 1780.25 1776.99 1779.21 3592 5 0
[3] 2021.08.16 06:00:00 1779.26 1779.26 1776.67 1776.79 2535 5 0
[4] 2021.08.16 07:00:00 1776.79 1777.59 1775.50 1777.05 2052 6 0
[5] 2021.08.16 08:00:00 1777.03 1777.19 1772.93 1774.35 3213 5 0
[6] 2021.08.16 09:00:00 1774.38 1775.41 1771.84 1773.33 4527 5 0
[7] 2021.08.16 10:00:00 1773.26 1777.42 1772.84 1774.57 4514 5 0
[8] 2021.08.16 11:00:00 1774.61 1776.67 1773.69 1775.95 3500 5 0
[9] 2021.08.16 12:00:00 1775.96 1776.12 1773.68 1774.44 2425 5 0
2
3
4
5
6
7
8
9
10
11
Bây giờ hãy xem dữ liệu đã đọc được.
[time] [open] [high] [low] [close] [tick_volume] [spread] [real_volume]
[0] 2021.08.16 03:00:00 1778.86 1780.58 1778.12 1780.56 0 0 0
[1] 2021.08.16 04:00:00 1780.61 1782.58 1777.10 1777.13 0 0 0
[2] 2021.08.16 05:00:00 1777.13 1780.25 1776.99 1779.21 0 0 0
[3] 2021.08.16 06:00:00 1779.26 1779.26 1776.67 1776.79 0 0 0
[4] 2021.08.16 07:00:00 1776.79 1777.59 1775.50 1777.05 0 0 0
[5] 2021.08.16 08:00:00 1777.03 1777.19 1772.93 1774.35 0 0 0
[6] 2021.08.16 09:00:00 1774.38 1775.41 1771.84 1773.33 0 0 0
[7] 2021.08.16 10:00:00 1773.26 1777.42 1772.84 1774.57 0 0 0
[8] 2021.08.16 11:00:00 1774.61 1776.67 1773.69 1775.95 0 0 0
[9] 2021.08.16 12:00:00 1775.96 1776.12 1773.68 1774.44 0 0 0
2
3
4
5
6
7
8
9
10
11
Các báo giá khớp nhau, nhưng ba trường cuối trong mỗi cấu trúc là trống.
Bạn có thể mở thư mục MQL5/Files/MQL5Book
và kiểm tra biểu diễn bên trong của tệp struct.raw
(sử dụng trình xem hỗ trợ chế độ nhị phân; một ví dụ được hiển thị bên dưới).
Caption: Tùy chọn trình bày tệp nhị phân với kho lưu trữ báo giá trong trình xem bên ngoài
Đây là cách hiển thị điển hình của các tệp nhị phân: cột bên trái hiển thị địa chỉ (độ lệch từ đầu tệp), mã byte ở cột giữa, và biểu diễn ký hiệu của các byte tương ứng được hiển thị ở cột bên phải. Cột đầu tiên và thứ hai sử dụng ký hiệu thập lục phân cho số. Các ký tự ở cột bên phải có thể khác nhau tùy thuộc vào trang mã ANSI được chọn. Chỉ có ý nghĩa khi chú ý đến chúng trong những đoạn mà sự hiện diện của văn bản được biết đến. Trong trường hợp của chúng ta, chữ ký "CANDLES1.0" được "biểu hiện" rõ ràng ngay từ đầu. Các số nên được phân tích bởi cột giữa. Trong cột này, ví dụ, sau chữ ký, bạn có thể thấy giá trị 4 byte 0x0A000000, tức là 0x0000000A ở dạng đảo ngược (hãy nhớ phần Kiểm soát Endianness trong số nguyên): đây là 10, số lượng cấu trúc đã ghi.