用C语言和SQLite构建一个轻量级英汉词典

最近尝试用C语言写了一个简单的英汉词典程序。这个小工具可以将文本格式的词典导入SQLite数据库,并支持用户交互查询。今天本文将深入分析这份代码的设计思路,并分享一些可以进一步优化的地方。

程序功能概览

程序主要完成两项任务:

  1. 自动建库 :如果当前目录下不存在dict.db数据库文件,程序会自动读取dict.txt文本文件,将单词和释义存入SQLite数据库。

  2. 交互查询 :进入循环,等待用户输入英文单词,从数据库中查询对应的中文释义并显示。输入.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,以及wordmean两个文本字段,满足词典需求。

使用事务批量插入

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,性能极低。通过显式地使用BEGINCOMMIT将多次插入包装在一个事务中,插入速度能够提升几十倍甚至上百倍,两万个单词的条件下,实测:如果不使用事务机制得花费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);
}
  • 首先通过fseekftell获取文件总长度,用于计算进度。

  • 在循环中,每插入一条记录就获取当前文件位置,计算并打印已导入的百分比。使用\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清空结构体,避免残留数据影响下一次查询。

程序亮点与好的设计

  1. 事务批量插入

    在数据导入时使用BEGINCOMMIT将成千上万条插入包裹在一个事务中,极大提升了性能。

  2. 进度反馈

    实时显示导入百分比,让用户了解处理进度,避免长时间无响应的焦虑。

  3. 回调机制解耦

    将查询结果处理分离到callback函数,使查询函数只需关注SQL构造和执行,代码清晰易扩展。

  4. 结构体传递上下文

    使用mean_t结构体同时作为输入和输出,避免了全局变量。

  5. 自适应的数据库创建

    通过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;
}
相关推荐
Z1eaf_complete2 小时前
SQL注入绕过详解与防御机制
数据库·sql
chushiyunen2 小时前
django数据库配置
数据库·python·django
xiaomin-Michael2 小时前
WSR报告解读
数据库
absunique2 小时前
Spring boot 3.3.1 官方文档 中文
java·数据库·spring boot
Yupureki2 小时前
《C++实战项目-高并发内存池》2.ObjectPool构造
linux·服务器·c语言·开发语言·jvm·c++
奇树谦2 小时前
边缘计算×AUV:解锁深海探索的“实时智能”密码
数据库·人工智能·边缘计算
2401_858936882 小时前
SQLite 数据库实战
jvm·数据库·sqlite
韩立学长2 小时前
基于Springboot医疗健康管理系统6sp2oz07(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
南 阳2 小时前
Python从入门到精通day49
数据库·python·sqlite
嵌入小生0072 小时前
数据库 --- SQLite/命令/select等增删改查语句/数据库编程 --- Linux
linux·数据库·sqlite·select·sql语句·update·数据库编程