知识图谱简单构建
知识图谱简单构建

知识图谱简单构建

import openai             # 用于与 LLM 交互
import json               # 用于解析 LLM 的响应
import networkx as nx     # 用于创建和管理图数据结构
import ipycytoscape       # 用于在 notebook 中进行交互式图可视化
import ipywidgets         # 用于交互式元素
import pandas as pd       # 用于以表格形式展示数据
import os                 # 用于访问环境变量(对 API 密钥更安全)
import math               # 用于基本的数学运算
import re                 # 用于基本的文本清理(正则表达式)
import warnings           # 用于抑制潜在的弃用警告
os.environ["OPENAI_API_KEY"]='ollama' # 对于 Ollama,可以是任何非空字符串
os.environ["OPENAI_API_BASE"]='http://175.27.143.201:11434/v1'
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("OPENAI_API_BASE") # 如果未设置(例如,对于标准 
# --- 定义 LLM 模型 ---
llm_model_name = "mistral-small:24b"
# --- 定义 LLM 调用参数 ---
llm_temperature = 0.0 # 较低的温度可获得更具确定性的事实性输出。0.0 最适合提取任务。
llm_max_tokens = 4096 # LLM 响应的最大令牌数(根据模型限制进行调整)
# 定义输入文本(原材料)
unstructured_text = """
以下是适合构建《红楼梦》知识图谱的万字内结构化总结,涵盖核心人物、家族关系、关键情节及主题思想,引用多篇资料综合整理:

---

### **一、四大家族背景**
1. **贾家**  
   - **宁国府**:贾演→贾代化→贾敬(出家)→贾珍(妻尤氏)→贾蓉(妻秦可卿)  
   - **荣国府**:贾源→贾代善(妻贾母)→贾赦(妻邢夫人)、贾政(妻王夫人)、贾敏(嫁林如海)  
     - **贾政子女**:贾珠(早逝)、贾元春(贵妃)、贾宝玉、贾探春(庶出)、贾环(庶出)  
     - **贾赦子女**:贾琏(妻王熙凤)、贾迎春(庶出)  
   - **核心事件**:元春省亲建大观园、王熙凤协理宁国府、贾府被抄家

2. **王家**  
   - 王夫人(贾政妻)、薛姨妈(嫁薛家)、王子腾(官至九省都检点)

3. **史家**  
   - 贾母(史太君)、史湘云(贾母侄孙女)

4. **薛家**  
   - 薛姨妈(子薛蟠、女薛宝钗),薛宝钗嫁贾宝玉

---

### **二、核心人物及关系**
1. **贾宝玉**  
   - **身份**:神瑛侍者转世,衔玉而生,封建叛逆者  
   - **关键情节**:摔玉抗议世俗、与黛玉共读《西厢记》、被骗与宝钗成婚、出家为僧  
   - **关系**:与黛玉(灵魂伴侣)、宝钗(婚姻)、袭人(贴身丫鬟)

2. **林黛玉**  
   - **身份**:绛珠仙草转世,贾敏之女,寄居贾府  
   - **性格**:多愁善感、孤傲率真,与宝玉同为叛逆者  
   - **关键情节**:黛玉葬花、诗社夺魁、泪尽而逝

3. **薛宝钗**  
   - **身份**:封建淑女典范,贾宝玉之妻  
   - **性格**:稳重世故,善于笼络人心  
   - **关键情节**:宝钗扑蝶、劝谏宝玉读书、婚后独守空闺

4. **王熙凤**  
   - **身份**:贾琏之妻,荣国府实际掌权者  
   - **性格**:精明强干、心狠手辣  
   - **关键情节**:协理宁国府、逼死尤二姐、放高利贷、病逝狱中

5. **其他重要人物**  
   - **史湘云**:豁达乐观,醉眠芍药裀,嫁卫若兰后守寡  
   - **贾探春**:精明志高,改革大观园承包制,远嫁和亲  
   - **晴雯**:率真叛逆,病补雀金裘,被逐含冤而死  

---

### **三、关键情节与场景**
1. **家族兴衰主线**  
   - 元春省亲(贾府鼎盛)→探春理家(改革尝试)→抄检大观园(内斗激化)→贾府被抄(彻底衰败)

2. **爱情悲剧线**  
   - 宝黛初见(木石前盟)→共读西厢(情感升华)→调包计成婚(黛玉泪尽、宝玉出家)

3. **社会众生相**  
   - **刘姥姥三进荣国府**:见证贾府由盛转衰,最终救巧姐  
   - **尤二姐之死**:揭露封建妻妾制度之恶  
   - **晴雯撕扇**:底层反抗的悲剧缩影

---

### **四、主题思想与象征**
1. **封建末世危机**  
   - 四大家族“一荣俱荣,一损俱损”,揭露官僚腐败与阶级压迫。

2. **人性与命运**  
   - **判词隐喻**:如黛玉“玉带林中挂”(才华被埋没)、宝钗“金簪雪里埋”(婚姻冰冷)  
   - **大观园象征**:理想世界 vs 现实牢笼

3. **宗教与哲学**  
   - 宝玉“赤条条来去无牵挂”体现佛家空幻观。

---

### **五、知识图谱构建建议**
1. **节点分类**  
   - **人物**:按家族、身份(主子/丫鬟)、性格标签(叛逆/守旧)划分。  
   - **事件**:标记时间线(如“元春省亲-第18回”)及关联人物。  
   - **地点**:大观园各居所(潇湘馆、怡红院等)映射人物命运。

2. **关系类型**  
   - 血缘/婚姻/敌对/主仆/情感(如宝黛“灵魂伴侣”、熙凤与尤二姐“迫害”)。

3. **数据来源**  
   - 参考判词(第五回)、人物关系图(附录于多篇资料)。

---

此总结可进一步拆解为人物关系图、事件时间轴、主题关键词网络等子图谱。若需完整人物关系模板或判词解析,可参考网页的思维导图及判词列表。"""
print("--- 输入文本已加载 ---")
print(unstructured_text)
print("-" * 25)
# 基本统计信息可视化
char_count = len(unstructured_text)
word_count = len(unstructured_text.split())
print(f"总字符数: {char_count}")
print(f"大致词数: {word_count}")
print("-" * 25)

