Quản lý vị trí trong tệp
Như chúng ta đã biết, hệ thống gắn một con trỏ nhất định với mỗi tệp được mở: nó xác định vị trí trong tệp (khoảng cách từ đầu tệp) nơi dữ liệu sẽ được ghi hoặc đọc từ lần gọi hàm I/O tiếp theo. Sau khi hàm được thực thi, con trỏ sẽ dịch chuyển theo kích thước của dữ liệu đã ghi hoặc đọc.
Trong một số trường hợp, bạn muốn thay đổi vị trí của con trỏ mà không thực hiện thao tác I/O. Đặc biệt, khi chúng ta cần thêm dữ liệu vào cuối tệp, chúng ta mở tệp ở chế độ "kết hợp" FILE_READ | FILE_WRITE
, và sau đó phải tìm cách đưa con trỏ đến cuối tệp (nếu không, chúng ta sẽ bắt đầu ghi đè dữ liệu từ đầu). Chúng ta có thể gọi các hàm đọc trong khi vẫn còn dữ liệu để đọc (do đó dịch chuyển con trỏ), nhưng điều này không hiệu quả. Tốt hơn là sử dụng hàm đặc biệt FileSeek
. Và hàm FileTell
cho phép lấy giá trị thực tế của con trỏ (vị trí trong tệp).
Trong phần này, chúng ta sẽ khám phá các hàm này cùng với một số hàm khác liên quan đến vị trí hiện tại trong tệp. Một số hàm hoạt động giống nhau đối với tệp ở chế độ văn bản và nhị phân, trong khi một số khác thì khác biệt.
bool FileSeek(int handle, long offset, ENUM_FILE_POSITION origin)
Hàm này di chuyển con trỏ tệp theo số byte offset
sử dụng origin
làm tham chiếu, là một trong những vị trí được định nghĩa trước trong liệt kê ENUM_FILE_POSITION
. Giá trị offset
có thể là dương (di chuyển đến cuối tệp và xa hơn) hoặc âm (di chuyển về đầu tệp). ENUM_FILE_POSITION
có các thành viên sau:
SEEK_SET
cho đầu tệpSEEK_CUR
cho vị trí hiện tạiSEEK_END
cho cuối tệp
Nếu phép tính vị trí mới so với điểm neo cho giá trị âm (tức là yêu cầu dịch chuyển sang trái đầu tệp), thì con trỏ tệp sẽ được đặt ở đầu tệp.
Nếu bạn đặt vị trí vượt quá cuối tệp (giá trị lớn hơn kích thước tệp), thì việc ghi tiếp theo vào tệp sẽ không được thực hiện từ cuối tệp mà từ vị trí đã đặt. Trong trường hợp này, các giá trị không xác định sẽ được ghi vào giữa phần cuối tệp trước đó và vị trí đã cho (xem bên dưới).
Hàm trả về true
nếu thành công và false
trong trường hợp xảy ra lỗi.
ulong FileTell(int handle)
Đối với tệp được mở với mô tả handle
, hàm trả về vị trí hiện tại của con trỏ nội bộ (khoảng cách so với đầu tệp). Trong trường hợp xảy ra lỗi, ULONG_MAX
((ulong)-1) sẽ được trả về. Mã lỗi có thể được truy xuất từ biến _LastError
hoặc qua hàm GetLastError.
bool FileIsEnding(int handle)
Hàm trả về dấu hiệu liệu con trỏ có đang ở cuối tệp handle
hay không. Nếu đúng, kết quả là true
.
bool FileIsLineEnding(int handle)
Đối với tệp văn bản với mô tả handle
, hàm trả về dấu hiệu liệu con trỏ tệp có đang ở cuối dòng (ngay sau ký tự xuống dòng \n
hoặc \r\n
) hay không. Nói cách khác, giá trị trả về true
có nghĩa là vị trí hiện tại ở đầu dòng tiếp theo (hoặc cuối tệp). Đối với tệp nhị phân, kết quả luôn là false
.
Tập lệnh kiểm tra cho các hàm trên được gọi là FileCursor.mq5
. Nó hoạt động với ba tệp: hai tệp nhị phân và một tệp văn bản.
const string fileraw = "MQL5Book/cursor.raw";
const string filetxt = "MQL5Book/cursor.csv";
const string file100 = "MQL5Book/k100.raw";
2
3
Để đơn giản hóa việc ghi nhật ký vị trí hiện tại, cùng với dấu hiệu cuối tệp (End-Of-File, EOF) và cuối dòng (End-Of-Line, EOL), chúng ta đã tạo một hàm trợ giúp FileState
.
string FileState(int handle)
{
return StringFormat("P:%I64d, F:%s, L:%s",
FileTell(handle),
(string)FileIsEnding(handle),
(string)FileIsLineEnding(handle));
}
2
3
4
5
6
7
Kịch bản kiểm tra các hàm trên tệp nhị phân bao gồm các bước sau.
Tạo mới hoặc mở tệp fileraw
("MQL5Book/cursor.raw") hiện có ở chế độ đọc/ghi. Ngay sau khi mở, và sau mỗi thao tác, chúng ta xuất trạng thái hiện tại của tệp bằng cách gọi FileState
.
void OnStart()
{
int handle;
Print("\n * Phase I. Binary file");
handle = PRTF(FileOpen(fileraw, FILE_BIN | FILE_WRITE | FILE_READ));
Print(FileState(handle));
...
}
2
3
4
5
6
7
8
Di chuyển con trỏ đến cuối tệp, điều này sẽ cho phép chúng ta thêm dữ liệu vào tệp này mỗi khi tập lệnh được thực thi (và không ghi đè từ đầu). Cách rõ ràng nhất để tham chiếu đến cuối tệp: offset
bằng 0 so với origin=SEEK_END
.
PRTF(FileSeek(handle, 0, SEEK_END));
Print(FileState(handle));
2
Nếu tệp không còn trống (không mới), chúng ta có thể đọc dữ liệu hiện có tại vị trí bất kỳ của nó (tương đối hoặc tuyệt đối). Đặc biệt, nếu tham số origin
của hàm FileSeek
bằng SEEK_CUR
, điều đó có nghĩa là với offset
âm, vị trí hiện tại sẽ di chuyển số byte tương ứng ngược lại (sang trái), và với giá trị dương sẽ di chuyển về phía trước (sang phải).
Trong ví dụ này, chúng ta cố gắng lùi lại theo kích thước của một giá trị kiểu int
. Một chút sau chúng ta sẽ thấy rằng ở vị trí này sẽ có trường day_of_year
(trường cuối cùng) của cấu trúc MqlDateTime, bởi vì chúng ta ghi nó vào tệp trong các lệnh tiếp theo, và dữ liệu này có sẵn từ tệp trong lần chạy tiếp theo. Giá trị đọc được sẽ được ghi nhật ký để so sánh với những gì đã lưu trước đó.
if(PRTF(FileSeek(handle, -1 * sizeof(int), SEEK_CUR)))
{
Print(FileState(handle));
PRTF(FileReadInteger(handle));
}
2
3
4
5
Trong một tệp trống mới, lệnh gọi FileSeek
sẽ kết thúc với lỗi 4003 (INVALID_PARAMETER
), và khối lệnh if
sẽ không được thực thi.
Tiếp theo, tệp được điền dữ liệu. Đầu tiên, thời gian cục bộ hiện tại của máy tính (8 byte kiểu datetime
) được ghi bằng FileWriteLong
.
datetime now = TimeLocal();
PRTF(FileWriteLong(handle, now));
Print(FileState(handle));
2
3
Sau đó, chúng ta cố gắng lùi lại từ vị trí hiện tại 4 byte (-4) và đọc long
.
PRTF(FileSeek(handle, -4, SEEK_CUR));
long x = PRTF(FileReadLong(handle));
Print(FileState(handle));
2
3
Nỗ lực này sẽ kết thúc với lỗi 5015 (FILE_READERROR
), vì chúng ta đang ở cuối tệp và sau khi lùi 4 byte sang trái, chúng ta không thể đọc 8 byte từ bên phải (kích thước long
). Tuy nhiên, như chúng ta sẽ thấy từ nhật ký, kết quả của nỗ lực không thành công này, con trỏ vẫn sẽ di chuyển ngược lại cuối tệp.
Nếu bạn lùi lại 8 byte (-8), việc đọc giá trị long
tiếp theo sẽ thành công, và cả hai giá trị thời gian, bao gồm giá trị ban đầu và giá trị nhận được từ tệp, phải khớp nhau.
PRTF(FileSeek(handle, -8, SEEK_CUR));
Print(FileState(handle));
x = PRTF(FileReadLong(handle));
PRTF((now == x));
2
3
4
Cuối cùng, ghi cấu trúc MqlDateTime
được điền cùng thời gian vào tệp. Vị trí trong tệp sẽ tăng thêm 32 (kích thước của cấu trúc tính bằng byte).
MqlDateTime mdt;
TimeToStruct(now, mdt);
StructPrint(mdt); // hiển thị ngày/giờ trong nhật ký một cách trực quan
PRTF(FileWriteStruct(handle, mdt)); // 32 = sizeof(MqlDateTime)
Print(FileState(handle));
FileClose(handle);
2
3
4
5
6
Sau lần chạy đầu tiên của tập lệnh cho kịch bản với tệp fileraw
(MQL5Book/cursor.raw), chúng ta nhận được kết quả tương tự như sau (thời gian sẽ khác):
first run
* Phase I. Binary file
FileOpen(fileraw,FILE_BIN|FILE_WRITE|FILE_READ)=1 / ok
P:0, F:true, L:false
FileSeek(handle,0,SEEK_END)=true / ok
P:0, F:true, L:false
FileSeek(handle,-1*sizeof(int),SEEK_CUR)=false / INVALID_PARAMETER(4003)
FileWriteLong(handle,now)=8 / ok
P:8, F:true, L:false
FileSeek(handle,-4,SEEK_CUR)=true / ok
FileReadLong(handle)=0 / FILE_READERROR(5015)
P:8, F:true, L:false
FileSeek(handle,-8,SEEK_CUR)=true / ok
P:0, F:false, L:false
FileReadLong(handle)=1629683392 / ok
(now==x)=true / ok
2021 8 23 1 49 52 1 234
FileWriteStruct(handle,mdt)=32 / ok
P:40, F:true, L:false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Theo trạng thái, kích thước tệp ban đầu là 0 vì vị trí là "P:0" sau khi dịch chuyển đến cuối tệp ("F:true"). Sau mỗi lần ghi (sử dụng FileWriteLong
và FileWriteStruct
), vị trí P tăng theo kích thước của dữ liệu đã ghi.
Sau lần chạy thứ hai của tập lệnh, bạn có thể nhận thấy một số thay đổi trong nhật ký:
second run
* Phase I. Binary file
FileOpen(fileraw,FILE_BIN|FILE_WRITE|FILE_READ)=1 / ok
P:0, F:false, L:false
FileSeek(handle,0,SEEK_END)=true / ok
P:40, F:true, L:false
FileSeek(handle,-1*sizeof(int),SEEK_CUR)=true / ok
P:36, F:false, L:false
FileReadInteger(handle)=234 / ok
FileWriteLong(handle,now)=8 / ok
P:48, F:true, L:false
FileSeek(handle,-4,SEEK_CUR)=true / ok
FileReadLong(handle)=0 / FILE_READERROR(5015)
P:48, F:true, L:false
FileSeek(handle,-8,SEEK_CUR)=true / ok
P:40, F:false, L:false
FileReadLong(handle)=1629683397 / ok
(now==x)=true / ok
2021 8 23 1 49 57 1 234
FileWriteStruct(handle,mdt)=32 / ok
P:80, F:true, L:false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Thứ nhất, kích thước tệp sau khi mở là 40 (theo vị trí "P:40" sau khi dịch chuyển đến cuối tệp). Mỗi lần chạy tập lệnh, tệp sẽ tăng thêm 40 byte.
Thứ hai, vì tệp không trống, có thể điều hướng qua nó và đọc dữ liệu "cũ". Đặc biệt, sau khi lùi lại -1*sizeof(int)
từ vị trí hiện tại (cũng là cuối tệp), chúng ta đọc thành công giá trị 234, là trường cuối cùng của cấu trúc MqlDateTime
(đó là số ngày trong năm và có thể sẽ khác với bạn).
Kịch bản kiểm tra thứ hai hoạt động với tệp văn bản csv filetxt
(MQL5Book/cursor.csv). Chúng ta cũng sẽ mở nó ở chế độ đọc và ghi kết hợp, nhưng sẽ không di chuyển con trỏ đến cuối tệp. Do đó, mỗi lần chạy tập lệnh sẽ ghi đè dữ liệu, bắt đầu từ đầu tệp. Để dễ nhận thấy sự khác biệt, các số trong cột đầu tiên của CSV được tạo ngẫu nhiên. Trong cột thứ hai, các chuỗi giống nhau luôn được thay thế từ mẫu trong hàm StringFormat
.
Print(" * Phase II. Text file");
srand(GetTickCount());
// tạo tệp mới hoặc mở tệp hiện có để ghi/ghi đè
// từ đầu và đọc tiếp theo; dữ liệu bên trong CSV (Unicode)
handle = PRTF(FileOpen(filetxt, FILE_CSV | FILE_WRITE | FILE_READ, ','));
// ba dòng dữ liệu (cặp số,chuỗi trong mỗi dòng), phân tách bằng '\n'
// lưu ý rằng phần tử cuối không kết thúc bằng ký tự xuống dòng '\n'
// điều này là tùy chọn, nhưng được phép
string content = StringFormat(
"%02d,abc\n%02d,def\n%02d,ghi",
rand() % 100, rand() % 100, rand() % 100);
// '\n' sẽ tự động được thay bằng '\r\n', nhờ FileWriteString
PRTF(FileWriteString(handle, content));
2
3
4
5
6
7
8
9
10
11
12
13
Dưới đây là ví dụ về dữ liệu được tạo:
34,abc
20,def
02,ghi
2
3
Sau đó, chúng ta quay lại đầu tệp và đọc nó trong một vòng lặp với FileReadString
, liên tục ghi nhật ký trạng thái.
PRTF(FileSeek(handle, 0, SEEK_SET));
Print(FileState(handle));
// đếm số dòng trong tệp bằng tính năng FileIsLineEnding
int lineCount = 0;
while(!FileIsEnding(handle))
{
PRTF(FileReadString(handle));
Print(FileState(handle));
// FileIsLineEnding cũng bằng true khi FileIsEnding bằng true,
// ngay cả khi không có ký tự '\n' ở cuối
if(FileIsLineEnding(handle)) lineCount++;
}
FileClose(handle);
PRTF(lineCount);
2
3
4
5
6
7
8
9
10
11
12
13
14
Dưới đây là nhật ký cho tệp filetxt
sau lần chạy đầu tiên và thứ hai của tập lệnh. Đầu tiên là lần đầu:
first run
* Phase II. Text file
FileOpen(filetxt,FILE_CSV|FILE_WRITE|FILE_READ,',')=1 / ok
FileWriteString(handle,content)=44 / ok
FileSeek(handle,0,SEEK_SET)=true / ok
P:0, F:false, L:false
FileReadString(handle)=08 / ok
P:8, F:false, L:false
FileReadString(handle)=abc / ok
P:18, F:false, L:true
FileReadString(handle)=37 / ok
P:24, F:false, L:false
FileReadString(handle)=def / ok
P:34, F:false, L:true
FileReadString(handle)=96 / ok
P:40, F:false, L:false
FileReadString(handle)=ghi / ok
P:46, F:true, L:true
lineCount=3 / ok
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Và đây là lần thứ hai:
second run
* Phase II. Text file
FileOpen(filetxt,FILE_CSV|FILE_WRITE|FILE_READ,',')=1 / ok
FileWriteString(handle,content)=44 / ok
FileSeek(handle,0,SEEK_SET)=true / ok
P:0, F:false, L:false
FileReadString(handle)=34 / ok
P:8, F:false, L:false
FileReadString(handle)=abc / ok
P:18, F:false, L:true
FileReadString(handle)=20 / ok
P:24, F:false, L:false
FileReadString(handle)=def / ok
P:34, F:false, L:true
FileReadString(handle)=02 / ok
P:40, F:false, L:false
FileReadString(handle)=ghi / ok
P:46, F:true, L:true
lineCount=3 / ok
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Như bạn thấy, kích thước tệp không thay đổi, nhưng các số khác nhau được ghi tại cùng các khoảng cách. Vì tệp CSV này có hai cột, sau mỗi giá trị thứ hai chúng ta đọc, chúng ta thấy cờ EOL ("L:true") được kích hoạt.
Số dòng phát hiện là 3, mặc dù chỉ có 2 ký tự xuống dòng trong tệp: dòng cuối cùng (thứ ba) kết thúc cùng với tệp.
Cuối cùng, kịch bản kiểm tra cuối cùng sử dụng tệp file100
(MQL5Book/k100.raw) để di chuyển con trỏ vượt quá cuối tệp (đến mốc 1000000 byte), và do đó tăng kích thước của nó (dự trữ không gian đĩa cho các thao tác ghi tiềm năng trong tương lai).
Print(" * Phase III. Allocate large file");
handle = PRTF(FileOpen(file100, FILE_BIN | FILE_WRITE));
PRTF(FileSeek(handle, 1000000, SEEK_END));
// để thay đổi kích thước, bạn cần ghi ít nhất một thứ gì đó
PRTF(FileWriteInteger(handle, 0xFF, 1));
PRTF(FileTell(handle));
FileClose(handle);
2
3
4
5
6
7
Đầu ra nhật ký cho tập lệnh này không thay đổi từ lần chạy này sang lần chạy khác, tuy nhiên, dữ liệu ngẫu nhiên xuất hiện trong không gian được phân bổ cho tệp có thể khác nhau (nội dung của nó không được hiển thị ở đây: hãy sử dụng trình xem nhị phân bên ngoài).
* Phase III. Allocate large file
FileOpen(file100,FILE_BIN|FILE_WRITE)=1 / ok
FileSeek(handle,1000000,SEEK_END)=true / ok
FileWriteInteger(handle,0xFF,1)=1 / ok
FileTell(handle)=1000001 / ok
2
3
4
5