372 lines
17 KiB
Python
372 lines
17 KiB
Python
"""
|
||
昆明市大学生创业园数据导入:
|
||
1. 入驻企业信息表 → 补充 park_companies 的 zone/founder/bio
|
||
2. 月报目录 → 解析所有企业运营情况表,写入 incubator_monthly_reports
|
||
"""
|
||
import asyncio
|
||
import json
|
||
import os
|
||
import re
|
||
import secrets
|
||
from pathlib import Path
|
||
|
||
import asyncmy
|
||
import openpyxl
|
||
import xlrd
|
||
|
||
DB = dict(host="47.108.226.213", port=8091, user="opc",
|
||
password="jjjsgysyujkwjwgb", database="opc")
|
||
TENANT_ID = "T001"
|
||
BASE = Path("/Volumes/Pine/mycode/opc/昆明市大学生创业园资料")
|
||
ENTERPRISE_XLSX = BASE / "昆明市大学生创业园入驻企业信息表.xlsx"
|
||
MONTHLY_DIR = BASE / "月报"
|
||
|
||
# ── 工具 ──────────────────────────────────────────────────────────────
|
||
def norm(s):
|
||
if s is None:
|
||
return ""
|
||
return str(s).strip().replace("\n", "").replace("\r", "").replace(" ", "")
|
||
|
||
def norm_name(s):
|
||
"""企业名归一化:去掉所有标点、括号、空格,用于模糊匹配"""
|
||
if s is None:
|
||
return ""
|
||
s = str(s)
|
||
for ch in "()()【】[]「」『』、,,。.·•—-—_~~!!??\"\"''\\/|":
|
||
s = s.replace(ch, "")
|
||
return s.strip().replace(" ", "")
|
||
|
||
def to_float(v):
|
||
if v is None or v == "" or v == "/":
|
||
return None
|
||
try:
|
||
return float(str(v).replace(",", "").replace("万", "").strip())
|
||
except (ValueError, TypeError):
|
||
return None
|
||
|
||
def to_int(v):
|
||
f = to_float(v)
|
||
return int(f) if f is not None else None
|
||
|
||
def read_sheet_rows(path):
|
||
"""读取第一个 sheet 的所有行,返回 list[list]"""
|
||
ext = path.suffix.lower()
|
||
rows = []
|
||
if ext == ".xlsx":
|
||
wb = openpyxl.load_workbook(str(path), data_only=True)
|
||
ws = wb[wb.sheetnames[0]]
|
||
rows = [list(r) for r in ws.iter_rows(values_only=True)]
|
||
elif ext in (".xls", ".et"):
|
||
book = xlrd.open_workbook(str(path))
|
||
sh = book.sheet_by_index(0)
|
||
for r in range(sh.nrows):
|
||
rows.append(sh.row_values(r))
|
||
return rows
|
||
|
||
def find_header_row(rows):
|
||
"""找到包含'孵化区域'和'单元号'的列头行(排除大标题行)"""
|
||
for i, row in enumerate(rows):
|
||
vals = [norm(c) for c in row]
|
||
if "孵化区域" in vals and any("单元号" in v for v in vals):
|
||
return i
|
||
# 降级:找包含"孵化区域"的行
|
||
for i, row in enumerate(rows):
|
||
vals = [norm(c) for c in row]
|
||
if "孵化区域" in vals:
|
||
return i
|
||
return None
|
||
|
||
def extract_monthly_data(rows, filename, month_hint):
|
||
"""从月报行数据中提取字段 dict"""
|
||
hdr_idx = find_header_row(rows)
|
||
if hdr_idx is None:
|
||
return None
|
||
headers = [norm(c) for c in rows[hdr_idx]]
|
||
# 数据行:表头之后跳过空行,找第3列(企业名)非空的行
|
||
data_row = None
|
||
name_col = 2 # 默认企业名在第3列(索引2)
|
||
for j, h in enumerate(headers):
|
||
if "创业企业" in h or "企业名称" in h or "项目名称" in h:
|
||
name_col = j
|
||
break
|
||
for i in range(hdr_idx + 1, min(hdr_idx + 6, len(rows))):
|
||
vals = [norm(c) for c in rows[i]]
|
||
if len(vals) > name_col and vals[name_col] and vals[name_col] not in ("创业企业(项目)名称",):
|
||
# 排除表头重复和"创业团队主要成员"行
|
||
if "创业团队主要成员" not in "".join(vals):
|
||
data_row = rows[i]
|
||
break
|
||
|
||
def col(name_keywords):
|
||
for j, h in enumerate(headers):
|
||
for kw in name_keywords:
|
||
if kw in h:
|
||
return data_row[j] if j < len(data_row) else None
|
||
return None
|
||
|
||
d = {}
|
||
d["zone"] = norm(col(["孵化区域"]))
|
||
d["unit_no"] = norm(col(["单元号"]))
|
||
d["company_name"] = norm(col(["创业企业", "企业名称", "项目名称"]))
|
||
d["park_entry_date"] = norm(col(["入园时间"]))
|
||
d["leader_name"] = norm(col(["负责人姓名"]))
|
||
d["registered_at"] = norm(col(["注册时间"]))
|
||
d["registered_capital_wan"] = to_float(col(["注册资金"]))
|
||
d["credit_code"] = norm(col(["统一信用代码", "统一社会信用代码"]))
|
||
d["rd_invest_wan"] = to_float(col(["研发", "研发投入"]))
|
||
d["month_revenue_wan"] = to_float(col(["当月营业额"]))
|
||
d["avg_year_revenue_wan"] = to_float(col(["年平均营业额", "年均营业额"]))
|
||
d["month_gross_profit_wan"] = to_float(col(["当月毛利润", "毛利润"]))
|
||
d["month_tax_wan"] = to_float(col(["当月缴税"]))
|
||
d["avg_year_tax_wan"] = to_float(col(["年平均缴税", "年均缴税"]))
|
||
d["loan_startup_wan"] = to_float(col(["创业担保贷款"]))
|
||
d["loan_yunling_wan"] = to_float(col(["云岭创业贷款"]))
|
||
d["contest_award_wan"] = to_float(col(["创业大赛扶持"]))
|
||
d["patents"] = to_int(col(["获得专利"]))
|
||
d["jobs_created"] = to_int(col(["带动就业"]))
|
||
|
||
# 团队成员:在数据行之后找"创业团队主要成员"行
|
||
team = []
|
||
for i in range(hdr_idx + 1, len(rows)):
|
||
vals = [norm(c) for c in rows[i]]
|
||
if "创业团队主要成员" in "".join(vals):
|
||
# 下一行是子表头(姓名/毕业院校/毕业时间/职务),再下一行开始是数据
|
||
for j in range(i + 2, min(i + 8, len(rows))):
|
||
row = rows[j]
|
||
name = norm(row[0]) if len(row) > 0 else ""
|
||
if not name or name in ("姓名",):
|
||
continue
|
||
school = norm(row[2]) if len(row) > 2 else ""
|
||
grad = norm(row[3]) if len(row) > 3 else ""
|
||
title = norm(row[5]) if len(row) > 5 else ""
|
||
if name:
|
||
team.append({"name": name, "school": school, "grad": grad, "title": title})
|
||
# 第二组(第9列开始)
|
||
name2 = norm(row[9]) if len(row) > 9 else ""
|
||
if name2:
|
||
school2 = norm(row[12]) if len(row) > 12 else ""
|
||
grad2 = norm(row[15]) if len(row) > 15 else ""
|
||
title2 = norm(row[17]) if len(row) > 17 else ""
|
||
team.append({"name": name2, "school": school2, "grad": grad2, "title": title2})
|
||
break
|
||
d["team_json"] = json.dumps(team, ensure_ascii=False) if team else ""
|
||
|
||
# 公司经营状况 / 建议
|
||
for i in range(hdr_idx + 1, len(rows)):
|
||
vals = [norm(c) for c in rows[i]]
|
||
joined = "".join(vals)
|
||
if "公司经营状况" in joined or "经营状况" in joined:
|
||
# 经营状况文本通常在第3列或整行
|
||
d["business_note"] = norm(rows[i][3]) if len(rows[i]) > 3 and rows[i][3] else (vals[1] if len(vals) > 1 else "")
|
||
if not d["business_note"]:
|
||
d["business_note"] = joined.replace("公司经营状况", "").strip()
|
||
if "建议" in joined and ("创业园" in joined or "管理服务" in joined):
|
||
d["suggestion"] = norm(rows[i][3]) if len(rows[i]) > 3 and rows[i][3] else ""
|
||
if not d["suggestion"]:
|
||
d["suggestion"] = joined.replace("对创业园管理服务的建议和意见:", "").strip()
|
||
|
||
# 填表人/邮箱/填表时间
|
||
for i in range(len(rows) - 1, max(len(rows) - 6, 0), -1):
|
||
vals = [str(c) for c in rows[i] if c]
|
||
joined = "".join(vals)
|
||
if "填表人" in joined:
|
||
m = re.search(r"填表人[::]\s*(\S+)", joined)
|
||
if m:
|
||
d["filler_name"] = m.group(1)
|
||
m = re.search(r"邮箱[::]\s*(\S+)", joined)
|
||
if m:
|
||
d["filler_email"] = m.group(1)
|
||
m = re.search(r"填表时间[::]\s*(\S+)", joined)
|
||
if m:
|
||
d["submitted_at"] = m.group(1)
|
||
break
|
||
|
||
# report_month:优先从 submitted_at 推断,其次用 month_hint
|
||
if d.get("submitted_at"):
|
||
m = re.search(r"(20\d{2})[年\-/](\d{1,2})", d["submitted_at"])
|
||
if m:
|
||
d["report_month"] = f"{m.group(1)}-{int(m.group(2)):02d}"
|
||
if not d.get("report_month"):
|
||
d["report_month"] = month_hint
|
||
|
||
d["status"] = "submitted"
|
||
return d
|
||
|
||
|
||
# ── 主流程 ────────────────────────────────────────────────────────────
|
||
async def main():
|
||
conn = await asyncmy.connect(**DB)
|
||
async with conn.cursor() as cur:
|
||
# ── 1. 入驻企业信息表 → 补充 park_companies ──
|
||
wb = openpyxl.load_workbook(str(ENTERPRISE_XLSX), data_only=True)
|
||
ws = wb[wb.sheetnames[0]]
|
||
enterprises = []
|
||
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
||
if i < 2:
|
||
continue
|
||
seq = row[0]
|
||
if not seq or not str(seq).strip().isdigit():
|
||
continue
|
||
name = norm(row[1])
|
||
founder = str(row[2]).strip() if row[2] else ""
|
||
bio = str(row[3]).strip() if row[3] else ""
|
||
zone = norm(row[4])
|
||
if name:
|
||
enterprises.append((name, founder, bio, zone))
|
||
|
||
print(f"入驻企业信息表: {len(enterprises)} 家")
|
||
updated = 0
|
||
for name, founder, bio, zone in enterprises:
|
||
# 模糊匹配:去掉空格和括号差异
|
||
await cur.execute(
|
||
"SELECT id, name FROM park_companies WHERE tenant_id=%s",
|
||
(TENANT_ID,))
|
||
all_rows = await cur.fetchall()
|
||
matched = None
|
||
nname = norm_name(name)
|
||
for cid, cname in all_rows:
|
||
cn = norm_name(cname)
|
||
if cn == nname or nname in cn or cn in nname:
|
||
matched = cid
|
||
break
|
||
if matched:
|
||
await cur.execute(
|
||
"""UPDATE park_companies SET zone=%s, founder=%s, bio=%s
|
||
WHERE id=%s""",
|
||
(zone, founder, bio, matched))
|
||
updated += 1
|
||
else:
|
||
print(f" ⚠ 未匹配到企业: {name}")
|
||
await conn.commit()
|
||
print(f"park_companies 补充/创建: {updated} 家")
|
||
|
||
# ── 2. 月报导入 ──
|
||
month_map = {"4月": "2026-04", "5月": "2026-05",
|
||
"6、7月": None, "8月": "2026-08"}
|
||
|
||
# 预加载所有 T001 企业用于匹配
|
||
await cur.execute("SELECT id, name, founder FROM park_companies WHERE tenant_id=%s", (TENANT_ID,))
|
||
all_companies = list(await cur.fetchall())
|
||
|
||
async def match_company(name, leader):
|
||
"""按企业名→负责人姓名匹配,匹配不到则自动创建"""
|
||
nname = norm_name(name)
|
||
# 1. 企业名匹配
|
||
for cid, cname, founder in all_companies:
|
||
cn = norm_name(cname)
|
||
if cn == nname or nname in cn or cn in nname:
|
||
return cid
|
||
# 2. 负责人姓名匹配(founder 里通常含负责人名)
|
||
if leader:
|
||
nleader = norm_name(leader)
|
||
for cid, cname, founder in all_companies:
|
||
if founder and nleader in norm_name(founder):
|
||
return cid
|
||
# 3. 自动创建企业
|
||
cid = "PC" + secrets.token_hex(4)
|
||
await cur.execute(
|
||
"""INSERT INTO park_companies
|
||
(id, tenant_id, company_kind, name, zone, room, industry,
|
||
bio, founder, status, employees, legal_person, legal_phone,
|
||
registered_capital, company_type, honors, address, contact_phone,
|
||
founded_at, compute_discount, compute_quota, compute_quota_used,
|
||
engine_group, owner_user_id, created_at, compute_balance, compute_balance_used)
|
||
VALUES (%s,%s,'project',%s,'','','','',%s,'active',0,'','','','','','','',0,0,0,0,'','','',0,0)""",
|
||
(cid, TENANT_ID, name, leader or ""))
|
||
all_companies.append((cid, name, leader or ""))
|
||
print(f" + 自动创建企业: {name}")
|
||
return cid
|
||
|
||
total = 0
|
||
skipped = []
|
||
for month_dir, month_hint in month_map.items():
|
||
mdir = MONTHLY_DIR / month_dir
|
||
if not mdir.exists():
|
||
continue
|
||
for fname in sorted(os.listdir(mdir)):
|
||
if fname.startswith(".") or fname.endswith(".DS_Store"):
|
||
continue
|
||
fpath = mdir / fname
|
||
if fpath.suffix.lower() not in (".xls", ".xlsx", ".et"):
|
||
continue
|
||
# 跳过模板/统计表(非企业月报)
|
||
if "模版" in fname or "模板" in fname or "运行情况统计表" in fname:
|
||
continue
|
||
try:
|
||
rows = read_sheet_rows(fpath)
|
||
if not rows:
|
||
skipped.append((fname, "空文件"))
|
||
continue
|
||
data = extract_monthly_data(rows, fname, month_hint)
|
||
if not data or not data.get("company_name"):
|
||
skipped.append((fname, "无法解析企业名"))
|
||
continue
|
||
# 6、7月:从文件名或内容推断
|
||
if month_dir == "6、7月":
|
||
if "6月" in fname or "06" in fname:
|
||
data["report_month"] = "2026-06"
|
||
elif "7月" in fname or "07" in fname or "7" in fname:
|
||
data["report_month"] = "2026-07"
|
||
elif data.get("submitted_at"):
|
||
pass # 已从 submitted_at 推断
|
||
else:
|
||
data["report_month"] = "2026-07"
|
||
|
||
# 兜底确保 report_month 不为空
|
||
if not data.get("report_month"):
|
||
data["report_month"] = month_hint or "2026-07"
|
||
|
||
# 匹配 company_id(企业名→负责人→自动创建)
|
||
company_id = await match_company(data["company_name"], data.get("leader_name", ""))
|
||
|
||
# 幂等:同一企业同一月份只保留一条
|
||
await cur.execute(
|
||
"""DELETE FROM incubator_monthly_reports
|
||
WHERE tenant_id=%s AND company_id=%s AND report_month=%s""",
|
||
(TENANT_ID, company_id, data["report_month"]))
|
||
|
||
mid = "mr_" + secrets.token_hex(8)
|
||
now = "2026-09-08"
|
||
await cur.execute(
|
||
"""INSERT INTO incubator_monthly_reports
|
||
(id, tenant_id, company_id, company_name, report_month, status,
|
||
zone, unit_no, park_entry_date, leader_name, registered_at,
|
||
registered_capital_wan, credit_code, rd_invest_wan, month_revenue_wan,
|
||
avg_year_revenue_wan, month_gross_profit_wan, month_tax_wan,
|
||
avg_year_tax_wan, loan_startup_wan, loan_yunling_wan,
|
||
contest_award_wan, patents, jobs_created, team_json,
|
||
business_note, suggestion, filler_name, filler_email,
|
||
submitted_at, created_at, updated_at)
|
||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
||
(mid, TENANT_ID, company_id, data["company_name"],
|
||
data["report_month"], data["status"],
|
||
data.get("zone", ""), data.get("unit_no", ""),
|
||
data.get("park_entry_date", ""), data.get("leader_name", ""),
|
||
data.get("registered_at", ""), data.get("registered_capital_wan"),
|
||
data.get("credit_code", ""), data.get("rd_invest_wan"),
|
||
data.get("month_revenue_wan"), data.get("avg_year_revenue_wan"),
|
||
data.get("month_gross_profit_wan"), data.get("month_tax_wan"),
|
||
data.get("avg_year_tax_wan"), data.get("loan_startup_wan"),
|
||
data.get("loan_yunling_wan"), data.get("contest_award_wan"),
|
||
data.get("patents"), data.get("jobs_created"),
|
||
data.get("team_json", ""), data.get("business_note", ""),
|
||
data.get("suggestion", ""), data.get("filler_name", ""),
|
||
data.get("filler_email", ""), data.get("submitted_at", ""),
|
||
now, now))
|
||
total += 1
|
||
print(f" ✓ {data['report_month']} {data['company_name'][:20]}")
|
||
except Exception as e:
|
||
skipped.append((fname, str(e)[:80]))
|
||
print(f" ✗ {fname}: {e}")
|
||
|
||
await conn.commit()
|
||
print(f"\n月报导入完成: {total} 条")
|
||
if skipped:
|
||
print(f"跳过 {len(skipped)} 个文件:")
|
||
for f, r in skipped[:10]:
|
||
print(f" - {f}: {r}")
|
||
|
||
await conn.ensure_closed()
|
||
|
||
asyncio.run(main())
|