#### 预期输出(基于原文示例)####
# --- 输入文本已加载 ---
# Marie Curie, born Maria Skłodowska in Warsaw, Poland... (完整文本打印)
--- 输入文本已加载 ---

以下是适合构建《红楼梦》知识图谱的万字内结构化总结,涵盖核心人物、家族关系、关键情节及主题思想,引用多篇资料综合整理:

---

### **一、四大家族背景**
1. **贾家**  
   - **宁国府**:贾演→贾代化→贾敬(出家)→贾珍(妻尤氏)→贾蓉(妻秦可卿)  
   - **荣国府**:贾源→贾代善(妻贾母)→贾赦(妻邢夫人)、贾政(妻王夫人)、贾敏(嫁林如海)  
     - **贾政子女**:贾珠(早逝)、贾元春(贵妃)、贾宝玉、贾探春(庶出)、贾环(庶出)  
     - **贾赦子女**:贾琏(妻王熙凤)、贾迎春(庶出)  
   - **核心事件**:元春省亲建大观园、王熙凤协理宁国府、贾府被抄家

2. **王家**  
   - 王夫人(贾政妻)、薛姨妈(嫁薛家)、王子腾(官至九省都检点)

3. **史家**  
   - 贾母(史太君)、史湘云(贾母侄孙女)

4. **薛家**  
   - 薛姨妈(子薛蟠、女薛宝钗),薛宝钗嫁贾宝玉

---

### **二、核心人物及关系**
1. **贾宝玉**  
   - **身份**:神瑛侍者转世,衔玉而生,封建叛逆者  
   - **关键情节**:摔玉抗议世俗、与黛玉共读《西厢记》、被骗与宝钗成婚、出家为僧  
   - **关系**:与黛玉(灵魂伴侣)、宝钗(婚姻)、袭人(贴身丫鬟)

2. **林黛玉**  
   - **身份**:绛珠仙草转世,贾敏之女,寄居贾府  
   - **性格**:多愁善感、孤傲率真,与宝玉同为叛逆者  
   - **关键情节**:黛玉葬花、诗社夺魁、泪尽而逝

3. **薛宝钗**  
   - **身份**:封建淑女典范,贾宝玉之妻  
   - **性格**:稳重世故,善于笼络人心  
   - **关键情节**:宝钗扑蝶、劝谏宝玉读书、婚后独守空闺

4. **王熙凤**  
   - **身份**:贾琏之妻,荣国府实际掌权者  
   - **性格**:精明强干、心狠手辣  
   - **关键情节**:协理宁国府、逼死尤二姐、放高利贷、病逝狱中

5. **其他重要人物**  
   - **史湘云**:豁达乐观,醉眠芍药裀,嫁卫若兰后守寡  
   - **贾探春**:精明志高,改革大观园承包制,远嫁和亲  
   - **晴雯**:率真叛逆,病补雀金裘,被逐含冤而死  

---

### **三、关键情节与场景**
1. **家族兴衰主线**  
   - 元春省亲(贾府鼎盛)→探春理家(改革尝试)→抄检大观园(内斗激化)→贾府被抄(彻底衰败)

2. **爱情悲剧线**  
   - 宝黛初见(木石前盟)→共读西厢(情感升华)→调包计成婚(黛玉泪尽、宝玉出家)

3. **社会众生相**  
   - **刘姥姥三进荣国府**:见证贾府由盛转衰,最终救巧姐  
   - **尤二姐之死**:揭露封建妻妾制度之恶  
   - **晴雯撕扇**:底层反抗的悲剧缩影

---

### **四、主题思想与象征**
1. **封建末世危机**  
   - 四大家族“一荣俱荣,一损俱损”,揭露官僚腐败与阶级压迫。

2. **人性与命运**  
   - **判词隐喻**:如黛玉“玉带林中挂”(才华被埋没)、宝钗“金簪雪里埋”(婚姻冰冷)  
   - **大观园象征**:理想世界 vs 现实牢笼

3. **宗教与哲学**  
   - 宝玉“赤条条来去无牵挂”体现佛家空幻观。

---

### **五、知识图谱构建建议**
1. **节点分类**  
   - **人物**:按家族、身份(主子/丫鬟)、性格标签(叛逆/守旧)划分。  
   - **事件**:标记时间线(如“元春省亲-第18回”)及关联人物。  
   - **地点**:大观园各居所(潇湘馆、怡红院等)映射人物命运。

2. **关系类型**  
   - 血缘/婚姻/敌对/主仆/情感(如宝黛“灵魂伴侣”、熙凤与尤二姐“迫害”)。

3. **数据来源**  
   - 参考判词(第五回)、人物关系图(附录于多篇资料)。

---

此总结可进一步拆解为人物关系图、事件时间轴、主题关键词网络等子图谱。若需完整人物关系模板或判词解析,可参考网页的思维导图及判词列表。
-------------------------
总字符数: 1824
大致词数: 130
-------------------------
# --- 分块配置 ---
chunk_size = 150# 每个块的词数(根据需要调整)
overlap = 30     # 重叠的词数(必须小于 chunk_size)

print(f"分块大小设置为: {chunk_size} 词")
print(f"重叠词数设置为: {overlap} 词")

# --- 基本验证 ---
if overlap >= chunk_size and chunk_size > 0:
    print(f"错误:重叠词数 ({overlap}) 必须小于分块大小 ({chunk_size})。")
    # 在实际脚本中,这里应该引发错误或退出
    # raise SystemExit("分块配置错误。")
分块大小设置为: 150 词
重叠词数设置为: 30 词
import jieba
import string
words = list(jieba.cut(unstructured_text))
# 过滤空白和标点
words = [w for w in words if w.strip() and w not in string.punctuation]
total_words = len(words)

print(f"文本被分割成 {total_words} 个词。")
# 可视化前 20 个词
print(f"前 20 个词: {words[:20]}")

