☰
万年历数据入MySQL:从JSON导入到表设计与校验的完整指南
2026/10/13 17:38:30 网站建设 项目流程

简介:一套针对 MySQL 的完整万年历数据库脚本,时间横跨1970年至2100年,全面涵盖农历、二十四节气、财神方位、宜忌、星座、天干地支与五行等传统日历信息,适合需要快速搭建日期查询功能的开发者,也适合学习数据库设计与导入实践的初学者。压缩包共2个文件,分别为1个CSV数据文件和1个SQL脚本文件,整体大小约1.86MB。SQL脚本负责创建多张表结构并定义字段类型与约束,CSV数据文件可通过LOAD DATA INFILE批量导入,两者配合即可生成一套可直接使用的日历数据库。资源已吸引1609人学习浏览,兼具资料完整性与实战参考价值。拿到后可直接执行脚本建库导数据,再按年份、日期、节气、方位、星座等维度编写查询与应用,方便用于个人项目、工具网站或数据库课程练习。

1. 万年历数据入 MySQL:“最全”不是导入一次就完事

把“万年历脚本 mysql 数据库黄历”这几个词拼在一起,背后其实是一个很典型的落地需求:你要的不只是某一天的宜忌,而是把一整段历史区间内的公历、农历、干支、五行、冲煞、吉神凶煞全部结构化存进 MySQL,提供给业务系统查询。这个场景在传统行业特别常见——做内部管理系统、OA 审批、CRM 订单交付,经常要附带一个“今天适不适合开工/签约/出行”的提示。市面上的第三方 API 要么收费、要么限流、要么返回字段不齐,数据不在自己库里,出问题都找不到人赔。

“最全”这两个字才是真正的坑。数据源杂、格式乱、宜忌文本有歧义,导入脚本跑完不等于数据正确;等上线后领导问你“2 月 9 日到底宜不宜动土”而你答不上来的时候,才知道前面偷的懒全要还。这篇笔记就是把你从数据源选型一路带到表设计、Shell/Python 双脚本导入、SQL 校验和常见报错排查的完整路径,适合准备自己动手做内网黄历查询功能的人。新手能照着一步步跑通,熟手可以直接拿走核对清单和避坑经验。

2. 数据源与表设计:别急着写脚本,先定字段边界

2.1 数据源怎么选:结构化 JSON 优先于网页抓取

做万年历脚本的第一件事不是写代码,而是决定数据从哪来。我自己踩过几次网页抓取的坑:目标网站改个 class 名、加个验证码、或者把农历接口从 HTTP 切成 HTTPS,脚本就废了。更麻烦的是网页抓下来的数据是“给人看”的排版,公历日期、农历日期混在一段文本里,解析时错一位就是全表崩。

所以首选是找结构化程度高的数据源。常见做法是找一个覆盖区间足够大的 JSON 数据包,每条记录包含公历日期、农历月日、干支、五行、冲煞、宜忌列表这些字段,格式尽量统一。判断数据源好不好用,我一般看三点:一是区间覆盖,能不能覆盖 1900-2100 这种常见范围;二是字段是否拆分到“机器可读”的程度,比如宜忌最好是数组而不是一段用顿号分隔的字符串;三是授权是否清晰,注明来源和允许使用的范围,免得后面业务上线了惹麻烦。

拿到 JSON 之后再考虑要不要二次加工。比如原始数据里“宜”字段是“嫁娶.祭祀.祈福”这种点号分隔的字符串,最省事的办法是在导入脚本里按分隔符拆成数组再入库,而不是每次查询时再拆,后面写 SQL 会轻松很多。

2.2 建表:日期主键、宜忌用 JSON 还是拆列

表结构是万年历脚本里最值得花时间的部分。字段改一次,所有导入脚本和查询 SQL 全要跟着改,所以先想清楚再建表。我的建议是主表用公历日期做 PRIMARY KEY,因为公历日期是唯一的、连续的,天然适合做唯一标识;农历日期在同一天可能对应不同的公历日期(闰月),不适合做主键。

