"""从 方剂图谱/*.json 重建 Alpaca 格式的微调数据集,不再调用 LLM。
和 02_build_alpaca_dataset.py 是一对镜像脚本(那个管中药,这个管方剂)。
03_extract_formula_graph.py 抽取时会顺手往 alpaca_formula_dataset.jsonl 追加样本,但
那只覆盖「那一次抽的」方剂:断点续抽跳过的不在里面。这个脚本直接读已经落盘的图谱
json 重新生成全量数据集,所以是幂等的 —— 跑几次结果都一样,也不花钱。
input 取源 txt 原文(图谱 json 里的 source_file 指回去),output 取 entities+relations
的 json 串,instruction 固定。
输出:
_002__extract_information/alpaca_formula_dataset.jsonl 一行一条(默认)
_002__extract_information/alpaca_formula_dataset.json 标准 json 数组(--json)
想把中药和方剂两份语料合成一份训练集,直接 cat 两个 jsonl 即可 —— instruction 相同,
格式完全一致。
用法(在项目根目录执行):
python -X utf8 -m _002__extract_information.04_build_formula_alpaca_dataset
python -X utf8 -m _002__extract_information.04_build_formula_alpaca_dataset --json
python -X utf8 -m _002__extract_information.04_build_formula_alpaca_dataset --skip-empty
"""
import argparse
import json
import os
from tqdm import tqdm
from common.path_utils import get_file_path
from _002__extract_information.__000__extract_graph_data_utils import (
build_alpaca_record,
)
GRAPH_DIR = get_file_path("_002__extract_information/方剂图谱")
FORMULA_DIR = get_file_path("_001__clawler/方剂")
OUT_JSONL = get_file_path("_002__extract_information/alpaca_formula_dataset.jsonl")
OUT_JSON = get_file_path("_002__extract_information/alpaca_formula_dataset.json")
def load_source_text(graph, graph_path):
"""取这条图谱对应的方剂详情原文,作为 Alpaca 的 input。
优先用 json 里记的 source_file(相对仓库根),它不在或者文件没了就按
「方剂名.txt」回退到 方剂/ 目录。
"""
rel = graph.get("source_file")
if rel:
# json 里存的是 Windows 风格的相对路径,normpath 一下跨平台都能用
path = get_file_path(os.path.normpath(rel))
if os.path.exists(path):
return open(path, encoding="utf-8").read()
name = graph.get("name") or os.path.splitext(os.path.basename(graph_path))[0]
path = os.path.join(FORMULA_DIR, f"{name}.txt")
if os.path.exists(path):
return open(path, encoding="utf-8").read()
return None
def build(skip_empty=False, as_json=False):
if not os.path.isdir(GRAPH_DIR):
print(f"图谱目录不存在: {GRAPH_DIR}")
print("先跑 03_extract_formula_graph.py 抽取实体和关系。")
return
graph_files = sorted(
os.path.join(GRAPH_DIR, f)
for f in os.listdir(GRAPH_DIR)
if f.endswith(".json")
)
if not graph_files:
print(f"{GRAPH_DIR} 下没有图谱 json,先跑 03_extract_formula_graph.py。")
return
records = []
empty = missing_text = broken = 0
for path in tqdm(graph_files, desc="构建数据集", unit="条", ncols=90):
try:
with open(path, encoding="utf-8") as f:
graph = json.load(f)
except (OSError, json.JSONDecodeError):
broken += 1
continue
entities = graph.get("entities") or []
relations = graph.get("relations") or []
# 空结果是合法抽取,留着能教模型「这种文本不该硬凑关系」;
# 真要练抽取能力可以 --skip-empty 去掉。
if skip_empty and not entities and not relations:
empty += 1
continue
if not entities and not relations:
empty += 1
text = load_source_text(graph, path)
if text is None or not text.strip():
missing_text += 1
continue
records.append(build_alpaca_record(text, entities, relations))
if as_json:
out_path = OUT_JSON
payload = json.dumps(records, ensure_ascii=False, indent=2)
else:
out_path = OUT_JSONL
payload = "\n".join(json.dumps(r, ensure_ascii=False) for r in records)
payload += "\n" if records else ""
# 整体重写,不是追加:这个脚本的语义是「按当前图谱重建」,幂等
temp_path = f"{out_path}.part"
with open(temp_path, "w", encoding="utf-8") as f:
f.write(payload)
os.replace(temp_path, out_path)
print(f"\n图谱 json {len(graph_files)} 个 → 样本 {len(records)} 条")
if empty:
print(f" 其中空抽取结果 {empty} 条{'(已跳过)' if skip_empty else '(已保留)'}")
if missing_text:
print(f" 找不到源 txt 跳过 {missing_text} 条")
if broken:
print(f" json 读取失败跳过 {broken} 条")
print(f"输出: {out_path}")
def main():
parser = argparse.ArgumentParser(
description="从已抽取的方剂图谱 json 重建 Alpaca 微调数据集"
)
parser.add_argument(
"--json",
action="store_true",
dest="as_json",
help="输出标准 json 数组,默认输出 jsonl(一行一条)",
)
parser.add_argument(
"--skip-empty",
action="store_true",
help="丢掉 entities/relations 都为空的样本(默认保留)",
)
args = parser.parse_args()
build(skip_empty=args.skip_empty, as_json=args.as_json)
if __name__ == "__main__":
main()