Initial: integrated 2025 LawGraph (graphrag_pipeline) + 2026 kg_project

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 10:13:39 +08:00
commit 6c1a69af0d
83 changed files with 14295 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.venv/
__pycache__/
*.pyc
*.pyo
config/api_keys.yaml
output/
logs/
*.log
.DS_Store
Thumbs.db
@@ -0,0 +1,122 @@
llm:
model: "deepseek-chat"
temperature: 0.0
max_tokens: 8192
max_retries: 3
retry_delay: 2
request_timeout: 120
batch_timeout: 600
batch_processing:
batch_size: 10
concurrent_requests: 5
test_batch_size: 5
incremental_save: true
deduplication:
similarity_threshold: 0.85
enable_fuzzy_matching: true
text_normalization: true
validation:
enable_entity_validation: true
enable_relationship_validation: true
min_confidence_score: 0.7
strict_mode: false
filter_generic_concepts: true
output:
nodes_file: "output/nodes_llm_v2.csv"
relationships_file: "output/rels_llm_v2.csv"
report_file: "output/extraction_report_v2.md"
encoding: "utf-8-sig"
progress:
progress_file: "output/progress_v2.json"
checkpoint_interval: 10
logging:
log_file: "logs/deep_extraction_v2.log"
level: "INFO"
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# ============================================================
# 第二轮本体:城市更新法规政策工具箱 (21实体 + 34关系)
# ============================================================
entity_type_prompts:
Agency: "主体机构:发布主体、实施主体、监管主体、责任主体,包括政府部门(全国人大常委会、国务院、自然资源部、住建部、生态环境部、发改委、地方政府)、实施机构、平台公司、社区组织、第三方机构等。属性:level(国家/省级/市级/县级/乡镇街道/社区)、agency_type(立法机关/行政机关/主管部门/实施主体/监管主体/责任主体/市场主体/社区组织/第三方机构)"
LegalObject: "法规事项:法规政策所规制的事项,回答'管什么事项'。如规划编制、土地利用、建设管理、历史文化保护、生态保护、财政支持、金融支持、产权登记、公众参与、行政审批、监督管理、城市更新、公共服务、基础设施、安全韧性等。属性:category"
SpatialObject: "空间对象:城市更新中的具体空间对象,回答'作用于什么空间'。如老旧小区、城中村、低效用地、工业遗存、历史文化街区、历史建筑、公共空间、公共服务设施、市政基础设施、存量建筑、建设用地、城市更新单元、生态保护红线、永久基本农田、城镇开发边界等。属性:spatial_type"
RenewalScene: "更新场景:城市更新应用场景,回答'用于什么更新场景'。如老旧小区改造、城中村改造、低效用地再开发、工业遗存更新、历史文化保护更新、完整社区建设、公共空间提升、基础设施补短板、城市安全韧性提升、城市生态修复等。属性:scene_type、derivation_method(原文提取/语义归纳/人工标注/模型推理)"
PolicyTool: "具体政策工具:城市更新中用于实现治理目标的具体政策、制度、措施或治理手段。属性:tool_type(命令型/激励型/劝诫型/能力型/系统变革型)、mandatory_level(强制/引导/鼓励/支持/禁止/限制)、source_text"
ToolCategory: "政策工具类别:归类政策工具的分类节点。属性:classification_system(一般政策工具分类/城市更新工具分类)、urban_renewal_category(规划引导/土地配置/开发激励/财政金融/权益协调/实施管理/监督反馈)"
Procedure: "程序:审批、备案、评估、论证、公示、听证、入库、验收、监督、征收、补偿等行政或治理程序。属性:procedure_type、time_limit"
Obligation: "权利义务:法规规定的权利、义务、禁止或责任。属性:obligation_type(权利/义务/禁止/职责/鼓励/支持)、modality(应当/可以/不得/禁止/鼓励/支持/负责/依法)"
Condition: "适用条件:政策工具或程序适用时的前置条件。如'经批准后''符合XX条件'。属性:condition_type(规划条件/审批条件/权属条件/安全条件/环保条件/公众参与条件/程序条件/资金条件)"
Constraint: "约束条件:对行为设置的边界性、底线性或限制性要求。如'不得突破''严守''控制线'。属性:constraint_type(用途管制/空间边界/强制标准/历史保护/生态保护/权益保护/安全底线/公共利益)"
Penalty: "法律责任:违反法规后的处罚。属性:penalty_type(行政处罚/刑事处罚/民事责任/行政处分/信用惩戒/整改责任)、amount_or_measure"
TimePoint: "时间节点:生效、修订、废止、截止、过渡期等时间信息。属性:time_type(发布日期/生效日期/修订日期/废止日期/截止日期/过渡期)"
Region: "适用区域:全国、省、市、县、特定区域等。属性:level(全国/区域/省/市/县/乡镇街道/社区/特定区域)"
relationship_type_prompts:
# 文档层次
has_chapter: "文档包含章"
has_section: "章包含节"
has_article: "文件/章/节包含条文"
has_clause: "条文包含款"
article_in_document: "条文或款属于某一法规文件"
# 法条依据
cites: "引用其他法规:如'根据《城乡规划法》第X条'、'依据《土地管理法》规定'、'按照国务院有关规定'"
implements: "实施上位法:如'为实施《城乡规划法》制定本办法'、'根据《土地管理法》授权'"
amends: "修正/修订:如'对《XX法》作如下修改'、'将第X条修改为'"
replaces: "替代/废止:如'本法自施行之日起,XX法同时废止'"
supplements: "补充规定:如'在XX基础上补充规定'"
basis_for_planning: "法规或条文作为规划编制、审批、实施或政策工具设立的依据"
# 主体关系
issued_by: "法规文件由某主体发布或制定"
implemented_by: "条文、政策工具、程序或义务由某主体负责实施"
assigned_to: "监管职责、管理职责、实施任务分配给某主体"
# 语义规制
regulates: "条文对某法规事项进行规制(准入、禁止、限制、鼓励、保护等)"
defines_object: "条文对法规事项、空间对象或更新场景进行定义或界定"
applies_to: "条文、程序或政策工具适用于某法规事项、空间对象、更新场景或区域"
sets_obligation: "条文设定权利、义务、禁止、职责或支持性要求"
sets_condition: "条文、政策工具或程序设置适用条件(前置条件)"
sets_constraint: "条文、政策工具或程序设置边界性、底线性或限制性要求"
prescribes_penalty: "条文规定了违法行为的法律后果"
# 政策工具
extracts_tool: "从条文中识别出具体政策工具"
belongs_to_category: "具体政策工具属于某类政策工具类别"
applies_to_scene: "政策工具适用于某类城市更新场景"
targets_object: "政策工具作用于某一法规事项或空间对象"
coordinates_with: "两个政策工具之间存在并列协同或组合使用关系"
supports_tool: "一个政策工具对另一个形成前置支撑或条件支撑"
# 程序
requires_procedure: "条文、政策工具或法规事项要求履行某类程序"
procedure_for: "某一程序对应某类法规事项、空间对象、更新场景或政策工具"
precedes_procedure: "一个程序在时序上先于另一个程序"
# 辅助
governs_region: "法规政策或条文适用于某一区域"
effective_timeline: "法规文件关联时间节点(发布/生效/修订/废止)"
standard_for: "标准、规范或条文适用于某一法规事项、空间对象、程序或政策工具"
article_effective_timeline: "条文关联时间节点(生效/截止/过渡期)"
extraction_tips:
citation_patterns: "注意识别法条引用模式:'根据《XX法》'、'依照第X条规定'、'按照国务院XX规定'、'参照XX标准执行'"
article_classification: "法条类型判断:管理性(应当/必须)、禁止性(不得/禁止)、授权性(可以/有权)、程序性(申请/审批/备案)、处罚性(罚款/责令/没收)"
cross_reference: "特别注意跨文档引用:当条文引用其他法规时,创建cites关系并记录引用上下文"
entity_boundary_rules: |
实体边界规则(严格遵守):
- LegalObject:法规事项,回答"管什么事项",如规划编制、土地利用、财政支持
- SpatialObject:空间对象,回答"作用于什么空间",如老旧小区、城中村、低效用地
- RenewalScene:更新场景,回答"用于什么更新场景",如老旧小区改造、城中村改造
- Procedure:程序,强调流程步骤(审批/备案/评估/公示/验收/入库)
- PolicyTool:政策工具,强调治理手段、政策措施、制度安排或支持机制
- Obligation:权利义务,强调主体应当/可以/不得做什么
- Condition:适用条件,强调前置条件(经批准/符合/满足/具备)
- Constraint:约束条件,强调底线限制(不得突破/严守/控制线/强制性标准)
do_not_extract: |
- 不要新增本体之外的实体类型或关系类型
- 文本中没有依据时不要强行抽取
- conflicts_with和has_gap不纳入本阶段抽取
@@ -0,0 +1,86 @@
project_name: "urban_planning_law_kg"
project_description: "城市规划法律法规知识图谱构建"
data:
source_dir: "E:\\Project\\SI\\2026_KG_PlanningLaw\\data\\城市规划法律法规\\城市更新法规数据库"
total_documents: 296
categories:
- {id: 0, name: "0知识图谱目录", type: "Index"}
- {id: 1, name: "1法律", type: "Law"}
- {id: 2, name: "2行政法规", type: "AdministrativeRegulation"}
- {id: 3, name: "3部门规章", type: "DepartmentalRule"}
- {id: 4, name: "4党中央国务院文件", type: "PolicyDocument"}
- {id: 5, name: "5国家主管部门文件", type: "PolicyDocument"}
- {id: 6, name: "6主要技术标准", type: "PolicyDocument"}
min_text_length: 50
api:
config_file: "config/api_keys.yaml"
model: "deepseek-chat"
max_tokens: 8192
temperature: 0.0
processing:
concurrent_requests: 5
max_retries: 3
retry_delay: 2
batch_size: 10
paths:
config_dir: "config"
data_dir: "data"
output_dir: "output"
logs_dir: "logs"
ontology_dir: "ontology"
scripts_dir: "scripts"
src_dir: "src"
ontology:
ontology_file: "ontology/legal_ontology.json"
entity_ontology_file: "config/legal_ontology.yaml"
neo4j:
uri: "bolt://localhost:7687"
username: "neo4j"
password: "password"
database: "neo4j"
extraction:
entity_types:
- Law
- AdministrativeRegulation
- DepartmentalRule
- PolicyDocument
- Chapter
- Section
- Article
- GovernmentBody
- LegalSubject
- SpatialConcept
- AdministrativeProcedure
- Obligation
- Penalty
- TimePoint
- Region
relation_types:
- cites
- implements
- amends
- replaces
- supplements
- issued_by
- implemented_by
- regulates
- defines_spatial
- requires_procedure
- sets_obligation
- prescribes_penalty
- applies_to
- governs_region
confidence_threshold:
entity: 0.8
relation: 0.7
logging:
level: "INFO"
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
@@ -0,0 +1,246 @@
entity_types:
Law:
prefix: "LAW"
description: "全国人大及其常委会制定的法律"
required_attributes: [name]
optional_attributes:
- full_title
- promulgation_date
- effective_date
- status
- amendment_dates
AdministrativeRegulation:
prefix: "REG"
description: "国务院制定的行政法规"
required_attributes: [name]
optional_attributes:
- full_title
- promulgation_date
- effective_date
- status
- order_number
DepartmentalRule:
prefix: "RULE"
description: "各部委制定的部门规章"
required_attributes: [name]
optional_attributes:
- full_title
- promulgation_date
- effective_date
- status
- order_number
PolicyDocument:
prefix: "DOC"
description: "党中央/国务院/主管部门文件、技术标准"
required_attributes: [name]
optional_attributes:
- full_title
- issue_date
- document_number
- document_type
- status
Chapter:
prefix: "CH"
description: "法律文件的章"
required_attributes: [name, number]
optional_attributes:
- title
Section:
prefix: "SEC"
description: "法律文件的节"
required_attributes: [name, number]
optional_attributes:
- title
Article:
prefix: "ART"
description: "法律文件的具体条文"
required_attributes: [number, text]
optional_attributes:
- article_type
GovernmentBody:
prefix: "GOV"
description: "立法/行政/主管部门"
required_attributes: [name]
optional_attributes:
- full_name
- level
- type
LegalSubject:
prefix: "SUBJ"
description: "法律规制的事项或主体"
required_attributes: [name]
optional_attributes:
- category
- description
SpatialConcept:
prefix: "SPAT"
description: "国土空间规划中的空间概念"
required_attributes: [name]
optional_attributes:
- type
- definition
AdministrativeProcedure:
prefix: "PROC"
description: "规划/用地/建设的行政程序"
required_attributes: [name]
optional_attributes:
- type
- time_limit
- applicable_subjects
Obligation:
prefix: "OBL"
description: "法律条文规定的权利、义务或禁止"
required_attributes: [text]
optional_attributes:
- type
Penalty:
prefix: "PEN"
description: "违反法律规定的处罚"
required_attributes: [type, description]
optional_attributes:
- applicable_violation
- legal_basis
TimePoint:
prefix: "TIME"
description: "法规中提到的具体时间"
required_attributes: [description]
optional_attributes:
- year
- time_type
Region:
prefix: "REGION"
description: "法规适用的区域"
required_attributes: [name]
optional_attributes:
- level
- feature
relationship_types:
has_chapter:
description: "包含章"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule", "PolicyDocument"]
target_types: ["Chapter"]
has_section:
description: "包含节"
source_types: ["Chapter"]
target_types: ["Section"]
contains_article:
description: "包含条"
source_types: ["Chapter", "Section"]
target_types: ["Article"]
article_in_document:
description: "条属于文件"
source_types: ["Article"]
target_types: ["Law", "AdministrativeRegulation", "DepartmentalRule", "PolicyDocument"]
cites:
description: "引用其他法规"
source_types: ["Article", "PolicyDocument"]
target_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
implements:
description: "实施/落实上位法"
source_types: ["PolicyDocument", "DepartmentalRule"]
target_types: ["Law", "AdministrativeRegulation"]
amends:
description: "修正/修订"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
target_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
replaces:
description: "替代/废止"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
target_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
supplements:
description: "补充"
source_types: ["PolicyDocument"]
target_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
issued_by:
description: "发布机关"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule", "PolicyDocument"]
target_types: ["GovernmentBody"]
implemented_by:
description: "实施机关"
source_types: ["Article", "AdministrativeProcedure"]
target_types: ["GovernmentBody"]
regulates:
description: "规制事项"
source_types: ["Article", "Obligation"]
target_types: ["LegalSubject"]
defines_spatial:
description: "界定空间概念"
source_types: ["Article"]
target_types: ["SpatialConcept"]
applies_to:
description: "适用于"
source_types: ["Article", "AdministrativeProcedure"]
target_types: ["LegalSubject"]
sets_obligation:
description: "定义务"
source_types: ["Article"]
target_types: ["Obligation"]
prescribes_penalty:
description: "规定处罚"
source_types: ["Article", "Obligation"]
target_types: ["Penalty"]
requires_procedure:
description: "要求程序"
source_types: ["Article"]
target_types: ["AdministrativeProcedure"]
procedure_for:
description: "程序对应事项"
source_types: ["AdministrativeProcedure"]
target_types: ["LegalSubject"]
governs_region:
description: "管辖区域"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
target_types: ["Region"]
effective_timeline:
description: "生效时间线"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule"]
target_types: ["TimePoint"]
related_to:
description: "相关"
source_types: ["Any"]
target_types: ["Any"]
standard_for:
description: "标准适用于"
source_types: ["PolicyDocument"]
target_types: ["LegalSubject"]
category_index:
description: "分类索引"
source_types: ["Law", "AdministrativeRegulation", "DepartmentalRule", "PolicyDocument"]
target_types: ["LegalSubject"]
+139
View File
@@ -0,0 +1,139 @@
"""
Neo4j数据导入脚本 - 从CSV导入法规知识图谱数据
"""
import pandas as pd
from pathlib import Path
from neo4j import GraphDatabase
import json
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class Neo4jImporter:
"""Neo4j数据导入器"""
def __init__(self, uri: str, username: str, password: str, database: str = 'neo4j'):
self.driver = GraphDatabase.driver(uri, auth=(username, password))
self.database = database
logger.info(f"连接Neo4j: {uri}")
def close(self):
self.driver.close()
def _run_query(self, query, parameters=None):
with self.driver.session(database=self.database) as session:
result = session.run(query, parameters or {})
return [record.data() for record in result]
def create_constraints(self):
"""创建约束和索引"""
schema_file = Path(__file__).parent / 'schema.cypher'
if schema_file.exists():
with open(schema_file, 'r', encoding='utf-8') as f:
content = f.read()
for line in content.split('\n'):
line = line.strip()
if line.startswith('CREATE ') and not line.startswith('//'):
try:
self._run_query(line)
logger.info(f"执行: {line[:60]}...")
except Exception as e:
if 'already exists' not in str(e):
logger.warning(f"约束跳过: {e}")
def import_nodes(self, nodes_csv: str):
"""导入节点"""
nodes_df = pd.read_csv(nodes_csv, encoding='utf-8-sig')
logger.info(f"导入 {len(nodes_df)} 个节点")
# 按类型分批导入
for node_type in nodes_df['type'].unique():
type_df = nodes_df[nodes_df['type'] == node_type]
label = node_type
for _, row in type_df.iterrows():
props = {}
try:
props = json.loads(row.get('properties', '{}'))
except:
pass
props['id'] = row['id']
props['name'] = row['label']
query = f"MERGE (n:{label} {{id: $id}}) SET n += $props"
self._run_query(query, {'id': row['id'], 'props': props})
logger.info(f" {label}: {len(type_df)}")
def import_relationships(self, rels_csv: str):
"""导入关系"""
rels_df = pd.read_csv(rels_csv, encoding='utf-8-sig')
logger.info(f"导入 {len(rels_df)} 个关系")
imported = 0
for _, row in rels_df.iterrows():
rel_type = row['type']
props = {}
try:
props = json.loads(row.get('properties', '{}'))
except:
pass
query = f"""
MATCH (a {{id: $source}})
MATCH (b {{id: $target}})
MERGE (a)-[r:{rel_type}]->(b)
SET r += $props
"""
try:
self._run_query(query, {
'source': row['source'],
'target': row['target'],
'props': props
})
imported += 1
except Exception as e:
logger.warning(f"关系导入失败: {row['source']} -> {row['target']} ({rel_type}): {e}")
logger.info(f"成功导入 {imported}/{len(rels_df)} 个关系")
def import_all(self, nodes_csv: str, rels_csv: str):
"""完整导入流程"""
logger.info("开始导入...")
self.create_constraints()
self.import_nodes(nodes_csv)
self.import_relationships(rels_csv)
logger.info("导入完成!")
def main():
import yaml
config_file = Path(__file__).parent.parent / 'config' / 'legal_config.yaml'
with open(config_file, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
neo4j_config = config['neo4j']
importer = Neo4jImporter(
neo4j_config['uri'],
neo4j_config['username'],
neo4j_config['password'],
neo4j_config['database']
)
output_dir = Path(__file__).parent.parent / 'output'
nodes_csv = str(output_dir / 'nodes_merged.csv')
rels_csv = str(output_dir / 'rels_merged.csv')
try:
importer.import_all(nodes_csv, rels_csv)
finally:
importer.close()
if __name__ == '__main__':
main()
+62
View File
@@ -0,0 +1,62 @@
// Neo4j数据模型 - 城市规划法律法规知识图谱
// ============================================================
// 约束和索引
// ============================================================
CREATE CONSTRAINT law_id IF NOT EXISTS FOR (l:Law) REQUIRE l.law_id IS UNIQUE;
CREATE CONSTRAINT reg_id IF NOT EXISTS FOR (r:AdministrativeRegulation) REQUIRE r.reg_id IS UNIQUE;
CREATE CONSTRAINT rule_id IF NOT EXISTS FOR (r:DepartmentalRule) REQUIRE r.rule_id IS UNIQUE;
CREATE CONSTRAINT doc_id IF NOT EXISTS FOR (d:PolicyDocument) REQUIRE d.doc_id IS UNIQUE;
CREATE CONSTRAINT chapter_id IF NOT EXISTS FOR (c:Chapter) REQUIRE c.chapter_id IS UNIQUE;
CREATE CONSTRAINT section_id IF NOT EXISTS FOR (s:Section) REQUIRE s.section_id IS UNIQUE;
CREATE CONSTRAINT article_id IF NOT EXISTS FOR (a:Article) REQUIRE a.article_id IS UNIQUE;
CREATE CONSTRAINT body_id IF NOT EXISTS FOR (g:GovernmentBody) REQUIRE g.body_id IS UNIQUE;
CREATE CONSTRAINT subject_id IF NOT EXISTS FOR (s:LegalSubject) REQUIRE s.subject_id IS UNIQUE;
CREATE CONSTRAINT concept_id IF NOT EXISTS FOR (s:SpatialConcept) REQUIRE s.concept_id IS UNIQUE;
CREATE CONSTRAINT proc_id IF NOT EXISTS FOR (p:AdministrativeProcedure) REQUIRE p.proc_id IS UNIQUE;
// 索引
CREATE INDEX law_name IF NOT EXISTS FOR (l:Law) ON (l.name);
CREATE INDEX law_status IF NOT EXISTS FOR (l:Law) ON (l.status);
CREATE INDEX article_type IF NOT EXISTS FOR (a:Article) ON (a.article_type);
CREATE INDEX body_name IF NOT EXISTS FOR (g:GovernmentBody) ON (g.name);
CREATE INDEX subject_category IF NOT EXISTS FOR (s:LegalSubject) ON (s.category);
// 全文搜索
CREATE FULLTEXT INDEX law_search IF NOT EXISTS FOR (l:Law) ON EACH [l.name, l.full_title];
CREATE FULLTEXT INDEX article_search IF NOT EXISTS FOR (a:Article) ON EACH [a.text];
// ============================================================
// 导入命令示例
// ============================================================
// LOAD CSV WITH HEADERS FROM 'file:///nodes.csv' AS row
// WITH row WHERE row.type = 'Law'
// CREATE (:Law {law_id: row.id, name: row.label, full_title: apoc.text.replace(row.label, '"', '')});
//
// LOAD CSV WITH HEADERS FROM 'file:///rels.csv' AS row
// MATCH (a {id: row.source})
// MATCH (b {id: row.target})
// CALL apoc.create.relationship(a, row.type, {}, b) YIELD rel
// RETURN rel;
// ============================================================
// 常用查询
// ============================================================
// 查询某法律的所有条文
// MATCH (l:Law {name: '城乡规划法'})-[:HAS_CHAPTER]->(ch)-[:CONTAINS_ARTICLE]->(art:Article)
// RETURN ch.title, art.number, art.text;
// 查询引用某法律的法规
// MATCH (d)-[r:CITES]->(l:Law {name: '城乡规划法'})
// RETURN d.name, type(r), r.context;
// 查询涉及某主题的所有条文
// MATCH (art:Article)-[:REGULATES]->(s:LegalSubject {name: '建设用地'})
// RETURN art.text;
// 查询某机关发布的所有文件
// MATCH (d)-[:ISSUED_BY]->(g:GovernmentBody {name: '自然资源部'})
// RETURN d.name, labels(d);
@@ -0,0 +1,213 @@
{
"ontology": {
"name": "Urban Planning Law Knowledge Graph Ontology",
"version": "1.0",
"description": "城市规划法律法规知识图谱本体模型",
"created_date": "2026-05-13",
"concepts": [
{
"id": "Law",
"name": "法律",
"description": "全国人大及其常委会制定的法律",
"properties": [
{"name": "law_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "full_title", "type": "string"},
{"name": "promulgation_date", "type": "string"},
{"name": "effective_date", "type": "string"},
{"name": "amendment_dates", "type": "array<string>"},
{"name": "status", "type": "string", "enum": ["现行有效", "修订", "废止", "草案"]},
{"name": "legal_hierarchy", "type": "string", "default": "法律"}
]
},
{
"id": "AdministrativeRegulation",
"name": "行政法规",
"description": "国务院制定的行政法规",
"properties": [
{"name": "reg_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "full_title", "type": "string"},
{"name": "promulgation_date", "type": "string"},
{"name": "effective_date", "type": "string"},
{"name": "status", "type": "string"},
{"name": "order_number", "type": "string"},
{"name": "legal_hierarchy", "type": "string", "default": "行政法规"}
]
},
{
"id": "DepartmentalRule",
"name": "部门规章",
"description": "各部委制定的部门规章",
"properties": [
{"name": "rule_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "full_title", "type": "string"},
{"name": "promulgation_date", "type": "string"},
{"name": "effective_date", "type": "string"},
{"name": "status", "type": "string"},
{"name": "order_number", "type": "string"},
{"name": "legal_hierarchy", "type": "string", "default": "部门规章"}
]
},
{
"id": "PolicyDocument",
"name": "政策文件",
"description": "党中央/国务院/主管部门文件、技术标准",
"properties": [
{"name": "doc_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "full_title", "type": "string"},
{"name": "issue_date", "type": "string"},
{"name": "document_number", "type": "string"},
{"name": "document_type", "type": "string", "enum": ["意见", "通知", "纲要", "指南", "标准", "规范", "办法", "规定", "决定", "批复", "函", "其他"]},
{"name": "status", "type": "string"},
{"name": "legal_hierarchy", "type": "string", "default": "政策文件"}
]
},
{
"id": "Chapter",
"name": "章",
"description": "法律文件的章",
"properties": [
{"name": "chapter_id", "type": "string", "required": true, "unique": true},
{"name": "number", "type": "string"},
{"name": "title", "type": "string"}
]
},
{
"id": "Section",
"name": "节",
"description": "法律文件的节",
"properties": [
{"name": "section_id", "type": "string", "required": true, "unique": true},
{"name": "number", "type": "string"},
{"name": "title", "type": "string"}
]
},
{
"id": "Article",
"name": "条",
"description": "法律文件的具体条文",
"properties": [
{"name": "article_id", "type": "string", "required": true, "unique": true},
{"name": "number", "type": "string"},
{"name": "text", "type": "string"},
{"name": "article_type", "type": "string", "enum": ["管理性", "禁止性", "授权性", "程序性", "处罚性", "定义性"]}
]
},
{
"id": "GovernmentBody",
"name": "政府机构",
"description": "立法/行政/主管部门",
"properties": [
{"name": "body_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "full_name", "type": "string"},
{"name": "level", "type": "string", "enum": ["国家", "省级", "市级", "县级"]},
{"name": "type", "type": "string", "enum": ["立法机关", "行政机关", "主管部门", "地方机关"]}
]
},
{
"id": "LegalSubject",
"name": "法定事项",
"description": "法律规制的事项或主体",
"properties": [
{"name": "subject_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "category", "type": "string", "enum": ["土地", "规划", "建设", "环境", "文物", "交通", "防灾", "生态", "基础设施", "更新", "保护", "测绘", "登记", "处罚", "住房", "市政", "园林", "水资源", "矿业"]},
{"name": "description", "type": "string"}
]
},
{
"id": "SpatialConcept",
"name": "空间概念",
"description": "国土空间规划中的空间概念",
"properties": [
{"name": "concept_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "type", "type": "string", "enum": ["国土空间", "生态红线", "永久基本农田", "城镇开发边界", "规划区", "建设用地", "农用地", "未利用地", "历史文化保护区", "控制线", "城市更新单元"]},
{"name": "definition", "type": "string"}
]
},
{
"id": "AdministrativeProcedure",
"name": "行政程序",
"description": "规划/用地/建设的行政程序",
"properties": [
{"name": "proc_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "type", "type": "string", "enum": ["审批", "核准", "备案", "登记", "许可", "验收", "监督", "听证", "征收", "拆迁", "出让", "转让"]},
{"name": "time_limit", "type": "string"}
]
},
{
"id": "Obligation",
"name": "权利义务",
"description": "法律条文规定的权利、义务或禁止",
"properties": [
{"name": "oblig_id", "type": "string", "required": true, "unique": true},
{"name": "text", "type": "string"},
{"name": "type", "type": "string", "enum": ["权利", "义务", "禁止", "限制", "鼓励"]}
]
},
{
"id": "Penalty",
"name": "法律责任",
"description": "违反法律规定的处罚",
"properties": [
{"name": "penalty_id", "type": "string", "required": true, "unique": true},
{"name": "type", "type": "string", "enum": ["行政处罚", "刑事处罚", "民事责任", "行政处分"]},
{"name": "description", "type": "string"}
]
},
{
"id": "TimePoint",
"name": "时间节点",
"description": "法规中提到的具体时间",
"properties": [
{"name": "time_id", "type": "string", "required": true, "unique": true},
{"name": "year", "type": "integer"},
{"name": "description", "type": "string"},
{"name": "time_type", "type": "string", "enum": ["生效日期", "废止日期", "修订日期", "过渡期", "截止日期"]}
]
},
{
"id": "Region",
"name": "区域",
"description": "法规适用的区域",
"properties": [
{"name": "region_id", "type": "string", "required": true, "unique": true},
{"name": "name", "type": "string", "required": true},
{"name": "level", "type": "string", "enum": ["全国", "区域", "省", "市", "县", "特定区域"]}
]
}
],
"relationships": [
{"name": "has_chapter", "description": "包含章", "from": "Law/AdministrativeRegulation/DepartmentalRule/PolicyDocument", "to": "Chapter", "properties": [{"name": "order_number", "type": "integer"}]},
{"name": "has_section", "description": "包含节", "from": "Chapter", "to": "Section", "properties": [{"name": "order_number", "type": "integer"}]},
{"name": "contains_article", "description": "包含条", "from": "Chapter/Section", "to": "Article", "properties": [{"name": "order_number", "type": "integer"}]},
{"name": "article_in_document", "description": "条属于文件", "from": "Article", "to": "Law/AdministrativeRegulation/DepartmentalRule/PolicyDocument", "properties": []},
{"name": "cites", "description": "引用", "from": "Article/PolicyDocument", "to": "Law/AdministrativeRegulation/DepartmentalRule", "properties": [{"name": "context", "type": "string"}]},
{"name": "implements", "description": "实施/落实", "from": "PolicyDocument/DepartmentalRule", "to": "Law/AdministrativeRegulation", "properties": [{"name": "scope", "type": "string"}]},
{"name": "amends", "description": "修正/修订", "from": "Law/Regulation/Rule", "to": "Law/Regulation/Rule", "properties": [{"name": "amendment_date", "type": "string"}]},
{"name": "replaces", "description": "替代/废止", "from": "Law/Regulation/Rule", "to": "Law/Regulation/Rule", "properties": [{"name": "replacement_date", "type": "string"}]},
{"name": "supplements", "description": "补充", "from": "PolicyDocument", "to": "Law/Regulation/Rule", "properties": [{"name": "scope", "type": "string"}]},
{"name": "issued_by", "description": "发布机关", "from": "Law/Regulation/Rule/PolicyDocument", "to": "GovernmentBody", "properties": [{"name": "role", "type": "string"}]},
{"name": "implemented_by", "description": "实施机关", "from": "Article/AdministrativeProcedure", "to": "GovernmentBody", "properties": [{"name": "role", "type": "string"}]},
{"name": "regulates", "description": "规制", "from": "Article/Obligation", "to": "LegalSubject", "properties": [{"name": "regulation_type", "type": "string"}]},
{"name": "defines_spatial", "description": "界定空间", "from": "Article", "to": "SpatialConcept", "properties": [{"name": "scope", "type": "string"}]},
{"name": "applies_to", "description": "适用于", "from": "Article/AdministrativeProcedure", "to": "LegalSubject", "properties": [{"name": "context", "type": "string"}]},
{"name": "sets_obligation", "description": "定义务", "from": "Article", "to": "Obligation", "properties": [{"name": "obligation_type", "type": "string"}]},
{"name": "prescribes_penalty", "description": "规定处罚", "from": "Article/Obligation", "to": "Penalty", "properties": [{"name": "conditions", "type": "string"}]},
{"name": "requires_procedure", "description": "要求程序", "from": "Article", "to": "AdministrativeProcedure", "properties": [{"name": "mandatory", "type": "string"}]},
{"name": "procedure_for", "description": "程序对应事项", "from": "AdministrativeProcedure", "to": "LegalSubject", "properties": [{"name": "purpose", "type": "string"}]},
{"name": "procedure_step", "description": "程序步骤", "from": "AdministrativeProcedure", "to": "AdministrativeProcedure", "properties": [{"name": "step_order", "type": "integer"}]},
{"name": "governs_region", "description": "管辖区域", "from": "Law/Regulation/Rule", "to": "Region", "properties": [{"name": "scope", "type": "string"}]},
{"name": "effective_timeline", "description": "生效时间线", "from": "Law/Regulation/Rule", "to": "TimePoint", "properties": [{"name": "timeline_type", "type": "string"}]},
{"name": "related_to", "description": "相关", "from": "Any", "to": "Any", "properties": [{"name": "description", "type": "string"}]},
{"name": "standard_for", "description": "标准适用于", "from": "PolicyDocument", "to": "LegalSubject", "properties": [{"name": "standard_type", "type": "string"}]},
{"name": "category_index", "description": "分类索引", "from": "Law/Regulation/Rule/PolicyDocument", "to": "LegalSubject", "properties": [{"name": "source", "type": "string"}]}
]
}
}
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
# 城市规划法律法规知识图谱构建 - 依赖包
# LLM相关
langchain-deepseek>=0.1.0
langchain-core>=0.1.0
# 文档读取
python-docx>=1.1.0
# Neo4j
neo4j>=5.15.0
# 数据处理
pandas>=2.0.0
numpy>=1.24.0
# 文本相似度
python-Levenshtein>=0.23.0
# 可视化
networkx>=3.1
matplotlib>=3.7.0
# 配置
pyyaml>=6.0
python-dotenv>=1.0.0
# 工具
tqdm>=4.65.0
+221
View File
@@ -0,0 +1,221 @@
"""
Word文档读取器 - 读取城市更新法规数据库中所有.docx文件
"""
import json
import re
import logging
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Any
try:
from docx import Document
except ImportError:
print("错误: 请先安装 python-docx")
print("运行: pip install python-docx")
raise
class DocxReader:
"""Word文档读取器"""
# 目录类别映射
CATEGORY_MAP = {
'0知识图谱目录': {'id': 0, 'type': 'Index', 'name': '知识图谱目录'},
'1法律': {'id': 1, 'type': 'Law', 'name': '法律'},
'2行政法规': {'id': 2, 'type': 'AdministrativeRegulation', 'name': '行政法规'},
'3部门规章': {'id': 3, 'type': 'DepartmentalRule', 'name': '部门规章'},
'4党中央国务院文件': {'id': 4, 'type': 'PolicyDocument', 'name': '党中央国务院文件'},
'5国家主管部门文件': {'id': 5, 'type': 'PolicyDocument', 'name': '国家主管部门文件'},
'6主要技术标准': {'id': 6, 'type': 'PolicyDocument', 'name': '主要技术标准'},
}
def __init__(self, source_dir: str):
self.source_dir = Path(source_dir)
self.logger = self._setup_logger()
self.documents = []
def _setup_logger(self):
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def _extract_text_from_docx(self, filepath: Path) -> str:
"""从Word文档提取纯文本"""
doc = Document(str(filepath))
paragraphs = []
for para in doc.paragraphs:
text = para.text.strip()
if text:
paragraphs.append(text)
# 也提取表格中的文本
for table in doc.tables:
for row in table.rows:
row_text = []
for cell in row.cells:
cell_text = cell.text.strip()
if cell_text:
row_text.append(cell_text)
if row_text:
paragraphs.append(' | '.join(row_text))
return '\n'.join(paragraphs)
def _parse_filename_info(self, filename: str) -> Dict[str, Any]:
"""从文件名解析基本信息"""
info = {
'original_filename': filename,
'document_number': None,
'document_name': None,
'is_draft': False,
}
# 去掉扩展名
name_without_ext = Path(filename).stem
# 检测是否为草案/征求意见稿
if '草案' in name_without_ext or '征求意见稿' in name_without_ext:
info['is_draft'] = True
# 尝试提取编号前缀 (如 "3-中华人民共和国城乡规划法")
match = re.match(r'^(\d+)[-—]\s*(.+)$', name_without_ext)
if match:
info['file_number'] = int(match.group(1))
name_part = match.group(2)
else:
name_part = name_without_ext
# 提取书名号中的名称
title_match = re.search(r'《(.+?)》', name_part)
if title_match:
info['document_name'] = title_match.group(1)
else:
# 去掉常见前缀
cleaned = re.sub(r'^(中华人民共和国|国务院|国土资源部|建设部|住房城乡建设部|自然资源部)\s*', '', name_part)
info['document_name'] = cleaned if cleaned else name_part
return info
def read_all_documents(self) -> List[Dict[str, Any]]:
"""读取所有Word文档"""
self.logger.info(f"开始读取文档目录: {self.source_dir}")
self.documents = []
errors = []
for subdir_name, category_info in self.CATEGORY_MAP.items():
subdir = self.source_dir / subdir_name
if not subdir.exists():
self.logger.warning(f"子目录不存在: {subdir}")
continue
docx_files = sorted(subdir.glob('*.docx'))
self.logger.info(f" {subdir_name}: 发现 {len(docx_files)} 个文件")
for docx_file in docx_files:
try:
text = self._extract_text_from_docx(docx_file)
filename_info = self._parse_filename_info(docx_file.name)
doc_record = {
'doc_id': f"{category_info['type'][:3].upper()}-{len(self.documents):04d}",
'filename': docx_file.name,
'category_id': category_info['id'],
'category_name': category_info['name'],
'document_type': category_info['type'],
'subdirectory': subdir_name,
'raw_text': text,
'text_length': len(text),
'conversion_timestamp': datetime.now().isoformat(),
**filename_info,
}
self.documents.append(doc_record)
except Exception as e:
error_msg = f"读取失败: {docx_file.name} - {str(e)}"
self.logger.error(error_msg)
errors.append({
'filename': docx_file.name,
'subdirectory': subdir_name,
'error': str(e)
})
self.logger.info(f"读取完成: {len(self.documents)} 个文档, {len(errors)} 个错误")
return self.documents
def save_to_json(self, output_path: str):
"""保存到JSON文件"""
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
result = {
'metadata': {
'total_documents': len(self.documents),
'source_dir': str(self.source_dir),
'created_at': datetime.now().isoformat(),
'category_distribution': {},
},
'documents': self.documents,
}
# 统计分类分布
for doc in self.documents:
cat = doc['category_name']
result['metadata']['category_distribution'][cat] = \
result['metadata']['category_distribution'].get(cat, 0) + 1
with open(output, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
self.logger.info(f"已保存到: {output}")
return result
def print_summary(self):
"""打印摘要"""
print("\n" + "=" * 60)
print("文档读取摘要")
print("=" * 60)
print(f"总文档数: {len(self.documents)}")
# 按类别统计
category_counts = {}
total_chars = 0
for doc in self.documents:
cat = doc['category_name']
category_counts[cat] = category_counts.get(cat, 0) + 1
total_chars += doc['text_length']
print("\n类别分布:")
for cat, count in sorted(category_counts.items()):
print(f" {cat}: {count}")
print(f"\n总字符数: {total_chars:,}")
print(f"平均字符数: {total_chars // max(len(self.documents), 1):,}")
# 文本长度分布
lengths = [doc['text_length'] for doc in self.documents]
if lengths:
print(f"\n文本长度:")
print(f" 最短: {min(lengths):,} 字符")
print(f" 最长: {max(lengths):,} 字符")
print(f" 中位数: {sorted(lengths)[len(lengths)//2]:,} 字符")
print("=" * 60)
def main():
source_dir = r"E:\Project\SI\2026_KG_PlanningLaw\data\城市规划法律法规\城市更新法规数据库"
output_path = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\all_documents.json"
reader = DocxReader(source_dir)
reader.read_all_documents()
reader.save_to_json(output_path)
reader.print_summary()
if __name__ == '__main__':
main()
@@ -0,0 +1,314 @@
"""
从结构化法规数据生成知识图谱CSV文件(nodes.csv + rels.csv
"""
import json
import re
import pandas as pd
from pathlib import Path
from typing import Dict, List, Any, Tuple
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 节点类型到ID前缀的映射
TYPE_PREFIX = {
'Law': 'LAW',
'AdministrativeRegulation': 'REG',
'DepartmentalRule': 'RULE',
'PolicyDocument': 'DOC',
'Index': 'DOC',
'Chapter': 'CH',
'Section': 'SEC',
'Article': 'ART',
'GovernmentBody': 'GOV',
}
def generate_id(prefix: str, text: str) -> str:
"""根据文本哈希生成ID"""
return f"{prefix}-{abs(hash(text)) % 100000:05d}"
def build_document_nodes(doc: Dict[str, Any], doc_index: int) -> Tuple[List[Dict], List[Dict]]:
"""从单个文档构建节点和关系"""
nodes = []
rels = []
rel_id = 0
# 文档类型映射
doc_type = doc['document_type']
if doc_type == 'Index':
doc_type = 'PolicyDocument'
# 1. 创建文档节点
doc_id = generate_id(TYPE_PREFIX.get(doc_type, 'DOC'), doc['title'])
doc_node = {
'id': doc_id,
'label': doc['title'],
'type': doc_type,
'properties': json.dumps({
'full_title': doc.get('document_name', ''),
'promulgation_date': doc.get('promulgation_date'),
'effective_date': doc.get('effective_date'),
'document_number': doc.get('document_number'),
'is_draft': doc.get('is_draft', False),
'category': doc.get('category_name', ''),
'filename': doc.get('filename', ''),
'text_length': doc.get('text_length', 0),
'article_count': doc.get('article_count', 0),
}, ensure_ascii=False)
}
nodes.append(doc_node)
# 2. 创建发布机关节点和关系
if doc.get('issuing_body'):
body_name = doc['issuing_body']
body_id = generate_id('GOV', body_name)
# 检查是否已存在
body_node = {
'id': body_id,
'label': body_name,
'type': 'GovernmentBody',
'properties': json.dumps({}, ensure_ascii=False)
}
nodes.append(body_node)
rel_id += 1
rels.append({
'source': doc_id,
'target': body_id,
'type': 'ISSUED_BY',
'properties': json.dumps({}, ensure_ascii=False)
})
# 3. 创建章节条文节点和层次关系
for ch_idx, chapter in enumerate(doc.get('chapters', [])):
ch_title = chapter.get('title', f'{ch_idx+1}')
ch_id = generate_id('CH', f"{doc_id}_{ch_title}")
ch_node = {
'id': ch_id,
'label': ch_title,
'type': 'Chapter',
'properties': json.dumps({
'name': chapter.get('name', ''),
'number': str(ch_idx + 1),
}, ensure_ascii=False)
}
nodes.append(ch_node)
# 文档→章
rel_id += 1
rels.append({
'source': doc_id,
'target': ch_id,
'type': 'HAS_CHAPTER',
'properties': json.dumps({'order_number': ch_idx + 1}, ensure_ascii=False)
})
# 章下的条文
for art_idx, article in enumerate(chapter.get('articles', [])):
art_number = article.get('number', str(art_idx + 1))
art_text = article.get('text', '')
art_id = generate_id('ART', f"{doc_id}_{art_number}")
art_node = {
'id': art_id,
'label': f"{art_number}",
'type': 'Article',
'properties': json.dumps({
'number': art_number,
'text': art_text[:500],
'article_type': article.get('article_type', ''),
}, ensure_ascii=False)
}
nodes.append(art_node)
# 章→条
rel_id += 1
rels.append({
'source': ch_id,
'target': art_id,
'type': 'CONTAINS_ARTICLE',
'properties': json.dumps({'order_number': art_idx + 1}, ensure_ascii=False)
})
# 条→文档
rel_id += 1
rels.append({
'source': art_id,
'target': doc_id,
'type': 'ARTICLE_IN_DOCUMENT',
'properties': json.dumps({}, ensure_ascii=False)
})
# 节
for sec_idx, section in enumerate(chapter.get('sections', [])):
sec_title = section.get('title', f'{sec_idx+1}')
sec_id = generate_id('SEC', f"{doc_id}_{ch_title}_{sec_title}")
sec_node = {
'id': sec_id,
'label': sec_title,
'type': 'Section',
'properties': json.dumps({
'name': section.get('name', ''),
'number': str(sec_idx + 1),
}, ensure_ascii=False)
}
nodes.append(sec_node)
# 章→节
rel_id += 1
rels.append({
'source': ch_id,
'target': sec_id,
'type': 'HAS_SECTION',
'properties': json.dumps({'order_number': sec_idx + 1}, ensure_ascii=False)
})
# 节下的条文
for art_idx, article in enumerate(section.get('articles', [])):
art_number = article.get('number', str(art_idx + 1))
art_text = article.get('text', '')
art_id = generate_id('ART', f"{doc_id}_{art_number}")
art_node = {
'id': art_id,
'label': f"{art_number}",
'type': 'Article',
'properties': json.dumps({
'number': art_number,
'text': art_text[:500],
'article_type': article.get('article_type', ''),
}, ensure_ascii=False)
}
nodes.append(art_node)
# 节→条
rel_id += 1
rels.append({
'source': sec_id,
'target': art_id,
'type': 'CONTAINS_ARTICLE',
'properties': json.dumps({'order_number': art_idx + 1}, ensure_ascii=False)
})
# 条→文档
rel_id += 1
rels.append({
'source': art_id,
'target': doc_id,
'type': 'ARTICLE_IN_DOCUMENT',
'properties': json.dumps({}, ensure_ascii=False)
})
return nodes, rels
def deduplicate_nodes(nodes: List[Dict]) -> List[Dict]:
"""去重节点(基于id"""
seen = {}
for node in nodes:
if node['id'] not in seen:
seen[node['id']] = node
else:
# 合并properties
existing = seen[node['id']]
if existing['properties'] == '{}' and node['properties'] != '{}':
existing['properties'] = node['properties']
return list(seen.values())
def generate_report(nodes_df: pd.DataFrame, rels_df: pd.DataFrame, output_dir: Path):
"""生成统计报告"""
lines = ["# 结构化CSV提取报告\n"]
lines.append(f"生成时间: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
# 节点统计
lines.append("## 节点统计\n\n")
lines.append(f"**节点总数**: {len(nodes_df)}\n\n")
node_type_counts = nodes_df['type'].value_counts().sort_index()
lines.append("| 节点类型 | 数量 | 占比 |\n")
lines.append("|---------|------|------|\n")
for nt, count in node_type_counts.items():
lines.append(f"| {nt} | {count} | {count/len(nodes_df)*100:.1f}% |\n")
# 关系统计
lines.append("\n## 关系统计\n\n")
lines.append(f"**关系总数**: {len(rels_df)}\n\n")
rel_type_counts = rels_df['type'].value_counts().sort_index()
lines.append("| 关系类型 | 数量 | 占比 |\n")
lines.append("|---------|------|------|\n")
for rt, count in rel_type_counts.items():
lines.append(f"| {rt} | {count} | {count/len(rels_df)*100:.1f}% |\n")
report_path = output_dir / 'extraction_report_structured.md'
with open(report_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
print(f"\n报告已保存: {report_path}")
def main():
input_path = Path(r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\structured_documents.json")
output_dir = Path(r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output")
output_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"读取结构化文档: {input_path}")
with open(input_path, 'r', encoding='utf-8') as f:
data = json.load(f)
documents = data['documents']
logger.info(f"{len(documents)} 个文档")
all_nodes = []
all_rels = []
for i, doc in enumerate(documents):
nodes, rels = build_document_nodes(doc, i)
all_nodes.extend(nodes)
all_rels.extend(rels)
if (i + 1) % 50 == 0:
logger.info(f" 已处理 {i + 1}/{len(documents)}")
# 去重
all_nodes = deduplicate_nodes(all_nodes)
# 保存
nodes_df = pd.DataFrame(all_nodes)
rels_df = pd.DataFrame(all_rels)
nodes_path = output_dir / 'nodes_structured.csv'
rels_path = output_dir / 'rels_structured.csv'
nodes_df.to_csv(nodes_path, index=False, encoding='utf-8-sig')
rels_df.to_csv(rels_path, index=False, encoding='utf-8-sig')
logger.info(f"节点已保存: {nodes_path} ({len(nodes_df)} 行)")
logger.info(f"关系已保存: {rels_path} ({len(rels_df)} 行)")
# 统计
print(f"\n{'='*60}")
print("结构化CSV提取完成")
print(f"{'='*60}")
print(f"节点总数: {len(nodes_df)}")
for nt, count in nodes_df['type'].value_counts().sort_index().items():
print(f" - {nt}: {count}")
print(f"关系总数: {len(rels_df)}")
for rt, count in rels_df['type'].value_counts().sort_index().items():
print(f" - {rt}: {count}")
print(f"{'='*60}")
generate_report(nodes_df, rels_df, output_dir)
if __name__ == '__main__':
main()
@@ -0,0 +1,354 @@
"""
法规元数据解析器 - 从文档文本中解析章节结构、发布机关、日期等
"""
import json
import re
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
class LegalMetadataExtractor:
"""法规元数据解析器"""
def __init__(self):
self.logger = self._setup_logger()
def _setup_logger(self):
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def _extract_document_title(self, text: str, filename: str) -> str:
"""提取文档标题"""
# 尝试从书名号中提取
match = re.search(r'《(.+?)》', text[:2000])
if match:
return match.group(1)
# 尝试从前几行中提取
lines = text[:500].split('\n')
for line in lines:
line = line.strip()
if not line:
continue
# 过滤掉编号前缀
cleaned = re.sub(r'^\d+[-—]\s*', '', line)
if len(cleaned) > 4 and len(cleaned) < 100:
return cleaned
# 从文件名提取
name = Path(filename).stem
name = re.sub(r'^\d+[-—]\s*', '', name)
return name
def _extract_issuing_body(self, text: str) -> Optional[str]:
"""提取发布机关"""
# 常见模式:在文末签名块
patterns = [
r'(?:发布|公布|印发)[^\n]*?[机关部院会委局厅处]\s*[:]\s*(.+?)(?:\n|$)',
r'^[\s]*(.{4,20}(?:部|委员会|院|局|厅|处|办公室|小组))\s*$',
]
# 从后往前搜索(发布机关通常在文末)
text_end = text[-3000:] if len(text) > 3000 else text
lines = text_end.split('\n')
for line in reversed(lines):
line = line.strip()
if not line:
continue
# 匹配机关名称
if re.match(r'^.{2,15}(?:部|委员会|院|局|厅|处|办公室|小组|政府|大会|人大常委会)$', line):
return line
# 从标题区域搜索
text_start = text[:1000]
body_patterns = [
r'((?:全国人民代表大会(?:常务委员会)?|国务院|.{2,10}部|.{2,10}委员会|.{2,10}局|.{2,10}厅))\s*(?:令|公告|通知|制定)',
r'(.{2,10}(?:部|委员会|局|厅))\s*(?:令|公告|通知|印发)',
]
for pattern in body_patterns:
match = re.search(pattern, text_start)
if match:
return match.group(1)
return None
def _extract_dates(self, text: str) -> Dict[str, Optional[str]]:
"""提取日期信息"""
dates = {'promulgation_date': None, 'effective_date': None}
# 搜索发布日期
date_patterns = [
r'(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*(?:起?\s*)?(?:施行|实施|生效|执行)',
r'\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*起?\s*(?:施行|实施|生效|执行)',
]
for pattern in date_patterns:
match = re.search(pattern, text)
if match:
dates['effective_date'] = f"{match.group(1)}-{match.group(2).zfill(2)}-{match.group(3).zfill(2)}"
break
# 搜索公布日期(通常在文末)
pub_patterns = [
r'(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*(?:公布|发布|印发)',
r'(?:公布|发布|印发)\s*[:]*\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日',
]
text_end = text[-2000:] if len(text) > 2000 else text
for pattern in pub_patterns:
match = re.search(pattern, text_end)
if match:
dates['promulgation_date'] = f"{match.group(1)}-{match.group(2).zfill(2)}-{match.group(3).zfill(2)}"
break
# 如果没有明确日期,尝试从文末找任意日期
if not dates['promulgation_date']:
match = re.findall(r'(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日', text_end)
if match:
last_date = match[-1]
dates['promulgation_date'] = f"{last_date[0]}-{last_date[1].zfill(2)}-{last_date[2].zfill(2)}"
return dates
def _extract_document_number(self, text: str) -> Optional[str]:
"""提取文号"""
patterns = [
r'[(]\s*(\d{4})\s*[)]\s*[^\s]*?\s*第?\s*(\d+)\s*号',
r'第?\s*(\d+)\s*号\s*[令公告通知]',
r'([A-Za-z一-鿿]+[(]\d{4}[)][^\s]*?号)',
r'([国发|国办发|建发|自然资发|国土资发|建城|建规|住建|建住房|建村|办发|发改委][〔(]\d{4}[)][^\s]*?号)',
]
for pattern in patterns:
match = re.search(pattern, text[:2000])
if match:
return match.group(0).strip()
return None
def _parse_chapter_structure(self, text: str) -> List[Dict[str, Any]]:
"""解析章节结构"""
chapters = []
current_chapter = None
current_section = None
current_articles = []
# 按行处理
lines = text.split('\n')
def flush_articles():
nonlocal current_articles
result = current_articles
current_articles = []
return result
for line in lines:
line_stripped = line.strip()
if not line_stripped:
continue
# 检测章标题
ch_match = re.match(r'^第[一二三四五六七八九十百]+[章节部分]\s*(.*)$', line_stripped)
if ch_match:
# 先保存之前的章/节的文章
if current_section:
current_section['articles'] = flush_articles()
elif current_chapter:
current_chapter['articles'].extend(flush_articles())
current_chapter = {
'title': line_stripped,
'name': ch_match.group(1).strip() if ch_match.group(1) else line_stripped,
'sections': [],
'articles': [],
}
chapters.append(current_chapter)
current_section = None
continue
# 检测节标题
sec_match = re.match(r'^第[一二三四五六七八九十百]+节\s*(.*)$', line_stripped)
if sec_match and current_chapter:
if current_section:
current_section['articles'] = flush_articles()
current_section = {
'title': line_stripped,
'name': sec_match.group(1).strip() if sec_match.group(1) else line_stripped,
'articles': [],
}
current_chapter['sections'].append(current_section)
continue
# 检测条文
art_match = re.match(r'^第[一二三四五六七八九十百零千]+条\s*(.*)$', line_stripped)
if art_match:
article = {
'number': line_stripped.split('')[0].replace('', ''),
'text': art_match.group(1).strip(),
'full_text': line_stripped,
}
current_articles.append(article)
continue
# 续接上一条
if current_articles and line_stripped:
current_articles[-1]['text'] += ' ' + line_stripped
current_articles[-1]['full_text'] += ' ' + line_stripped
# 刷新最后的文章
if current_section:
current_section['articles'].extend(flush_articles())
elif current_chapter:
current_chapter['articles'].extend(flush_articles())
return chapters
def _classify_article_type(self, text: str) -> str:
"""分类条文类型"""
if re.search(r'不得|禁止|严禁', text):
return '禁止性'
elif re.search(r'可以|有权|依法享有', text):
return '授权性'
elif re.search(r'应当|必须|须|应当依法', text):
return '管理性'
elif re.search(r'申请|审批|备案|登记|许可|核准', text):
return '程序性'
elif re.search(r'罚款|责令|没收|吊销|刑事|处分', text):
return '处罚性'
else:
return '定义性'
def process_document(self, doc: Dict[str, Any]) -> Dict[str, Any]:
"""处理单个文档"""
text = doc.get('raw_text', '')
filename = doc.get('filename', '')
# 提取元数据
title = self._extract_document_title(text, filename)
issuing_body = self._extract_issuing_body(text)
dates = self._extract_dates(text)
doc_number = self._extract_document_number(text)
# 解析章节结构
chapters = self._parse_chapter_structure(text)
# 统计条文数
total_articles = 0
for ch in chapters:
total_articles += len(ch.get('articles', []))
for sec in ch.get('sections', []):
total_articles += len(sec.get('articles', []))
# 分类条文
for ch in chapters:
for art in ch.get('articles', []):
art['article_type'] = self._classify_article_type(art['text'])
for sec in ch.get('sections', []):
for art in sec.get('articles', []):
art['article_type'] = self._classify_article_type(art['text'])
result = {
'doc_id': doc['doc_id'],
'document_type': doc['document_type'],
'category_name': doc['category_name'],
'title': title,
'document_name': doc.get('document_name', title),
'issuing_body': issuing_body,
'promulgation_date': dates['promulgation_date'],
'effective_date': dates['effective_date'],
'document_number': doc_number,
'is_draft': doc.get('is_draft', False),
'text_length': doc['text_length'],
'filename': filename,
'chapter_count': len(chapters),
'article_count': total_articles,
'chapters': chapters,
}
return result
def process_all(self, input_path: str, output_path: str) -> List[Dict[str, Any]]:
"""处理所有文档"""
self.logger.info(f"读取文档数据: {input_path}")
with open(input_path, 'r', encoding='utf-8') as f:
data = json.load(f)
documents = data['documents']
self.logger.info(f"{len(documents)} 个文档待处理")
results = []
for i, doc in enumerate(documents):
if doc['text_length'] == 0:
self.logger.warning(f"跳过空文档: {doc['filename']}")
continue
try:
result = self.process_document(doc)
results.append(result)
if (i + 1) % 50 == 0:
self.logger.info(f" 已处理 {i + 1}/{len(documents)}")
except Exception as e:
self.logger.error(f"处理失败: {doc['filename']} - {str(e)}")
# 保存
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
save_data = {
'metadata': {
'total_documents': len(results),
'processed_at': datetime.now().isoformat(),
},
'documents': results,
}
with open(output, 'w', encoding='utf-8') as f:
json.dump(save_data, f, indent=2, ensure_ascii=False)
self.logger.info(f"已保存到: {output}")
self._print_summary(results)
return results
def _print_summary(self, results: List[Dict]):
"""打印摘要"""
total_articles = sum(r['article_count'] for r in results)
total_chapters = sum(r['chapter_count'] for r in results)
docs_with_body = sum(1 for r in results if r['issuing_body'])
print(f"\n{'='*60}")
print("法规元数据解析摘要")
print(f"{'='*60}")
print(f"处理文档数: {len(results)}")
print(f"解析出章数: {total_chapters}")
print(f"解析出条文数: {total_articles}")
print(f"提取发布机关: {docs_with_body}/{len(results)} ({docs_with_body/max(len(results),1)*100:.1f}%)")
# 按类型统计
type_counts = {}
for r in results:
t = r['document_type']
type_counts[t] = type_counts.get(t, 0) + 1
print("\n按类型统计:")
for t, c in sorted(type_counts.items()):
print(f" {t}: {c}")
print(f"{'='*60}")
def main():
input_path = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\all_documents.json"
output_path = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\structured_documents.json"
extractor = LegalMetadataExtractor()
extractor.process_all(input_path, output_path)
if __name__ == '__main__':
main()
@@ -0,0 +1,239 @@
"""
法规知识图谱可视化工具
使用networkx和matplotlib绘制知识图谱
"""
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from pathlib import Path
import numpy as np
import json
def setup_chinese_font():
"""设置中文字体"""
for font in ['Microsoft YaHei', 'SimHei', 'SimSun', 'KaiTi']:
try:
plt.rcParams['font.sans-serif'] = [font]
plt.rcParams['axes.unicode_minus'] = False
return
except:
continue
# 节点类型颜色
NODE_COLORS = {
'Law': '#E74C3C',
'AdministrativeRegulation': '#E67E22',
'DepartmentalRule': '#F1C40F',
'PolicyDocument': '#2ECC71',
'Chapter': '#3498DB',
'Section': '#9B59B6',
'Article': '#1ABC9C',
'GovernmentBody': '#8E44AD',
'LegalSubject': '#E91E63',
'SpatialConcept': '#00BCD4',
'AdministrativeProcedure': '#FF9800',
'Obligation': '#F44336',
'Penalty': '#795548',
'TimePoint': '#607D8B',
'Region': '#4CAF50',
}
# 节点类型大小
NODE_SIZES = {
'Law': 600,
'AdministrativeRegulation': 500,
'DepartmentalRule': 400,
'PolicyDocument': 350,
'Chapter': 250,
'Section': 200,
'Article': 100,
'GovernmentBody': 400,
'LegalSubject': 300,
'SpatialConcept': 250,
'AdministrativeProcedure': 200,
'Obligation': 150,
'Penalty': 150,
'TimePoint': 100,
'Region': 150,
}
def load_data(nodes_csv, rels_csv):
"""加载数据"""
nodes_df = pd.read_csv(nodes_csv, encoding='utf-8-sig')
rels_df = pd.read_csv(rels_csv, encoding='utf-8-sig')
print(f"加载节点: {len(nodes_df)}, 关系: {len(rels_df)}")
return nodes_df, rels_df
def build_graph(nodes_df, rels_df):
"""构建NetworkX图"""
G = nx.DiGraph()
for _, row in nodes_df.iterrows():
node_id = row['id']
label = str(row['label'])
node_type = row['type']
display_label = label[:12] + '...' if len(label) > 12 else label
G.add_node(node_id,
label=display_label,
full_label=label,
node_type=node_type,
color=NODE_COLORS.get(node_type, '#CCCCCC'),
size=NODE_SIZES.get(node_type, 150))
for _, row in rels_df.iterrows():
source = row['source']
target = row['target']
rel_type = row['type']
if source in G.nodes() and target in G.nodes():
G.add_edge(source, target, rel_type=rel_type)
print(f"图: {G.number_of_nodes()} 节点, {G.number_of_edges()}")
return G
def draw_graph(G, output_path, title="法规知识图谱", max_nodes=300):
"""绘制图谱"""
if G.number_of_nodes() > max_nodes:
degrees = dict(G.degree())
top_nodes = sorted(degrees, key=degrees.get, reverse=True)[:max_nodes]
G = G.subgraph(top_nodes).copy()
plt.figure(figsize=(24, 18))
pos = nx.spring_layout(G, k=2.5, iterations=50, seed=42)
# 边
nx.draw_networkx_edges(G, pos, alpha=0.2, width=0.5, edge_color='gray',
arrows=True, arrowsize=8)
# 按类型绘制节点
node_types = {}
for node, data in G.nodes(data=True):
nt = data.get('node_type', 'Unknown')
node_types.setdefault(nt, []).append(node)
for nt, nodes in node_types.items():
color = NODE_COLORS.get(nt, '#CCCCCC')
size = NODE_SIZES.get(nt, 150)
nx.draw_networkx_nodes(G, pos, nodelist=nodes, node_color=color,
node_size=size, alpha=0.8, edgecolors='white', linewidths=1)
# 标签(只显示非Article类型)
labels = {n: d['label'] for n, d in G.nodes(data=True)
if d.get('node_type') != 'Article'}
if len(labels) <= 150:
nx.draw_networkx_labels(G, pos, labels=labels, font_size=7, font_weight='bold')
# 图例
patches = [mpatches.Patch(color=NODE_COLORS[nt], label=nt)
for nt in node_types if nt in NODE_COLORS]
plt.legend(handles=patches, loc='upper right', fontsize=10, framealpha=0.9)
plt.title(title, fontsize=16, fontweight='bold', pad=20)
plt.axis('off')
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close()
print(f"保存: {output_path}")
def draw_subgraphs(G, output_dir):
"""绘制子图"""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 1. 文档层次图
doc_types = ['Law', 'AdministrativeRegulation', 'DepartmentalRule', 'PolicyDocument', 'Chapter']
G1_nodes = [n for n, d in G.nodes(data=True) if d.get('node_type') in doc_types]
if G1_nodes:
G1 = G.subgraph(G1_nodes).copy()
draw_graph(G1, output_dir / 'kg_document_hierarchy.png', '法规文档层次结构')
# 2. 引用网络
ref_rels = ['CITES', 'IMPLEMENTS', 'AMENDS', 'REPLACES', 'SUPPLEMENTS']
ref_edges = [(u, v) for u, v, d in G.edges(data=True) if d.get('rel_type') in ref_rels]
if ref_edges:
G2 = G.edge_subgraph(ref_edges).copy()
draw_graph(G2, output_dir / 'kg_citation_network.png', '法规引用网络')
# 3. 主题规制图
subject_types = ['LegalSubject', 'SpatialConcept']
subject_nodes = [n for n, d in G.nodes(data=True) if d.get('node_type') in subject_types]
if subject_nodes:
neighbors = set(subject_nodes)
for node in subject_nodes:
neighbors.update(G.predecessors(node))
neighbors.update(G.successors(node))
G3 = G.subgraph(neighbors).copy()
draw_graph(G3, output_dir / 'kg_subject_regulation.png', '法规主题规制关系')
# 4. 机构关系图
gov_nodes = [n for n, d in G.nodes(data=True) if d.get('node_type') == 'GovernmentBody']
if gov_nodes:
neighbors = set(gov_nodes)
for node in gov_nodes:
neighbors.update(G.predecessors(node))
neighbors.update(G.successors(node))
G4 = G.subgraph(neighbors).copy()
draw_graph(G4, output_dir / 'kg_government_body.png', '政府机构关系')
def print_statistics(G):
"""打印统计"""
print(f"\n{'='*60}")
print("图谱统计")
print(f"{'='*60}")
print(f"节点: {G.number_of_nodes()}, 边: {G.number_of_edges()}")
type_counts = {}
for _, d in G.nodes(data=True):
nt = d.get('node_type', 'Unknown')
type_counts[nt] = type_counts.get(nt, 0) + 1
print("\n节点类型:")
for nt, c in sorted(type_counts.items()):
print(f" {nt}: {c} ({c/G.number_of_nodes()*100:.1f}%)")
rel_counts = {}
for _, _, d in G.edges(data=True):
rt = d.get('rel_type', 'Unknown')
rel_counts[rt] = rel_counts.get(rt, 0) + 1
print("\n关系类型:")
for rt, c in sorted(rel_counts.items()):
print(f" {rt}: {c}")
degrees = [d for _, d in G.degree()]
print(f"\n平均连接度: {np.mean(degrees):.2f}")
top = sorted(G.degree(), key=lambda x: x[1], reverse=True)[:10]
print("连接度最高:")
for node, deg in top:
data = G.nodes[node]
print(f" [{data.get('node_type')}] {data.get('full_label', node)}: {deg}")
print(f"{'='*60}")
def main():
setup_chinese_font()
base = Path(r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output")
# 优先使用合并后的文件
nodes_csv = base / 'nodes_merged.csv' if (base / 'nodes_merged.csv').exists() else base / 'nodes_structured.csv'
rels_csv = base / 'rels_merged.csv' if (base / 'rels_merged.csv').exists() else base / 'rels_structured.csv'
nodes_df, rels_df = load_data(nodes_csv, rels_csv)
G = build_graph(nodes_df, rels_df)
print_statistics(G)
draw_subgraphs(G, base / 'visualizations')
if __name__ == '__main__':
main()
View File
@@ -0,0 +1,176 @@
"""
实体规范化器 - 去重、ID生成和规范化
"""
import json
import hashlib
import logging
from typing import Dict, List, Any, Optional
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None
logger = logging.getLogger(__name__)
class LegalEntityNormalizer:
"""法规实体规范化器"""
# 法律领域常见同义词映射
SYNONYM_MAP = {
'城乡规划': '城市规划',
'住建部': '住房和城乡建设部',
'建设部': '住房和城乡建设部',
'自然资源部': '自然资源部',
'国土部': '自然资源部',
'国土资源部': '自然资源部',
'环保部': '生态环境部',
'环境保护部': '生态环境部',
'国家发改委': '国家发展和改革委员会',
'发改委': '国家发展和改革委员会',
'国务院': '国务院',
# 第一轮旧名称 → 第二轮新名称
'GovernmentBody': 'Agency',
'LegalSubject': 'LegalObject',
'SpatialConcept': 'SpatialObject',
'AdministrativeProcedure': 'Procedure',
# 城市更新领域同义词
'老旧小区': '老旧小区',
'城中村': '城中村',
'棚户区': '棚户区',
'旧城改造': '城市更新',
'旧区改造': '城市更新',
'三旧改造': '城市更新',
}
def __init__(self, ontology_file: str = None):
self.entity_registry = {}
self.text_to_id_map = {}
self.type_counters = {}
if ontology_file:
self._load_ontology(ontology_file)
logger.info("LegalEntityNormalizer 初始化完成")
def _load_ontology(self, ontology_file: str):
if yaml is None:
return
path = Path(ontology_file)
if path.exists():
with open(path, 'r', encoding='utf-8') as f:
self.ontology = yaml.safe_load(f)
def normalize_text(self, text: str) -> str:
"""标准化文本"""
if not text:
return ""
text = text.strip()
text = text.replace(' ', ' ').replace('\xa0', ' ')
return text
def generate_entity_id(self, entity_type: str, entity_text: str) -> str:
"""生成实体ID"""
type_prefixes = {
# 第二轮本体:城市更新法规政策工具箱
'Agency': 'AGENCY',
'LegalObject': 'OBJ',
'SpatialObject': 'SPAT',
'RenewalScene': 'SCENE',
'PolicyTool': 'TOOL',
'ToolCategory': 'TCAT',
'Procedure': 'PROC',
'Obligation': 'OBL',
'Condition': 'COND',
'Constraint': 'CONST',
'Penalty': 'PEN',
'TimePoint': 'TIME',
'Region': 'REGION',
# 兼容第一轮旧名称
'GovernmentBody': 'AGENCY',
'LegalSubject': 'OBJ',
'SpatialConcept': 'SPAT',
'AdministrativeProcedure': 'PROC',
}
prefix = type_prefixes.get(entity_type, 'ENT')
if entity_type not in self.type_counters:
self.type_counters[entity_type] = 0
self.type_counters[entity_type] += 1
text_hash = abs(hash(entity_text)) % 100000
counter = self.type_counters[entity_type]
return f"{prefix}-{text_hash:05d}-{counter}"
def normalize_entity(self, raw_entity: Dict[str, Any]) -> Optional[str]:
"""规范化单个实体"""
entity_text = raw_entity.get('text', '')
entity_type = raw_entity.get('type', '')
attributes = raw_entity.get('attributes', {})
if not entity_text or not entity_type:
return None
# 标准化文本
normalized_text = self.normalize_text(entity_text)
# 同义词映射
if normalized_text in self.SYNONYM_MAP:
normalized_text = self.SYNONYM_MAP[normalized_text]
# 检查是否已存在
key = f"{entity_type}:{normalized_text}"
if key in self.text_to_id_map:
return self.text_to_id_map[key]
# 生成新ID
entity_id = self.generate_entity_id(entity_type, normalized_text)
# 注册
self.entity_registry[entity_id] = {
'canonical_name': normalized_text,
'display_name': entity_text,
'type': entity_type,
'attributes': attributes,
}
self.text_to_id_map[key] = entity_id
return entity_id
def normalize_batch(self, extraction_results: List[Dict]) -> Dict[str, str]:
"""批量规范化"""
for result in extraction_results:
if not result or 'entities' not in result:
continue
for entity in result.get('entities', []):
self.normalize_entity(entity)
logger.info(f"规范化完成: {len(self.entity_registry)} 个唯一实体")
return self.text_to_id_map.copy()
def get_entity_nodes(self) -> List[Dict[str, Any]]:
"""获取所有实体节点"""
nodes = []
for entity_id, entity_data in self.entity_registry.items():
node = {
'id': entity_id,
'label': entity_data['display_name'],
'type': entity_data['type'],
'properties': json.dumps(entity_data['attributes'], ensure_ascii=False)
}
nodes.append(node)
return nodes
def get_statistics(self) -> Dict[str, Any]:
"""获取统计信息"""
stats = {
'total_entities': len(self.entity_registry),
'entities_by_type': {},
}
for entity_data in self.entity_registry.values():
et = entity_data['type']
stats['entities_by_type'][et] = stats['entities_by_type'].get(et, 0) + 1
return stats
@@ -0,0 +1,189 @@
"""
关系构建器 - 从LLM抽取结果构建关系
"""
import json
import logging
from typing import Dict, List, Any, Optional
logger = logging.getLogger(__name__)
class RelationshipBuilder:
"""关系构建器"""
# LLM关系类型到大写关系类型的映射
REL_TYPE_MAP = {
# 文档层次
'has_chapter': 'HAS_CHAPTER',
'has_section': 'HAS_SECTION',
'has_article': 'HAS_ARTICLE',
'has_clause': 'HAS_CLAUSE',
'article_in_document': 'ARTICLE_IN_DOCUMENT',
# 法条依据
'cites': 'CITES',
'implements': 'IMPLEMENTS',
'amends': 'AMENDS',
'replaces': 'REPLACES',
'supplements': 'SUPPLEMENTS',
'basis_for_planning': 'BASIS_FOR_PLANNING',
# 主体关系
'issued_by': 'ISSUED_BY',
'implemented_by': 'IMPLEMENTED_BY',
'assigned_to': 'ASSIGNED_TO',
# 语义规制
'regulates': 'REGULATES',
'defines_object': 'DEFINES_OBJECT',
'applies_to': 'APPLIES_TO',
'sets_obligation': 'SETS_OBLIGATION',
'sets_condition': 'SETS_CONDITION',
'sets_constraint': 'SETS_CONSTRAINT',
'prescribes_penalty': 'PRESCRIBES_PENALTY',
# 政策工具
'extracts_tool': 'EXTRACTS_TOOL',
'belongs_to_category': 'BELONGS_TO_CATEGORY',
'applies_to_scene': 'APPLIES_TO_SCENE',
'targets_object': 'TARGETS_OBJECT',
'coordinates_with': 'COORDINATES_WITH',
'supports_tool': 'SUPPORTS_TOOL',
# 程序
'requires_procedure': 'REQUIRES_PROCEDURE',
'procedure_for': 'PROCEDURE_FOR',
'precedes_procedure': 'PRECEDES_PROCEDURE',
# 辅助
'governs_region': 'GOVERNS_REGION',
'effective_timeline': 'EFFECTIVE_TIMELINE',
'standard_for': 'STANDARD_FOR',
'article_effective_timeline': 'ARTICLE_EFFECTIVE_TIMELINE',
# 兼容第一轮旧名称
'defines_spatial': 'DEFINES_OBJECT',
}
def __init__(self):
self.relationships = []
def build_from_extraction(
self,
extraction_results: List[Dict],
entity_id_map: Dict[str, str],
source_doc_id: str = None
) -> List[Dict[str, Any]]:
"""
从LLM抽取结果构建关系
Args:
extraction_results: LLM抽取结果列表
entity_id_map: 文本到ID的映射
source_doc_id: 源文档ID
"""
relationships = []
rel_counter = 0
for result in extraction_results:
if not result or 'relationships' not in result:
continue
# 建立实体文本到ID的快速查找
local_entity_map = {}
if 'entities' in result:
for entity in result['entities']:
text = entity.get('text', '')
etype = entity.get('type', '')
key = f"{etype}:{text}"
if key in entity_id_map:
local_entity_map[text] = entity_id_map[key]
for rel in result.get('relationships', []):
from_text = rel.get('from_entity', '')
to_text = rel.get('to_entity', '')
rel_type = rel.get('type', '')
context = rel.get('context', '')
confidence = rel.get('confidence', 0.8)
# 查找实体ID
from_id = local_entity_map.get(from_text)
to_id = local_entity_map.get(to_text)
if not from_id or not to_id:
continue
rel_type_upper = self.REL_TYPE_MAP.get(rel_type, rel_type.upper())
rel_counter += 1
relationships.append({
'source': from_id,
'target': to_id,
'type': rel_type_upper,
'properties': json.dumps({
'context': context,
'confidence': confidence,
'source_doc': source_doc_id,
}, ensure_ascii=False)
})
self.relationships.extend(relationships)
logger.info(f"构建了 {len(relationships)} 个关系")
return relationships
def build_reference_relationships(
self,
resolved_refs: List[Dict],
doc_id_map: Dict[str, str]
) -> List[Dict[str, Any]]:
"""
从引用解析结果构建引用关系
Args:
resolved_refs: CitationResolver解析的引用列表
doc_id_map: 文档标题到ID的映射
"""
relationships = []
for ref in resolved_refs:
source_title = ref.get('source_title', '')
target_id = ref.get('target_id', '')
rel_type = ref.get('relation_type', 'CITES')
context = ref.get('context', '')
source_id = doc_id_map.get(source_title)
if not source_id:
continue
relationships.append({
'source': source_id,
'target': target_id,
'type': rel_type,
'properties': json.dumps({'context': context}, ensure_ascii=False)
})
logger.info(f"构建了 {len(relationships)} 个引用关系")
return relationships
def validate_relationships(
self,
relationships: List[Dict],
valid_node_ids: set
) -> Dict[str, Any]:
"""验证关系数据"""
valid = 0
invalid = 0
broken_links = []
for rel in relationships:
source = rel.get('source', '')
target = rel.get('target', '')
if source in valid_node_ids and target in valid_node_ids:
valid += 1
else:
invalid += 1
if source not in valid_node_ids:
broken_links.append(f"source not found: {source}")
if target not in valid_node_ids:
broken_links.append(f"target not found: {target}")
return {
'valid_relationships': valid,
'invalid_relationships': invalid,
'broken_links': broken_links[:50],
}
@@ -0,0 +1,335 @@
"""
深度法规实体抽取管道
协调LLM抽取、实体规范化、关系构建
"""
import asyncio
import json
import pandas as pd
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any
import sys
sys.path.insert(0, str(Path(__file__).parent))
from knowledge_extraction.llm_legal_extractor import LegalLLMExtractor
from knowledge_extraction.citation_resolver import CitationResolver
from data_processing.entity_normalizer import LegalEntityNormalizer
from data_processing.relationship_builder import RelationshipBuilder
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class DeepExtractionPipeline:
"""深度法规实体抽取管道"""
def __init__(self, config_file: str):
self.config_file = config_file
self.extractor = LegalLLMExtractor(config_file)
self.config = self.extractor.config
self.normalizer = LegalEntityNormalizer()
self.builder = RelationshipBuilder()
self.project_root = Path(__file__).parent.parent
def load_structured_documents(self, input_file: str) -> List[Dict]:
"""加载结构化文档数据"""
with open(input_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return data['documents']
def _prepare_sections(self, documents: List[Dict]) -> List[Dict]:
"""准备章节级别的抽取单元"""
sections = []
for doc in documents:
doc_title = doc.get('title', '')
chapters = doc.get('chapters', [])
for chapter in chapters:
# 将章节下所有条文合并为一个抽取单元
texts = []
for art in chapter.get('articles', []):
art_text = art.get('full_text', art.get('text', ''))
if art_text:
texts.append(art_text)
for section in chapter.get('sections', []):
for art in section.get('articles', []):
art_text = art.get('full_text', art.get('text', ''))
if art_text:
texts.append(art_text)
combined_text = '\n'.join(texts)
if len(combined_text.strip()) > 20:
sections.append({
'text': combined_text,
'document_title': doc_title,
'section_title': chapter.get('title', ''),
'doc_id': doc.get('doc_id', ''),
})
logger.info(f"准备了 {len(sections)} 个章节抽取单元")
return sections
def _prepare_documents_for_references(self, documents: List[Dict]) -> List[Dict]:
"""准备文档级引用抽取"""
doc_list = []
for doc in documents:
doc_list.append({
'title': doc.get('title', ''),
'text': doc.get('text_preview', ''), # 需要原始文本
'doc_id': doc.get('doc_id', ''),
})
return doc_list
async def run(
self,
structured_input: str,
raw_input: str,
output_dir: str,
max_sections: int = None,
) -> Dict[str, Any]:
"""
执行完整抽取流程
Args:
structured_input: 结构化文档JSON路径
raw_input: 原始文档JSON路径(用于引用抽取)
output_dir: 输出目录
max_sections: 最大处理章节数(测试用)
"""
start_time = datetime.now()
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# 1. 加载数据
logger.info("加载结构化文档...")
documents = self.load_structured_documents(structured_input)
logger.info(f"加载 {len(documents)} 个文档")
# 2. 准备章节抽取单元
sections = self._prepare_sections(documents)
if max_sections:
sections = sections[:max_sections]
logger.info(f"限制处理: {max_sections} 个章节")
# 3. 加载已有的结构化节点(用于引用解析)
nodes_csv = output_path / 'nodes_structured.csv'
known_docs = []
if nodes_csv.exists():
nodes_df = pd.read_csv(nodes_csv, encoding='utf-8-sig')
known_docs = [
{'id': row['id'], 'label': row['label'], 'type': row['type']}
for _, row in nodes_df.iterrows()
if row['type'] in ['Law', 'AdministrativeRegulation', 'DepartmentalRule', 'PolicyDocument']
]
logger.info(f"已知文档节点: {len(known_docs)}")
# 4. LLM章节级抽取
logger.info("开始LLM章节级抽取...")
extraction_results = await self.extractor.batch_extract_sections(
sections,
concurrent=self.extractor.config.get('batch_processing', {}).get('concurrent_requests', 5)
)
# 5. 实体规范化
logger.info("规范化实体...")
entity_id_map = self.normalizer.normalize_batch(extraction_results)
entity_nodes = self.normalizer.get_entity_nodes()
stats = self.normalizer.get_statistics()
logger.info(f"规范化后实体: {stats['total_entities']}")
# 6. 关系构建
logger.info("构建关系...")
relationships = self.builder.build_from_extraction(
extraction_results, entity_id_map
)
# 7. 文档级引用抽取
logger.info("开始文档级引用抽取...")
# 加载原始文本用于引用抽取
with open(raw_input, 'r', encoding='utf-8') as f:
raw_data = json.load(f)
doc_id_map = {d['label']: d['id'] for d in known_docs}
# 补充从文档数据中获取原始文本
docs_for_ref = []
raw_docs_map = {d['filename']: d for d in raw_data['documents']}
for doc in documents:
filename = doc.get('filename', '')
raw_doc = raw_docs_map.get(filename, {})
raw_text = raw_doc.get('raw_text', '')
if raw_text and len(raw_text) > 50:
docs_for_ref.append({
'title': doc['title'],
'text': raw_text[:5000], # 截取前5000字符
})
ref_results = await self.extractor.batch_extract_references(docs_for_ref, concurrent=3)
# 8. 引用解析
logger.info("解析法规引用...")
resolver = CitationResolver(known_docs)
all_references = []
for result in ref_results:
if not result or 'references' not in result:
continue
doc_title = result.get('metadata', {}).get('document_title', '')
for ref in result['references']:
ref['document_title'] = doc_title
all_references.append(ref)
resolved_refs, unresolved_refs = resolver.resolve_batch(all_references)
ref_relationships = self.builder.build_reference_relationships(resolved_refs, doc_id_map)
relationships.extend(ref_relationships)
# 9. 保存结果
logger.info("保存结果...")
# 从配置获取输出文件名
output_cfg = self.config.get('output', {})
llm_nodes_name = output_cfg.get('nodes_file', 'output/nodes_llm_v2.csv').split('/')[-1]
llm_rels_name = output_cfg.get('relationships_file', 'output/rels_llm_v2.csv').split('/')[-1]
report_name = output_cfg.get('report_file', 'output/extraction_report_v2.md').split('/')[-1]
# LLM节点
llm_nodes_df = pd.DataFrame(entity_nodes)
llm_nodes_path = output_path / llm_nodes_name
llm_nodes_df.to_csv(llm_nodes_path, index=False, encoding='utf-8-sig')
# LLM关系
llm_rels_df = pd.DataFrame(relationships)
llm_rels_path = output_path / llm_rels_name
llm_rels_df.to_csv(llm_rels_path, index=False, encoding='utf-8-sig')
# 10. 合并
logger.info("合并结构化和LLM结果...")
if nodes_csv.exists():
structured_nodes = pd.read_csv(nodes_csv, encoding='utf-8-sig')
merged_nodes = pd.concat([structured_nodes, llm_nodes_df], ignore_index=True)
merged_nodes = merged_nodes.drop_duplicates(subset=['id'], keep='first')
else:
merged_nodes = llm_nodes_df
rels_csv = output_path / 'rels_structured.csv'
if rels_csv.exists():
structured_rels = pd.read_csv(rels_csv, encoding='utf-8-sig')
merged_rels = pd.concat([structured_rels, llm_rels_df], ignore_index=True)
else:
merged_rels = llm_rels_df
merged_nodes_path = output_path / 'nodes_merged_v2.csv'
merged_rels_path = output_path / 'rels_merged_v2.csv'
merged_nodes.to_csv(merged_nodes_path, index=False, encoding='utf-8-sig')
merged_rels.to_csv(merged_rels_path, index=False, encoding='utf-8-sig')
# 验证
valid_ids = set(merged_nodes['id'].tolist())
validation = self.builder.validate_relationships(
merged_rels.to_dict('records'), valid_ids
)
# 生成报告
duration = (datetime.now() - start_time).total_seconds()
self._generate_report(
output_path, sections, extraction_results, entity_nodes,
relationships, resolved_refs, unresolved_refs, validation, duration,
report_name=report_name
)
result = {
'total_sections': len(sections),
'total_entities': len(entity_nodes),
'total_relationships': len(relationships),
'total_references_resolved': len(resolved_refs),
'total_references_unresolved': len(unresolved_refs),
'duration_seconds': duration,
}
logger.info(f"\n抽取完成!耗时: {duration:.1f}s")
logger.info(f"章节: {result['total_sections']}, 实体: {result['total_entities']}, "
f"关系: {result['total_relationships']}, 引用: {result['total_references_resolved']}")
return result
def _generate_report(self, output_path, sections, extraction_results,
entity_nodes, relationships, resolved_refs,
unresolved_refs, validation, duration,
report_name='extraction_report_v2.md'):
"""生成抽取报告"""
lines = ["# 第二轮深度抽取报告(城市更新政策工具箱本体)\n"]
lines.append(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
lines.append(f"**耗时**: {duration:.1f}\n\n")
lines.append("## 统计\n\n")
lines.append(f"- 处理章节: {len(sections)}\n")
lines.append(f"- 抽取实体: {len(entity_nodes)}\n")
lines.append(f"- 构建关系: {len(relationships)}\n")
lines.append(f"- 解析引用: {len(resolved_refs)} 成功, {len(unresolved_refs)} 未匹配\n")
# 实体类型分布
lines.append("\n## 实体类型分布\n\n")
type_counts = {}
for node in entity_nodes:
t = node['type']
type_counts[t] = type_counts.get(t, 0) + 1
lines.append("| 类型 | 数量 |\n|------|------|\n")
for t, c in sorted(type_counts.items()):
lines.append(f"| {t} | {c} |\n")
# 关系类型分布
lines.append("\n## 关系类型分布\n\n")
rel_type_counts = {}
for rel in relationships:
t = rel['type']
rel_type_counts[t] = rel_type_counts.get(t, 0) + 1
lines.append("| 类型 | 数量 |\n|------|------|\n")
for t, c in sorted(rel_type_counts.items()):
lines.append(f"| {t} | {c} |\n")
# 数据质量
lines.append("\n## 数据质量\n\n")
lines.append(f"- 有效关系: {validation['valid_relationships']}\n")
lines.append(f"- 无效关系: {validation['invalid_relationships']}\n")
report_path = output_path / report_name
with open(report_path, 'w', encoding='utf-8') as f:
f.writelines(lines)
logger.info(f"报告已保存: {report_path}")
async def main():
config_file = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\config\deep_extraction_config.yaml"
structured_input = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\structured_documents.json"
raw_input = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\all_documents.json"
output_dir = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output"
pipeline = DeepExtractionPipeline(config_file)
# 测试模式:只处理前20个章节
import sys
max_sections = 20
if '--full' in sys.argv:
max_sections = None
results = await pipeline.run(
structured_input, raw_input, output_dir,
max_sections=max_sections
)
print("\n结果:")
print(json.dumps(results, indent=2, ensure_ascii=False))
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,134 @@
"""
法规引用解析器 - 将文本中的法规引用映射到已知文档节点
"""
import re
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from difflib import SequenceMatcher
logger = logging.getLogger(__name__)
class CitationResolver:
"""法规引用解析器"""
def __init__(self, known_documents: List[Dict]):
"""
Args:
known_documents: 已知文档列表,每个包含 id, label, type
"""
self.known_docs = {}
self.name_variants = {}
for doc in known_documents:
name = doc['label']
doc_id = doc['id']
self.known_docs[name] = doc_id
# 生成名称变体
variants = self._generate_variants(name)
for variant in variants:
self.name_variants[variant] = doc_id
logger.info(f"CitationResolver 初始化: {len(self.known_docs)} 个已知文档")
def _generate_variants(self, name: str) -> List[str]:
"""生成法规名称变体"""
variants = [name]
# 去掉书名号
cleaned = name.replace('', '').replace('', '')
if cleaned != name:
variants.append(cleaned)
# 去掉"中华人民共和国"前缀
if name.startswith('中华人民共和国'):
short = name.replace('中华人民共和国', '')
variants.append(short)
# 简称
if '实施条例' in name:
variants.append(name.replace('实施条例', '条例'))
if '实施办法' in name:
variants.append(name.replace('实施办法', '办法'))
return list(set(variants))
def resolve(self, referenced_name: str, threshold: float = 0.7) -> Optional[str]:
"""
解析引用名称到文档ID
Args:
referenced_name: 被引用的法规名称
threshold: 相似度阈值
Returns:
文档ID,未找到返回None
"""
if not referenced_name:
return None
# 清理名称
cleaned = referenced_name.strip()
cleaned = re.sub(r'[《》]', '', cleaned)
# 精确匹配
if cleaned in self.known_docs:
return self.known_docs[cleaned]
if cleaned in self.name_variants:
return self.name_variants[cleaned]
# 模糊匹配
best_match = None
best_score = 0
for known_name, doc_id in self.known_docs.items():
score = SequenceMatcher(None, cleaned, known_name).ratio()
if score > best_score and score >= threshold:
best_score = score
best_match = doc_id
if best_match:
logger.info(f"模糊匹配: '{referenced_name}' -> {best_match} (score={best_score:.2f})")
return best_match
def resolve_batch(self, references: List[Dict]) -> List[Tuple[str, str]]:
"""
批量解析引用
Returns:
List of (source_doc_title, target_doc_id) pairs
"""
resolved = []
unresolved = []
for ref in references:
doc_title = ref.get('document_title', '')
referenced_law = ref.get('referenced_law', '')
relation_type = ref.get('relation_type', 'cites')
context = ref.get('context', '')
target_id = self.resolve(referenced_law)
if target_id:
resolved.append({
'source_title': doc_title,
'target_id': target_id,
'target_name': referenced_law,
'relation_type': relation_type.upper(),
'context': context,
})
else:
unresolved.append({
'source_title': doc_title,
'target_name': referenced_law,
'relation_type': relation_type,
})
logger.info(f"引用解析完成: {len(resolved)} 成功, {len(unresolved)} 未匹配")
return resolved, unresolved
@@ -0,0 +1,303 @@
"""
基于DeepSeek的法规知识抽取器
从法条文本中抽取法律实体和关系
使用OpenAI SDK直接调用DeepSeek API
"""
import os
# 解决Windows上httpx代理检测导致的连接失败
os.environ['NO_PROXY'] = '*'
import yaml
import asyncio
import json
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Any
try:
from openai import OpenAI
except ImportError:
raise ImportError("请安装: pip install openai")
class LegalLLMExtractor:
"""基于DeepSeek的法规知识抽取器"""
def __init__(self, config_file: str):
self.config = self._load_config(config_file)
self.api_key = self._load_api_key()
self.client = OpenAI(
api_key=self.api_key,
base_url='https://api.deepseek.com',
)
self.logger = logging.getLogger(__name__)
def _load_config(self, config_file: str) -> Dict:
config_path = Path(config_file)
if not config_path.exists():
config_path = Path(__file__).parent.parent.parent / 'config' / 'deep_extraction_config.yaml'
with open(config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _load_api_key(self) -> str:
api_file = Path(__file__).parent.parent.parent / 'config' / 'api_keys.yaml'
if api_file.exists():
with open(api_file, 'r', encoding='utf-8') as f:
api_config = yaml.safe_load(f)
key = api_config.get('deepseek_api_key', '')
if key and key != 'YOUR_API_KEY_HERE':
return key
key = os.environ.get('DEEPSEEK_API_KEY', '')
if key:
return key
raise ValueError("未找到DeepSeek API密钥,请配置 config/api_keys.yaml 或设置 DEEPSEEK_API_KEY 环境变量")
def _extract_json(self, response_text: str) -> str:
"""从响应中提取JSON"""
if '```json' in response_text:
start = response_text.find('```json') + 7
end = response_text.find('```', start)
if end > start:
return response_text[start:end].strip()
elif '```' in response_text:
start = response_text.find('```') + 3
end = response_text.find('```', start)
if end > start:
content = response_text[start:end].strip()
if content.startswith('json'):
content = content[4:].strip()
return content
return response_text.strip()
def _call_api(self, prompt: str, max_retries: int = 3) -> Optional[str]:
"""同步调用API"""
for attempt in range(max_retries):
try:
response = self.client.chat.completions.create(
model=self.config['llm']['model'],
messages=[{'role': 'user', 'content': prompt}],
temperature=self.config['llm']['temperature'],
max_tokens=self.config['llm']['max_tokens'],
)
return response.choices[0].message.content
except Exception as e:
self.logger.error(f"API调用失败 (attempt {attempt+1}/{max_retries}): {e}")
if attempt == max_retries - 1:
return None
import time
time.sleep(self.config['llm'].get('retry_delay', 2))
async def _call_api_async(self, prompt: str, max_retries: int = 3) -> Optional[str]:
"""异步调用API"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(None, lambda: self._call_api(prompt, max_retries))
async def extract_from_section(
self,
section_text: str,
document_title: str,
section_title: str,
max_retries: int = 3
) -> Optional[Dict[str, Any]]:
"""从章节文本中抽取实体和关系(基于动态配置的本体)"""
if len(section_text) > 5000:
section_text = section_text[:5000]
# 从配置动态构建实体类型列表
entity_prompts = self.config.get('entity_type_prompts', {})
entity_types_desc = '\n'.join(
f"- {etype}{desc}"
for etype, desc in entity_prompts.items()
)
# 从配置动态构建关系类型列表
rel_prompts = self.config.get('relationship_type_prompts', {})
rel_types_desc = '\n'.join(
f"- {rtype}{desc}"
for rtype, desc in rel_prompts.items()
)
# 边界规则
boundary_rules = self.config.get('extraction_tips', {}).get('entity_boundary_rules', '')
do_not = self.config.get('extraction_tips', {}).get('do_not_extract', '')
prompt = f"""你是一位中国城市更新法规政策分析专家。请从以下法规章节中识别实体和关系。
所属法规:{document_title}
章节名称:{section_title}
文本内容:
{section_text}
## 需要识别的实体类型
{entity_types_desc}
## 需要识别的关系类型
{rel_types_desc}
{boundary_rules}
{do_not}
## 输出JSON格式
严格输出以下JSON格式,不要输出其他内容:
{{
"entities": [
{{
"text": "实体原文表述",
"type": "实体类型(必须是上述类型之一)",
"attributes": {{
"description": "实体简要描述"
}}
}}
],
"relationships": [
{{
"from_entity": "源实体文本",
"from_type": "源实体类型",
"to_entity": "目标实体文本",
"to_type": "目标实体类型",
"type": "关系类型(必须是上述类型之一)",
"context": "关系上下文原文",
"confidence": 0.9
}}
]
}}
## 注意事项
- 只抽取文本中明确提到的实体和关系,不要推测
- 关系的from_entity和to_entity必须是entities中出现的实体
- 保持原文表述,不要改写实体名称
- confidence范围0.0-1.0,表示抽取的可信度
- 重点关注PolicyTool(政策工具)的识别,这是本次抽取的核心目标
- 确保输出有效JSON,不要输出JSON以外的内容"""
response_text = await self._call_api_async(prompt, max_retries)
if not response_text:
return None
try:
json_text = self._extract_json(response_text)
result = json.loads(json_text)
result['metadata'] = {
'document_title': document_title,
'section_title': section_title,
'extraction_time': datetime.now().isoformat(),
}
return result
except json.JSONDecodeError as e:
self.logger.warning(f"JSON解析失败: {e}")
return None
async def extract_document_references(
self,
document_title: str,
full_text: str,
max_retries: int = 3
) -> Optional[Dict[str, Any]]:
"""从文档全文中抽取跨文档引用关系"""
text_sample = full_text[:3000]
if len(full_text) > 5000:
text_sample += '\n...\n' + full_text[-2000:]
prompt = f"""你是一位中国法律专家。请从以下法规文本中识别引用的其他法规文件。
法规名称:{document_title}
文本片段:
{text_sample}
请识别以下引用关系:
- cites(引用):文本中提到的其他法规
- implements(实施):文本声明为实施某上位法而制定
- amends(修正):文本对其他法规的修改
- replaces(替代):文本声明替代或废止其他法规
输出JSON格式:
{{
"references": [
{{
"referenced_law": "被引用的法规名称",
"relation_type": "cites/implements/amends/replaces",
"context": "引用上下文原文"
}}
]
}}
注意:只识别文本中明确提到的法规名称(书名号《》内的名称),不要推测。"""
response_text = await self._call_api_async(prompt, max_retries)
if not response_text:
return None
try:
json_text = self._extract_json(response_text)
result = json.loads(json_text)
result['metadata'] = {
'document_title': document_title,
'extraction_time': datetime.now().isoformat(),
}
return result
except json.JSONDecodeError:
return None
async def batch_extract_sections(
self,
sections: List[Dict[str, str]],
concurrent: int = 5,
progress_callback=None
) -> List[Optional[Dict]]:
"""批量抽取章节实体"""
results = []
batch_size = concurrent
for i in range(0, len(sections), batch_size):
batch = sections[i:i + batch_size]
batch_num = i // batch_size + 1
total_batches = (len(sections) + batch_size - 1) // batch_size
self.logger.info(f"处理章节批次 {batch_num}/{total_batches}: {len(batch)}")
tasks = [
self.extract_from_section(
s['text'], s['document_title'], s['section_title']
)
for s in batch
]
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
results.extend(batch_results)
if progress_callback:
await progress_callback(batch_num, total_batches, batch_results, batch)
return results
async def batch_extract_references(
self,
documents: List[Dict[str, str]],
concurrent: int = 3
) -> List[Optional[Dict]]:
"""批量抽取文档引用"""
results = []
batch_size = concurrent
for i in range(0, len(documents), batch_size):
batch = documents[i:i + batch_size]
tasks = [
self.extract_document_references(d['title'], d['text'])
for d in batch
]
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
results.extend(batch_results)
self.logger.info(f"引用抽取进度: {min(i+batch_size, len(documents))}/{len(documents)}")
return results
+166
View File
@@ -0,0 +1,166 @@
"""
城市规划法律法规知识图谱构建 - 主入口
"""
import sys
import json
import asyncio
from pathlib import Path
# 添加项目路径
sys.path.insert(0, str(Path(__file__).parent))
def step1_read_documents():
"""Step1: 读取Word文档"""
from importlib import import_module
# 动态导入scripts目录下的模块
sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))
docx_reader = import_module('docx_reader')
source_dir = r"E:\Project\SI\2026_KG_PlanningLaw\data\城市规划法律法规\城市更新法规数据库"
output_path = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\all_documents.json"
reader = docx_reader.DocxReader(source_dir)
reader.read_all_documents()
reader.save_to_json(output_path)
reader.print_summary()
def step2_extract_metadata():
"""Step2: 解析法规元数据"""
sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))
metadata_extractor = import_module('legal_metadata_extractor')
input_path = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\all_documents.json"
output_path = r"E:\Project\SI\2026_KG_PlanningLaw\dofile\kg_project\output\structured_documents.json"
extractor = metadata_extractor.LegalMetadataExtractor()
extractor.process_all(input_path, output_path)
def step3_generate_csv():
"""Step3: 生成结构化CSV"""
sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))
extract_csv = import_module('extract_legal_csv')
extract_csv.main()
def step4_deep_extraction():
"""Step4: LLM深度抽取"""
from deep_extraction_pipeline import DeepExtractionPipeline
config_file = str(Path(__file__).parent.parent / 'config' / 'deep_extraction_config.yaml')
structured_input = str(Path(__file__).parent.parent / 'output' / 'structured_documents.json')
raw_input = str(Path(__file__).parent.parent / 'output' / 'all_documents.json')
output_dir = str(Path(__file__).parent.parent / 'output')
pipeline = DeepExtractionPipeline(config_file)
results = asyncio.run(pipeline.run(
structured_input, raw_input, output_dir,
max_sections=None # 完整抽取
))
print("\n结果:")
print(json.dumps(results, indent=2, ensure_ascii=False))
def step5_visualize():
"""Step5: 可视化"""
from importlib import import_module
sys.path.insert(0, str(Path(__file__).parent.parent / 'scripts'))
visualize = import_module('visualize_legal_kg')
visualize.main()
def show_status():
"""显示项目状态"""
output_dir = Path(__file__).parent.parent / 'output'
print("\n" + "=" * 60)
print("城市规划法律法规知识图谱 - 项目状态")
print("=" * 60)
files = {
'all_documents.json': 'Step1: Word文档读取',
'structured_documents.json': 'Step2: 法规元数据解析',
'nodes_structured.csv': 'Step3: 结构化节点',
'rels_structured.csv': 'Step3: 结构化关系',
'nodes_llm.csv': 'Step4: LLM抽取节点',
'rels_llm.csv': 'Step4: LLM抽取关系',
'nodes_merged.csv': 'Step5: 合并节点',
'rels_merged.csv': 'Step5: 合并关系',
}
for filename, description in files.items():
filepath = output_dir / filename
if filepath.exists():
import os
size = os.path.getsize(filepath)
print(f" [OK] {description} ({filename}, {size:,} bytes)")
else:
print(f" [--] {description} ({filename}, 未生成)")
print("=" * 60)
def importlib_safe_import(module_name):
from importlib import import_module
return import_module(module_name)
def main():
import argparse
parser = argparse.ArgumentParser(description='城市规划法律法规知识图谱构建')
parser.add_argument('--step', type=int, choices=[1, 2, 3, 4, 5], help='执行指定步骤')
parser.add_argument('--full', action='store_true', help='运行完整流程')
parser.add_argument('--status', action='store_true', help='查看项目状态')
parser.add_argument('--test', action='store_true', help='测试模式(少量数据)')
args = parser.parse_args()
if args.status:
show_status()
return
steps = {
1: ("读取Word文档", step1_read_documents),
2: ("解析法规元数据", step2_extract_metadata),
3: ("生成结构化CSV", step3_generate_csv),
4: ("LLM深度抽取", step4_deep_extraction),
5: ("可视化", step5_visualize),
}
if args.step:
name, func = steps[args.step]
print(f"\n{'='*60}")
print(f"执行 Step {args.step}: {name}")
print(f"{'='*60}")
func()
elif args.full:
print("\n运行完整流程...")
for step_num, (name, func) in steps.items():
print(f"\n{'='*60}")
print(f"Step {step_num}: {name}")
print(f"{'='*60}")
try:
func()
except Exception as e:
print(f"Step {step_num} 失败: {e}")
break
else:
print("请指定操作:")
print(" python src/main.py --step 1 # 读取Word文档")
print(" python src/main.py --step 2 # 解析法规元数据")
print(" python src/main.py --step 3 # 生成结构化CSV")
print(" python src/main.py --step 4 # LLM深度抽取")
print(" python src/main.py --step 5 # 可视化")
print(" python src/main.py --full # 运行完整流程")
print(" python src/main.py --status # 查看项目状态")
print(" python src/main.py --test # 测试模式")
if __name__ == '__main__':
main()
+29
View File
@@ -0,0 +1,29 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 城市规划法律法规知识图谱构建工具
echo ========================================
echo.
echo 请选择操作:
echo 1. 读取Word文档 (Step1)
echo 2. 解析法规元数据 (Step2)
echo 3. 生成结构化CSV (Step3)
echo 4. LLM深度抽取 (Step4)
echo 5. 实体规范化与合并 (Step5)
echo 6. 可视化 (Step6)
echo 7. 运行完整流程
echo 8. 查看进度
echo.
set /p choice=请输入选项:
if "%choice%"=="1" python scripts/docx_reader.py
if "%choice%"=="2" python scripts/legal_metadata_extractor.py
if "%choice%"=="3" python scripts/extract_legal_csv.py
if "%choice%"=="4" python src/deep_extraction_pipeline.py
if "%choice%"=="5" python src/data_processing/merge_data.py
if "%choice%"=="6" python scripts/visualize_legal_kg.py
if "%choice%"=="7" python src/main.py --full
if "%choice%"=="8" python src/main.py --status
pause