在成功將 mac 由 10.10 升級到 10.12 後,我發現除了新增一個並不怎麼好用的 Siri 外,原來支持 NTFS 硬盤的驅動居然也成功失效了。我那塊 500 GB 的東芝硬盤,雖不至於成磚,但一塊只能讀不能寫的硬盤,實在讓人慾哭無淚。巧的是,最近需要頻繁地將一些數據文件( GB 級別)拷貝到其他電腦,而手頭又僅剩一些小容量 U 盤。於是,我突然萌生了寫一個文件分割器的想法,將大的壓縮文件分片後,再用這些小 U 盤搬到到其他電腦上去。
有人會問,這樣的軟件明明網上有的是,何必自己寫呢?沒錯,我就是這麼無聊的人。
需求分析
其實也不用怎麼分析,功能非常簡單。我需要兩個函數(分別用於分割和合成),分割函數的輸入是:一個文件、分片數量,輸出是:分片文件、一個配置文件(記錄分片文件的順序);合成函數的輸入是:配置文件,輸出是:完整的數據文件(根據配置,程序會尋找分片文件用於合成)。
基於此,其實要實現的是兩個這樣的函數:
// 分割文件的函數,第三個參數指定配置文件名稱
void segment(string file_name, int segment_num, string json_file);
// 合成文件的函數,參數爲分割時生成的配置文件
void merge(string json_file);
配置文件的格式,我使用了 json
(其實用簡單的字符串記錄一下也是可以的)。
另外,爲了方便使用,最好再用一個類將兩個方法封裝一下。
難點分析
這麼小的程序會有難點?!其實還是有一丟丟,就是切割文件的時候,由於文件可能太大,因此不能一口氣讀入內存中,所以這裏採用分塊的方法,讀一小塊寫一小塊。當然啦,速度方面的優化,這裏先不考慮了。
程序實現
首先,我們把所有功能放在一個類FileSegment
裏面實現,對外只暴露上面的兩個函數接口。
segment
上面的難度分析已經指出,我們需要分塊讀取文件,然後分塊寫入。
首先需要定義分塊大小:const int FileSegment::kBlockSize = 1024 * 1024;
,這裏設定一個塊大小爲1 MB。
我們再定義兩個輔助函數,用來分塊讀文件、寫文件:
// 從input流中讀取size(默認大小kBlockSize)大小的字節到data裏面
inline void read_file_in_block(char* data, ifstream &input, int size=kBlockSize) {
input.read(data, size);
}
// 從data中將size(默認大小kBlockSize)大小的字節寫入到output流
inline void write_file_in_block(char* data, ofstream &output, int size=kBlockSize) {
output.write(data, size);
}
這兩個函數因爲要經常用到,所以把它們作爲內聯函數使用。
綜合這兩個輔助函數,我們定義另一個輔助函數,用於從輸入文件中將大批量的數據寫入到輸出文件中:
// 將input流中讀取input_size大小的字節內容,寫入到output流中
void FileSegment::copy_file(ifstream &input, ofstream &output, size_t input_size) {
char* data = new char[kBlockSize];
for (size_t block = 0; block < input_size / kBlockSize; block++) {
read_file_in_block(data, input);
write_file_in_block(data, output);
}
// 讀取剩餘的字節
size_t left_size = input_size % kBlockSize;
if (left_size != 0) {
read_file_in_block(data, input, left_size);
write_file_in_block(data, output, left_size);
}
delete [] data;
data = nullptr;
}
有了上面的輔助函數後,我們可以聚焦於segment()
函數的核心代碼部分了。
我們只需要利用copy_file()
函數,將源文件分片寫入到幾個分片文件中即可。
// 分片文件名
vector<string> segment_files;
for (int i = 0; i < segment_num; i++) {
segment_files.push_back(file_name + to_string(i+1) + ".tmp");
cout << "segment_file --- " << segment_files[i] << endl;
}
ifstream src_file_input(file_name);
// 輸入文件大小
size_t src_file_size = file_size(src_file_input);
// 分片文件大小
size_t segment_size = src_file_size / segment_num;
// 分片輸出文件
for (int i = 0; i < segment_num; i++) {
ofstream segment_file_output(segment_files[i]);
if (i == segment_num-1) { // 最後一次,要將剩餘文件片全部寫入
size_t left_size = src_file_size % segment_size;
copy_file(src_file_input, segment_file_output, segment_size + left_size);
} else {
copy_file(src_file_input, segment_file_output, segment_size);
}
segment_file_output.close();
}
src_file_input.close();
另外,我們需要將分片文件的文件名和分割順序等信息寫入配置文件中,這裏使用json
格式,並用這個第三方庫來操縱json
對象。
const string FileSegment::kSegmentFileNum = "SegmentNum";
const string FileSegment::kSourceFileName = "SourceFileName";
const string FileSegment::kSegmentFiles = "SegmentFiles";
ofstream json_output(json_file);
json j;
j[kSegmentFileNum] = segment_num;
j[kSourceFileName] = file_name;
j[kSegmentFiles] = segment_files; // 這裏segment_files是vector對象
json_output << j;
json_output.close();
下面給出segment()
函數的完整代碼:
void FileSegment::segment(string file_name, int segment_num, string json_file) {
// 檢查源文件是否存在
if (!exist(file_name)) {
cout << "file [" << file_name << "] doesn't exist!" << endl;
return;
}
// 檢查分片數量是否大於0
if (segment_num <= 0) {
cout << "segment number should be greater than 0!" << endl;
return;
}
// 分片文件名
vector<string> segment_files;
for (int i = 0; i < segment_num; i++) {
segment_files.push_back(file_name + to_string(i+1) + ".tmp");
cout << "segment_file --- " << segment_files[i] << endl;
}
ifstream src_file_input(file_name);
// 輸入文件大小
size_t src_file_size = file_size(src_file_input);
// 分片文件大小
size_t segment_size = src_file_size / segment_num;
// 分片輸出文件
for (int i = 0; i < segment_num; i++) {
ofstream segment_file_output(segment_files[i]);
if (i == segment_num-1) { // 最後一次,要將剩餘文件片全部寫入
size_t left_size = src_file_size % segment_size;
copy_file(src_file_input, segment_file_output, segment_size + left_size);
} else {
copy_file(src_file_input, segment_file_output, segment_size);
}
segment_file_output.close();
}
src_file_input.close();
ofstream json_output(json_file);
json j;
j[kSegmentFileNum] = segment_num;
j[kSourceFileName] = file_name;
j[kSegmentFiles] = segment_files;
json_output << j;
json_output.close();
}
merge
有了前面的輔助函數後,merge()
函數的實現基本是依葫蘆畫瓢。首先需要從配置文件中讀取出json
對象,根據配置信息去合成文件:
json j;
if (!exist(json_file)) {
cout << "json file [" << json_file << "] doesn't exist!" << endl;
return;
}
ifstream json_input(json_file);
json_input >> j;
// 源文件名
string src_file = j[kSourceFileName];
// 檢查源文件是否已經存在
if (exist(src_file)) {
src_file += ".copy";
}
ofstream result(src_file);
// 文件分片數量
int segment_num = j[kSegmentFileNum];
// 分片文件名
vector<string> segment_files = j[kSegmentFiles];
之後,根據分片文件來合成大文件:
// 合併文件
for (auto it = segment_files.begin(); it != segment_files.end(); it++) {
cout << "copy file [" << *it << "]" << endl;
ifstream seg_input(*it);
size_t seg_input_size = file_size(seg_input); // 計算分片文件大小
copy_file(seg_input, result, seg_input_size);
seg_input.close();
}
接下來照例給出merge()
函數完整實現:
void FileSegment::merge(string json_file) {
json j;
if (!exist(json_file)) {
cout << "json file [" << json_file << "] doesn't exist!" << endl;
return;
}
ifstream json_input(json_file);
json_input >> j;
// 源文件名
string src_file = j[kSourceFileName];
// 檢查源文件是否已經存在
if (exist(src_file)) {
src_file += ".copy";
}
ofstream result(src_file);
// 文件分片數量
int segment_num = j[kSegmentFileNum];
// 分片文件名
vector<string> segment_files = j[kSegmentFiles];
// 檢查文件分片是否齊全
for (auto it = segment_files.begin(); it != segment_files.end(); ++it) {
if (!exist(*it)) {
cout << "segment file [" << *it << "] doesn't exist!" << endl;
return;
}
}
// 合併文件
for (auto it = segment_files.begin(); it != segment_files.end(); it++) {
cout << "copy file [" << *it << "]" << endl;
ifstream seg_input(*it);
size_t seg_input_size = file_size(seg_input);
copy_file(seg_input, result, seg_input_size);
seg_input.close();
}
json_input.close();
result.close();
}
main
在main()
中,直接實例化FileSegment
類,通過segment()
和merge()
函數分割或者合成文件。
int main(int argc, char const *argv[]) {
FileSegment fs;
// 分割data.zip文件,分爲4片
fs.segment("data.zip", 4, "config.json");
// 根據config.json文件合成最終文件
fs.merge("config.json");
}
另外,爲了方便使用,我特意寫了一個解析命令的類InputParser
,然後,我們可以按照如下方式使用該程序:
分割文件
./main -s data.zip 4 config.json
合成文件
./main -m config.json
完整工程代碼,請看:https://github.com/Jermmy/file_segmentation
測試結果
在我的 mac (雙核,2.7 GHz Intel Core i5) 上,將一個 7.35 G 的 zip 文件分割爲 10 片,所用時間爲 37.7 s。
同樣的機器,將上面的 10 片文件合成原來的大文件,所用時間爲 31.8 s。