上个学期获得了一个强生出租车的数据集,老师给的要求首先是要提取出每辆车的记录,并将每辆车的所有记录存放到以这辆车命名的文件里。
(担心文章很长,可爱的大家可能会只读一部分,如果最后觉得本文还可以的话请大家点点赞~您的鼓励是我更博的最大动力噢)
原始数据有车机号 业务状态 载客状态 顶灯状态 道路状态 刹车状态 接受日期 GPS时间 经度 纬度 速度 方向卫星数,这么些字段。需要提取每辆车的记录内容为:车机号,GPS时间,经度,纬度,速度,方向。格式为:<车机号,GPS时间,经度,纬度,速度,方向>。
也算是老师让我练习C++嘛,所以我只用了第一部分的数据。但是这个数据也好大,有9G多。下面记一记我从刚拿到这个数据集到最后处理完的心路历程。
第一个想法:单纯读入,写出,存储。
按照常理,加上之前学校做哈利波特检索的经验,我是想把原始数据文件全部读到内存里,然后一条一条分析。
但是这个文件实在是太大了…直接读入内存,可是能让我用的内存并没有那么大…所以我在读入方面,打算按行读入。
第二个想法:按行读入,抽取数据,分条写出。
按行读入有了,抽取每一行的数据也有了,分条写到对应的文件夹里也有了。
那么,就有了我的初代代码:
#pragma warning(disable:4996)
//VS2019在直接使用sprintf这类函数的时候会报错,用这个能不让它报错。
#include<iostream>
#include<fstream>
#include<string>
#include<cstring>//对char字符数组的操作
using namespace std;
const int FILE_NAME_LENGTH(1000);
const int LINE_LENGTH(10000);
const int DATA_LENGTH(100);
char line[LINE_LENGTH];
string line_str;
char carID[DATA_LENGTH]; //车机号
char ctrl_Flag[DATA_LENGTH]; //控制字
char service_State[DATA_LENGTH]; //业务状态
char carry_State[DATA_LENGTH]; //载客状态
char light_State[DATA_LENGTH]; //顶灯状态
char road_State[DATA_LENGTH]; //道路状态
char brake_State[DATA_LENGTH]; //刹车状态
char receive_Date[DATA_LENGTH]; //接收日期
char GPS_Time[DATA_LENGTH]; //GPS时间
char longitude[DATA_LENGTH]; //经度
char latitude[DATA_LENGTH]; //纬度
char speed[DATA_LENGTH]; //速度
char direction[DATA_LENGTH]; //方向
char satellite_Num[DATA_LENGTH]; //卫星数
以上是全局变量定义的部分。
下面是函数原型和函数定义。
void inputData(ifstream&input);//按行读入数据集中的数据
void outputData();//将提取好的数据输出到目标的文件夹
void dispatcher(const char* path_of_data, const char* path_of_target);
//调度函数
int main()
{
dispatcher("part-00000","origin01.txt");
return 0;
}
void inputData(ifstream& input)
{
getline(input, line_str);
strcpy(line, line_str.c_str());
//cout << line << endl;
//这里也有一点点需要注意,我在下面会说。
sscanf(line, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%s",
carID,
service_State,
carry_State,
light_State,
road_State,
brake_State,
receive_Date,
GPS_Time,
longitude,
latitude,
speed,
direction,
satellite_Num);
}
void outputData()
{
ofstream output;
char path_of_target[FILE_NAME_LENGTH];
strcpy(path_of_target, "cars\\");
strcat(path_of_target, carID);
strcat(path_of_target, ".txt");
output.open(path_of_target,ios_base::app);
if (!output){
cout << "can't open target data file!\nplease check your code!\n";
}
output << "<" << carID << "," << GPS_Time << "," << longitude << ","
<< latitude << "," << speed << "," << direction << ">" << '\n';
output.close();
}
void dispatcher(const char* path_of_data, const char* path_of_target)
{
ifstream input;
input.open(path_of_data);
if (!input)
{
cout << "can't open source data file!\nplease check your code!\n";
}
while (!input.eof())
{
inputData(input);
outputData();
}
input.close();
}
上面这段函数原型和函数定义中还是有一些东西值得总结学习的。下面我就分条按顺序列出来我的想法,权当加深记忆(大佬能够提出指点的话更是不胜感激)。
- 调度函数的使用:
使用调度函数,能够避免在main函数中有过多的代码量,能够增加程序的可维护性。 - sscanf的使用之按照规定格式读入
我在之前的一篇博客中也讲到过sscanf的使用。但是那个时候还是比较肤浅的,而且好多东西并不像我所想的那样。比如,在这个数据集中,每条数据占一行,每行的不同数据按照逗号隔开。
那么,如果想要按照逗号分隔,并且实现被分开的不同数据存入到不同的buffer中,就必须要用到这样的神奇表达式——正则表达式,来进行sscanf的分段截取字符串。
具体代码如下:
sscanf(line, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%s",
carID,
service_State,
carry_State,
light_State,
road_State,
brake_State,
receive_Date,
GPS_Time,
longitude,
latitude,
speed,
direction,
satellite_Num);
}
sscanf中正则表达式的用法,我也会专门发个博客来总结下。总结好之后会把网站也放到这里,这里先留一个小小的接口。
- 输出时打开文件模式的选取
将内容按流输入到对应的目标文件中,需要注意ofstream对象的打开文件的方式。
在我的代码中有体现,就是这样一句话:
output.open(path_of_target,ios_base::app);
后面的ios_base::app,就是打开目标文件的方式。如果不加这个,默认是后一次打开之后会把前一次的内容覆盖掉。加上这个ios_base::app,就可以在原先的内容之后追加了。这些参数还有好多,具体的用法之后再总结或者直接附上大佬总结好的文章好了。也是,留一个接口,哈哈。
第二种想法的分析——可行之处与不可行之处:
可行之处:
第二种方法其实是可行的,而且占用的内存极小。由于每次都是从源数据文件中读取一行,然后将此行存入到对应的数据文件中,所以占用的内存是不大的。
不可行之处:
第二种方法的不可行之处,就在于系统每读入一条数据,就要打开&关闭一次目标文件。9G的原始数据文件,其中的每一条数据都要经历这样一个打开+关闭目标文件的过程,其速度可想而知的慢。当时大概是运行了300多分钟,才处理好了1.4G的数据,其效率真是太低了…
所以,我看了看《算法笔记》,学了学STL,请教了大佬,终于有了一种更快一些的解决方案。
第三种想法:用空间换时间,输出的时候手动设置一个缓冲区。
这第三种方法的主题思想就是:设置一个输出的缓冲区,利用map来分别对每辆车建立一个“车机号-存储该车”的map,使用map实现对读入的数据的缓存。每读入一定量数据之后(程序中设置是40960),检测每个车对应的数据向量的条数,若达到一定数值(程序中设置为100),则打开该车所对应的文件,将向量的内容全部存入文件,关闭文件,然后将对应向量清空。
这其实是实现了一个缓存的机制,即使用一部分内存对每辆车的数据进行缓存,缓存达到一定程度后将缓存的内容存入到文件中,然后清空该车对应的缓存。
话不多说,上代码,然后在注释里会分析下。
大部分都没有变,就是新增了两个文件是重点,思想也是重点。
/****WMapVector.h****/
/*这里相当于是手动实现了一个专门用来存储、管理每辆车的数据的类,*/
/*这个类的许多方法都是用来管理数据的。*/
#pragma once
#include <map>
#include <vector>
#include <string>
using namespace std;
class WMapVector
{
private:
map<string, vector<string>> dataMap;
private:
int addItemCount;
//添加到系统中的数据记录总数
public:
long linesItemCount;
//这个是要为每处理多少条数据之后在黑窗口里显示用的
//这样处理的时候在黑窗口中还是会有提醒,
//就不怕出现死机或者是异常之后人不知道的情况了。
WMapVector();
~WMapVector();
void Refresh();
//这个是用来将内存中的数据全部输出到对应的文件中,然后重置用的。
//也就是reset吧。
void Add(string keyName,string valItem);
//往map中对应的车对应的vector中添加数据
void SaveBuffer2File(bool allSave, int itemSize=100);
//每100条保存一次文件
};
我是一条分割线=====================================================
/****WMapVector.cpp****/
#include "WMapVector.h"
#include<fstream>
WMapVector::WMapVector()
{
//构造函数的时候把这些数据全部置0
addItemCount = 0;
linesItemCount = 0;
}
void WMapVector::Refresh()
{
SaveBuffer2File(true); //全部存储到文件中
dataMap.clear();
addItemCount = 0;
linesItemCount = 0;
}
WMapVector::~WMapVector()
{
SaveBuffer2File(true); //全部存储到文件中
dataMap.clear();
}
void WMapVector::Add(string keyName, string valItem)
{
dataMap[keyName].push_back(valItem);
addItemCount++;
linesItemCount++;
if (addItemCount > 40960)//4万条记录判断一次存储
{
SaveBuffer2File(false);
addItemCount = 0;
}
}
void WMapVector::SaveBuffer2File(bool allSave,int itemSize = 100)
{
//第一个参数是模式,同一个函数实现两种存储方法。
///如果allSave为真,则不做条数判断,全部存储到文件,并清空vector所有内容
///判断每个vector中的记录是否超过指定itemSize条数
if (allSave == true)//如果是“全存”模式:
{
for (auto beg = dataMap.begin(); beg != dataMap.end(); ++beg)
{
//打开文件
ofstream output;
output.open("k:\\cars\\"+beg->first+".txt", ios_base::app);
if (!output) {
//cout << "can't open target data file!\nplease check your code!\n";
}
for (auto begvec = beg->second.begin(); begvec != beg->second.end(); ++begvec)
//此处的迭代器类型为vector<string>::iterator
{
output << begvec->c_str() << '\n';
}
beg->second.clear();//输出到文件后,清空容器中的内容
//关闭文件
output.close();
}
}
else//如果是“存单个”模式:
{
for (auto beg = dataMap.begin(); beg != dataMap.end(); ++beg)
{
//打开文件
ofstream output;
output.open("k:\\cars\\" + beg->first + ".txt", ios_base::app);
if (!output) {
//cout << "can't open target data file!\nplease check your code!\n";
}
if (beg->second.size() >= itemSize)
//如果缓存区域中某车的数据条数超过了制定的数据条数
//那么就把这个车的数据都存入对应的文件中,然后清空缓存容器中的内容。
{
for (auto begvec = beg->second.begin(); begvec != beg->second.end(); ++begvec)
//此处的迭代器类型为vector< pair<string, string> >::iterator
{
output << begvec->c_str() << '\n';
}
beg->second.clear(); //输出到文件后,清空容器中的内容
}
//关闭文件
output.close();
}
}
}
我是一条分割线=====================================================
/****main.cpp****/
#pragma warning(disable:4996)
#include<iostream>
#include<fstream>
#include<string>
#include<cstring>//对char字符数组的操作
#include "WMapVector.h"
#include <time.h>
using namespace std;
const int FILE_NAME_LENGTH(256);//这些数据也不用开那么大,够用就行了
const int LINE_LENGTH(256);
const int DATA_LENGTH(100);
char line[LINE_LENGTH];
string line_str;
long linesCount;
char carID[DATA_LENGTH]; //车机号
char ctrl_Flag[DATA_LENGTH]; //控制字
char service_State[DATA_LENGTH]; //业务状态
char carry_State[DATA_LENGTH]; //载客状态
char light_State[DATA_LENGTH]; //顶灯状态
char road_State[DATA_LENGTH]; //道路状态
char brake_State[DATA_LENGTH]; //刹车状态
char receive_Date[DATA_LENGTH]; //接收日期
char GPS_Time[DATA_LENGTH]; //GPS时间
char longitude[DATA_LENGTH]; //经度
char latitude[DATA_LENGTH]; //纬度
char speed[DATA_LENGTH]; //速度
char direction[DATA_LENGTH]; //方向
char satellite_Num[DATA_LENGTH]; //卫星数
void inputData(ifstream&input);//按行读入数据集中的数据
void outputData();//将提取好的数据输出到目标的文件夹
void dispatcher(const char* path_of_data, const char* path_of_target);
WMapVector wmapVector; //声明缓存记录对象
int main()
{
dispatcher("part-00000","origin01.txt");
return 0;
}
void inputData(ifstream& input)
{
getline(input, line_str);
strcpy(line, line_str.c_str());
sscanf(line, "%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%s",
carID,
service_State,
carry_State,
light_State,
road_State,
brake_State,
receive_Date,
GPS_Time,
longitude,
latitude,
speed,
direction,
satellite_Num);
}
void outputData()
{
ofstream output;
char path_of_target[FILE_NAME_LENGTH];
strcpy(path_of_target, "cars\\");
strcat(path_of_target, carID);
strcat(path_of_target, ".txt");
output.open(path_of_target,ios_base::app);
if (!output){
cout << "can't open target data file!\nplease check your code!\n";
}
output << "<" << carID << "," << GPS_Time << "," << longitude << ","
<< latitude << "," << speed << "," << direction << ">" << '\n';
output.close();
}
void dispatcher(const char* path_of_data, const char* path_of_target)
{
ifstream input;
char lineBuffer[1024];
time_t timest, timeEnd;
int itemsCount = 0;
time(×t); /*获取time_t类型的当前时间*/
cout << "start:" << asctime(gmtime(×t)) << '\n';
input.open("k:\\part-00000");
if (!input)
{
cout << "can't open source data file!\nplease check your code!\n";
}
while (!input.eof())
{
inputData(input);
sprintf(lineBuffer,"<%s,%s,%s,%s,%s,%s>",
carID , GPS_Time,longitude,latitude ,speed , direction );
wmapVector.Add(string(carID), string(lineBuffer));
if (itemsCount++ == 102400)
//这个用来显示每处理102400条数据所用的时间。
//让人知道这个程序还在跑,没有出现异常情况。
{
time(&timeEnd); /*获取time_t类型的当前时间*/
cout << "going:" << asctime(gmtime(&timeEnd))<< "处理条数:"
<< wmapVector.linesItemCount <<'\n';
itemsCount = 0;
}
}
input.close();
time(&timeEnd); /*获取time_t类型的当前时间*/
cout << "end:" << asctime(gmtime(&timeEnd));
}
总结一下,这个想法就是使用了计算机中的缓存思想(暂且这么叫),就是用空间换时间,就像是CPU和主存中间的cache一样,先把处理好的数据都存在中间的缓存区域内,缓存到了一定程度之后一次性地将缓存好的多条数据存入文件中。这样能够大大减少打开+关闭文件的次数,从而能够大大提高程序的出结果的速度。
对第三种方法的反思
第三种方法,虽然比前两种方法快很多,但是依然不算很快,还是有很多能够继续优化的地方的。
比如,第三种方法的读入数据,依然采取的是按行读取,每次依然要将每一条数据从磁盘读入到内存中,这样也是很慢的。
其实可以采取一次读入许多数据,或者说,一次读入一定的字节数的数据,然后进行处理。
碰到截取了一半的数据怎么办?可以整一个环形的存储结构,这样先被截取了一半的数据,在之后截取另一半的时候,依然能够很好地衔接。
当然,结构越复杂,就需要越高的技术水平。我此次就不继续优化了,毕竟处理成为这些数据只是一个开始,后续还要继续处理……
写在最后
这次的写程序给我的启示是什么?
我认为就是:掌握的程序设计语言只是基础的实现思想的手段,真正想要做好程序设计,还是要到前辈的思想中寻找答案。
比如,缓存思想,这次就帮了我大忙。