### 预期输出 ###
# 文本被分割成 324 个词。
# 前 20 个词: ['Marie', 'Curie,', 'born', 'Maria', 'Skłodowska', 'in', 'Warsaw,', 'Poland,', 'was', 'a', 'pioneering', 'physicist', 'and', 'chemist.', 'She', 'conducted', 'groundbreaking', 'research', 'on', 'radioactivity.']
Building prefix dict from the default dictionary ...
Dumping model to file cache /var/folders/rd/gmbvmbl170n8vqsv5hyb_ym00000gn/T/jieba.cache
Loading model cost 0.321 seconds.
Prefix dict has been built successfully.

文本被分割成 709 个词。
前 20 个词: ['以下', '是', '适合', '构建', '《', '红楼梦', '》', '知识', '图谱', '的', '万字', '内', '结构化', '总结', ',', '涵盖', '核心人物', '、', '家族', '关系']
chunks = []
start_index = 0
chunk_number = 1

print(f"开始分块处理...")

while start_index < total_words:
    end_index = min(start_index + chunk_size, total_words)
    chunk_text = " ".join(words[start_index:end_index])
    chunks.append({"text": chunk_text, "chunk_number": chunk_number})

    # print(f"  已创建块 {chunk_number}: 词语 {start_index} 到 {end_index-1}") # 取消注释以查看详细日志

    # 计算下一个块的起始索引
    next_start_index = start_index + chunk_size - overlap

    # 确保处理有进展
    if next_start_index <= start_index:
        if end_index == total_words:
             break# 已经处理完最后一部分
        # 如果没有进展且未到末尾,则至少前进一个词
        next_start_index = start_index + 1

    start_index = next_start_index
    chunk_number += 1

    # 安全中断(可选)
    if chunk_number > total_words: # 简单的安全措施
        print("警告:分块循环次数超过总词数,已中断。")
        break

print(f"\n文本成功分割成 {len(chunks)} 个块。")
开始分块处理...

文本成功分割成 6 个块。
print("--- 块详情 ---")
if chunks:
    # 创建 DataFrame 以便更好地可视化
    chunks_df = pd.DataFrame(chunks)
    chunks_df['word_count'] = chunks_df['text'].apply(lambda x: len(x.split()))
    # 在 Jupyter 环境中,display() 会以更美观的表格形式显示 DataFrame
    # 如果在普通 Python 脚本中,可以使用 print(chunks_df[['chunk_number', 'word_count', 'text']])
    try:
        display(chunks_df[['chunk_number', 'word_count', 'text']])
    except NameError: # 'display' 可能未定义在非 Jupyter 环境
        print(chunks_df[['chunk_number', 'word_count', 'text']])
else:
    print("没有创建任何块(文本可能短于分块大小)。")
