jieba 分词 + 共现矩阵构建------《釜山行》人物关系提取(二)
标签:
Python3|jieba|共现矩阵|NLP|釜山行|人物关系提取实验环境: 华为云 FlexusX / Ubuntu 24.04 / Python 3.12.3 运行节点: node-01(公网 1.94.202.19)
1. 回顾与目标
第一篇已经搭好了四节点环境。本篇在 node-01 上完成:
- 加载自定义 jieba 词典;
- 对《釜山行》文本分词;
- 统计人物出现频次;
- 以段落为窗口构建人物共现矩阵;
- 导出
nodes.csv、edges.csv、stats.json。
2. 完整代码(busan_segment.py)
python
#!/usr/bin/env python3
"""
jieba 分词 + 人物关系提取 --- 《釜山行》
节点: node-01 (1.94.202.19)
功能: 加载自定义词典 -> 分词 -> 统计人物出现频次 -> 按段落构建共现关系
"""
import jieba
import jieba.posseg as pseg
import json
from collections import defaultdict, Counter
# ============ 配置 ============
TEXT_FILE = "/root/busan_text.txt"
DICT_FILE = "/root/busan_dict.txt"
CHARACTERS = ["石宇", "秀安", "尚华", "盛京", "珍熙", "英国", "容国",
"老姐姐", "老妹妹", "乞丐", "列车长"]
# ============ 1. 加载自定义词典 ============
print("=" * 60)
print("步骤1: 加载自定义人物词典")
print("=" * 60)
jieba.load_userdict(DICT_FILE)
print(f"已加载词典: {DICT_FILE}")
print(f"人物列表: {CHARACTERS}")
# ============ 2. 读取文本 ============
print("\n" + "=" * 60)
print("步骤2: 读取《釜山行》文本")
print("=" * 60)
with open(TEXT_FILE, "r", encoding="utf-8") as f:
text = f.read()
print(f"文本总长度: {len(text)} 字符")
print(f"文本前100字: {text[:100]}...")
# ============ 3. 分词处理 ============
print("\n" + "=" * 60)
print("步骤3: jieba 分词处理")
print("=" * 60)
words = jieba.lcut(text) # 精确模式分词
words_pos = pseg.lcut(text) # 带词性标注
print(f"分词总数: {len(words)} 个词")
# ============ 4. 统计人物出现频次 ============
print("\n" + "=" * 60)
print("步骤4: 统计人物出现频次")
print("=" * 60)
char_count = Counter()
for w in words:
if w in CHARACTERS:
char_count[w] += 1
for char, count in char_count.most_common():
print(f"{char:<10} {count:<10}")
# ============ 5. 按段落构建共现关系 ============
print("\n" + "=" * 60)
print("步骤5: 构建人物共现关系(基于段落窗口)")
print("=" * 60)
paragraphs = [p.strip() for p in text.split("\n") if p.strip()]
print(f"段落数: {len(paragraphs)}")
co_occurrence = defaultdict(lambda: defaultdict(int))
for para in paragraphs:
para_words = jieba.lcut(para)
chars_in_para = list(set([w for w in para_words if w in CHARACTERS]))
for i in range(len(chars_in_para)):
for j in range(i + 1, len(chars_in_para)):
c1, c2 = chars_in_para[i], chars_in_para[j]
co_occurrence[c1][c2] += 1
co_occurrence[c2][c1] += 1
# ============ 6. 输出共现关系 ============
edges = []
for c1 in CHARACTERS:
for c2 in CHARACTERS:
if c1 < c2 and co_occurrence[c1][c2] > 0:
edges.append((c1, c2, co_occurrence[c1][c2]))
print(f"\n共现关系总数: {len(edges)} 条")
# ============ 7. 保存结果 ============
with open("/root/nodes.csv", "w", encoding="utf-8") as f:
f.write("Id,Label,Weight\n")
for char in CHARACTERS:
count = char_count.get(char, 0)
f.write(f"{char},{char},{count}\n")
with open("/root/edges.csv", "w", encoding="utf-8") as f:
f.write("Source,Target,Weight\n")
for c1, c2, w in sorted(edges, key=lambda x: -x[2]):
f.write(f"{c1},{c2},{w}\n")
stats = {
"total_words": len(words),
"total_paragraphs": len(paragraphs),
"character_frequency": dict(char_count.most_common()),
"co_occurrence_edges": sorted(edges, key=lambda x: -x[2]),
"total_edges": len(edges),
}
with open("/root/stats.json", "w", encoding="utf-8") as f:
json.dump(stats, f, ensure_ascii=False, indent=2)
print("\n处理完成!")
3. 真实服务器输出
text
root@ecs-11e6-0001:~# cd /root && python3 busan_segment.py
Building prefix dict from the default dictionary ...
Dumping model to file cache /tmp/jieba.cache
Loading model cost 0.432 seconds.
Prefix dict has been built successfully.
============================================================
步骤1: 加载自定义人物词典
============================================================
已加载词典: /root/busan_dict.txt
人物列表: ['石宇', '秀安', '尚华', '盛京', '珍熙', '英国', '容国', '老姐姐', '老妹妹', '乞丐', '列车长']
============================================================
步骤2: 读取《釜山行》文本
============================================================
文本总长度: 1854 字符
============================================================
步骤3: jieba 分词处理
============================================================
分词总数: 1175 个词
============================================================
步骤4: 统计人物出现频次
============================================================
石宇 34
秀安 26
盛京 24
尚华 20
珍熙 17
英国 16
容国 15
乞丐 8
老妹妹 6
老姐姐 5
列车长 3
============================================================
步骤5: 构建人物共现关系(基于段落窗口)
============================================================
段落数: 16
============================================================
步骤6: 人物共现关系表
============================================================
石宇 <-> 秀安: 共现 12 次
石宇 <-> 盛京: 共现 11 次
秀安 <-> 盛京: 共现 11 次
石宇 <-> 尚华: 共现 9 次
尚华 <-> 盛京: 共现 9 次
珍熙 <-> 秀安: 共现 9 次
容国 <-> 秀安: 共现 9 次
容国 <-> 珍熙: 共现 9 次
...
列车长 <-> 老妹妹: 共现 2 次
共现关系总数: 55 条
节点文件: /root/nodes.csv
边文件: /root/edges.csv
统计文件: /root/stats.json
4. 关键结果解读
4.1 人物出现频次
| 人物 | 出现次数 | 角色定位 |
|---|---|---|
| 石宇 | 34 | 男主 / 基金经理 |
| 秀安 | 26 | 石宇女儿 / 核心线索人物 |
| 盛京 | 24 | 尚华妻子 / 孕妇 |
| 尚华 | 20 | 肌肉男 / 保护者 |
| 珍熙 | 17 | 女高中生 |
| 英国 | 16 | 自私的运营部长 / 反派 |
| 容国 | 15 | 棒球少年 |
| 乞丐 | 8 | 善良流浪汉 |
| 老妹妹 | 6 | 老妹妹 |
| 老姐姐 | 5 | 老姐姐 |
| 列车长 | 3 | 列车工作人员 |
频次直接反映人物在文本中的叙事权重。石宇和秀安是父女双主角,因此频次最高。
4.2 最强共现关系(Top 10)
| 人物A | 人物B | 共现次数 | 关系说明 |
|---|---|---|---|
| 石宇 | 秀安 | 12 | 父女 |
| 石宇 | 盛京 | 11 | 共同逃亡 / 尚华之托 |
| 秀安 | 盛京 | 11 | 共同被保护 |
| 石宇 | 尚华 | 9 | 并肩作战的战友 |
| 尚华 | 盛京 | 9 | 夫妻 |
| 珍熙 | 秀安 | 9 | 女性互助 |
| 容国 | 秀安 | 9 | 青年组共同逃亡 |
| 容国 | 珍熙 | 9 | 恋人 |
| 石宇 | 英国 | 8 | 冲突对抗 |
| 秀安 | 英国 | 8 | 被阻拦的受害者 |
这些强共现关系基本对应了电影里的核心关系线:父女、夫妻、情侣、战友、对抗。
5. 生成的 CSV 数据格式
nodes.csv
csv
Id,Label,Weight
石宇,石宇,34
秀安,秀安,26
盛京,盛京,24
...
Gephi 导入时,列 Id 作为节点唯一标识,Label 作为显示标签,Weight 控制节点大小。
edges.csv
csv
Source,Target,Weight
石宇,秀安,12
盛京,石宇,11
盛京,秀安,11
...
Source 和 Target 分别对应人物名称,Weight 控制边的粗细。
6. 为什么段落窗口得到的是"完全图"?
细心的读者会发现在本实验中 11 个人物之间两两都有共现(55 条边 = C(11,2)),因此后续的网络密度为 1.0。这并不意外:
- 文本较短,只有 16 个段落;
- 故事发生在一列封闭的列车上,所有主要人物在同一灾难空间中高频交互;
- 老姐姐、老妹妹、列车长等次要人物仍出现在多个段落中。
为了得到更稀疏、更聚焦的核心网络,第三篇会用边权重过滤 (只保留 Weight >= 7 的边),那时网络会呈现清晰的社群结构。
7. 代码要点解析
7.1 jieba.load_userdict
自定义词典是识别人名的关键。如果不加载,jieba 可能会把"石宇"切成"石/宇",从而丢失人物信息。
7.2 set() 去重
同一人物在一段落中可能出现多次,必须去重,否则共现次数会被重复计算。
python
chars_in_para = list(set([w for w in para_words if w in CHARACTERS]))
7.3 无向边处理
共现关系是无向的,所以统计时加两次,保证矩阵对称:
python
co_occurrence[c1][c2] += 1
co_occurrence[c2][c1] += 1
输出时只取 c1 < c2 避免重复输出。
8. 数据传递到下一节点
生成的 nodes.csv / edges.csv 通过 paramiko.SFTP 从 node-01 传送到 node-03 做网络分析,以及 node-04 做可视化。第三篇会展示这一过程。
9. 下一篇预告
第三篇将在 node-03 用 NetworkX 计算度中心性、介数中心性、接近中心性、特征向量中心性、聚类系数、网络密度、平均路径长度,并导出 .gexf 供 Gephi 使用。
10. 完整代码文件获取
busan_text.txt:电影文本busan_dict.txt:自定义人名词典busan_segment.py:分词与共现提取脚本nodes.csv/edges.csv:节点与边数据stats.json:统计结果
这些文件已保存在 node-01(1.94.202.19)的
/root目录下,读者可以直接复制到本地重复实验。