C 簡單處理excel 轉成 json,exceljson
引言
工作中常需要處理excel轉json問題. 希望這篇博文能簡單描述這個問題.並提供一種解決
思路.提升感悟.
今天我們處理的事就是為了把 xlsm => json. 一種方式是. 去 google 在 stackover上搜
c readxlsm/readxls 庫. 也可以解決. 但是跨平台需要配置. 這裡介紹一種有意思的方式. 來處理read.
首先我們的目標文件是
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110112964.png)
生成的最後內容希望是
前言
這裡會扯一點C能夠處理xlsm, 但是這些高級文件格式還是上層語言處理的爽一點. C去處理真的是屠龍刀殺泥鳅.
心累.但是本文一定會處理的哪怕再復雜. 就是要倚天劍烤雞翅.
首先來點開胃小菜. C 如何生成一個 下面的demo.xlsm
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.png)
處理代碼 demo.c
#include <stdio.h>
/* 生成一個 xlsm 文件
*
* demo.xlsm
* hello world
* begin end
* 加油 新的開始
*/
int main(int argc, char* argv[]) {
FILE* xlsm = fopen("demo.xlsm", "wb");
if(xlsm == NULL) {
fprintf(stderr, "fopen demo.xlsm is error\n");
return -1;
}
// 處理輸出
fprintf(xlsm,"hello\tworld\n");
fprintf(xlsm,"begin\tend\n");
fprintf(xlsm,"\t加油\t新的開始\n");
fclose(xlsm);
puts("demo.xlsm 生成成功!");
return 0;
}
是不是很有意思. 其實生成的本質, 我們用 Notepad++ 打開 demo.xlsm
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113066.png)
其實是我們使用的 execel處理工具為我們解析成了excel格式. 也是一種方式吧. 歡迎嘗試. 這裡扯一點.
我一直使用WPS, 也很喜歡WPS. 但是覺得WPS適合個人閱讀. 對於工作處理特別是VBA 感覺不好. 關於工作
中 excel => json都是內部VBA 代碼在 Ctrl + S的時候執行的. 最近想優化一下公司的 vba代碼,但是自己用的
是wps 有心無力. (wps 處理文件還是有點慢, 宏和vba專業功能支持的蛋疼.)
推薦逼格高, 還是用微軟的 office最新版專業(旗艦)破解版吧. 畢竟Microsoft 是通過 office 騰飛的. 值得用破解版.
下面我們要到正題了.
本文需要用的輔助文件. 後面源碼中處理的函數調用實現都在下面文件找找到定義.
schead.h
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#ifndef _H_SCHEAD
#define _H_SCHEAD
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <stddef.h>
/*
* 1.0 錯誤定義宏 用於判斷返回值狀態的狀態碼 _RF表示返回標志
* 使用舉例 :
int flag = scconf_get("pursue");
if(flag != _RT_OK){
sclog_error("get config %s error! flag = %d.", "pursue", flag);
exit(EXIT_FAILURE);
}
* 這裡是內部 使用的通用返回值 標志
*/
#define _RT_OK (0) //結果正確的返回宏
#define _RT_EB (-1) //錯誤基類型,所有錯誤都可用它,在不清楚的情況下
#define _RT_EP (-2) //參數錯誤
#define _RT_EM (-3) //內存分配錯誤
#define _RT_EC (-4) //文件已經讀取完畢或表示鏈接關閉
#define _RT_EF (-5) //文件打開失敗
/*
* 1.1 定義一些 通用的函數指針幫助,主要用於基庫的封裝中
* 有構造函數, 釋放函數, 比較函數等
*/
typedef void* (*pnew_f)();
typedef void (*vdel_f)(void* node);
// icmp_f 最好 是 int cmp(const void* ln,const void* rn); 標准結構
typedef int (*icmp_f)();
/*
* c 如果是空白字符返回 true, 否則返回false
* c : 必須是 int 值,最好是 char 范圍
*/
#define sh_isspace(c) \
((c==' ')||(c>='\t'&&c<='\r'))
/*
* 2.0 如果定義了 __GNUC__ 就假定是 使用gcc 編譯器,為Linux平台
* 否則 認為是 Window 平台,不可否認宏是丑陋的
*/
#if defined(__GNUC__)
//下面是依賴 Linux 實現,等待毫秒數
#include <unistd.h>
#include <sys/time.h>
#define SLEEPMS(m) \
usleep(m * 1000)
#else
// 這裡創建等待函數 以毫秒為單位 , 需要依賴操作系統實現
#include <Windows.h>
#include <direct.h> // 加載多余的頭文件在 編譯階段會去掉
#define rmdir _rmdir
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
extern int gettimeofday(struct timeval* tv, void* tz);
//為了解決 不通用功能
#define localtime_r(t, tm) localtime_s(tm, t)
#define SLEEPMS(m) \
Sleep(m)
#endif /*__GNUC__ 跨平台的代碼都很丑陋 */
//3.0 浮點數據判斷宏幫助, __開頭表示不希望你使用的宏
#define __DIFF(x, y) ((x)-(y)) //兩個表達式做差宏
#define __IF_X(x, z) ((x)<z&&(x)>-z) //判斷宏,z必須是宏常量
#define EQ(x, y, c) EQ_ZERO(__DIFF(x,y), c) //判斷x和y是否在誤差范圍內相等
//3.1 float判斷定義的宏
#define _FLOAT_ZERO (0.000001f) //float 0的誤差判斷值
#define EQ_FLOAT_ZERO(x) __IF_X(x,_FLOAT_ZERO) //float 判斷x是否為零是返回true
#define EQ_FLOAT(x, y) EQ(x, y, _FLOAT_ZERO) //判斷表達式x與y是否相等
//3.2 double判斷定義的宏
#define _DOUBLE_ZERO (0.000000000001) //double 0誤差判斷值
#define EQ_DOUBLE_ZERO(x) __IF_X(x,_DOUBLE_ZERO) //double 判斷x是否為零是返回true
#define EQ_DOUBLE(x,y) EQ(x, y, _DOUBLE_ZERO) //判斷表達式x與y是否相等
//4.0 控制台打印錯誤信息, fmt必須是雙引號括起來的宏
#ifndef CERR
#define CERR(fmt, ...) \
fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\
__FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__)
#endif/* !CERR */
//4.1 控制台打印錯誤信息並退出, t同樣fmt必須是 ""括起來的字符串常量
#ifndef CERR_EXIT
#define CERR_EXIT(fmt,...) \
CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE)
#endif/* !ERR */
#ifndef IF_CERR
/*
*4.2 if 的 代碼檢測
*
* 舉例:
* IF_CERR(fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), "socket create error!");
* 遇到問題打印日志直接退出,可以認為是一種簡單模板
* code : 要檢測的代碼
* fmt : 必須是""括起來的字符串宏
* ... : 後面的參數,參照printf
*/
#define IF_CERR(code, fmt, ...) \
if((code) < 0) \
CERR_EXIT(fmt, ##__VA_ARGS__)
#endif //!IF_CERR
#ifndef IF_CHECK
/*
* 是上面IF_CERR 的簡化版很好用
*/
#define IF_CHECK(code) \
if((code) < 0) \
CERR_EXIT(#code)
#endif // !IF_CHECK
//5.0 獲取數組長度,只能是數組類型或""字符串常量,後者包含'\0'
#ifndef LEN
#define LEN(arr) \
(sizeof(arr)/sizeof(*(arr)))
#endif/* !ARRLEN */
//6.0 程序清空屏幕函數
#ifndef CONSOLE_CLEAR
#ifndef _WIN32
#define CONSOLE_CLEAR() \
system("printf '\ec'")
#else
#define CONSOLE_CLEAR() \
system("cls")
#endif/* _WIN32 */
#endif /*!CONSOLE_CLEAR*/
//7.0 置空操作
#ifndef BZERO
//v必須是個變量
#define BZERO(v) \
memset(&v,0,sizeof(v))
#endif/* !BZERO */
//9.0 scanf 健壯的
#ifndef SAFETY_SCANF
#define SAFETY_SCANF(scanf_code,...) \
while(printf(__VA_ARGS__),scanf_code){\
while(getchar()!='\n');\
puts("輸入出錯,請按照提示重新操作!");\
}\
while(getchar()!='\n')
#endif /*!SAFETY_SCANF*/
//10.0 簡單的time幫助宏
#ifndef TIME_PRINT
#define TIME_PRINT(code) {\
clock_t __st,__et;\
__st=clock();\
code\
__et=clock();\
printf("當前代碼塊運行時間是:%lf秒\n",(0.0+__et-__st)/CLOCKS_PER_SEC);\
}
#endif /*!TIME_PRINT*/
/*
* 10.1 這裡是一個 在 DEBUG 模式下的測試宏
*
* 用法 :
* DEBUG_CODE({
* puts("debug start...");
* });
*/
#ifndef DEBUG_CODE
# ifdef _DEBUG
# define DEBUG_CODE(code) code
# else
# define DEBUG_CODE(code)
# endif // ! _DEBUG
#endif // !DEBUG_CODE
//11.0 等待的宏 是個單線程沒有加鎖
#define _STR_PAUSEMSG "請按任意鍵繼續. . ."
extern void sh_pause(void);
#ifndef INIT_PAUSE
# ifdef _DEBUG
# define INIT_PAUSE() atexit(sh_pause)
# else
# define INIT_PAUSE() (void)316 /* 別說了,都重新開始吧 */
# endif
#endif/* !INIT_PAUSE */
//12.0 判斷是大端序還是小端序,大端序返回true
extern bool sh_isbig(void);
/**
* sh_free - 簡單的釋放內存函數,對free再封裝了一下
**可以避免野指針
**pobj:指向待釋放內存的指針(void*)
**/
extern void sh_free(void** pobj);
/**
* 獲取 當前時間串,並塞入tstr中長度並返回
** 使用舉例
char tstr[64];
sh_times(tstr, LEN(tstr));
puts(tstr);
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
extern int sh_times(char tstr[], int len);
/*
* 比較兩個結構體棧上內容是否相等,相等返回true,不等返回false
* a : 第一個結構體值
* b : 第二個結構體值
* : 相等返回true, 否則false
*/
#define STRUCTCMP(a, b) \
(!memcmp(&a, &b, sizeof(a)))
#endif/* ! _H_SCHEAD */
View Code
schead.c
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#include <schead.h>
//簡單通用的等待函數
void
sh_pause(void)
{
rewind(stdin);
printf(_STR_PAUSEMSG);
getchar();
}
//12.0 判斷是大端序還是小端序,大端序返回true
bool
sh_isbig(void)
{
static union {
unsigned short _s;
unsigned char _c;
} __u = { 1 };
return __u._c == 0;
}
/**
* sh_free - 簡單的釋放內存函數,對free再封裝了一下
**可以避免野指針
**@pobj:指向待釋放內存的指針(void*)
**/
void
sh_free(void** pobj)
{
if (pobj == NULL || *pobj == NULL)
return;
free(*pobj);
*pobj = NULL;
}
#if defined(_MSC_VER)
/**
* Linux sys/time.h 中獲取時間函數在Windows上一種移植實現
**tv : 返回結果包含秒數和微秒數
**tz : 包含的時區,在window上這個變量沒有用不返回
** : 默認返回0
**/
int
gettimeofday(struct timeval* tv, void* tz)
{
time_t clock;
struct tm tm;
SYSTEMTIME wtm;
GetLocalTime(&wtm);
tm.tm_year = wtm.wYear - 1900;
tm.tm_mon = wtm.wMonth - 1; //window的計數更好寫
tm.tm_mday = wtm.wDay;
tm.tm_hour = wtm.wHour;
tm.tm_min = wtm.wMinute;
tm.tm_sec = wtm.wSecond;
tm.tm_isdst = -1; //不考慮夏令時
clock = mktime(&tm);
tv->tv_sec = (long)clock; //32位使用,接口已經老了
tv->tv_usec = wtm.wMilliseconds * 1000;
return _RT_OK;
}
#endif
/**
* 獲取 當前時間串,並塞入tstr中C長度並返回
** 使用舉例
char tstr[64];
puts(gettimes(tstr, LEN(tstr)));
**tstr : 保存最後生成的最後串
**len : tstr數組的長度
** : 返回tstr首地址
**/
int
sh_times(char tstr[], int len)
{
struct tm st;
time_t t = time(NULL);
localtime_r(&t, &st);
return (int)strftime(tstr, len, "%F %X", &st);
}
View Code
sclog.h
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#ifndef _H_SCLOG
#define _H_SCLOG
//-------------------------------------------------------------------------------------------|
// 第一部分 共用的參數宏
//-------------------------------------------------------------------------------------------|
//
//關於日志切分,需要用第三方插件例如crontab , 或者下次我自己寫一個監測程序.
#define _INT_LITTLE (64) //保存時間或IP長度
#define _INT_LOG (1024<<3) //最多8k日志
#define _STR_SCLOG_PATH "log" //日志相對路徑目錄,如果不需要需要配置成""
#define _STR_SCLOG_LOG "sc.log" //普通log日志 DEBUG,INFO,NOTICE,WARNING,FATAL都會輸出
#define _STR_SCLOG_WFLOG "sc.log.wf" //級別比較高的日志輸出 FATAL和WARNING
/**
* fstr : 為標識串 例如 _STR_SCLOG_FATAL, 必須是雙引號括起來的串
**
** 拼接一個 printf 輸出格式串
**/
#define SCLOG_PUTS(fstr) \
"%s][" fstr "][%s:%d:%s][logid:%u][reqip:%s][mod:%s]"
#define _STR_SCLOG_FATAL "FATAL" //錯誤,後端使用
#define _STR_SCLOG_WARNING "WARNING" //警告,前端使用錯誤,用這個
#define _STR_SCLOG_NOTICE "NOTICE" //系統使用,一般標記一條請求完成,使用這個日志
#define _STR_SCLOG_INFO "INFO" //普通的日志打印
#define _STR_SCLOG_TRACE "TRACE"
#define _STR_SCLOG_DEBUG "DEBUG" //測試用的日志打印,在發布版這些日志會被清除掉
/**
* fstr : 只能是 _STR_SCLOG_* 開頭的宏
** fmt : 必須是""括起來的宏.單獨輸出的格式宏
** ... : 對映fmt參數集
**
** 拼接這裡使用的宏,為sl_printf 打造一個模板,這裡存在一個坑,在Window \n表示 CRLF, Unix就是LF
**/
#define SCLOG_PRINTF(fstr, fmt, ...) \
sl_printf(SCLOG_PUTS(fstr) fmt "\n", sl_get_times(), __FILE__, __LINE__, __func__, \
sl_get_logid(), sl_get_reqip(), sl_get_mod(), ##__VA_ARGS__)
/**
* FATAL... 日志打印宏
** fmt : 輸出的格式串,需要""包裹起來
** ... : 後面的參數,服務於fmt
**/
#define SL_FATAL(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_FATAL, fmt, ##__VA_ARGS__)
#define SL_WARNING(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_WARNING, fmt, ##__VA_ARGS__)
#define SL_NOTICE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_NOTICE, fmt, ##__VA_ARGS__)
#define SL_INFO(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_INFO, fmt, ##__VA_ARGS__)
// 發布狀態下,關閉SL_DEBUG 宏,需要重新編譯,沒有改成運行時的判斷,這個框架主要圍繞單機部分多服務器
#if defined(_DEBUG)
# define SL_TRACE(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_TRACE, fmt, ##__VA_ARGS__)
# define SL_DEBUG(fmt, ...) SCLOG_PRINTF(_STR_SCLOG_DEBUG, fmt, ##__VA_ARGS__)
#else
# define SL_TRACE(fmt, ...) (void)0x123 /* 人生難道就是123*/
# define SL_DEBUG(fmt, ...) (void)0xa91 /* 愛過哎 */
#endif
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
extern int sl_pecific_init(const char* mod, const char* reqip);
/**
* 重新設置線程計時時間
** 正常返回 _RT_OK, _RT_EM表示內存沒有分配
**/
int sl_set_timev(void);
/**
* 獲取日志信息體的唯一的logid
**/
unsigned sl_get_logid(void);
/**
* 獲取日志信息體的請求ip串,返回NULL表示沒有初始化
**/
const char* sl_get_reqip(void);
/**
* 獲取日志信息體的時間串,返回NULL表示沒有初始化
**/
const char* sl_get_times(void);
/**
* 獲取日志信息體的名稱,返回NULL表示沒有初始化
**/
const char* sl_get_mod(void);
//-------------------------------------------------------------------------------------------|
// 第三部分 對日志系統具體的輸出輸入接口部分
//-------------------------------------------------------------------------------------------|
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**/
extern void sl_start(void);
/**
* 這個函數不希望你使用,是一個內部限定死的日志輸出內容.推薦使用相應的宏
**打印相應級別的日志到對映的文件中.
**
** format : 必須是""號括起來的宏,開頭必須是 [FALTAL:%s]後端錯誤
** [WARNING:%s]前端錯誤, [NOTICE:%s]系統使用, [INFO:%s]普通信息,
** [DEBUG:%s] 開發測試用
**
** return : 返回輸出內容長度
**/
int sl_printf(const char* format, ...);
#endif // !_H_SCLOG
View Code
sclog.c
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#include <sclog.h>
#include <schead.h>
#include <scatom.h>
#include <pthread.h>
#include <stdarg.h>
//-------------------------------------------------------------------------------------------|
// 第二部分 對日志信息體操作的get和set,這裡隱藏了信息體的實現
//-------------------------------------------------------------------------------------------|
//線程私有數據 __lkey, __lonce為了__lkey能夠正常初始化
static pthread_key_t __lkey;
static pthread_once_t __lonce = PTHREAD_ONCE_INIT;
static unsigned __logid = 0; //默認的全局logid,唯一標識
//內部簡單的釋放函數,服務於pthread_key_create 防止線程資源洩露
static void __slinfo_destroy(void* slinfo)
{
//printf("pthread 0x%p:0x%p destroy!\n", pthread_self().p, slinfo);
free(slinfo);
}
static void __gkey(void)
{
pthread_key_create(&__lkey, __slinfo_destroy);
}
struct slinfo {
unsigned logid; //請求的logid,唯一id
char reqip[_INT_LITTLE]; //請求方ip
char times[_INT_LITTLE]; //當前時間串
struct timeval timev; //處理時間,保存值,統一用毫秒
char mod[_INT_LITTLE]; //當前線程的模塊名稱,不能超過_INT_LITTLE - 1
};
/**
* 線程的私有數據初始化
**
** mod : 當前線程名稱
** reqip : 請求的ip
** return : _RT_OK 表示正常,_RF_EM內存分配錯誤
**/
int
sl_pecific_init(const char* mod, const char* reqip)
{
struct slinfo* pl;
//保證 __gkey只被執行一次
pthread_once(&__lonce, __gkey);
if((pl = pthread_getspecific(__lkey)) == NULL){
//重新構建
if ((pl = malloc(sizeof(struct slinfo))) == NULL)
return _RT_EM;
//printf("pthread 0x%p:0x%p create!\n", pthread_self().p,pl);
}
gettimeofday(&pl->timev, NULL);
pl->logid = ATOM_ADD_FETCH(__logid, 1); //原子自增
strcpy(pl->mod, mod); //復制一些數據
strcpy(pl->reqip, reqip);
//設置私有變量
pthread_setspecific(__lkey, pl);
return _RT_OK;
}
/**
* 重新設置線程計時時間
** 正常返回 _RT_OK, _RT_EM表示內存沒有分配
**/
int
sl_set_timev(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl)
return _RT_EM;
gettimeofday(&pl->timev, NULL);
return _RT_OK;
}
/**
* 獲取日志信息體的唯一的logid
**/
unsigned
sl_get_logid(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回0表示沒有找見
return 0u;
return pl->logid;
}
/**
* 獲取日志信息體的請求ip串,返回NULL表示沒有初始化
**/
const char*
sl_get_reqip(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
return pl->reqip;
}
/**
* 獲取日志信息體的時間串,返回NULL表示沒有初始化
**/
const char*
sl_get_times(void)
{
struct timeval et; //記錄時間
unsigned td;
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
gettimeofday(&et, NULL);
//同一用微秒記
td = 1000000 * (et.tv_sec - pl->timev.tv_sec) + et.tv_usec - pl->timev.tv_usec;
snprintf(pl->times, LEN(pl->times), "%u", td);
return pl->times;
}
/**
* 獲取日志信息體的名稱,返回NULL表示沒有初始化
**/
const char*
sl_get_mod(void)
{
struct slinfo* pl = pthread_getspecific(__lkey);
if (NULL == pl) //返回NULL表示沒有找見
return NULL;
return pl->mod;
}
//-------------------------------------------------------------------------------------------|
// 第三部分 對日志系統具體的輸出輸入接口部分
//-------------------------------------------------------------------------------------------|
//錯誤重定向宏 具體應用 於 "mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR
#define _STR_TOOUT "__out__"
#define _STR_TOERR "__err__"
#define _STR_LOGID "__lid__" //保存logid,持久化
static struct { //內部用的私有變量
FILE* log;
FILE* wf;
bool isdir; //標志是否創建了目錄
} __slmain;
/**
* 日志關閉時候執行,這個接口,關閉打開的文件句柄
**/
static void __sl_end(void)
{
FILE* lid;
void* pl;
// 在簡單地方多做安全操作值得,在核心地方用算法優化的才能穩固
if (!__slmain.isdir)
return;
//重置當前系統打開文件結構體
fclose(__slmain.log);
fclose(__slmain.wf);
BZERO(__slmain);
//寫入文件
lid = fopen(_STR_LOGID, "w");
if (NULL != lid) {
fprintf(lid, "%u", __logid);
fclose(lid);
}
//主動釋放私有變量,其實主進程 相當於一個線程是不合理的!還是不同的生存周期的
pl = pthread_getspecific(__lkey);
__slinfo_destroy(pl);
pthread_setspecific(__lkey, NULL);
}
/**
* 日志系統首次使用初始化,找對對映日志文件路徑,創建指定路徑
**/
void
sl_start(void)
{
FILE *lid;
//單例只執行一次
if (!__slmain.isdir) {
__slmain.isdir = true;
//先多級創建目錄,簡易不借助宏實現跨平台,system返回值是很復雜,默認成功!
system("mkdir -p \"" _STR_SCLOG_PATH "\" >" _STR_TOOUT " 2>" _STR_TOERR);
rmdir("-p");
remove(_STR_TOOUT);
remove(_STR_TOERR);
}
if (NULL == __slmain.log) {
__slmain.log = fopen(_STR_SCLOG_PATH "/" _STR_SCLOG_LOG, "a+");
if (NULL == __slmain.log)
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_LOG);
}
//繼續打開 wf 文件
if (NULL == __slmain.wf) {
__slmain.wf = fopen(_STR_SCLOG_PATH "/" _STR_SCLOG_WFLOG, "a+");
if (NULL == __slmain.wf) {
fclose(__slmain.log); //其實這都沒有必要,圖個心安
CERR_EXIT("__slmain.log fopen %s error!", _STR_SCLOG_WFLOG);
}
}
//讀取文件內容
if ((lid = fopen(_STR_LOGID, "r")) != NULL) { //讀取文件內容,持久化
fscanf(lid, "%u", &__logid);
}
//這裡可以單獨開啟一個線程或進程,處理日志整理但是 這個模塊可以讓運維做,按照規則搞
sl_pecific_init("main thread","0.0.0.0");
//注冊退出操作
atexit(__sl_end);
}
int
sl_printf(const char* format, ...)
{
char tstr[_INT_LITTLE];// [%s] => [2016-01-08 23:59:59]
int len;
va_list ap;
char logs[_INT_LOG]; //這個不是一個好的設計,最新c 中支持 int a[n];
if (!__slmain.isdir) {
CERR("%s fopen %s | %s error!",_STR_SCLOG_PATH, _STR_SCLOG_LOG, _STR_SCLOG_WFLOG);
return _RT_EF;
}
//初始化參數
sh_times(tstr, _INT_LITTLE - 1);
len = snprintf(logs, LEN(logs), "[%s ", tstr);
va_start(ap, format);
vsnprintf(logs + len, LEN(logs) - len, format, ap);
va_end(ap);
// 寫普通文件 log
fputs(logs, __slmain.log); //把鎖機制去掉了,fputs就是線程安全的
// 寫警告文件 wf
if (format[4] == 'F' || format[4] == 'W') { //當為FATAL或WARNING需要些寫入到警告文件中
fputs(logs, __slmain.wf);
}
return _RT_OK;
}
View Code
sccsv.h
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#ifndef _H_SCCSV
#define _H_SCCSV
/*
* 這裡是一個解析 csv 文件的 簡單解析器.
* 它能夠幫助我們切分文件內容,保存在數組中.
*/
struct sccsv { //內存只能在堆上
int rlen; //數據行數,索引[0, rlen)
int clen; //數據列數,索引[0, clen)
const char* data[]; //保存數據一維數組,希望他是二維的 rlen*clen
};
typedef struct sccsv* sccsv_t;
/*
* 從文件中構建csv對象, 最後需要調用 sccsv_die 釋放
* path : csv文件內容
* : 返回構建好的 sccsv_t 對象
*/
extern sccsv_t sccsv_new(const char* path);
/*
* 釋放由sccsv_new構建的對象
* pcsv : 由sccsv_new 返回對象
*/
extern void sccsv_die(sccsv_t* pcsv);
/*
* 獲取某個位置的對象內容,這個函數 推薦聲明為內聯的, window上不支持
* csv : sccsv_t 對象, new返回的
* ri : 查找的行索引 [0, csv->rlen)
* ci : 查找的列索引 [0, csv->clen)
* : 返回這一項中內容,後面可以用 atoi, atof, str_dup 等處理了...
*/
extern inline const char* sccsv_get(sccsv_t csv, int ri, int ci);
#endif // !_H_SCCSV
View Code
sccsv.c
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#include <schead.h>
#include <sccsv.h>
#include <sclog.h>
#include <tstring.h>
//從文件中讀取 csv文件內容
char* __get_csv(FILE* txt, int* prl, int* pcl)
{
int c, n;
int cl = 0, rl = 0;
TSTRING_CREATE(ts);
while((c=fgetc(txt))!=EOF){
if('"' == c){ //處理這裡數據
while((c=fgetc(txt))!=EOF){
if('"' == c) {
if((n=fgetc(txt)) == EOF) { //判斷下一個字符
SL_WARNING("The CSV file is invalid one!");
free(ts.str);
return NULL;
}
if(n != '"'){ //有效字符再次壓入棧
ungetc(n, txt);
break;
}
}
//都是合法字符 保存起來
if (_RT_OK != tstring_append(&ts, c)) {
free(ts.str);
return NULL;
}
}
//繼續判斷,只有是c == '"' 才會下來,否則都是錯的
if('"' != c){
SL_WARNING("The CSV file is invalid two!");
free(ts.str);
return NULL;
}
}
else if(',' == c){
if (_RT_OK != tstring_append(&ts, '\0')) {
free(ts.str);
return NULL;
}
++cl;
}
else if('\r' == c)
continue;
else if('\n' == c){
if (_RT_OK != tstring_append(&ts, '\0')) {
free(ts.str);
return NULL;
}
++cl;
++rl;
}
else {//其它所有情況只添加數據就可以了
if (_RT_OK != tstring_append(&ts, c)) {
free(ts.str);
return NULL;
}
}
}
if(cl % rl){ //檢測 , 號是個數是否正常
SL_WARNING("now csv file is illegal! need check!");
return NULL;
}
// 返回最終內容
*prl = rl;
*pcl = cl;
return ts.str;
}
// 將 __get_csv 得到的數據重新構建返回, 執行這個函數認為語法檢測都正確了
sccsv_t __get_csv_new(const char* cstr, int rl, int cl)
{
int i = 0;
sccsv_t csv = malloc(sizeof(struct sccsv) + sizeof(char*)*cl);
if(NULL == csv){
SL_FATAL("malloc is error one !");
return NULL;
}
// 這裡開始構建內容了
csv->rlen = rl;
csv->clen = cl / rl;
do {
csv->data[i] = cstr;
while(*cstr++) //找到下一個位置處
;
}while(++i<cl);
return csv;
}
/*
* 從文件中構建csv對象, 最後需要調用 sccsv_die 釋放
* path : csv文件內容
* : 返回構建好的 sccsv_t 對象
*/
sccsv_t
sccsv_new(const char* path)
{
FILE* txt;
char* cstr;
int rl, cl;
DEBUG_CODE({
if(!path || !*path){
SL_WARNING("params is check !path || !*path .");
return NULL;
}
});
// 打開文件內容
if((txt=fopen(path, "r")) == NULL){
SL_WARNING("fopen %s r is error!", path);
return NULL;
}
// 如果解析 csv 文件內容失敗直接返回
cstr = __get_csv(txt, &rl, &cl);
fclose(txt);
// 返回最終結果
return cstr ? __get_csv_new(cstr, rl, cl) : NULL;
}
/*
* 釋放由sccsv_new構建的對象
* pcsv : 由sccsv_new 返回對象
*/
void
sccsv_die(sccsv_t* pcsv)
{
if (pcsv && *pcsv) { // 這裡 開始釋放
free(*pcsv);
*pcsv = NULL;
}
}
/*
* 獲取某個位置的對象內容
* csv : sccsv_t 對象, new返回的
* ri : 查找的行索引 [0, csv->rlen)
* ci : 查找的列索引 [0, csv->clen)
* : 返回這一項中內容,後面可以用 atoi, atof, str_dup 等處理了...
*/
inline const char*
sccsv_get(sccsv_t csv, int ri, int ci)
{
DEBUG_CODE({
if(!csv || ri<0 || ri>=csv->rlen || ci<0 || ci >= csv->clen){
SL_WARNING("params is csv:%p, ri:%d, ci:%d.", csv, ri, ci);
return NULL;
}
});
// 返回最終結果
return csv->data[ri*csv->clen + ci];
}
View Code
tstring.h
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#ifndef _H_TSTRING
#define _H_TSTRING
#include <schead.h>
//------------------------------------------------簡單字符串輔助操作----------------------------------
/*
* 主要采用jshash 返回計算後的hash值
* 不沖突率在 80% 左右還可以, 不要傳入NULL
*/
extern unsigned str_hash(const char* str);
/*
* 這是個不區分大小寫的比較函數
* ls : 左邊比較字符串
* rs : 右邊比較字符串
* : 返回 ls>rs => >0 ; ls = rs => 0 ; ls<rs => <0
*/
extern int str_icmp(const char* ls, const char* rs);
/*
* 這個代碼是 對 strdup 的再實現, 調用之後需要free
* str : 待復制的源碼內容
* : 返回 復制後的串內容
*/
extern char* str_dup(const char* str);
//------------------------------------------------簡單文本字符串輔助操作----------------------------------
#ifndef _STRUCT_TSTRING
#define _STRUCT_TSTRING
//簡單字符串結構,並定義文本字符串類型tstring
struct tstring {
char* str; //字符串實際保存的內容
int len; //當前字符串大小
int size; //字符池大小
};
typedef struct tstring* tstring;
#endif // !_STRUCT_TSTRING
//文本串棧上創建內容,不想用那些技巧了,就這樣吧
#define TSTRING_CREATE(var) \
struct tstring var = { NULL, 0, 0}
#define TSTRING_DESTROY(var) \
free(var.str)
/*
* tstring 的創建函數, 會根據str創建一個 tstring結構的字符串
*
* str : 待創建的字符串
*
* ret : 返回創建好的字符串,如果創建失敗返回NULL
*/
extern tstring tstring_create(const char* str);
/*
* tstring 完全銷毀函數
* tstr : 指向tsting字符串指針量的指針
*/
extern void tstring_destroy(tstring* tstr);
/*
* 向簡單文本字符串tstr中添加 一個字符c
* tstr : 簡單字符串對象
* c : 待添加的字符
* ret : 返回狀態碼 見 schead 中 _RT_EB 碼等
*/
extern int tstring_append(tstring tstr, int c);
/*
* 向簡單文本串中添加只讀字符串
* tstr : 文本串
* str : 待添加的素材串
* ret : 返回狀態碼主要是 _RT_EP _RT_EM
*/
extern int tstring_appends(tstring tstr, const char* str);
/*
* 復制tstr中內容,得到char* 返回,需要自己free釋放
*假如你要清空tstring 字符串只需要 設置 len = 0.就可以了
* tstr : 待分配的字符串
* : 返回分配好的字符串首地址
*/
extern char* tstring_mallocstr(tstring tstr);
//------------------------------------------------簡單文件輔助操作----------------------------------
/*
* 簡單的文件幫助類,會讀取完畢這個文件內容返回,失敗返回NULL.
* 需要事後使用 tstring_destroy(&ret); 銷毀這個字符串對象
* path : 文件路徑
* ret : 返回創建好的字符串內容,返回NULL表示讀取失敗
*/
extern tstring file_malloc_readend(const char* path);
/*
* 文件寫入,沒有好說的,會返回 _RT_EP _RT_EM _RT_OK
* path : 文件路徑
* str : 待寫入的字符串
* ret : 返回寫入的結果
*/
extern int file_writes(const char* path, const char* str);
/*
* 文件追加內容, 添加str內同
* path : 文件路徑
* str : 待追加的文件內同
* : 返回值,主要是 _RT_EP _RT_EM _RT_OK 這些狀態
*/
extern int file_append(const char* path, const char* str);
#endif // !_H_TSTRING
View Code
tstring.c
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113097.gif)
![]()
#include <tstring.h>
#include <sclog.h>
/*
* 主要采用jshash 返回計算後的hash值
* 不沖突率在 80% 左右還可以, 不要傳入NULL
*/
unsigned
str_hash(const char* str)
{
unsigned i, h = (unsigned)strlen(str), sp = (h >> 5) + 1;
unsigned char* ptr = (unsigned char*)str;
for (i = h; i >= sp; i -= sp)
h ^= ((h<<5) + (h>>2) + ptr[i-1]);
return h ? h : 1;
}
/*
* 這是個不區分大小寫的比較函數
* ls : 左邊比較字符串
* rs : 右邊比較字符串
* : 返回 ls>rs => >0 ; ls = rs => 0 ; ls<rs => <0
*/
int
str_icmp(const char* ls, const char* rs)
{
int l, r;
if(!ls || !rs)
return (int)(ls - rs);
do {
if((l=*ls++)>='a' && l<='z')
l -= 'a' - 'A';
if((r=*rs++)>='a' && r<='z')
r -= 'a' - 'A';
} while(l && l==r);
return l-r;
}
/*
* 這個代碼是 對 strdup 的再實現, 調用之後需要free
* str : 待復制的源碼內容
* : 返回 復制後的串內容
*/
char*
str_dup(const char* str)
{
size_t len;
char* nstr;
DEBUG_CODE({
if (NULL == str) {
SL_WARNING("check is NULL == str!!");
return NULL;
}
});
len = sizeof(char) * (strlen(str) + 1);
if (!(nstr = malloc(len))) {
SL_FATAL("malloc is error! len = %d.", len);
return NULL;
}
// 返回最後結果
return memcpy(nstr, str, len);
}
//------------------------------------------------簡單文本字符串輔助操作----------------------------------
/*
* tstring 的創建函數, 會根據str創建一個 tstring結構的字符串
*
* str : 待創建的字符串
*
* ret : 返回創建好的字符串,如果創建失敗返回NULL
*/
tstring
tstring_create(const char* str)
{
tstring tstr = calloc(1, sizeof(struct tstring));
if (NULL == tstr) {
SL_NOTICE("calloc is sizeof struct tstring error!");
return NULL;
}
tstring_appends(tstr, str);
return tstr;
}
/*
* tstring 完全銷毀函數
* tstr : 指向tsting字符串指針量的指針
*/
void tstring_destroy(tstring* tstr)
{
if (tstr && *tstr) { //展現內容
free((*tstr)->str);
free(*tstr);
*tstr = NULL;
}
}
//文本字符串創建的度量值
#define _INT_TSTRING (32)
//簡單分配函數,智力一定會分配內存的, len > size的時候調用這個函數
static int __tstring_realloc(tstring tstr, int len)
{
int size = tstr->size;
for (size = size < _INT_TSTRING ? _INT_TSTRING : size; size < len; size <<= 1)
;
//分配內存
char *nstr = realloc(tstr->str, size);
if (NULL == nstr) {
SL_NOTICE("realloc(tstr->str:0x%p, size:%d) is error!", tstr->str, size);
return _RT_EM;
}
tstr->str = nstr;
tstr->size = size;
return _RT_OK;
}
/*
* 向簡單文本字符串tstr中添加 一個字符c
* tstr : 簡單字符串對象
* c : 待添加的字符
* ret : 返回狀態碼 見 schead 中 _RT_EM 碼等
*/
int tstring_append(tstring tstr, int c)
{
//不做安全檢查
int len = tstr->len + 2; // c + '\0' 而len只指向 字符串strlen長度
//需要進行內存分配,唯一損失
if ((len > tstr->size) && (_RT_EM == __tstring_realloc(tstr, len)))
return _RT_EM;
tstr->len = --len;
tstr->str[len - 1] = c;
tstr->str[len] = '\0';
return _RT_OK;
}
/*
* 向簡單文本串中添加只讀字符串
* tstr : 文本串
* str : 待添加的素材串
* ret : 返回狀態碼主要是 _RT_EP _RT_EM
*/
int tstring_appends(tstring tstr, const char* str)
{
int len;
if (!tstr || !str || !*str) {
SL_NOTICE("check param '!tstr || !str || !*str'");
return _RT_EP;
}
len = tstr->len + (int)strlen(str) + 1;
if ((len > tstr->size) && (_RT_EM == __tstring_realloc(tstr, len)))
return _RT_EM;
//這裡復制內容
strcpy(tstr->str + tstr->len, str);
tstr->len = len - 1;
return _RT_OK;
}
//------------------------------------------------簡單文件輔助操作----------------------------------
/*
* 簡單的文件幫助類,會讀取完畢這個文件內容返回,失敗返回NULL.
* 需要事後使用 tstring_destroy(&ret); 銷毀這個字符串對象
* path : 文件路徑
* ret : 返回創建好的字符串內容,返回NULL表示讀取失敗
*/
tstring file_malloc_readend(const char* path)
{
int c;
tstring tstr;
FILE* txt = fopen(path, "r");
if (NULL == txt) {
SL_NOTICE("fopen r path = '%s' error!", path);
return NULL;
}
//這裡創建文件對象,創建失敗直接返回
if ((tstr = tstring_create(NULL)) == NULL) {
fclose(txt);
return NULL;
}
//這裡讀取文本內容
while ((c = fgetc(txt))!=EOF)
if (_RT_OK != tstring_append(tstr, c)){ //出錯了就直接銷毀已經存在的內容
tstring_destroy(&tstr);
break;
}
fclose(txt);//很重要創建了就要釋放,否則會出現隱藏的句柄bug
return tstr;
}
/*
* 文件寫入,沒有好說的,會返回 _RT_EP _RT_EM _RT_OK
* path : 文件路徑
* str : 待寫入的字符串
* ret : 返回寫入的結果
*/
int file_writes(const char* path, const char* str)
{
FILE* txt;
//檢查參數問題
if (!path || !str) {
SL_NOTICE("check is '!path || !str'");
return _RT_EP;
}
if ((txt = fopen(path, "w")) == NULL) {
SL_NOTICE("fopen w path = '%s' error!", path);
return _RT_EF;
}
//這裡寫入信息
fputs(str, txt);
fclose(txt);
return _RT_OK;
}
/*
* 文件追加內容, 添加str內同
* path : 文件路徑
* str : 待追加的文件內同
* : 返回值,主要是 _RT_EP _RT_EM _RT_OK 這些狀態
*/
int
file_append(const char* path, const char* str)
{
FILE* txt;
//檢查參數問題
if (!path || !str) {
SL_NOTICE("check is '!path || !str'");
return _RT_EP;
}
if ((txt = fopen(path, "a")) == NULL) {
SL_NOTICE("fopen a path = '%s' error!", path);
return _RT_EF;
}
//這裡寫入信息
fputs(str, txt);
fclose(txt);
return _RT_OK;
}
/*
* 復制tstr中內容,得到char* 返回,需要自己free釋放
* tstr : 待分配的字符串
* : 返回分配好的字符串首地址
*/
char*
tstring_mallocstr(tstring tstr)
{
char* str;
if (!tstr || tstr->len <= 0) {
SL_NOTICE("params is check '!tstr || tstr->len <= 0' error!");
return NULL;
}
if ((str = malloc(tstr->len + 1)) == NULL){
SL_NOTICE("malloc %d+1 run is error!",tstr->len);
return NULL;
}
//下面就可以復制了,采用最快的一種方式
return memcpy(str, tstr->str, tstr->len + 1);
}
View Code
正文
那我們開始把將一個xlsm如何轉成工作中用的json文件. 再繼續重復扯一點, 最優方法是vba, 次優方法利用上層框架例如.net 通過npoi寫個批量
處理工具. 最難受的方法就是本文中采用c處理業務代碼. 好吧繼續. 首先 我們仍然使用 destiny.xlsm文件 . 另存為
destiny.csv
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113048.png)
最後的 保存後結果為
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113173.png)
我們查看內部詳細文本編碼協議
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113179.png)
最後得到這個文件. 我們處理方式是.
先讀取 這個csv 文件 通過sccsv.h 提供的接口. 拼接一個生成的json內容. 處理代碼
test_xlsmtojson.c
#include <schead.h>
#include <sclog.h>
#include <sccsv.h>
#include <tstring.h>
// 將csv轉換成json文件輸出, 成功返回0, 錯誤見狀態碼<0
int csvtojson(const char* path);
int main(int argc, char* argv[]) {
int i = 0, rt;
// 簡單參數檢查
if(argc < 2) {
CERR("uage: %s [*.csv]", argv[0]);
return 0;
}
// sccsv 使用了 sclog 需要啟動 sclog
sl_start();
// 開始解析處理
while(++i<argc){
rt = csvtojson(argv[i]);
if(0 == rt)
printf("%s 轉換成功\n", argv[i]);
else
printf("%s 轉換失敗, rt = %d\n", argv[i], rt);
}
return 0;
}
// 得到生成json文件的名稱, 需要自己free
static char* _csvtojsonpath(const char* path) {
char *tarp;
int len = strlen(path);
// 判斷後綴名
if(str_icmp(path+len-4, ".csv")){
CERR("path is %s need *.csv", path);
return NULL;
}
// 這裡申請內存進行處理
if((tarp = malloc(len+2))==NULL) {
CERR("malloc is error!");
return NULL;
}
// 返回最終結果
memcpy(tarp, path, len - 3);
// 添加json後綴名
tarp[len-3] = 'j';
tarp[len-2] = 's';
tarp[len-1] = 'o';
tarp[len] = 'n';
tarp[len + 1] = '\0';
return tarp;
}
// csv read -> json write
static void _csvtojson(sccsv_t csv, FILE* json) {
// 第一行, 第一列都是不處理的
int c, r;
// 第二行 內容是對象中主鍵內容
int clen = csv->clen - 1, rlen = csv->rlen - 1;
// 先確定最優行和列
while (rlen > 2) {
if (*sccsv_get(csv, rlen, 1))
break;
--rlen;
}
while (clen > 1) {
if (*sccsv_get(csv, 0, clen))
break;
--clen;
}
// 最外層是個數組
fputs("[\n", json);
for (r = 2; r <= rlen; ++r) {
// 當對象處理
fputs("\t{\n", json);
// 輸出當前對象中內容
for (c = 1; c <= clen; ++c) {
fprintf(json, "\t\t\"%s\":%s", sccsv_get(csv, 1, c),sccsv_get(csv, r, c));
fputs(c == clen ? "\n" : ",\n", json);
}
// 最新的json語法支持多個 ','
fputs(r == rlen ? "\t}\n" : "\t},\n", json);
}
fputs("]", json);
}
// 將csv轉換成json文件輸出, 成功返回0, 錯誤見狀態碼<0
int
csvtojson(const char* path) {
char* tarp;
FILE* json;
sccsv_t csv;
if(!path || !*path) {
CERR("path is null!");
return _RT_EP;
}
// 繼續判斷後綴名
if((tarp = _csvtojsonpath(path)) == NULL ) {
CERR("path = %s is error!", path);
return _RT_EP;
}
// 這裡開始打開文件, 並判斷
if((csv = sccsv_new(path)) == NULL) {
free(tarp);
CERR("sccsv_new %s is error!", path);
return _RT_EF;
}
if((json = fopen(tarp, "wb")) == NULL ) {
sccsv_die(&csv);
free(tarp);
CERR("fopen %s wb is error!", tarp);
return _RT_EF;
}
// 到這裡一切前戲都好了開始轉換了
_csvtojson(csv, json);
fclose(json);
sccsv_die(&csv);
free(tarp);
return _RT_OK;
}
其實還是比較短的. 分析如下
static char* _csvtojsonpath(const char* path)
功能是將 *csv 路徑字符串 編程 *.json路徑字符串,保存在堆上,需要後續 free.
static void _csvtojson(sccsv_t csv, FILE* json)
上面函數將解析好的 csv結構保存輸出到json中.
對於 sccsv.h 的具體使用可以參見
C 封裝一個csv 解析庫 http://www.cnblogs.com/life2refuel/p/5265167.html
對於開頭的兩個 while是為了確定最終范圍的. 拼接最終結果. 是不是很簡單. 從上面看出, 實際處理代碼其實很少,主要是
對參數檢測健壯性代碼占了很多. 其它都是業務轉換代碼. 核心還是解析csv文件結構和json個數的拼接.
運行結果如下
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113182.png)
最終生成的 destiny.json文件結構圖
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113203.png)
到這裡話. 基本的處理方式方法都已經介紹完畢了. 總結C處理業務功能事有點不合時宜了. 上層語言還是值得我們深究的.
有興趣的可以用上層語言寫一個工具來幫我們批量處理excel文件生成json.
再扯一點. json 文件相比xml 文件, 首先內存上小了很多. 處理速度快了. 但是json文件的性能浪費在
![](https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017012110113268.png)
重復的鍵值. 浪費空間.
再再擴展一下下. 怎麼處理優化呢. 一種空間換時間, 轉成內存文件避免動態解析生成. 就是承認浪費. 但是一次加載解析到內存中.
還有一種 將 對象編程數組去掉鍵值但是這樣可讀性很不好.
其它,希望有更好的協議出現. 或者誕生在博客園...
後記
錯誤是難免的, 希望這些對問題的分析和深究對你有幫助. 激發大家的思考. 有興趣的可以寫寫更好的工具. 交流分享.
拜~~.