print("-" * 25)
--- 块详情 ---
chunk_number word_count text
0 1 150 以下 是 适合 构建 《 红楼梦 》 知识 图谱 的 万字 内 结构化 总结 , 涵盖 核心...
1 2 150 ) 、 贾迎春 ( 庶出 ) 核心 事件 : 元春 省亲 建 大观园 、 王熙凤 协理 宁国...
2 3 150 、 袭人 ( 贴身 丫鬟 ) 2 林黛玉 身份 : 绛 珠 仙草 转世 , 贾敏 之 女 ,...
3 4 150 探春 : 精明 志高 , 改革 大观园 承包制 , 远嫁 和亲 晴雯 : 率真 叛逆 , 病...
4 5 150 悲剧 缩影 --- ### 四 、 主题思想 与 象征 1 封建 末世 危机 四大家族 “ ...
5 6 109 省亲 第 18 回 ” ) 及 关联 人物 。 地点 : 大观园 各 居所 ( 潇湘 馆 、...
-------------------------
# --- System Prompt: Sets the context/role for the LLM ---
extraction_system_prompt = """
You are an AI expert specialized in knowledge graph extraction.
Your task is to identify and extract factual Subject-Predicate-Object (SPO) triples from the given text.
Focus on accuracy and adhere strictly to the JSON output format requested in the user prompt.
Extract core entities and the most direct relationship.
"""

# --- User Prompt Template: Contains specific instructions and the text ---
extraction_user_prompt_template = """
Please extract Subject-Predicate-Object (S-P-O) triples from the text below.

**VERY IMPORTANT RULES:**
1.  **Output Format:** Respond ONLY with a single, valid JSON array. Each element MUST be an object with keys "subject", "predicate", "object".
2.  **JSON Only:** Do NOT include any text before or after the JSON array (e.g., no 'Here is the JSON:' or explanations). Do NOT use markdown ```json ... ``` tags.
3.  **Concise Predicates:** Keep the 'predicate' value concise (1-3 words, ideally 1-2). Use verbs or short verb phrases (e.g., 'discovered', 'was born in', 'won').
4.  **Lowercase:** ALL values for 'subject', 'predicate', and 'object' MUST be lowercase.
5.  **Pronoun Resolution:** Replace pronouns (she, he, it, her, etc.) with the specific lowercase entity name they refer to based on the text context (e.g., 'marie curie').
6.  **Specificity:** Capture specific details (e.g., 'nobel prize in physics' instead of just 'nobel prize' if specified).
7.  **Completeness:** Extract all distinct factual relationships mentioned.

**Text to Process:**

{text_chunk}
"""
print("--- 系统提示 ---")
print(extraction_system_prompt)
print("\n" + "-" * 25 + "\n")

print("--- 用户提示模板(结构) ---")
# 显示结构,替换占位符以便更清晰
print(extraction_user_prompt_template.replace("{text_chunk}", "[... 文本块放在这里 ...]"))
print("\n" + "-" * 25 + "\n")

# 显示将为第一个块发送的 *实际* 提示示例
print("--- 填充后的用户提示示例(针对块 1) ---")
if chunks:
    example_filled_prompt = extraction_user_prompt_template.format(text_chunk=chunks[0]['text'])
    # 为简洁起见,仅显示一部分
    print(example_filled_prompt[:600] + "\n[... 块文本的其余部分 ...]\n" + example_filled_prompt[-200:])
else:
    print("没有可用的块来创建填充后的提示示例。")
print("\n" + "-" * 25)

#### 预期输出 ####
# --- 系统提示 ---
# 你是一位专门从事知识图谱提取的 AI 专家... (完整系统提示)
# -------------------------
#
# --- 用户提示模板(结构) ---
# 请从以下文本中提取主谓宾 (S-P-O) 三元组。
# **非常重要的规则:**
# [... 规则打印在这里 ...]
# **待处理文本:**
# [... 文本块放在这里 ...]
# **你的 JSON 输出:**
# -------------------------
#
# --- 填充后的用户提示示例(针对块 1) ---
# 请从以下文本中提取主谓宾 (S-P-O) 三元组。
# ... (规则) ...
# **待处理文本:**
# Marie Curie, born Maria Skłodowska in Warsaw, Poland, was a pioneering physicist and chemist.
# She conducted groundbreaking research on radioactivity. Together with her husband, Pierre Curie,
# she discovered the elements polonium and radium. Marie Curie was the first woman to win a Nobel Prize,
# the first person and only woman to win the Nobel Prize twice, and the only person to win the Nobel Prize
# in two different scientific fields. She won the Nobel Prize in Physics in 1903 with Pierre Curie
# and Henri Becquerel. Later, she won the Nobel Prize in Chemistry in 1911 for her work on radium and
# polonium. During World War I, she developed mobile radiography units, known as 'petites Curies',
# [... 块文本的其余部分 ...]
# **你的 JSON 输出:**
# -------------------------
--- 系统提示 ---

You are an AI expert specialized in knowledge graph extraction.
Your task is to identify and extract factual Subject-Predicate-Object (SPO) triples from the given text.
Focus on accuracy and adhere strictly to the JSON output format requested in the user prompt.
Extract core entities and the most direct relationship.

--- 用户提示模板(结构) ---

Please extract Subject-Predicate-Object (S-P-O) triples from the text below.

**VERY IMPORTANT RULES:**
1.  **Output Format:** Respond ONLY with a single, valid JSON array. Each element MUST be an object with keys "subject", "predicate", "object".
2.  **JSON Only:** Do NOT include any text before or after the JSON array (e.g., no 'Here is the JSON:' or explanations). Do NOT use markdown ``json ... `` tags.
3.  **Concise Predicates:** Keep the 'predicate' value concise (1-3 words, ideally 1-2). Use verbs or short verb phrases (e.g., 'discovered', 'was born in', 'won').
4.  **Lowercase:** ALL values for 'subject', 'predicate', and 'object' MUST be lowercase.
5.  **Pronoun Resolution:** Replace pronouns (she, he, it, her, etc.) with the specific lowercase entity name they refer to based on the text context (e.g., 'marie curie').
6.  **Specificity:** Capture specific details (e.g., 'nobel prize in physics' instead of just 'nobel prize' if specified).
7.  **Completeness:** Extract all distinct factual relationships mentioned.

**Text to Process:**

[... 文本块放在这里 ...]

--- 填充后的用户提示示例(针对块 1) ---

Please extract Subject-Predicate-Object (S-P-O) triples from the text below.

**VERY IMPORTANT RULES:**
1.  **Output Format:** Respond ONLY with a single, valid JSON array. Each element MUST be an object with keys "subject", "predicate", "object".
2.  **JSON Only:** Do NOT include any text before or after the JSON array (e.g., no 'Here is the JSON:' or explanations). Do NOT use markdown ``json ... `` tags.
3.  **Concise Predicates:** Keep the 'predicate' value concise (1-3 words, ideally 1-2). Use verbs or short verb phrases (e.g., 'discovered', 'was born in', 'won').
4.  **Lowercase:** ALL
[... 块文本的其余部分 ...]
夫人 ) 、 贾政 ( 妻 王夫人 ) 、 贾敏 ( 嫁 林如海 ) 贾政 子女 : 贾珠 ( 早逝 ) 、 贾元春 ( 贵妃 ) 、 贾宝玉 、 贾 探春 ( 庶出 ) 、 贾环 ( 庶出 ) 贾赦 子女 : 贾琏 ( 妻 王熙凤 ) 、 贾迎春 ( 庶出 ) 核心 事件 : 元春 省亲 建 大观园 、 王熙凤 协理 宁国府 、 贾府 被 抄家 2 王家 王夫人 ( 贾政妻 ) 、 薛姨妈 (

# 初始化列表以存储结果和失败记录
all_extracted_triples = []
failed_chunks = []

# 假设 client 已经根据之前的 'Configuring Our LLM Connection' 部分正确初始化
# 例如: client = openai.OpenAI(api_key=api_key, base_url=base_url)
# 为了代码可运行性,这里添加一个简单的 client 初始化(需要替换为实际的)
try:
    client = openai.OpenAI(api_key=api_key, base_url=base_url)
except Exception as e:
    print(f"无法初始化 OpenAI 客户端: {e}")
    print("请确保 API 密钥和基础 URL 已正确设置。")
    client = None# 标记 client 无效

print(f"开始从 {len(chunks)} 个块中提取三元组,使用模型 '{llm_model_name}'...")
# 我们将在接下来的单元格中逐一处理块。
开始从 6 个块中提取三元组,使用模型 'mistral-small:24b'...
if client and chunks:  # 确保 client 有效且 chunks 不为空
    for chunk_index, chunk in enumerate(chunks):
        print(f"\n--- 正在处理块 {chunk['chunk_number']}/{len(chunks)} ---")
        prompt = extraction_user_prompt_template.format(text_chunk=chunk['text'])

        raw_response = None  # 初始化原始响应变量
        parsed_data = None  # 初始化解析后的数据变量
        triples_in_chunk = []  # 初始化当前块的三元组列表

        try:
            print("1. 格式化用户提示...")
            print("2. 向 LLM 发送请求...")

            res = client.chat.completions.create(
                model=llm_model_name,
                messages=[
                    {"role": "system", "content": extraction_system_prompt},
                    {"role": "user", "content": prompt}
                ],
                temperature=llm_temperature,
                max_tokens=llm_max_tokens,
                response_format={"type": "json_object"},
            )

            print("   LLM 响应已接收。")
            print("3. 提取原始响应内容...")
            raw_response = res.choices[0].message.content.strip()

            print(f"\n--- 原始 LLM 输出 (块 {chunk['chunk_number']}) ---")
            print(raw_response)
            print("-" * 15)

            print("\n4. 尝试从响应中解析 JSON...")

            try:
                parsed_data = json.loads(raw_response)

                if isinstance(parsed_data, dict):
                    potential_list = next(
                        (v for v in parsed_data.values() if isinstance(v, list)),
                        None
                    )
                    if potential_list is not None:
                        parsed_data = potential_list
                    else:
                        if all(k in parsed_data for k in ["subject", "predicate", "object"]):
                            parsed_data = [parsed_data]
                        else:
                            print("   警告:收到字典,但既不是三元组也不是包含三元组列表的包装器。")
                            parsed_data = []

                if not isinstance(parsed_data, list):
                    print(f"   警告:解析结果不是列表,而是 {type(parsed_data)}。尝试查找列表。")
                    parsed_data = []

                print(f"   成功解析 JSON 列表(或将其转换/找到)。包含 {len(parsed_data)} 个项目。")

            except json.JSONDecodeError as json_e:
                print(f"   直接 JSON 解析失败: {json_e}")
                print("   尝试使用正则表达式提取 JSON 数组...")

                match = re.search(r'\[.*?\]', raw_response, re.DOTALL)
                if match:
                    try:
                        parsed_data = json.loads(match.group(0))
                        print(f"   通过正则表达式成功提取并解析了 JSON 列表。包含 {len(parsed_data)} 个项目。")
                    except json.JSONDecodeError as regex_json_e:
                        print(f"   从正则表达式匹配中解析 JSON 失败: {regex_json_e}")
                        parsed_data = []
                else:
                    print("   未找到符合 [...] 格式的 JSON 数组。")
                    parsed_data = []

            print("\n5. 验证结构并提取三元组...")

            if isinstance(parsed_data, list):
                valid_triples_count = 0

                for item in parsed_data:
                    if (
                        isinstance(item, dict)
                        and all(k in item and isinstance(item[k], str) for k in ["subject", "predicate", "object"])
                    ):
                        item_with_chunk = dict(item, chunk=chunk['chunk_number'])
                        triples_in_chunk.append(item_with_chunk)
                        valid_triples_count += 1
                    else:
                        print(f"   警告:跳过无效项目:{item}")

                print(f"   在此块中找到 {valid_triples_count} 个有效三元组。")

                if triples_in_chunk:
                    try:
                        print(f"   --- 提取的有效三元组 (块 {chunk['chunk_number']}) ---")
                        display(pd.DataFrame(triples_in_chunk))
                    except NameError:
                        print(pd.DataFrame(triples_in_chunk))

                    all_extracted_triples.extend(triples_in_chunk)

            else:
                print("   未能获取有效的 JSON 列表,无法提取三元组。")
                failed_chunks.append({
                    'chunk_number': chunk['chunk_number'],
                    'error': '未能解析出有效的 JSON 列表',
                    'response': raw_response
                })

        except Exception as e:
            print(f"处理块 {chunk['chunk_number']} 时发生错误:{e}")
            failed_chunks.append({
                'chunk_number': chunk['chunk_number'],
                'error': str(e),
                'response': raw_response or '请求失败,无响应'
            })

        # 打印当前累计结果
        print(f"\n--- 当前累计提取的三元组总数: {len(all_extracted_triples)} ---")
        print(f"--- 到目前为止失败的块数: {len(failed_chunks)} ---")
        print(f"\n完成处理块 {chunk['chunk_number']}。")

elif not client:
    print("错误:LLM 客户端未初始化。无法处理块。")

else:
    print("没有可处理的块。")
--- 正在处理块 1/6 ---
1. 格式化用户提示...
2. 向 LLM 发送请求...
   LLM 响应已接收。
3. 提取原始响应内容...

--- 原始 LLM 输出 (块 1) ---
{"subject":"贾演","predicate":"has child","object":"贾代化"}
---------------

4. 尝试从响应中解析 JSON...
   成功解析 JSON 列表(或将其转换/找到)。包含 1 个项目。

5. 验证结构并提取三元组...
   在此块中找到 1 个有效三元组。
   --- 提取的有效三元组 (块 1) ---
subject predicate object chunk
0 贾演 has child 贾代化 1


--- 当前累计提取的三元组总数: 1 ---
--- 到目前为止失败的块数: 0 ---

完成处理块 1。

--- 正在处理块 2/6 ---
1. 格式化用户提示...
2. 向 LLM 发送请求...
   LLM 响应已接收。
3. 提取原始响应内容...

--- 原始 LLM 输出 (块 2) ---
{"subject":"jia yingchun","predicate":"is a","object":"regional daughter"}
---------------

4. 尝试从响应中解析 JSON...
   成功解析 JSON 列表(或将其转换/找到)。包含 1 个项目。

5. 验证结构并提取三元组...
   在此块中找到 1 个有效三元组。
   --- 提取的有效三元组 (块 2) ---
subject predicate object chunk
0 jia yingchun is a regional daughter 2


--- 当前累计提取的三元组总数: 2 ---
--- 到目前为止失败的块数: 0 ---

完成处理块 2。

--- 正在处理块 3/6 ---
1. 格式化用户提示...
2. 向 LLM 发送请求...
   LLM 响应已接收。
3. 提取原始响应内容...

--- 原始 LLM 输出 (块 3) ---
{"subject":"林黛玉","predicate":"is the reincarnation of","object":"glazed pearl fairy grass"}
---------------

4. 尝试从响应中解析 JSON...
   成功解析 JSON 列表(或将其转换/找到)。包含 1 个项目。

5. 验证结构并提取三元组...
   在此块中找到 1 个有效三元组。
   --- 提取的有效三元组 (块 3) ---
subject predicate object chunk
0 林黛玉 is the reincarnation of glazed pearl fairy grass 3


--- 当前累计提取的三元组总数: 3 ---
--- 到目前为止失败的块数: 0 ---

完成处理块 3。

--- 正在处理块 4/6 ---
1. 格式化用户提示...
2. 向 LLM 发送请求...
   LLM 响应已接收。
3. 提取原始响应内容...

--- 原始 LLM 输出 (块 4) ---
{"subject":"探春","predicate":"implemented","object":"contract system in great view garden"}
---------------

4. 尝试从响应中解析 JSON...
   成功解析 JSON 列表(或将其转换/找到)。包含 1 个项目。

5. 验证结构并提取三元组...
   在此块中找到 1 个有效三元组。
   --- 提取的有效三元组 (块 4) ---
subject predicate object chunk
0 探春 implemented contract system in great view garden 4


--- 当前累计提取的三元组总数: 4 ---
--- 到目前为止失败的块数: 0 ---

完成处理块 4。

--- 正在处理块 5/6 ---
1. 格式化用户提示...
2. 向 LLM 发送请求...
   LLM 响应已接收。
3. 提取原始响应内容...

--- 原始 LLM 输出 (块 5) ---
{"subject":"daiyu","predicate":"has talent","object":"buried"}
---------------

4. 尝试从响应中解析 JSON...
   成功解析 JSON 列表(或将其转换/找到)。包含 1 个项目。

5. 验证结构并提取三元组...
   在此块中找到 1 个有效三元组。
   --- 提取的有效三元组 (块 5) ---
subject predicate object chunk
0 daiyu has talent buried 5


--- 当前累计提取的三元组总数: 5 ---
--- 到目前为止失败的块数: 0 ---

完成处理块 5。

--- 正在处理块 6/6 ---
1. 格式化用户提示...
2. 向 LLM 发送请求...
   LLM 响应已接收。
3. 提取原始响应内容...

--- 原始 LLM 输出 (块 6) ---
{"subject":"大观园","predicate":"contains","object":"潇湘馆"}
---------------

4. 尝试从响应中解析 JSON...
   成功解析 JSON 列表(或将其转换/找到)。包含 1 个项目。

5. 验证结构并提取三元组...
   在此块中找到 1 个有效三元组。
   --- 提取的有效三元组 (块 6) ---
subject predicate object chunk
0 大观园 contains 潇湘馆 6


--- 当前累计提取的三元组总数: 6 ---
--- 到目前为止失败的块数: 0 ---

完成处理块 6。
# ===== 提取过程总结 (反映单数据块演示后 / 或完整运行后的状态) =====
print(f"\n===== 整体提取总结 =====\n")
print(f"定义的总数据块数: {len(chunks)}")
print(f"已处理 (尝试处理) 的数据块数: {len(chunks)}") # 我们循环遍历的数据块
print(f"所有已处理数据块中提取的有效三元组总数: {len(all_extracted_triples)}")
print(f"API 调用或解析失败的数据块数量: {len(failed_chunks)}")

if failed_chunks:
    print("\n失败数据块详情:")
    failed_df = pd.DataFrame(failed_chunks)
    display(failed_df[['chunk_number', 'error']]) # 清晰展示失败的数据块
    # for failure in failed_chunks:
    #     print(f"  数据块 {failure['chunk_number']}: 错误: {failure['error']}")
print("-" * 25)

# 使用 Pandas 展示所有提取的三元组
print("\n===== 所有提取的三元组 (规范化之前) =====\n")
if all_extracted_triples:
    all_triples_df = pd.DataFrame(all_extracted_triples)
    display(all_triples_df)
else:
    print("未能成功提取任何三元组。")
print("-" * 25)
===== 整体提取总结 =====

定义的总数据块数: 6
已处理 (尝试处理) 的数据块数: 6
所有已处理数据块中提取的有效三元组总数: 6
API 调用或解析失败的数据块数量: 0
-------------------------

===== 所有提取的三元组 (规范化之前) =====
subject predicate object chunk
0 贾演 has child 贾代化 1
1 jia yingchun is a regional daughter 2
2 林黛玉 is the reincarnation of glazed pearl fairy grass 3
3 探春 implemented contract system in great view garden 4
4 daiyu has talent buried 5
5 大观园 contains 潇湘馆 6
-------------------------
normalized_triples = []
seen_triples = set() # 用于跟踪 (subject, predicate, object) 元组
original_count = len(all_extracted_triples)
empty_removed_count = 0
duplicates_removed_count = 0

print(f"开始对 {original_count} 个三元组进行规范化和去重处理...")

#### 输出 ####
开始对 6 个三元组进行规范化和去重处理...
print("正在处理三元组 (展示前 5 个):")

for i, t in enumerate(all_extracted_triples):
    # 提取主语、谓语和宾语;去除首尾空格并转换为小写;如果不是字符串,则设置为空字符串
    s, p, o = [
        t.get(k, '').strip().lower() if isinstance(t.get(k), str) else ''
        for k in ['subject', 'predicate', 'object']
    ]

    # 将谓语中的多个空格替换为单个空格
    p = re.sub(r'\s+', ' ', p)

    # 确保主语、谓语、宾语都不为空
    if all([s, p, o]):
        key = (s, p, o)  # 创建用于检查重复的键

        if key not in seen_triples:  # 如果这个三元组是新的
            normalized_triples.append({
                'subject': s,
                'predicate': p,
                'object': o,
                'source_chunk': t.get('chunk', '?')
            })
            seen_triples.add(key)  # 记录下来,避免重复

            if i < 5:  # 打印前 5 个的处理信息
                print(f"\n#{i + 1}: {key}\n状态: 保留")

        else:  # 如果是重复的
            duplicates_removed_count += 1
            if i < 5:
                print(f"\n#{i + 1}: 重复 - 跳过")

    else:  # 如果清理后有空的部分
        empty_removed_count += 1
        if i < 5:
            print(f"\n#{i + 1}: 无效 - 跳过")

print(f"\n处理完成。总计: {len(all_extracted_triples)}, 保留: {len(normalized_triples)}, 重复: {duplicates_removed_count}, 空值: {empty_removed_count}")
正在处理三元组 (展示前 5 个):

#1: ('贾演', 'has child', '贾代化')
状态: 保留

#2: ('jia yingchun', 'is a', 'regional daughter')
状态: 保留

#3: ('林黛玉', 'is the reincarnation of', 'glazed pearl fairy grass')
状态: 保留

#4: ('探春', 'implemented', 'contract system in great view garden')
状态: 保留

#5: ('daiyu', 'has talent', 'buried')
状态: 保留

处理完成。总计: 6, 保留: 6, 重复: 0, 空值: 0
print(f"\n===== 规范化与去重总结 =====\n")
print(f"原始提取的三元组数量: {original_count}\n")
print(f"因包含空/无效部分而被移除的三元组数量: {empty_removed_count}\n")
print(f"被移除的重复三元组数量: {duplicates_removed_count}\n")
final_count = len(normalized_triples)
print(f"最终唯一的、规范化后的三元组数量: {final_count}\n")
print("-" * 25)

# 使用 Pandas 展示规范化后三元组的样本
print("\n===== 最终规范化后的三元组 =====\n")
if normalized_triples:
    normalized_df = pd.DataFrame(normalized_triples)
    display(normalized_df)
else:
    print("规范化后没有剩余的有效三元组。")
print("-" * 25)

#### 输出 ####
===== 规范化与去重总结 =====

原始提取的三元组数量: 6

因包含空/无效部分而被移除的三元组数量: 0

被移除的重复三元组数量: 0

最终唯一的、规范化后的三元组数量: 6

-------------------------

===== 最终规范化后的三元组 =====
subject predicate object source_chunk
0 贾演 has child 贾代化 1
1 jia yingchun is a regional daughter 2
2 林黛玉 is the reincarnation of glazed pearl fairy grass 3
3 探春 implemented contract system in great view garden 4
4 daiyu has talent buried 5
5 大观园 contains 潇湘馆 6
-------------------------
# 创建一个空的有向图
knowledge_graph = nx.DiGraph()

print("已初始化一个空的 NetworkX DiGraph。")
# 可视化初始空图状态
print("===== 初始图信息 =====\n")
try:
    # 尝试使用较新版本的方法
    print(nx.info(knowledge_graph))
except AttributeError:
    # 兼容不同 NetworkX 版本的备选方法
    print(f"类型: {type(knowledge_graph).__name__}")
    print(f"节点数: {knowledge_graph.number_of_nodes()}")
    print(f"边数: {knowledge_graph.number_of_edges()}")
print("-" * 25)
已初始化一个空的 NetworkX DiGraph。
===== 初始图信息 =====

类型: DiGraph
节点数: 0
边数: 0
-------------------------
print("正在将三元组添加到 NetworkX 图中...")

added_edges_count = 0
update_interval = 10# 打印图信息更新的频率

if not normalized_triples:
    print("警告:没有规范化后的三元组可添加到图中。")
else:
    for i, triple in enumerate(normalized_triples):
        subject_node = triple['subject']
        object_node = triple['object']
        predicate_label = triple['predicate']

        # 添加边时会自动添加节点,但显式调用 add_node 也可以 
        # knowledge_graph.add_node(subject_node)
        # knowledge_graph.add_node(object_node)

        # 添加带有谓语作为 'label' 属性的有向边
        knowledge_graph.add_edge(subject_node, object_node, label=predicate_label)
        added_edges_count += 1

        # ===== 可视化图的增长 =====
        if (i + 1) % update_interval == 0 or (i + 1) == len(normalized_triples):
            print(f"\n===== 添加第 {i+1} 个三元组后的图信息 ===== ({subject_node} -> {object_node})")
            try:
                # 尝试使用较新版本的方法
                print(nx.info(knowledge_graph))
            except AttributeError:
                # 兼容不同 NetworkX 版本的备选方法
                print(f"类型: {type(knowledge_graph).__name__}")
                print(f"节点数: {knowledge_graph.number_of_nodes()}")
                print(f"边数: {knowledge_graph.number_of_edges()}")
            # 对于非常大的图,过于频繁地打印信息可能会很慢,请调整 update_interval。

print(f"\n完成添加三元组。共处理了 {added_edges_count} 条边。")
正在将三元组添加到 NetworkX 图中...

===== 添加第 6 个三元组后的图信息 ===== (大观园 -> 潇湘馆)
类型: DiGraph
节点数: 12
边数: 6

完成添加三元组。共处理了 6 条边。
# ===== 最终图统计 =====
num_nodes = knowledge_graph.number_of_nodes()
num_edges = knowledge_graph.number_of_edges()

print(f"\n===== 最终 NetworkX 图总结 =====\n")
print(f"总唯一节点数 (实体): {num_nodes}")
print(f"总唯一边数 (关系): {num_edges}")

if num_edges != added_edges_count and isinstance(knowledge_graph, nx.DiGraph):
     print(f"注意: 添加了 {added_edges_count} 条边,但图中只有 {num_edges} 条。DiGraph 会覆盖具有相同源节点和目标节点的边。如果需要保留多条相同方向的边,请使用 MultiDiGraph。")

if num_nodes > 0:
    try:
       density = nx.density(knowledge_graph) # 图密度:衡量图的连接紧密程度
       print(f"图密度: {density:.4f}")
       if nx.is_weakly_connected(knowledge_graph): # 弱连通:忽略边的方向,图中所有节点是否都相互可达?
           print("该图是弱连通的 (忽略方向,所有节点都可达)。")
       else:
           num_components = nx.number_weakly_connected_components(knowledge_graph) # 弱连通分量的数量
           print(f"该图包含 {num_components} 个弱连通分量。")
    except Exception as e:
        print(f"无法计算某些图指标: {e}") # 处理空图或小图可能出现的错误
else:
    print("图为空,无法计算指标。")
print("-" * 25)

# ===== 节点样本 =====
print("\n===== 节点样本 (前 10 个) =====\n")
if num_nodes > 0:
    nodes_sample = list(knowledge_graph.nodes())[:10]
    display(pd.DataFrame(nodes_sample, columns=['节点样本']))
else:
    print("图中没有节点。")

# ===== 边样本 =====
print("\n===== 边样本 (前 10 个,带标签) =====\n")
if num_edges > 0:
    edges_sample = []
    # 提取前10条边及其数据(包括标签)
    for u, v, data in list(knowledge_graph.edges(data=True))[:10]:
        edges_sample.append({'源节点': u, '目标节点': v, '标签': data.get('label', 'N/A')})
    display(pd.DataFrame(edges_sample))
else:
    print("图中没有边。")
print("-" * 25)
===== 最终 NetworkX 图总结 =====

总唯一节点数 (实体): 12
总唯一边数 (关系): 6
图密度: 0.0455
该图包含 6 个弱连通分量。
-------------------------

===== 节点样本 (前 10 个) =====
节点样本
0 贾演
1 贾代化
2 jia yingchun
3 regional daughter
4 林黛玉
5 glazed pearl fairy grass
6 探春
7 contract system in great view garden
8 daiyu
9 buried


===== 边样本 (前 10 个,带标签) =====

源节点 目标节点 标签
0 贾演 贾代化 has child
1 jia yingchun regional daughter is a
2 林黛玉 glazed pearl fairy grass is the reincarnation of
3 探春 contract system in great view garden implemented
4 daiyu buried has talent
5 大观园 潇湘馆 contains
-------------------------
print("准备交互式可视化...")

# ===== 检查图是否有效以进行可视化 =====
can_visualize = False

# 检查变量是否存在,类型是否为 nx.Graph(含 DiGraph、MultiGraph 等子类)
if 'knowledge_graph' not in locals():
    print("错误: 未找到变量 'knowledge_graph'。")
elif not isinstance(knowledge_graph, nx.Graph):
    print("错误: 'knowledge_graph' 不是 NetworkX 图对象。")
elif knowledge_graph.number_of_nodes() == 0:
    print("NetworkX 图为空,无法进行可视化。")
else:
    num_nodes = knowledge_graph.number_of_nodes()
    print(f"图有效,可用于可视化,共包含 {num_nodes} 个节点。")
    can_visualize = True
准备交互式可视化...
图有效,可用于可视化,共包含 12 个节点。
import json

cytoscape_nodes = []
cytoscape_edges = []

if can_visualize:
    print("正在转换节点...")

    # 计算节点度数,用于调整节点大小
    node_degrees = dict(knowledge_graph.degree())
    max_degree = max(node_degrees.values()) if node_degrees else 1  # 避免除以零

    for node_id in knowledge_graph.nodes():
        degree = node_degrees.get(node_id, 0)
        node_size = 15 + (degree / max_degree) * 50 if max_degree > 0 else 15

        cytoscape_nodes.append({
            'data': {
                'id': str(node_id),  # ID 必须为字符串
                'label': str(node_id).replace(' ', '\n'),  # 可读性更强的显示标签
                'degree': degree,
                'size': node_size,
                'tooltip_text': f"实体: {str(node_id)}\n度数: {degree}"
            }
        })

    print(f"已转换 {len(cytoscape_nodes)} 个节点。")

    print("正在转换边...")

    edge_count = 0
    for u, v, data in knowledge_graph.edges(data=True):
        edge_id = f"edge_{edge_count}"
        predicate_label = data.get('label', '')

        cytoscape_edges.append({
            'data': {
                'id': edge_id,
                'source': str(u),
                'target': str(v),
                'label': predicate_label,
                'tooltip_text': f"关系: {predicate_label}"
            }
        })

        edge_count += 1

    print(f"已转换 {len(cytoscape_edges)} 条边。")

    # 组合成 Cytoscape 使用的数据结构
    cytoscape_graph_data = {
        'nodes': cytoscape_nodes,
        'edges': cytoscape_edges
    }

    # 输出样本数据用于检查
    print("\n===== Cytoscape 节点数据样本 (前 2 个) =====\n")
    print(json.dumps(cytoscape_graph_data['nodes'][:2], indent=2, ensure_ascii=False))

    print("\n===== Cytoscape 边数据样本 (前 2 个) =====\n")
    print(json.dumps(cytoscape_graph_data['edges'][:2], indent=2, ensure_ascii=False))

    print("-" * 40)

else:
    print("由于图无效,跳过数据转换。")
    cytoscape_graph_data = {'nodes': [], 'edges': []}
正在转换节点...
已转换 12 个节点。
正在转换边...
已转换 6 条边。

===== Cytoscape 节点数据样本 (前 2 个) =====

[
  {
    "data": {
      "id": "贾演",
      "label": "贾演",
      "degree": 1,
      "size": 65.0,
      "tooltip_text": "实体: 贾演\n度数: 1"
    }
  },
  {
    "data": {
      "id": "贾代化",
      "label": "贾代化",
      "degree": 1,
      "size": 65.0,
      "tooltip_text": "实体: 贾代化\n度数: 1"
    }
  }
]

===== Cytoscape 边数据样本 (前 2 个) =====

[
  {
    "data": {
      "id": "edge_0",
      "source": "贾演",
      "target": "贾代化",
      "label": "has child",
      "tooltip_text": "关系: has child"
    }
  },
  {
    "data": {
      "id": "edge_1",
      "source": "jia yingchun",
      "target": "regional daughter",
      "label": "is a",
      "tooltip_text": "关系: is a"
    }
  }
]
----------------------------------------
from ipycytoscape import CytoscapeWidget

# 创建 Cytoscape 可视化控件
cyto = CytoscapeWidget()

# 自定义样式(可选)
cyto.set_style([
    {
        'selector': 'node',
        'style': {
            'label': 'data(label)',
            'background-color': '#1f77b4',
            'width': 'data(size)',
            'height': 'data(size)',
            'font-size': 10,
            'text-valign': 'center',
            'text-halign': 'center',
            'color': '#ffffff'
        }
    },
    {
        'selector': 'edge',
        'style': {
            'label': 'data(label)',
            'curve-style': 'bezier',
            'target-arrow-shape': 'triangle',
            'line-color': '#ccc',
            'target-arrow-color': '#ccc',
            'font-size': 8,
            'text-rotation': 'autorotate'
        }
    }
])

# 清空旧图(可选)
cyto.graph.clear()

# 加载转换好的图数据
cyto.graph.add_graph_from_json(cytoscape_graph_data)

# 渲染可视化图谱
cyto

发表回复

您的电子邮箱地址不会被公开。