Commit 48a7a0a8 authored by 林洋洋's avatar 林洋洋

提交代码

parent 8c497c1f
Pipeline #312 failed with stages
-- 1. 发电量指标表
CREATE TABLE IF NOT EXISTS power_generation_indicators (
id SERIAL PRIMARY KEY, -- 自增主键
company_name VARCHAR(100), -- 公司名称(如:风电、乌海等)
subsidiary_name VARCHAR(100), -- 子公司名称(如:99MW、20MW等)
daily_generation NUMERIC(10,2), -- 日发电量(万kW.h)
monthly_generation NUMERIC(10,2), -- 月发电量(万kW.h)
monthly_plan NUMERIC(10,2), -- 月计划发电量(万kW.h)
monthly_completion_rate NUMERIC(10,2), -- 月计划完成率(%)
ytd_generation NUMERIC(10,2), -- 年累计发电量(万kW.h)
yearly_plan NUMERIC(10,2), -- 年度计划发电量(万kW.h)
yearly_completion_rate NUMERIC(10,2), -- 年计划完成率(%)
last_year_generation NUMERIC(10,2), -- 去年同期发电量(万kW.h)
resource_indicator VARCHAR(50), -- 资源指标(风速m/s或辐照量MJ/㎡)
monthly_utilization_hours NUMERIC(10,2), -- 月利用小时数(h)
auxiliary_power_rate NUMERIC(10,2), -- 厂用电率(%)
power_limit NUMERIC(10,2), -- 日限电量(万kW.h)
fault_count INTEGER, -- 月故障次数
fault_power_loss NUMERIC(10,2), -- 月故障损失电量(万kW.h)
record_date DATE -- 记录日期
);
-- 2. 故障停机记录表
CREATE TABLE IF NOT EXISTS fault_record (
id SERIAL PRIMARY KEY, -- 自增主键
type Int2, -- 类型 1 计划性停机停运情况 2 7天以内非计划停机停运情况 3 7天以上非计划停机停运情况
sequence_number INTEGER, -- 序号
plant_name VARCHAR(100), -- 电场名称
equipment_code VARCHAR(50), -- 设备名称编号
shutdown_reason VARCHAR(100), -- 停运原因
fault_description TEXT, -- 停机描述
shutdown_start_time TIMESTAMP, -- 停机开始时间
total_shutdown_hours NUMERIC(10,2), -- 累计已停机时间(h)
power_loss NUMERIC(10,2), -- 停机损失电量(万kW.h)
processing_result TEXT, -- 目前处理进度
spare TEXT, -- 备注
record_date DATE -- 记录日期
);
-- 为发电量指标表添加表注释
COMMENT ON TABLE power_generation_indicators IS '发电量指标统计表 - 记录各发电单位的日常发电量、计划完成情况等指标数据';
-- 为发电量指标表字段添加注释
COMMENT ON COLUMN power_generation_indicators.id IS '自增主键ID';
COMMENT ON COLUMN power_generation_indicators.company_name IS '公司名称:表示发电单位的主体公司名称,例如:风电、乌海等';
COMMENT ON COLUMN power_generation_indicators.subsidiary_name IS '子公司名称:具体的发电单位名称,可能包含容量信息,例如:99MW、20MW等';
COMMENT ON COLUMN power_generation_indicators.daily_generation IS '日发电量:当日的发电量数据,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.monthly_generation IS '月发电量:当月累计发电量,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.monthly_plan IS '月计划发电量:当月的计划发电量目标,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.monthly_completion_rate IS '月计划完成率:当月实际发电量与计划发电量的比率,单位:%';
COMMENT ON COLUMN power_generation_indicators.ytd_generation IS '年累计发电量:从年初至今的累计发电量,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.yearly_plan IS '年度计划发电量:全年的计划发电量目标,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.yearly_completion_rate IS '年计划完成率:年累计发电量与年度计划的完成比率,单位:%';
COMMENT ON COLUMN power_generation_indicators.last_year_generation IS '去年同期发电量:上一年度同期的发电量数据,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.resource_indicator IS '资源指标:风电场记录风速(m/s),光伏电站记录辐照量(MJ/㎡)';
COMMENT ON COLUMN power_generation_indicators.monthly_utilization_hours IS '月利用小时数:当月设备利用时间,单位:h';
COMMENT ON COLUMN power_generation_indicators.auxiliary_power_rate IS '厂用电率:电厂内部用电占比,单位:%';
COMMENT ON COLUMN power_generation_indicators.power_limit IS '日限电量:当日限电量,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.fault_count IS '月故障次数:当月发生的故障事件总数';
COMMENT ON COLUMN power_generation_indicators.fault_power_loss IS '月故障损失电量:当月因故障造成的发电量损失,单位:万kW.h';
COMMENT ON COLUMN power_generation_indicators.record_date IS '记录日期:数据统计的日期';
-- 为故障停机记录表添加表注释
COMMENT ON TABLE fault_record IS '故障停机记录表 - 记录设备停机事件的详细信息,包括计划内和计划外停机';
-- 为故障停机记录表字段添加注释
COMMENT ON COLUMN fault_record.id IS '自增主键ID';
COMMENT ON COLUMN fault_record.type IS '停机类型:1-计划性停机停运情况,2-7天以内非计划停机停运情况,3-7天以上非计划停机停运情况';
COMMENT ON COLUMN fault_record.sequence_number IS '序号:停机事件的顺序编号,用于排序和引用';
COMMENT ON COLUMN fault_record.plant_name IS '电场名称:发生停机事件的具体电场名称';
COMMENT ON COLUMN fault_record.equipment_code IS '设备编号:发生故障的设备编号,例如:风机编号等';
COMMENT ON COLUMN fault_record.shutdown_reason IS '停运原因:设备停机的具体原因描述';
COMMENT ON COLUMN fault_record.fault_description IS '停机描述:详细的故障现象和原因说明';
COMMENT ON COLUMN fault_record.shutdown_start_time IS '停机开始时间:设备停机的开始时间点';
COMMENT ON COLUMN fault_record.total_shutdown_hours IS '累计停机时间:从停机开始到当前的累计时间,单位:h';
COMMENT ON COLUMN fault_record.power_loss IS '停机损失电量:因停机造成的发电量损失,单位:万kW.h';
COMMENT ON COLUMN fault_record.processing_result IS '处理进度:故障处理的当前状态和进展';
COMMENT ON COLUMN fault_record.spare IS '备注:其他补充说明信息';
COMMENT ON COLUMN fault_record.record_date IS '记录日期:数据录入的日期';
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_pgi_company_name ON power_generation_indicators(company_name);
CREATE INDEX IF NOT EXISTS idx_pgi_record_date ON power_generation_indicators(record_date);
CREATE INDEX IF NOT EXISTS idx_pgi_company_date ON power_generation_indicators(company_name, record_date);
CREATE INDEX IF NOT EXISTS idx_fr_type ON fault_record(type);
CREATE INDEX IF NOT EXISTS idx_fr_plant_name ON fault_record(plant_name);
CREATE INDEX IF NOT EXISTS idx_fr_equipment ON fault_record(equipment_code);
CREATE INDEX IF NOT EXISTS idx_fr_start_time ON fault_record(shutdown_start_time);
CREATE INDEX IF NOT EXISTS idx_fr_record_date ON fault_record(record_date);
import os
from docx import Document
import psycopg2
from datetime import datetime, date
import re
# 数据库连接配置
DB_CONFIG = {
'host': '81.70.183.25',
'port': 15432,
'database': 'test',
'user': 'postgres',
'password': '123qweasd'
}
# 文件夹配置
WORD_DOCS_DIR = "C:\\Users\\11523\\Desktop\\项目\\科环\\24年新能源日报" # Word文档存放的文件夹名称
def connect_to_db():
"""建立数据库连接"""
try:
conn = psycopg2.connect(**DB_CONFIG)
return conn
except Exception as e:
print(f"数据库连接错误: {str(e)}")
return None
def extract_date_from_filename(filename):
"""从文件名中提取日期
例如:从 '科环集团电力运营日报(24.1.2).docx' 提取并返回 2024-01-02
"""
try:
# 使用正则表达式匹配日期格式,支持中文括号和英文括号
match = re.search(r'[((](\d{2})\.(\d{1,2})\.(\d{1,2})[))]', filename)
if match:
year = int(match.group(1))
month = int(match.group(2))
day = int(match.group(3))
# 处理两位数年份
if year < 50: # 假设 00-49 表示 2000-2049
year += 2000
else: # 假设 50-99 表示 1950-1999
year += 1900
# 使用 date 对象确保日期有效
record_date = date(year, month, day)
print(f"从文件名 '{filename}' 解析出日期: {record_date}")
return record_date
except Exception as e:
print(f"解析文件名中的日期出错: {filename}")
print(f"错误信息: {str(e)}")
current_date = date.today()
print(f"使用当前日期作为替代: {current_date}")
return current_date
print(f"无法从文件名中提取日期: {filename}")
current_date = date.today()
print(f"使用当前日期作为替代: {current_date}")
return current_date
def get_cell_text(cell):
"""获取单元格文本,处理空值和特殊字符"""
text = cell.text.strip()
# 处理换行符
text = text.replace('\n', '') if text else text
return text if text and text != '—' else None
def convert_to_float(value):
"""转换字符串到浮点数,处理特殊情况"""
if value is None or value == '—':
return None
try:
# 移除百分号并转换为浮点数
value = value.replace('%', '')
return float(value)
except (ValueError, AttributeError):
return None
def process_power_generation_table(table, record_date):
"""处理发电量指标表,根据列位置解析数据
列位置说明:
0: company_name (单位名称)
1: subsidiary_name (子公司名称)
2: 日发电量
3: 月发电量
4: 月计划
5: 月完成率
6: 年累计发电量
7: 年计划
8: 年完成率
9: 去年同期
10: 资源指标
11: 月利用小时
12: 厂用电率
13: 限电量
14: 故障次数
15: 故障损失电量
"""
rows = []
# 跳过表头
for row in table.rows[1:]:
cells = [get_cell_text(cell) for cell in row.cells]
print(f"当前行数据: {cells}") # 打印当前行数据
if not cells[0]: # 跳过空行
continue
# 跳过合计行
if cells[0] == "合计":
continue
try:
# 直接根据列位置获取数据
row_data = {
'company_name': cells[0], # 单位名称
'subsidiary_name': cells[1], # 子公司名称
'daily_generation': convert_to_float(cells[2]), # 日发电量
'monthly_generation': convert_to_float(cells[3]), # 月发电量
'monthly_plan': convert_to_float(cells[4]), # 月计划
'monthly_completion_rate': convert_to_float(cells[5]), # 月完成率
'ytd_generation': convert_to_float(cells[6]), # 年累计发电量
'yearly_plan': convert_to_float(cells[7]), # 年计划
'yearly_completion_rate': convert_to_float(cells[8]), # 年完成率
'last_year_generation': convert_to_float(cells[9]), # 去年同期
'resource_indicator': cells[10], # 资源指标
'monthly_utilization_hours': convert_to_float(cells[11]), # 月利用小时
'auxiliary_power_rate': convert_to_float(cells[12]), # 厂用电率
'power_limit': convert_to_float(cells[13]), # 限电量
'fault_count': int(cells[14]) if cells[14] and cells[14] != '—' else None, # 故障次数
'fault_power_loss': convert_to_float(cells[15]), # 故障损失电量
'record_date': record_date
}
rows.append(row_data)
except Exception as e:
print(f"处理行数据时出错,单位名称: {cells[0] if cells and len(cells) > 0 else '未知'}")
print(f"错误信息: {str(e)}")
print(f"原始数据: {cells}")
continue
return rows
def get_fault_type_from_title(table):
"""根据表格标题确定故障类型"""
if not table.rows or len(table.rows) == 0:
return None
# 获取表格上方的段落文本
title = ""
for paragraph in table._element.getprevious():
if hasattr(paragraph, 'text'):
title = paragraph.text.strip()
break
# 如果找不到段落,尝试获取表格第一行作为标题
if not title:
title = table.rows[0].cells[0].text.strip()
# 根据标题确定类型
if "计划性停机" in title:
return 1
elif "7天以内非计划停机" in title:
return 2
elif "超7天" in title or "14天非计划停机" in title:
return 3
return None
def process_fault_tables(tables, record_date):
"""处理故障停机记录表
tables: 所有表格列表
第1个表格(index=0): 发电量指标表(跳过)
第2个表格(index=1): type=3 超7天非计划停机
第3个表格(index=2): type=2 7天以内非计划停机
第4个表格(index=3): type=1 计划性停机
"""
fault_records = []
# 定义表格类型映射
table_type_map = {
1: 3, # 第2个表格:超7天非计划停机
2: 2, # 第3个表格:7天以内非计划停机
3: 1 # 第4个表格:计划性停机
}
type_descriptions = {
1: "计划性停机",
2: "7天以内非计划停机",
3: "超7天非计划停机"
}
# 检查表格数量
if len(tables) < 4:
print(f"警告:表格数量不足,预期4个表格,实际只有{len(tables)}个")
# 处理后面3个故障表(跳过第一个发电量指标表)
for table_index, table in enumerate(tables[1:4], 1): # 限制处理3个故障表
print(f"\n{'='*20} 处理第 {table_index} 个故障表 {'='*20}")
# 根据表格顺序确定故障类型
fault_type = table_type_map.get(table_index)
if fault_type is None:
print(f"未知的表格序号 {table_index},跳过此表格")
continue
print(f"处理类型: {fault_type} ({type_descriptions[fault_type]})")
if not table.rows:
print(f"表格为空,跳过处理")
continue
for row_index, row in enumerate(table.rows[1:], 1): # 跳过表头,序号从1开始
cells = [get_cell_text(cell) for cell in row.cells]
if len(cells) < 8 or not cells[0]: # 跳过空行
continue
try:
# 解析日期时间
shutdown_time = None
if cells[5]:
try:
shutdown_time = parse_datetime(cells[5])
except Exception as e:
print(f"解析停机时间出错: {cells[5]}")
print(f"错误信息: {str(e)}")
base_data = {
'type': fault_type, # 使用固定的类型映射
'sequence_number': row_index,
'plant_name': cells[1],
'equipment_code': cells[2],
'shutdown_reason': cells[3],
'fault_description': cells[4],
'shutdown_start_time': shutdown_time,
'total_shutdown_hours': convert_to_float(cells[7]),
'power_loss': convert_to_float(cells[8]),
'processing_result': cells[9] if len(cells) > 9 else None,
'spare': cells[10] if len(cells) > 10 else None,
'record_date': record_date
}
fault_records.append(base_data)
except Exception as e:
print(f"处理故障记录出错,行号: {row_index}")
print(f"错误信息: {str(e)}")
print(f"原始数据: {cells}")
continue
print(f"完成处理第 {table_index} 个故障表,成功解析 {len(fault_records)} 条记录")
return fault_records
def parse_datetime(date_str):
"""解析日期时间字符串"""
if not date_str:
return None
try:
return datetime.strptime(date_str, '%Y/%m/%d %H:%M')
except ValueError:
try:
return datetime.strptime(date_str, '%Y-%m-%d %H:%M')
except ValueError:
print(f"无法解析日期时间: {date_str}")
return None
def save_to_database(conn, table_name, data_rows):
"""保存数据到数据库"""
if not data_rows:
return
cursor = conn.cursor()
# 构建INSERT语句,添加public模式
columns = data_rows[0].keys()
placeholders = ','.join(['%s'] * len(columns))
column_names = ','.join(columns)
insert_query = f"INSERT INTO public.{table_name} ({column_names}) VALUES ({placeholders})"
try:
for row in data_rows:
values = [row[column] for column in columns]
cursor.execute(insert_query, values)
conn.commit()
print(f"成功保存 {len(data_rows)} 条记录到 public.{table_name}")
except Exception as e:
conn.rollback()
print(f"保存数据时出错: {str(e)}")
finally:
cursor.close()
def ensure_directory_exists():
"""确保文件夹存在,如果不存在则创建"""
if not os.path.exists(WORD_DOCS_DIR):
os.makedirs(WORD_DOCS_DIR)
print(f"创建文件夹: {WORD_DOCS_DIR}")
return os.path.abspath(WORD_DOCS_DIR)
def get_processed_files(conn):
"""获取已经处理过的文件列表"""
cursor = conn.cursor()
try:
# 从power_generation_indicators表中获取已处理的日期
cursor.execute("SELECT DISTINCT record_date FROM public.power_generation_indicators")
processed_dates = [row[0] for row in cursor.fetchall()]
return processed_dates
except Exception as e:
print(f"获取已处理文件列表时出错: {str(e)}")
return []
finally:
cursor.close()
def should_process_file(filename, processed_dates):
"""判断文件是否需要处理"""
try:
# 从文件名中提取日期
match = re.search(r'[((](\d{2})\.(\d{1,2})\.(\d{1,2})[))]', filename)
if match:
year = int(match.group(1))
month = int(match.group(2))
day = int(match.group(3))
# 处理两位数年份
if year < 50:
year += 2000
else:
year += 1900
file_date = date(year, month, day)
# 如果文件日期不在已处理列表中,则需要处理
return file_date not in processed_dates
except Exception as e:
print(f"检查文件 {filename} 是否需要处理时出错: {str(e)}")
return False
def main():
# 确保文件夹存在
docs_dir = ensure_directory_exists()
print(f"使用文件夹: {docs_dir}")
# 连接数据库
conn = connect_to_db()
if not conn:
return
try:
# 获取已处理的文件列表
processed_dates = get_processed_files(conn)
print(f"已处理的日期数量: {len(processed_dates)}")
# 获取文件夹中的所有docx文件
docx_files = [f for f in os.listdir(docs_dir) if f.endswith('.docx')]
print(f"发现 {len(docx_files)} 个Word文档")
if not docx_files:
print(f"在 {docs_dir} 目录下没有找到Word文档文件(.docx)")
return
# 处理每个文档
for docx_file in docx_files:
print(f"\n{'='*50}")
print(f"检查文件: {docx_file}")
# 检查文件是否需要处理
if not should_process_file(docx_file, processed_dates):
print(f"文件 {docx_file} 已处理过,跳过")
continue
print(f"开始处理文件: {docx_file}")
print(f"{'='*50}")
try:
# 从文件名提取日期
record_date = extract_date_from_filename(docx_file)
print(f"解析到的日期: {record_date} (类型: {type(record_date)})")
# 读取Word文档
doc = Document(os.path.join(docs_dir, docx_file))
# 处理发电量指标表(第一个表格)
if doc.tables:
print("\n开始处理发电量指标表...")
power_data = process_power_generation_table(doc.tables[0], record_date)
if power_data:
print(f"成功解析 {len(power_data)} 条发电量数据")
save_to_database(conn, 'power_generation_indicators', power_data)
else:
print("未能解析到发电量数据")
# 处理故障停机记录表
print("\n开始处理故障停机记录表...")
fault_data = process_fault_tables(doc.tables, record_date)
if fault_data:
print(f"成功解析 {len(fault_data)} 条故障记录")
save_to_database(conn, 'fault_record', fault_data)
else:
print("未能解析到故障记录")
else:
print("文档中没有找到表格")
except Exception as e:
print(f"\n处理文件 {docx_file} 时出错")
print(f"错误信息: {str(e)}")
continue
except Exception as e:
print(f"\n程序执行出错: {str(e)}")
finally:
conn.close()
print("\n数据库连接已关闭")
if __name__ == "__main__":
main()
\ No newline at end of file
python-docx==0.8.11
pandas==2.1.4
psycopg2-binary==2.9.9
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment