写写记记_C++初试数据处理的收获01_强生出租车的数据处理_积累

上个学期获得了一个强生出租车的数据集,老师给的要求首先是要提取出每辆车的记录,并将每辆车的所有记录存放到以这辆车命名的文件里。

(担心文章很长,可爱的大家可能会只读一部分,如果最后觉得本文还可以的话请大家点点赞~您的鼓励是我更博的最大动力噢)

原始数据有车机号 业务状态 载客状态 顶灯状态 道路状态 刹车状态 接受日期 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(&timest); /*获取time_t类型的当前时间*/

	cout << "start:" << asctime(gmtime(&timest)) << '\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一样,先把处理好的数据都存在中间的缓存区域内,缓存到了一定程度之后一次性地将缓存好的多条数据存入文件中。这样能够大大减少打开+关闭文件的次数,从而能够大大提高程序的出结果的速度。

对第三种方法的反思

第三种方法,虽然比前两种方法快很多,但是依然不算很快,还是有很多能够继续优化的地方的。

比如,第三种方法的读入数据,依然采取的是按行读取,每次依然要将每一条数据从磁盘读入到内存中,这样也是很慢的。

其实可以采取一次读入许多数据,或者说,一次读入一定的字节数的数据,然后进行处理。

碰到截取了一半的数据怎么办?可以整一个环形的存储结构,这样先被截取了一半的数据,在之后截取另一半的时候,依然能够很好地衔接。

当然,结构越复杂,就需要越高的技术水平。我此次就不继续优化了,毕竟处理成为这些数据只是一个开始,后续还要继续处理……

写在最后

这次的写程序给我的启示是什么?
我认为就是:掌握的程序设计语言只是基础的实现思想的手段,真正想要做好程序设计,还是要到前辈的思想中寻找答案。
比如,缓存思想,这次就帮了我大忙。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值