CREATE TABLE calendar_info ( solar_date DATE PRIMARY KEY COMMENT '公历日期', lunar_date VARCHAR(20) NOT NULL COMMENT '农历日期,如2024年腊月廿三', lunar_year VARCHAR(10) COMMENT '农历年,如癸卯', lunar_month VARCHAR(10) COMMENT '农历月,如腊月', lunar_day VARCHAR(10) COMMENT '农历日,如廿三', ganzhi_year VARCHAR(10) COMMENT '年柱干支,如癸卯', ganzhi_month VARCHAR(10) COMMENT '月柱干支', ganzhi_day VARCHAR(10) COMMENT '日柱干支', wuxing_day VARCHAR(20) COMMENT '日五行,如金/木/水/火/土', chong VARCHAR(30) COMMENT '冲煞,如冲虎(壬寅)煞南', sha VARCHAR(5) COMMENT '煞方,如南', yi JSON COMMENT '宜,JSON数组,如["嫁娶","祭祀"]', ji JSON COMMENT '忌,JSON数组', pengzu VARCHAR(100) COMMENT '彭祖百忌,如庚不經絡', jishen VARCHAR(100) COMMENT '吉神宜趋', xiongshen VARCHAR(100) COMMENT '凶煞宜忌', jieqi VARCHAR(20) COMMENT '节气,如立春/雨水', is_holiday TINYINT DEFAULT 0 COMMENT '是否法定节假日,0否1是', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_solar_date (solar_date), KEY idx_lunar_date (lunar_date(10)), KEY idx_jieqi (jieqi) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='万年历黄历主表';

几个字段设计的细节说一下。lunar_date保留完整字符串用于直接展示,同时拆出lunar_month、lunar_day方便按“腊月廿三”这种条件做查询;如果你只存完整字符串,以后想筛“所有初五”的时候就要写 LIKE 或者 SUBSTRING_INDEX,查询性能和使用体验都不好。ganzhi_year和ganzhi_month虽然可以从lunar_date反推,但反推逻辑涉及农历编码表,在 SQL 里写会很痛苦,入库时直接冗余存一份,查询时免计算。

yi和ji用了 JSON 类型。这里有一个取舍:如果拆成单独的calendar_yi、calendar_ji关联表,确实更符合第三范式,但万年历查询是典型“一次查一行、整行展示”的场景,拆表之后每次查询都要做两次 JOIN,收益很小。用 JSON 的好处是一行存完,Python 读出来直接就是 list,前端拿到就能渲染;MySQL 从 5.7 开始支持 JSON 类型,查询时也可以用JSON_CONTAINS做匹配,功能上不差。

is_holiday默认值设为 0,这是为了配合后面脚本导入时只更新有值的字段。如果默认值允许 NULL,业务查询里WHERE is_holiday = 0就永远查不到新导入的行,这种小细节很容易让人排查半天。

2.3 边界字段:节气、时辰、彭祖百忌要不要单独建表

主表之外还有两类字段要想清楚:节气和时辰。节气是每个公历日期最多一个,属于“一日期一值”,直接放进主表的jieqi字段就够;但如果团队里做农事系统、排产系统,需要精确到节气交接时刻(比如“2024 年立春时间是 2 月 4 日 16:26”),那就要单独建一张节气表,存节气的精确时间、公历日期、农历对应关系。

时辰表和节气表不一样,它和日期是一对多的关系,一天有 12 个时辰,每个时辰有各自的吉凶宜忌。如果业务要展示“今日每个时辰宜什么、忌什么”,我建议单独建表,不要往主表里塞 12 个字段:

CREATE TABLE calendar_shichen ( id INT AUTO_INCREMENT PRIMARY KEY, solar_date DATE NOT NULL, shichen VARCHAR(10) NOT NULL COMMENT '时辰,如子时/丑时', time_range VARCHAR(20) COMMENT '时间段,如23:00-00:59', yi VARCHAR(100) COMMENT '此时辰宜', ji VARCHAR(100) COMMENT '此时辰忌', wuxing VARCHAR(20) COMMENT '此时辰五行', UNIQUE KEY uk_date_shichen (solar_date, shichen), CONSTRAINT fk_shichen_date FOREIGN KEY (solar_date) REFERENCES calendar_info(solar_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='时辰宜忌表';

这里注意:如果不是刚需,不要一上来就建时辰表。我见过一个项目把时辰表建好了,结果数据源里根本没有对应的时辰宜忌数据,表空置了半年。字段边界的原则是“数据源有什么,业务要什么,再建什么”,千万不要先建表再找数据,否则导入脚本写起来会非常痛苦。

3. 全量导入脚本:Shell 与 Python 两条路怎么走

3.1 环境准备:MySQL 8.0 建库授权与连接参数

脚本写得再好,连不上数据库也白搭。这里先解决环境问题,顺便说一句,很多人在 mysql 安装配置教程里卡住的点,大多不是安装本身,而是安装之后不知道要给脚本账号单独授权。我习惯的做法是建一个专用账号,而不是用 root 跑业务脚本,免得哪天脚本里一个 DELETE 忘写 WHERE 把整个库清空。

CREATE DATABASE calendar_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'calendar_app'@'localhost' IDENTIFIED BY 'your_strong_password'; GRANT SELECT, INSERT, UPDATE, DELETE ON calendar_db.* TO 'calendar_app'@'localhost'; FLUSH PRIVILEGES;

三个要点说清楚。第一,建库时直接指定 utf8mb4,这是 MySQL 8.0 的默认字符集,也是中文数据不出现乱码的前提。第二,授权粒度控制在单库calendar_db.*,不是*.*,这样即使脚本被注入,影响范围也被限制住。第三,FLUSH PRIVILEGES在通过 GRANT 语句授权时其实不是必须的,但如果你手动改过mysql.user表,那这句就是必须的;写上没坏处,养成习惯。

连接参数方面,脚本里请统一使用127.0.0.1而不是localhost。这两个的区别在 MySQL 连接时很微妙:用localhost会走 Unix socket,用127.0.0.1会走 TCP。后面排查 ERROR 2002 时,这个区别往往是罪魁祸首,这里提前埋个伏笔。

3.2 Shell 脚本方案:for 循环 + mysql 命令行,适合一次性全量

如果你手里的 JSON 数据是按年份拆成多个文件的,Shell 脚本是最直白的方案。用for循环遍历目录下的所有 JSON 文件,逐个调用mysql命令行导入,脚本短、依赖少、部署简单。下面这个脚本是我实际用过的简化版:

#!/bin/bash # load_calendar.sh - 批量导入万年历 JSON 数据到 MySQL MYSQL_CMD="mysql -ucalendar_app -pyour_strong_password -h127.0.0.1 --default-character-set=utf8mb4 calendar_db" INPUT_DIR="./calendar_data" for f in "$INPUT_DIR"/*.json; do echo "processing: $f" "$MYSQL_CMD" -e " SET @json := LOAD_FILE('$(realpath "$f")'); INSERT INTO calendar_info (solar_date, lunar_date, lunar_year, ganzhi_day, wuxing_day, chong, sha, yi, ji, pengzu, jieqi) SELECT JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.solar_date')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.lunar_date')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.lunar_year')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.ganzhi_day')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.wuxing_day')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.chong')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.sha')), JSON_EXTRACT(j.doc, '$.yi'), JSON_EXTRACT(j.doc, '$.ji'), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.pengzu')), JSON_UNQUOTE(JSON_EXTRACT(j.doc, '$.jieqi')) FROM (SELECT @json AS doc) j; " if [ $? -eq 0 ]; then echo "OK: $f" else echo "FAIL: $f" exit 1 fi done

这个脚本的核心逻辑是:先把 JSON 文件用LOAD_FILE()读成字符串,再用JSON_EXTRACT()从 JSON 里取出每个字段,最后 INSERT 到表中。@json是 MySQL 会话变量,j.doc是它的别名引用,这种写法避免了把 JSON 内容直接拼进 SQL 字符串里产生的转义地狱。

参数说明:realpath是为了把相对路径转成绝对路径,因为LOAD_FILE()对路径很敏感,如果 MySQL 的secure_file_priv限制了可读目录,这里会直接 FAIL;--default-character-set=utf8mb4必须加,否则中文内容在命令行传输时可能被转成 latin1;$?检查上一条命令的返回值,非 0 就退出,这个习惯能让你第一时间发现导入失败的文件,而不是跑完整个目录才发现数据是空的。

这个方案的局限也很明显:Shell 里做数据清洗很笨拙,比如 JSON 里如果有某个字段缺失,JSON_EXTRACT返回 NULL 值,你得在 SQL 里写一堆IFNULL兜底。所以 Shell 方案适合“数据已经非常规整、只是需要灌进去”的全量导入场景。

3.3 Python 脚本方案:pymysql + executemany 批量写入

数据源字段不干净时,我一般会改用 Python。Python 的优势在于:读 JSON 用原生json.load(),清洗逻辑写在 Python 里比写在 SQL 里直观得多;写库用executemany()批量执行,性能比逐条 INSERT 快一个数量级;还能顺手打印导入进度和错误日志。

#!/usr/bin/env python3 # load_calendar.py - 从 JSON 导入万年历数据,支持重复执行 import json import glob import logging import pymysql logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") DB_CONFIG = { "host": "127.0.0.1", "port": 3306, "user": "calendar_app", "password": "your_strong_password", "database": "calendar_db", "charset": "utf8mb4", "autocommit": False, } INSERT_SQL = """ INSERT INTO calendar_info (solar_date, lunar_date, lunar_year, lunar_month, lunar_day, ganzhi_year, ganzhi_month, ganzhi_day, wuxing_day, chong, sha, yi, ji, pengzu, jieqi, is_holiday) VALUES (%(solar_date)s, %(lunar_date)s, %(lunar_year)s, %(lunar_month)s, %(lunar_day)s, %(ganzhi_year)s, %(ganzhi_month)s, %(ganzhi_day)s, %(wuxing_day)s, %(chong)s, %(sha)s, %(yi)s, %(ji)s, %(pengzu)s, %(jieqi)s, %(is_holiday)s) ON DUPLICATE KEY UPDATE lunar_date=VALUES(lunar_date), lunar_year=VALUES(lunar_year), ganzhi_day=VALUES(ganzhi_day), wuxing_day=VALUES(wuxing_day), yi=VALUES(yi), ji=VALUES(ji), pengzu=VALUES(pengzu), jieqi=VALUES(jieqi) """ def clean_item(raw): """清洗单条 JSON 记录,兜底缺字段的情况""" return { "solar_date": raw.get("solar_date") or "1970-01-01", "lunar_date": raw.get("lunar_date") or "", "lunar_year": raw.get("lunar_year") or "", "lunar_month": raw.get("lunar_month") or "", "lunar_day": raw.get("lunar_day") or "", "ganzhi_year": raw.get("ganzhi_year") or "", "ganzhi_month": raw.get("ganzhi_month") or "", "ganzhi_day": raw.get("ganzhi_day") or "", "wuxing_day": raw.get("wuxing_day") or "", "chong": raw.get("chong") or "", "sha": raw.get("sha") or "", "yi": json.dumps(raw.get("yi", []), ensure_ascii=False), "ji": json.dumps(raw.get("ji", []), ensure_ascii=False), "pengzu": raw.get("pengzu") or "", "jieqi": raw.get("jieqi") or "", "is_holiday": 1 if raw.get("is_holiday") else 0, } def main(): conn = pymysql.connect(**DB_CONFIG) files = sorted(glob.glob("./calendar_data/*.json")) total_rows = 0 try: with conn.cursor() as cursor: for fp in files: with open(fp, encoding="utf-8") as f: raw_list = json.load(f) rows = [clean_item(item) for item in raw_list] cursor.executemany(INSERT_SQL, rows) conn.commit() total_rows += len(rows) logging.info("imported %d rows from %s", len(rows), fp) logging.info("done, total %d rows", total_rows) except Exception as e: conn.rollback() logging.error("import failed, rollback: %s", e) raise finally: conn.close() if __name__ == "__main__": main()

代码逻辑拆开说。clean_item()是清洗层,raw.get("yi", [])保证缺字段时给一个空数组,json.dumps(..., ensure_ascii=False)把 list 转成 JSON 字符串再存进 MySQL 的 JSON 字段,这样存进去的内容是中文而不是\uXXXX转义序列。INSERT_SQL里的ON DUPLICATE KEY UPDATE是幂等设计:同一份数据跑两遍,第二条会变成更新而不是新增,这个设计避免了重复导入时主键冲突直接报错。

事务方面,autocommit=False下每导入一个文件提交一次,而不是每行都提交,也不等全部文件跑完再提交。每文件提交的粒度是性能和安全的折中:一个文件几万条数据,事务太大容易撑爆 undo log;按文件粒度提交,失败时最多回滚一个文件,不影响已导入的其他年份。

这里有一个参数选择值得说:executemany()的第二个参数传的是 dict 列表,对应 SQL 里的%(字段名)s占位符。如果你改成INSERT INTO ... VALUES (%s, %s)那种位置占位符,rows就必须是 tuple 列表;我用 dict 是为了让 SQL 可读性更好,字段多的时候不容易错位。

4. 查询与数据校验:三个核对 SQL 让导入不翻车

4.1 常用查询:按公历查黄历、按月列出宜忌

数据进库之后,第一步永远是用最基础的查询验证“能不能查到”。先跑一个最简单的按日期查询,确认字段映射没搞反:

-- 查指定日期的完整黄历信息 SELECT solar_date, lunar_date, ganzhi_year, ganzhi_month, ganzhi_day, wuxing_day, chong, sha, JSON_UNQUOTE(JSON_EXTRACT(yi, '$[0]')) AS yi_first, JSON_UNQUOTE(JSON_EXTRACT(ji, '$[0]')) AS ji_first, pengzu, jieqi FROM calendar_info WHERE solar_date = '2024-02-10';

注意yi字段是 JSON 类型,直接用SELECT yi查出来的还是一段 JSON 文本。业务上通常需要取第一条宜、第一条忌,所以这里用JSON_EXTRACT(yi, '$[0]')取数组第一个元素,再包一层JSON_UNQUOTE把引号去掉。不要小看这个细节,前端拿到带引号的字符串再渲染,很容易出现页面显示"嫁娶"带双引号的翻车现场。

按月查询,比如查 2024 年 2 月哪天宜嫁娶,可以这样写:

SELECT solar_date, lunar_date, yi FROM calendar_info WHERE solar_date BETWEEN '2024-02-01' AND '2024-02-29' AND JSON_CONTAINS(yi, '"嫁娶"') ORDER BY solar_date;

JSON_CONTAINS(yi, '"嫁娶"')是 JSON 数组的包含查询,注意第二个参数必须带双引号,因为 JSON 数组里的元素是字符串。这里如果写成JSON_CONTAINS(yi, '嫁娶')会报错,因为这不是合法的 JSON 字符串。ORDER BY solar_date让结果按公历日期排序,这是查询里最常用的排序方式;如果你需要按农历日期排序,那就要靠lunar_date字段了,但字符串排序结果并不符合农历语义,所以业务上一般还是按公历日期排序展示。

4.2 三个核对 SQL:总量、空值率、重复农历日期

查询能跑通不等于数据正确。导入完成之后,我每次都会跑下面三个核对 SQL,这三条是验收的基本门槛,也是以后数据出问题时的第一排查手段。

-- 核对 1:总量是否覆盖预期区间(1900-2100 约 73000+ 天) SELECT MIN(solar_date) AS min_date, MAX(solar_date) AS max_date, COUNT(*) AS total_rows, COUNT(DISTINCT solar_date) AS distinct_dates FROM calendar_info; -- 核对 2:关键字段的空值率,超过 1% 就要查数据源 SELECT COUNT(*) AS total_rows, SUM(CASE WHEN lunar_date IS NULL OR lunar_date = '' THEN 1 ELSE 0 END) AS null_lunar_date, SUM(CASE WHEN ganzhi_day IS NULL OR ganzhi_day = '' THEN 1 ELSE 0 END) AS null_ganzhi_day, SUM(CASE WHEN yi IS NULL OR yi = '[]' THEN 1 ELSE 0 END) AS null_yi, SUM(CASE WHEN ji IS NULL OR ji = '[]' THEN 1 ELSE 0 END) AS null_ji FROM calendar_info; -- 核对 3:农历日期重复检查(闰月场景下可能合法,但大量重复要警惕) SELECT lunar_date, COUNT(*) AS cnt FROM calendar_info GROUP BY lunar_date HAVING cnt > 1 ORDER BY cnt DESC LIMIT 10;

核对 1 看的是数据量。1900-2100 年是 201 个公历年,按 365 或 366 天算,总量在 73400 行左右。如果MIN/MAX覆盖到了但total_rows明显偏少,说明数据源中间有断层,常见原因是某几年的 JSON 文件没下载全或者脚本中途失败没被发现。

核对 2 看的是空值率。lunar_date为空说明导入映射写错了字段名,yi为空数组说明数据源里本来就没有宜的数据,也可能是清洗逻辑把字段丢了。这里我一般给自己定的标准是:关键字段空值率不超过 1%,超过 1% 就要回到数据源逐条检查。

核对 3 看重复的农历日期。大部分年份农历日期不会重复,只有闰月年才会出现两个“四月十五”之类的重复。如果非闰月年出现大量重复,说明 JSON 数据本身有脏数据。这里不要直接删,先用HAVING cnt > 1结合solar_date去查具体是哪几天,人工确认是否闰月导致的再决定怎么处理。

4.3 用随机抽检 + 外部对照打破“自说自话”

核对 SQL 全部通过也可能数据整体是错的——比如数据源把“宜”和“忌”整列搞反了。这种结构性错误是 SQL 检查不出来的,所以我还会做一个抽检:随机取 10 个日期,把库里查出来的农历日期、干支、宜忌,和公开的权威日历逐条人工比对。这个工作看起来土,但它是整个验证流程里最重要的一道关。

抽检不一定要全人工。可以找一个已经验证过的、你本地已有的日历数据文件做程序化比对;但如果两个来源本来是同一份数据,这个比对就没有意义了。所以关键是找一个“信得过的独立来源”,比如印刷版老黄历,或者多个不同来源的数据交叉验证。这个思路要贯穿数据全流程,别等到上线了才想起来验证,黄历数据错了用户一眼就能看出来。

5. 万年历入 MySQL 的避坑清单:从 2002 报错到重复导入翻车

5.1 ERROR 2002 连不上:socket 与 TCP 两种姿势别混用

现象:脚本跑起来报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。

原因:命令行里写的是mysql -ucalendar_app -p没带-h,此时 MySQL 客户端默认走 Unix socket 连接,而/tmp/mysql.sock文件不存在——可能是 MySQL 服务没启动,也可能是 socket 路径配置不在默认位置。我之前排查一个服务器,MySQL 的 socket 被配置到了/var/run/mysqld/mysqld.sock,客户端却去/tmp找,必然连不上。

解决:命令行加上-h127.0.0.1强制走 TCP;或者指定 socket 路径:mysql -ucalendar_app -p -S /var/run/mysqld/mysqld.sock。调试阶段推荐先用 MySQL Workbench 可视化客户端测试连接,Workbench 的连接配置里能看到具体是走 TCP 还是 socket,能省很多排查时间。Workbench 能连上而脚本连不上,那问题基本就出在连接参数上。

5.2 中文变问号:utf8mb4 三层一致才能救

现象:导入脚本跑完,SELECT lunar_date FROM calendar_info查出的是“????”或者一串乱码。

原因:字符集不一致。这条路上一共有四层:数据库建库字符集、表字符集、连接字符集、以及客户端文件读取编码。哪一层不是 utf8mb4,中文就可能在某一次转换里变成问号。最常见的情况是你建库时用了默认的 latin1,然后 Python 连接时指定charset='utf8mb4',两边掐架。

解决:建库时显式指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Python 连接参数里带上charset='utf8mb4';读取 JSON 文件时用open(fp, encoding="utf-8"),三层一致后基本能解决。还有一个坑是数据源本身可能不是 UTF-8 编码,比如某些数据包是 GBK 编码,这种情况下json.load()会直接报编码错误,需要先用iconv或 Python 的codecs模块转码再导入。

5.3 重复导入数据翻倍:唯一索引和幂等写入是后悔药

现象:第一次全量导入成功,跑脚本做增量更新时发现COUNT(*)从 7 万变成了 14 万,主键还是同一个日期,数据却重复了。

原因:表结构没问题的话,导入脚本没有处理“已存在的行”。Shell 方案里我用的是裸INSERT INTO,没有ON DUPLICATE KEY UPDATE;脚本第二次执行时主键冲突,MySQL 默认报错,但如果表没建唯一索引,就会直接插入重复行。

解决:两条路都得上。建表时一定要有PRIMARY KEY (solar_date),让数据库从结构上拒绝重复;写入 SQL 里加ON DUPLICATE KEY UPDATE,让脚本天然支持重复执行。我刚才给的两个方案,Shell 的简化版没做幂等,Python 版做了,实际使用时 Shell 版也要加上。这个教训我栽过一次,事前觉得“跑一次就不跑了”,结果数据源更新了,脚本又跑了一遍,全表数据翻倍,最后只能全量清掉重导。

5.4 存储过程黑匣子:没有日志,失败了你都不知道

现象:调用一个存储过程导入数据,MySQL 返回成功,但表中数据行数没变。

原因:存储过程内部可能因为某个字段类型不一致抛了异常,但异常被DECLARE CONTINUE HANDLER吞掉了;或者存储过程里的事务没有正确 COMMIT。这种问题的麻烦在于——存储过程本身像一个黑匣子,你看不到中间步骤的输出。

解决:开发阶段不要在存储过程里吞异常。MySQL 存储过程默认遇到错误会回滚整个事务,你把它包在事务里,出错时直接看错误信息;生产环境如果一定要用存储过程,导入成功与否不要只看“调用完成”,要拿SELECT COUNT(*)前后对比验证。我现在的习惯是任何导入任务跑完都打一条日志,包含文件名、导入行数、耗时,没有日志的脚本一律不认。

5.5 数据源的日期边界:1900 年之前和 2100 年之后怎么办

现象:导入脚本跑完,发现 1900-01-01 之前的日期也进了表,而且农历字段全为空。

原因:很多万年历数据源只保证 1900-2100 区间的准确性,区间之外的数据可能是推算出来的,农历和干支不一定可靠。如果你用的是“号称支持任意年份”的数据包,一定要检查极端日期的字段是否真的完整。

解决:在清洗层加一个白名单判断,if not (datetime(1900, 1, 1) <= raw_date <= datetime(2100, 12, 31)): continue。如果你确实需要更早的数据,比如做清宫表、八字排盘这种业务,那就要换更专业的历法数据源,并且单独做一轮人工校验,别拿一个通用 JSON 包凑合。

6. 从“能查”到“能用”:存储过程、增量修数与索引调优

6.1 用存储过程封装“查某天黄历”

业务查询语句写得太长,前端每次调用都要拼 SQL,容易出错。我可以把最常用的查询封装成存储过程,入参是公历日期,出参是完整的黄历信息。注意存储过程里要处理日期格式非法的情况,避免传一个'2024-13-45'进来把数据库搞崩溃。

DELIMITER $$ CREATE PROCEDURE sp_get_calendar_by_date( IN p_solar_date DATE ) BEGIN IF p_solar_date IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'solar_date cannot be null'; END IF; SELECT solar_date, lunar_date, ganzhi_year, ganzhi_month, ganzhi_day, wuxing_day, chong, sha, pengzu, jieqi, CASE WHEN is_holiday = 1 THEN '节假日' ELSE '工作日' END AS holiday_flag, yi, ji FROM calendar_info WHERE solar_date = p_solar_date; END$$ DELIMITER ;

调用方式:CALL sp_get_calendar_by_date('2024-02-10');。封装之后,业务层不需要关心表结构,也不用拼字符串,应用代码里简化为一行调用;同时存储过程可以加上权限控制,只给查询账号EXECUTE权限,避免业务方绕过封装直接操作表。

6.2 增量修正:过年期间宜忌调整用 UPDATE 而不是重导

术数这东西有一个很现实的麻烦:不同版本的黄历对同一天的宜忌结论可能不同。比如 2024 年正月初五,有的数据源宜“开市”,有的宜“祭祀”,业务方可能要按内部专家的口径做修正。这个场景不能删行重导,正确的做法是按日期做定点 UPDATE。

-- 修正 2024-02-14 的宜忌内容 UPDATE calendar_info SET yi = JSON_ARRAY('开市', '交易', '祭祀'), ji = JSON_ARRAY('动土', '安葬'), updated_at = CURRENT_TIMESTAMP WHERE solar_date = '2024-02-14';

用JSON_ARRAY('开市', '交易', '祭祀')直接把 JSON 数组写进去,比手工拼 JSON 字符串安全得多。注意updated_at是ON UPDATE CURRENT_TIMESTAMP,这张表上任何 UPDATE 都会自动刷新,所以这里的显式赋值其实可省;但列出来能让 SQL 自解释,方便以后排查是哪次修正改的数据。

6.3 索引与 EXPLAIN:百万行量级下的查询计划检查

如果数据量真到了百万行级别(比如你存了多个版本、多个来源的日历),那查询性能必须靠索引和 EXPLAIN 兜底。最常用的索引是uk_solar_date主键索引,按日期查天然走唯一索引;但如果业务经常按节气查,一定要给jieqi字段建索引,否则WHERE jieqi = '立春'会做全表扫描。

EXPLAIN SELECT solar_date, lunar_date, yi FROM calendar_info WHERE jieqi = '立春' AND solar_date BETWEEN '2020-01-01' AND '2029-12-31';

看 EXPLAIN 结果里的type字段:const和ref是好的,ALL就是全表扫描,必须处理。处理方式很简单:CREATE INDEX idx_jieqi_date ON calendar_info(jieqi, solar_date);建联合索引。这个索引同时满足按节气过滤和按日期排序,是这一场景下的推荐索引设计。

我最后说一个习惯:导入任何数据之后,先跑一遍核对 SQL,再抽查两个日期和权威日历比对,最后才交付给业务方。这套流程看着啰嗦,但每个环节都救过我的命——有一次数据源整体偏移了一天,要不是抽检发现了,整个业务部门会拿着错误的黄历跑一个月。数据的东西,多做一步验证永远不亏,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询