最近尝试用C语言写了一个简单的英汉词典程序。这个小工具可以将文本格式的词典导入SQLite数据库,并支持用户交互查询。今天本文将深入分析这份代码的设计思路,并分享一些可以进一步优化的地方。
程序功能概览
程序主要完成两项任务:
-
自动建库 :如果当前目录下不存在
dict.db数据库文件,程序会自动读取dict.txt文本文件,将单词和释义存入SQLite数据库。 -
交互查询 :进入循环,等待用户输入英文单词,从数据库中查询对应的中文释义并显示。输入
.quit退出。
代码结构分为数据结构定义、回调函数、数据导入函数、查询函数和主函数几个部分。
代码结构分析
1. 数据结构:mean_t
c
typedef struct wordmean
{
int flag; // 查询成功标志,回调函数中置1
char word[256]; // 单词
char mean[4096]; // 释义
} mean_t;
结构体:
-
word用于存放用户输入的单词,作为查询条件。 -
mean用于接收查询结果。 -
flag在回调函数中被置1,标记查询成功,避免了使用全局变量,使代码更模块化。
2. 回调函数:callback
c
int callback(void *arg, int column, char **pconent, char **pheaders)
{
mean_t *ptmp = arg;
strcpy(ptmp->mean, pconent[0]);
ptmp->flag++;
return 0;
}
SQLite的sqlite3_exec函数在执行查询时会为每一行结果调用一次回调函数。这里我们将mean_t指针作为参数传入,在回调中直接将查询到的释义拷贝到结构体中,并递增flag。这种使得查询函数与结果处理解耦。
3. 数据导入函数:AddWordToSqlfile
打开数据库与建表
c
ret = sqlite3_open(psqlname, &pdb);
if(ret != SQLITE_OK) { ... }
sprintf(command, "create table if not exists dict (id integer primary key asc, word text, mean text);");
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
if(ret != SQLITE_OK) { ... }
-
使用
sqlite3_open打开(或创建)数据库,并检查返回值。 -
建表语句使用了
IF NOT EXISTS,确保不会重复建表。表结构包含自增主键id,以及word和mean两个文本字段,满足词典需求。
使用事务批量插入
c
sprintf(command, "begin;");
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
if(ret != SQLITE_OK) { ... }
// 循环读取文件并插入
sprintf(command, "commit;");
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
这是一个非常重要点!如果逐条执行INSERT,SQLite默认会自动提交事务,导致大量磁盘I/O,性能极低。通过显式地使用BEGIN和COMMIT将多次插入包装在一个事务中,插入速度能够提升几十倍甚至上百倍,两万个单词的条件下,实测:如果不使用事务机制得花费15s左右,但添加事务机制只需280ms左右。
解析字典文件并显示进度
c
fseek(fd, 0, SEEK_END);
tlen = ftell(fd);
rewind(fd);
while(1) {
// 读取一行
ptmp = fgets(tmpbuff, sizeof(tmpbuff), fd);
if(NULL == ptmp) break;
// 分割单词和释义
strtok(tmpbuff, " ");
strcpy(word, tmpbuff);
ptmp = strtok(NULL, "\r");
strcpy(mean, ptmp);
// 构造INSERT并执行
clen = ftell(fd);
printf("已加载:%.2lf%%\r", (double)clen / (double)tlen * 100);
}
-
首先通过
fseek和ftell获取文件总长度,用于计算进度。 -
在循环中,每插入一条记录就获取当前文件位置,计算并打印已导入的百分比。使用
\r让进度在同一行刷新,增强用户体验。 -
文件解析使用
strtok,假设每行格式为"单词 释义",单词与释义之间用空格分隔,释义末尾可能有回车符。这种解析方式比较简单有效,适用于常见格式。
4. 查询函数:SelectWordInSql
c
int SelectWordInSql(char *psqlname, mean_t *pword_mean)
{
// 打开数据库
sprintf(command, "select mean from dict where word = \"%s\";", pword_mean->word);
ret = sqlite3_exec(pdb, command, callback, pword_mean, &errmsg);
// 错误处理及关闭
}
-
该函数接收数据库文件名和
mean_t指针,构造SQL语句后调用sqlite3_exec,并将pword_mean作为参数传递给回调函数。 -
由于回调函数会将查询到的释义填入
mean字段并置位flag,调用者只需检查flag即可知道是否找到单词。 -
查询结束后关闭数据库。
5. 主函数:main
c
int main(void)
{
// 检查数据库文件是否存在,若不存在则导入
if(-1 == access("dict.db", F_OK))
{
AddWordToSqlfile(sqlname, dictfile);
}
while(1)
{
printf("请输入要查询的单词:\n");
gets(word_mean.word);
if(0 == strcmp(word_mean.word, ".quit")) break;
SelectWordInSql(sqlname, &word_mean);
if(0 == word_mean.flag)
printf("单词没找到!\n");
else
printf("含义:%s\n", word_mean.mean);
// 清空结构体准备下一次查询
memset(&word_mean, 0, sizeof(word_mean));
}
return 0;
}
-
程序启动时,通过
access判断数据库是否存在,若不存在则自动导入,实现"开箱即用"。 -
主循环使用
gets读取用户输入,判断退出命令后调用查询函数。 -
查询结果通过
flag判断。 -
每次查询后
memset清空结构体,避免残留数据影响下一次查询。
程序亮点与好的设计
-
事务批量插入
在数据导入时使用
BEGIN和COMMIT将成千上万条插入包裹在一个事务中,极大提升了性能。 -
进度反馈
实时显示导入百分比,让用户了解处理进度,避免长时间无响应的焦虑。
-
回调机制解耦
将查询结果处理分离到
callback函数,使查询函数只需关注SQL构造和执行,代码清晰易扩展。 -
结构体传递上下文
使用
mean_t结构体同时作为输入和输出,避免了全局变量。 -
自适应的数据库创建
通过
access判断数据库是否存在,仅在首次运行时导入,避免重复覆盖已有数据。
可以进一步打磨的地方
如果想让程序更健壮、更通用,可以考虑以下几点:
-
输入函数的安全性
当前使用
gets读取单词,该函数不检查缓冲区大小,存在栈溢出风险。建议改用fgets(word_mean.word, sizeof(word_mean.word), stdin),并去除末尾换行符。 -
SQL语句的参数化
直接拼接用户输入到SQL中,若单词包含双引号可能导致语法错误。改用
sqlite3_prepare_v2+sqlite3_bind_text可以避免SQL注入和转义问题。 -
字符串边界保护
在
callback中直接strcpy,若释义长度超过4095会导致溢出。可使用strncpy并手动添加结尾\0,或动态分配内存。 -
字典解析的通用性
目前的
strtok按空格分割,如果单词本身包含空格(如"look up")会解析错误。可以考虑按第一个空格分割,或者使用固定的分隔符(如制表符)并明确要求用户。
这些改进点是让程序更完善,本身已经能够实现功能,不影响当前功能的正确运行,只是在极端条件下可能不够完美有待改进。
总结
希望通过本文的分析,能帮助大家更好地理解这段代码,也欢迎读者在此基础上继续完善,打造属于自己的词典工具。
源码:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sqlite3.h>
#include <unistd.h>
typedef struct wordmean
{
int flag;
char word[256];
char mean[4096];
}mean_t;
int callback(void *arg, int column, char **pconent, char ** pheaders)
{
mean_t *ptmp = NULL;
ptmp = arg;
strcpy(ptmp->mean, pconent[0]);
ptmp->flag++;
return 0;
}
int AddWordToSqlfile(char *psqlname, char *pdictfile)
{
int ret = 0;
sqlite3 *pdb = NULL;
FILE *fd = NULL;
char *errmsg = NULL;
char word[1024] = {0};
char mean[2048] = {0};
char command[1024] = {0};
char tmpbuff[4096] = {0};
char *ptmp = NULL;
long tlen = 0;
long clen = 0;
ret = sqlite3_open(psqlname, &pdb);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_open:%s", sqlite3_errmsg(pdb));
return -1;
}
sprintf(command, "create table if not exists dict (id integer primary key asc, word text, mean text);");
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_exec:%s", errmsg);
sqlite3_free(errmsg);
sqlite3_close(pdb);
return -1;
}
fd = fopen(pdictfile, "r");
if(NULL == fd)
{
perror("fail to fopen");
sqlite3_close(pdb);
return -1;
}
fseek(fd, 0, SEEK_END);
tlen = ftell(fd);
rewind(fd);
sprintf(command, "begin;");
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_exec:%s", errmsg);
sqlite3_free(errmsg);
sqlite3_close(pdb);
return -1;
}
while(1)
{
memset(command, 0, sizeof(command));
memset(tmpbuff, 0, sizeof(tmpbuff));
memset(word, 0, sizeof(word));
memset(mean, 0, sizeof(mean));
ptmp = fgets(tmpbuff, sizeof(tmpbuff), fd);
if(NULL == ptmp)
{
break;
}
strtok(tmpbuff, " ");
strcpy(word, tmpbuff);
ptmp = strtok(NULL, "\r");
strcpy(mean, ptmp);
sprintf(command, "insert into dict values (NULL, \"%s\", \"%s\");", word, mean);
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_exec:%s", errmsg);
sqlite3_free(errmsg);
sqlite3_close(pdb);
return -1;
}
clen = ftell(fd);
printf("已加载:%.2lf%%\r", (double)clen / (double)tlen * 100);
}
sprintf(command, "commit;");
ret = sqlite3_exec(pdb, command, NULL, NULL, &errmsg);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_exec:%s", errmsg);
sqlite3_free(errmsg);
sqlite3_close(pdb);
return -1;
}
printf("\n");
fclose(fd);
sqlite3_close(pdb);
return 0;
}
int SelectWordInSql(char *psqlname, mean_t *pword_mean)
{
int ret = 0;
sqlite3 *pdb = NULL;
char *errmsg = NULL;
char command[1024] = {0};
ret = sqlite3_open(psqlname, &pdb);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_open:%s", sqlite3_errmsg(pdb));
return -1;
}
sprintf(command, "select mean from dict where word = \"%s\";", pword_mean->word);
ret = sqlite3_exec(pdb, command, callback, pword_mean, &errmsg);
if(ret != SQLITE_OK)
{
fprintf(stderr, "fail to sqlite3_exec:%s", errmsg);
sqlite3_free(errmsg);
sqlite3_close(pdb);
return -1;
}
sqlite3_close(pdb);
return 0;
}
int main(void)
{
sqlite3 *pdb = NULL;
mean_t word_mean;
char dictfile[128] = {0};
char sqlname[128] = {0};
sprintf(sqlname, "dict.db");
sprintf(dictfile, "dict.txt");
if(-1 == access("dict.db", F_OK))
{
AddWordToSqlfile(sqlname, dictfile);
}
while(1)
{
memset(&word_mean, 0, sizeof(word_mean));
printf("请输入要查询的单词:\n");
gets(word_mean.word);
if(0 == strcmp(word_mean.word, ".quit"))
{
break;
}
SelectWordInSql(sqlname, &word_mean);
if(0 == word_mean.flag)
{
printf("单词没找到!\n");
}
else
{
printf("含义:%s\n", word_mean.mean);
}
}
return 0;
}