Compare commits
11 Commits
e13f9e775d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f5c0ecc31f | |||
| 44a9c5a5d5 | |||
| be4cabbb23 | |||
| 6e669ac098 | |||
| ebf59e15d1 | |||
| c41074b6f6 | |||
| 6dad547dcd | |||
| 6b0a9a5bdb | |||
| 9a7fad6538 | |||
| 32881e661b | |||
| deca0ed4f5 |
28
client/Reader/MainForm.Designer.cs
generated
28
client/Reader/MainForm.Designer.cs
generated
@@ -21,7 +21,6 @@ namespace Reader
|
|||||||
Filename = new DataGridViewTextBoxColumn();
|
Filename = new DataGridViewTextBoxColumn();
|
||||||
eryi_ID = new DataGridViewTextBoxColumn();
|
eryi_ID = new DataGridViewTextBoxColumn();
|
||||||
下载者用户名 = new DataGridViewTextBoxColumn();
|
下载者用户名 = new DataGridViewTextBoxColumn();
|
||||||
暗码写入时间 = new DataGridViewTextBoxColumn();
|
|
||||||
((System.ComponentModel.ISupportInitialize)dgv).BeginInit();
|
((System.ComponentModel.ISupportInitialize)dgv).BeginInit();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
//
|
//
|
||||||
@@ -58,12 +57,15 @@ namespace Reader
|
|||||||
dgv.AllowUserToDeleteRows = false;
|
dgv.AllowUserToDeleteRows = false;
|
||||||
dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
|
dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
|
||||||
dgv.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
dgv.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
dgv.Columns.AddRange(new DataGridViewColumn[] { Filename, eryi_ID, 下载者用户名, 暗码写入时间 });
|
dgv.Columns.AddRange(new DataGridViewColumn[] { Filename, eryi_ID, 下载者用户名 });
|
||||||
dgv.Location = new Point(12, 59);
|
dgv.Location = new Point(12, 59);
|
||||||
dgv.Name = "dgv";
|
dgv.Name = "dgv";
|
||||||
dgv.ReadOnly = true;
|
dgv.ReadOnly = true;
|
||||||
dgv.Size = new Size(748, 332);
|
dgv.RowHeadersWidth = 37;
|
||||||
|
dgv.RowHeadersWidthSizeMode = DataGridViewRowHeadersWidthSizeMode.DisableResizing;
|
||||||
|
dgv.Size = new Size(770, 332);
|
||||||
dgv.TabIndex = 2;
|
dgv.TabIndex = 2;
|
||||||
|
dgv.CellClick += dgv_CellClick;
|
||||||
//
|
//
|
||||||
// Filename
|
// Filename
|
||||||
//
|
//
|
||||||
@@ -71,18 +73,18 @@ namespace Reader
|
|||||||
Filename.FillWeight = 300F;
|
Filename.FillWeight = 300F;
|
||||||
Filename.Frozen = true;
|
Filename.Frozen = true;
|
||||||
Filename.HeaderText = "文件名";
|
Filename.HeaderText = "文件名";
|
||||||
Filename.MinimumWidth = 400;
|
Filename.MinimumWidth = 500;
|
||||||
Filename.Name = "Filename";
|
Filename.Name = "Filename";
|
||||||
Filename.ReadOnly = true;
|
Filename.ReadOnly = true;
|
||||||
Filename.Width = 400;
|
Filename.Width = 500;
|
||||||
//
|
//
|
||||||
// eryi_ID
|
// eryi_ID
|
||||||
//
|
//
|
||||||
eryi_ID.AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader;
|
eryi_ID.AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader;
|
||||||
eryi_ID.HeaderText = "二一资料ID";
|
eryi_ID.HeaderText = "二一暗码(资料ID)";
|
||||||
eryi_ID.Name = "eryi_ID";
|
eryi_ID.Name = "eryi_ID";
|
||||||
eryi_ID.ReadOnly = true;
|
eryi_ID.ReadOnly = true;
|
||||||
eryi_ID.Width = 94;
|
eryi_ID.Width = 100;
|
||||||
//
|
//
|
||||||
// 下载者用户名
|
// 下载者用户名
|
||||||
//
|
//
|
||||||
@@ -90,16 +92,7 @@ namespace Reader
|
|||||||
下载者用户名.HeaderText = "下载者用户名";
|
下载者用户名.HeaderText = "下载者用户名";
|
||||||
下载者用户名.Name = "下载者用户名";
|
下载者用户名.Name = "下载者用户名";
|
||||||
下载者用户名.ReadOnly = true;
|
下载者用户名.ReadOnly = true;
|
||||||
下载者用户名.Width = 105;
|
下载者用户名.Width = 100;
|
||||||
//
|
|
||||||
// 暗码写入时间
|
|
||||||
//
|
|
||||||
暗码写入时间.AutoSizeMode = DataGridViewAutoSizeColumnMode.ColumnHeader;
|
|
||||||
暗码写入时间.FillWeight = 300F;
|
|
||||||
暗码写入时间.HeaderText = "暗码写入时间";
|
|
||||||
暗码写入时间.Name = "暗码写入时间";
|
|
||||||
暗码写入时间.ReadOnly = true;
|
|
||||||
暗码写入时间.Width = 105;
|
|
||||||
//
|
//
|
||||||
// MainForm
|
// MainForm
|
||||||
//
|
//
|
||||||
@@ -125,6 +118,5 @@ namespace Reader
|
|||||||
private DataGridViewTextBoxColumn Filename;
|
private DataGridViewTextBoxColumn Filename;
|
||||||
private DataGridViewTextBoxColumn eryi_ID;
|
private DataGridViewTextBoxColumn eryi_ID;
|
||||||
private DataGridViewTextBoxColumn 下载者用户名;
|
private DataGridViewTextBoxColumn 下载者用户名;
|
||||||
private DataGridViewTextBoxColumn 暗码写入时间;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -14,6 +16,11 @@ namespace Reader
|
|||||||
{
|
{
|
||||||
private static readonly HttpClient http = new HttpClient();
|
private static readonly HttpClient http = new HttpClient();
|
||||||
private static readonly string ServerUrl;
|
private static readonly string ServerUrl;
|
||||||
|
private static readonly HashSet<string> DocumentExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
".doc", ".docx", ".pdf", ".ppt", ".pptx"
|
||||||
|
};
|
||||||
|
private MouseButtons lastGridMouseButton = MouseButtons.Left;
|
||||||
|
|
||||||
static MainForm()
|
static MainForm()
|
||||||
{
|
{
|
||||||
@@ -27,6 +34,8 @@ namespace Reader
|
|||||||
public MainForm()
|
public MainForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
dgv.CellMouseDown += dgv_CellMouseDown;
|
||||||
|
dgv.CellMouseClick += dgv_CellMouseClick;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnBrowse_Click(object sender, EventArgs e)
|
private void btnBrowse_Click(object sender, EventArgs e)
|
||||||
@@ -47,25 +56,29 @@ namespace Reader
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dgv.Rows.Clear();
|
dgv.Rows.Clear();
|
||||||
var files = Directory.GetFiles(folder, "*", SearchOption.AllDirectories);
|
var files = Directory
|
||||||
var shaList = new System.Collections.Generic.List<string>();
|
.GetFiles(folder, "*", SearchOption.AllDirectories)
|
||||||
var fileMap = new System.Collections.Generic.Dictionary<string, string>();
|
.Where(IsSupportedDocument)
|
||||||
|
.OrderBy(Path.GetFileName, StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
var fileEntries = new List<(string FilePath, string Sha)>();
|
||||||
|
var shaList = new List<string>();
|
||||||
foreach (var f in files)
|
foreach (var f in files)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sha = await Task.Run(() => ComputeSha256(f));
|
var sha = await Task.Run(() => ComputeSha256(f));
|
||||||
|
fileEntries.Add((f, sha));
|
||||||
shaList.Add(sha);
|
shaList.Add(sha);
|
||||||
fileMap[sha] = f;
|
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
if (shaList.Count == 0)
|
if (fileEntries.Count == 0)
|
||||||
{
|
{
|
||||||
MessageBox.Show("没有找到文件。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
MessageBox.Show("没有找到支持的文档文件。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var payload = new { shas = shaList };
|
var payload = new { shas = shaList.Distinct().ToList() };
|
||||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||||
var resp = await http.PostAsync(ServerUrl + "/read", content);
|
var resp = await http.PostAsync(ServerUrl + "/read", content);
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
@@ -77,23 +90,27 @@ namespace Reader
|
|||||||
using var doc = JsonDocument.Parse(text);
|
using var doc = JsonDocument.Parse(text);
|
||||||
var root = doc.RootElement;
|
var root = doc.RootElement;
|
||||||
var data = root.GetProperty("data");
|
var data = root.GetProperty("data");
|
||||||
foreach (var sha in shaList)
|
foreach (var entry in fileEntries)
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileName(fileMap[sha]);
|
var fileName = Path.GetFileName(entry.FilePath);
|
||||||
if (data.TryGetProperty(sha, out var item) && item.ValueKind != JsonValueKind.Null)
|
string up = "无";
|
||||||
|
string down = "无";
|
||||||
|
if (data.TryGetProperty(entry.Sha, out var item) && item.ValueKind != JsonValueKind.Null)
|
||||||
{
|
{
|
||||||
var up = item.GetProperty("upload_code").GetString() ?? "";
|
up = item.GetProperty("upload_code").GetString() ?? "";
|
||||||
var down = item.GetProperty("download_code").GetString() ?? "";
|
down = item.GetProperty("download_code").GetString() ?? "";
|
||||||
var created = item.GetProperty("created_at").GetString() ?? "";
|
|
||||||
dgv.Rows.Add(fileName, up, down, created);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dgv.Rows.Add(fileName, "无", "无", "无");
|
|
||||||
}
|
}
|
||||||
|
var rowIndex = dgv.Rows.Add(fileName, up, down);
|
||||||
|
dgv.Rows[rowIndex].Tag = entry.FilePath;
|
||||||
|
dgv.Rows[rowIndex].Cells["Filename"].ToolTipText = entry.FilePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsSupportedDocument(string filePath)
|
||||||
|
{
|
||||||
|
return DocumentExtensions.Contains(Path.GetExtension(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
private static string ComputeSha256(string filePath)
|
private static string ComputeSha256(string filePath)
|
||||||
{
|
{
|
||||||
using var sha = SHA256.Create();
|
using var sha = SHA256.Create();
|
||||||
@@ -106,30 +123,108 @@ namespace Reader
|
|||||||
|
|
||||||
private void MainForm_Load(object sender, EventArgs e)
|
private void MainForm_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// 1. 去除自动尺寸模式的限制,改用手动指定
|
|
||||||
dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
|
dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
|
||||||
|
dgv.ShowCellToolTips = true;
|
||||||
|
dgv.Columns["Filename"].Width = 500;
|
||||||
|
dgv.Columns["eryi_ID"].Width = 100;
|
||||||
|
dgv.Columns["下载者用户名"].Width = 100;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 计算每一列的理想宽度
|
private void dgv_CellClick(object sender, DataGridViewCellEventArgs e)
|
||||||
// 假设您有 N 列,除了最后一列,其余列平分空间
|
{
|
||||||
int columnCount = dgv.Columns.Count;
|
if (lastGridMouseButton == MouseButtons.Right)
|
||||||
int totalWidth = dgv.ClientSize.Width; // 获取控件当前宽度
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HandleGridClick(e);
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 遍历每一列
|
private void dgv_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
|
||||||
for (int i = 0; i < columnCount; i++)
|
{
|
||||||
|
lastGridMouseButton = e.Button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dgv_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.RowIndex < 0 || e.ColumnIndex < 0 || e.Button != MouseButtons.Right)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnName = dgv.Columns[e.ColumnIndex].Name;
|
||||||
|
if (columnName != "Filename")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = dgv.Rows[e.RowIndex];
|
||||||
|
var filePath = row.Tag as string;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
|
||||||
|
{
|
||||||
|
OpenFolderAndSelectFile(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleGridClick(DataGridViewCellEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.RowIndex < 0 || e.ColumnIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = dgv.Rows[e.RowIndex];
|
||||||
|
var columnName = dgv.Columns[e.ColumnIndex].Name;
|
||||||
|
if (columnName == "Filename")
|
||||||
|
{
|
||||||
|
var filePath = row.Tag as string;
|
||||||
|
if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
|
||||||
{
|
{
|
||||||
// 这里做一个简单的平均分配逻辑
|
OpenWithShell(filePath);
|
||||||
double width = totalWidth / columnCount;
|
|
||||||
|
|
||||||
// 设置列宽,注意:不能小于最小宽度
|
|
||||||
dgv.Columns[i].Width = (int)Math.Ceiling(width);
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果最后一列想稍微宽一点,或者确保宽度总和等于控件宽度,可以做微调
|
if (columnName == "eryi_ID")
|
||||||
int remaining = totalWidth - dgv.Columns.Cast<DataGridViewColumn>()
|
{
|
||||||
.Take(columnCount - 1)
|
var code = row.Cells["eryi_ID"].Value?.ToString()?.Trim() ?? "";
|
||||||
.Sum(col => col.Width);
|
if (!string.IsNullOrWhiteSpace(code) && code != "无")
|
||||||
|
{
|
||||||
|
OpenWithShell($"https://www.21cnjy.com/H/3/309824/{code}.shtml?token=mK2x9Pq7Rf4Tz8Wv1Lc3Yh5Nj6Bs0Ad9Eg2Hj3Kl4Mp5Qr6St7Uv8Wx9Yz0Ab1Cd");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dgv.Columns[columnCount - 1].Width = remaining;
|
private static void OpenFolderAndSelectFile(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "explorer.exe",
|
||||||
|
Arguments = $"/select,\"{filePath}\"",
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("打开文件所在位置失败: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OpenWithShell(string target)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = target,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("打开失败: " + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1139
server/association_detail_smartold.py
Normal file
1139
server/association_detail_smartold.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,18 +1,26 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
将 compressed_group_time_issues.json 数据导入到 MySQL 数据库
|
将 jsons/compressed_group_time_issues.json 导入 MySQL。
|
||||||
|
|
||||||
|
只导入以下四类,并把同一 row 中二一教育和学科网的数据合并为同一条记录:
|
||||||
|
- single_files
|
||||||
|
- same_child_archive_time
|
||||||
|
- zxxk_child_earlier
|
||||||
|
- twole_child_earlier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
import json
|
import json
|
||||||
import mysql.connector
|
|
||||||
from mysql.connector import Error
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
|
import mysql.connector
|
||||||
|
from mysql.connector import Error
|
||||||
|
|
||||||
|
|
||||||
# MySQL 连接配置
|
|
||||||
DB_CONFIG = {
|
DB_CONFIG = {
|
||||||
'host': '127.0.0.1',
|
'host': '127.0.0.1',
|
||||||
'port': 3306,
|
'port': 3306,
|
||||||
@@ -22,316 +30,362 @@ DB_CONFIG = {
|
|||||||
'charset': 'utf8mb4'
|
'charset': 'utf8mb4'
|
||||||
}
|
}
|
||||||
|
|
||||||
# JSON 文件路径
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
JSON_FILE_PATH = 'server/jsons/compressed_group_time_issues.json'
|
DEFAULT_JSON_FILE_PATH = BASE_DIR / "jsons" / "compressed_group_time_issues.json"
|
||||||
|
DEFAULT_TABLE_NAME = "compressed_group_time_records"
|
||||||
|
SELECTED_CATEGORIES = (
|
||||||
|
"single_files",
|
||||||
|
"same_child_archive_time",
|
||||||
|
"zxxk_child_earlier",
|
||||||
|
"twole_child_earlier",
|
||||||
|
)
|
||||||
|
|
||||||
# 建表 SQL
|
|
||||||
CREATE_TABLE_SQL = """
|
|
||||||
CREATE TABLE IF NOT EXISTS compressed_group_time_issues (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
|
|
||||||
row_index INT COMMENT '行索引',
|
|
||||||
|
|
||||||
-- Group1 字段
|
CREATE_TABLE_TEMPLATE = """
|
||||||
group1_latest_extracted_mtime DOUBLE COMMENT 'group1 最新提取文件的 mtime',
|
CREATE TABLE IF NOT EXISTS `{table_name}` (
|
||||||
group1_title VARCHAR(1000) COMMENT 'group1 标题',
|
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
|
||||||
group1_url VARCHAR(1000) COMMENT 'group1 URL',
|
category VARCHAR(64) NOT NULL COMMENT '分类',
|
||||||
group1_time VARCHAR(100) COMMENT 'group1 时间',
|
source_folder VARCHAR(512) NOT NULL COMMENT '来源文件夹',
|
||||||
group1_file_name VARCHAR(1000) COMMENT 'group1 第一个文件名',
|
word_doc VARCHAR(512) NOT NULL COMMENT '关联 Word 文档',
|
||||||
group1_file_mtime DOUBLE COMMENT 'group1 第一个文件 mtime',
|
row_index INT NOT NULL COMMENT 'Word 内容行索引',
|
||||||
group1_file_datetime VARCHAR(50) COMMENT 'group1 第一个文件 datetime',
|
|
||||||
|
|
||||||
-- Group2 字段
|
earlier_site VARCHAR(64) COMMENT '更早的网站中文名',
|
||||||
group2_latest_extracted_mtime DOUBLE COMMENT 'group2 最新提取文件的 mtime',
|
earlier_site_key VARCHAR(32) COMMENT '更早的网站 key: twole/zxxk',
|
||||||
group2_title VARCHAR(1000) COMMENT 'group2 标题',
|
child_time_diff_seconds DOUBLE COMMENT '学科网子文件时间 - 二一教育子文件时间',
|
||||||
group2_url VARCHAR(1000) COMMENT 'group2 URL',
|
|
||||||
group2_time VARCHAR(100) COMMENT 'group2 时间',
|
|
||||||
group2_file_name VARCHAR(1000) COMMENT 'group2 第一个文件名',
|
|
||||||
group2_file_mtime DOUBLE COMMENT 'group2 第一个文件 mtime',
|
|
||||||
group2_file_datetime VARCHAR(50) COMMENT 'group2 第一个文件 datetime',
|
|
||||||
|
|
||||||
-- 来源信息
|
twole_title TEXT COMMENT '二一教育标题',
|
||||||
source_folder VARCHAR(500) COMMENT '来源文件夹',
|
twole_url TEXT COMMENT '二一教育 URL',
|
||||||
word_doc VARCHAR(500) COMMENT '关联的 word 文档',
|
twole_source_time VARCHAR(100) COMMENT 'Word 中二一教育时间',
|
||||||
|
twole_id VARCHAR(100) COMMENT '二一教育 URL ID',
|
||||||
|
twole_file_kind VARCHAR(32) COMMENT '二一教育文件类型',
|
||||||
|
twole_file_name VARCHAR(1024) COMMENT '二一教育文件名',
|
||||||
|
twole_file_mtime DOUBLE COMMENT '二一教育文件 mtime',
|
||||||
|
twole_file_datetime DATETIME COMMENT '二一教育文件时间',
|
||||||
|
twole_archive_mtime DOUBLE COMMENT '二一教育压缩包 mtime',
|
||||||
|
twole_archive_datetime DATETIME COMMENT '二一教育压缩包时间',
|
||||||
|
twole_latest_child_mtime DOUBLE COMMENT '二一教育压缩包内最新子文件 mtime',
|
||||||
|
twole_latest_child_datetime DATETIME COMMENT '二一教育压缩包内最新子文件时间',
|
||||||
|
twole_child_archive_time_relation VARCHAR(32) COMMENT '二一教育子文件与压缩包时间关系',
|
||||||
|
twole_child_archive_time_diff_seconds DOUBLE COMMENT '二一教育子文件与压缩包时间差',
|
||||||
|
|
||||||
-- 新增字段
|
zxxk_title TEXT COMMENT '学科网标题',
|
||||||
21cn_son_file_datetime VARCHAR(50) COMMENT 'group1_latest_extracted_mtime 的日期时间格式',
|
zxxk_url TEXT COMMENT '学科网 URL',
|
||||||
xkw_son_file_datetime VARCHAR(50) COMMENT 'group2_latest_extracted_mtime 的日期时间格式',
|
zxxk_source_time VARCHAR(100) COMMENT 'Word 中学科网时间',
|
||||||
21ID VARCHAR(100) COMMENT 'group1_url 中的数字部分',
|
zxxk_id VARCHAR(100) COMMENT '学科网 URL ID',
|
||||||
xkwID VARCHAR(100) COMMENT 'group2_url 中的数字部分',
|
zxxk_file_kind VARCHAR(32) COMMENT '学科网文件类型',
|
||||||
|
zxxk_file_name VARCHAR(1024) COMMENT '学科网文件名',
|
||||||
|
zxxk_file_mtime DOUBLE COMMENT '学科网文件 mtime',
|
||||||
|
zxxk_file_datetime DATETIME COMMENT '学科网文件时间',
|
||||||
|
zxxk_archive_mtime DOUBLE COMMENT '学科网压缩包 mtime',
|
||||||
|
zxxk_archive_datetime DATETIME COMMENT '学科网压缩包时间',
|
||||||
|
zxxk_latest_child_mtime DOUBLE COMMENT '学科网压缩包内最新子文件 mtime',
|
||||||
|
zxxk_latest_child_datetime DATETIME COMMENT '学科网压缩包内最新子文件时间',
|
||||||
|
zxxk_child_archive_time_relation VARCHAR(32) COMMENT '学科网子文件与压缩包时间关系',
|
||||||
|
zxxk_child_archive_time_diff_seconds DOUBLE COMMENT '学科网子文件与压缩包时间差',
|
||||||
|
|
||||||
-- 索引
|
twole_json JSON COMMENT '二一教育原始 JSON',
|
||||||
INDEX idx_row_index (row_index),
|
zxxk_json JSON COMMENT '学科网原始 JSON',
|
||||||
INDEX idx_source_folder (source_folder)
|
raw_json JSON NOT NULL COMMENT '合并前原始 JSON',
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='压缩组时间问题数据表';
|
imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '导入时间',
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
|
||||||
|
UNIQUE KEY uniq_category_row (category, source_folder(191), word_doc(191), row_index),
|
||||||
|
KEY idx_category (category),
|
||||||
|
KEY idx_earlier_site_key (earlier_site_key),
|
||||||
|
KEY idx_source_row (source_folder(191), word_doc(191), row_index),
|
||||||
|
KEY idx_twole_id (twole_id),
|
||||||
|
KEY idx_zxxk_id (zxxk_id),
|
||||||
|
KEY idx_twole_latest_child_mtime (twole_latest_child_mtime),
|
||||||
|
KEY idx_zxxk_latest_child_mtime (zxxk_latest_child_mtime)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='压缩包时间分类记录';
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def create_connection():
|
INSERT_COLUMNS = (
|
||||||
"""创建数据库连接"""
|
"category", "source_folder", "word_doc", "row_index",
|
||||||
try:
|
"earlier_site", "earlier_site_key", "child_time_diff_seconds",
|
||||||
connection = mysql.connector.connect(**DB_CONFIG)
|
"twole_title", "twole_url", "twole_source_time", "twole_id", "twole_file_kind",
|
||||||
if connection.is_connected():
|
"twole_file_name", "twole_file_mtime", "twole_file_datetime",
|
||||||
print(f"成功连接到 MySQL 数据库")
|
"twole_archive_mtime", "twole_archive_datetime",
|
||||||
print(f"数据库版本:{connection.get_server_info()}")
|
"twole_latest_child_mtime", "twole_latest_child_datetime",
|
||||||
return connection
|
"twole_child_archive_time_relation", "twole_child_archive_time_diff_seconds",
|
||||||
except Error as e:
|
"zxxk_title", "zxxk_url", "zxxk_source_time", "zxxk_id", "zxxk_file_kind",
|
||||||
print(f"连接 MySQL 失败:{e}")
|
"zxxk_file_name", "zxxk_file_mtime", "zxxk_file_datetime",
|
||||||
|
"zxxk_archive_mtime", "zxxk_archive_datetime",
|
||||||
|
"zxxk_latest_child_mtime", "zxxk_latest_child_datetime",
|
||||||
|
"zxxk_child_archive_time_relation", "zxxk_child_archive_time_diff_seconds",
|
||||||
|
"twole_json", "zxxk_json", "raw_json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def mysql_identifier(name: str) -> str:
|
||||||
|
if not re.match(r"^[A-Za-z0-9_]+$", name):
|
||||||
|
raise ValueError(f"非法表名:{name}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def epoch_to_datetime(value) -> Optional[str]:
|
||||||
|
if value in (None, ""):
|
||||||
return None
|
return None
|
||||||
|
return datetime.fromtimestamp(float(value)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
|
||||||
def create_table(connection):
|
def normalize_datetime(value) -> Optional[str]:
|
||||||
"""创建表"""
|
if not value:
|
||||||
try:
|
|
||||||
cursor = connection.cursor()
|
|
||||||
cursor.execute(CREATE_TABLE_SQL)
|
|
||||||
connection.commit()
|
|
||||||
print("表 'compressed_group_time_issues' 创建成功!")
|
|
||||||
cursor.close()
|
|
||||||
except Error as e:
|
|
||||||
print(f"创建表失败:{e}")
|
|
||||||
|
|
||||||
|
|
||||||
def load_json_data(file_path):
|
|
||||||
"""加载 JSON 数据"""
|
|
||||||
try:
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
data = json.load(f)
|
|
||||||
print(f"成功加载 JSON 文件,共 {len(data)} 条记录")
|
|
||||||
return data
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"文件不存在:{file_path}")
|
|
||||||
return None
|
return None
|
||||||
except json.JSONDecodeError as e:
|
if isinstance(value, str) and re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$", value):
|
||||||
print(f"JSON 解析错误:{e}")
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def json_dumps(value) -> str:
|
||||||
|
return json.dumps(value or {}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_url_id(url: Optional[str], site: str) -> Optional[str]:
|
||||||
|
if not url:
|
||||||
return None
|
return None
|
||||||
|
if site == "twole":
|
||||||
|
match = re.search(r"/(\d+)\.shtml(?:[?#].*)?$", url)
|
||||||
def flatten_record(record):
|
|
||||||
"""
|
|
||||||
扁平化记录
|
|
||||||
"""
|
|
||||||
# 获取 group1 的第一个文件
|
|
||||||
group1_files = record.get('group1', {}).get('files', [])
|
|
||||||
group1_first_file = group1_files[0] if group1_files else {}
|
|
||||||
|
|
||||||
# 获取 group2 的第一个文件
|
|
||||||
group2_files = record.get('group2', {}).get('files', [])
|
|
||||||
group2_first_file = group2_files[0] if group2_files else {}
|
|
||||||
|
|
||||||
# 计算新字段
|
|
||||||
group1_mtime = record.get('group1_latest_extracted_mtime')
|
|
||||||
group1_datetime = datetime.fromtimestamp(group1_mtime).strftime('%Y-%m-%d %H:%M:%S') if group1_mtime else None
|
|
||||||
|
|
||||||
group2_mtime = record.get('group2_latest_extracted_mtime')
|
|
||||||
group2_datetime = datetime.fromtimestamp(group2_mtime).strftime('%Y-%m-%d %H:%M:%S') if group2_mtime else None
|
|
||||||
|
|
||||||
group1_url = record.get('group1', {}).get('url', '')
|
|
||||||
group1_id = None
|
|
||||||
if group1_url:
|
|
||||||
match = re.search(r'/(\d+)\.shtml$', group1_url)
|
|
||||||
if match:
|
if match:
|
||||||
group1_id = match.group(1)
|
return match.group(1)
|
||||||
|
if site == "zxxk":
|
||||||
group2_url = record.get('group2', {}).get('url', '')
|
match = re.search(r"/(?:soft/)?(\d+)\.html(?:[?#].*)?$", url)
|
||||||
group2_id = None
|
|
||||||
if group2_url:
|
|
||||||
match = re.search(r'/(\d+)\.html$', group2_url)
|
|
||||||
if match:
|
if match:
|
||||||
group2_id = match.group(1)
|
return match.group(1)
|
||||||
|
numbers = re.findall(r"\d+", url)
|
||||||
|
return numbers[-1] if numbers else None
|
||||||
|
|
||||||
|
|
||||||
|
def first_file_from_group(group: Dict) -> Dict:
|
||||||
|
files = group.get("files") or []
|
||||||
|
return files[0] if files else {}
|
||||||
|
|
||||||
|
|
||||||
|
def build_side_from_individual(record: Dict) -> Dict:
|
||||||
|
file_info = record.get("file") or {}
|
||||||
|
site = record.get("site") or ""
|
||||||
return {
|
return {
|
||||||
'row_index': record.get('row_index'),
|
"title": record.get("title"),
|
||||||
|
"url": record.get("url"),
|
||||||
# Group1 字段
|
"source_time": record.get("time"),
|
||||||
'group1_latest_extracted_mtime': record.get('group1_latest_extracted_mtime'),
|
"id": extract_url_id(record.get("url"), site),
|
||||||
'group1_title': record.get('group1', {}).get('title'),
|
"file_kind": record.get("file_kind"),
|
||||||
'group1_url': record.get('group1', {}).get('url'),
|
"file_name": file_info.get("name"),
|
||||||
'group1_time': record.get('group1', {}).get('time'),
|
"file_mtime": file_info.get("mtime"),
|
||||||
'group1_file_name': group1_first_file.get('name'),
|
"file_datetime": normalize_datetime(file_info.get("datetime")) or epoch_to_datetime(file_info.get("mtime")),
|
||||||
'group1_file_mtime': group1_first_file.get('mtime'),
|
"archive_mtime": record.get("archive_mtime"),
|
||||||
'group1_file_datetime': group1_first_file.get('datetime'),
|
"archive_datetime": normalize_datetime(record.get("archive_datetime")) or epoch_to_datetime(record.get("archive_mtime")),
|
||||||
|
"latest_child_mtime": record.get("latest_child_mtime"),
|
||||||
# Group2 字段
|
"latest_child_datetime": normalize_datetime(record.get("latest_child_datetime")) or epoch_to_datetime(record.get("latest_child_mtime")),
|
||||||
'group2_latest_extracted_mtime': record.get('group2_latest_extracted_mtime'),
|
"child_archive_time_relation": record.get("child_archive_time_relation"),
|
||||||
'group2_title': record.get('group2', {}).get('title'),
|
"child_archive_time_diff_seconds": record.get("child_archive_time_diff_seconds"),
|
||||||
'group2_url': record.get('group2', {}).get('url'),
|
"json": record,
|
||||||
'group2_time': record.get('group2', {}).get('time'),
|
|
||||||
'group2_file_name': group2_first_file.get('name'),
|
|
||||||
'group2_file_mtime': group2_first_file.get('mtime'),
|
|
||||||
'group2_file_datetime': group2_first_file.get('datetime'),
|
|
||||||
|
|
||||||
# 来源信息
|
|
||||||
'source_folder': record.get('source_folder'),
|
|
||||||
'word_doc': record.get('word_doc'),
|
|
||||||
|
|
||||||
# 新增字段
|
|
||||||
'21cn_son_file_datetime': group1_datetime,
|
|
||||||
'xkw_son_file_datetime': group2_datetime,
|
|
||||||
'21ID': group1_id,
|
|
||||||
'xkwID': group2_id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def insert_data(connection, data):
|
def build_side_from_pair(group: Dict, site: str, pair_record: Dict) -> Dict:
|
||||||
"""插入数据"""
|
file_info = first_file_from_group(group)
|
||||||
insert_sql = """
|
return {
|
||||||
INSERT INTO compressed_group_time_issues (
|
"title": group.get("title"),
|
||||||
row_index,
|
"url": group.get("url"),
|
||||||
group1_latest_extracted_mtime, group1_title, group1_url, group1_time,
|
"source_time": group.get("time"),
|
||||||
group1_file_name, group1_file_mtime, group1_file_datetime,
|
"id": extract_url_id(group.get("url"), site),
|
||||||
group2_latest_extracted_mtime, group2_title, group2_url, group2_time,
|
"file_kind": "archive",
|
||||||
group2_file_name, group2_file_mtime, group2_file_datetime,
|
"file_name": file_info.get("name"),
|
||||||
source_folder, word_doc,
|
"file_mtime": file_info.get("mtime"),
|
||||||
21cn_son_file_datetime, xkw_son_file_datetime, 21ID, xkwID
|
"file_datetime": normalize_datetime(file_info.get("datetime")) or epoch_to_datetime(file_info.get("mtime")),
|
||||||
) VALUES (
|
"archive_mtime": file_info.get("mtime"),
|
||||||
%s, %s, %s, %s, %s, %s, %s, %s,
|
"archive_datetime": normalize_datetime(file_info.get("datetime")) or epoch_to_datetime(file_info.get("mtime")),
|
||||||
%s, %s, %s, %s, %s, %s, %s, %s, %s,
|
"latest_child_mtime": pair_record.get(f"{site}_latest_child_mtime"),
|
||||||
%s, %s, %s, %s
|
"latest_child_datetime": normalize_datetime(pair_record.get(f"{site}_latest_child_datetime")) or epoch_to_datetime(pair_record.get(f"{site}_latest_child_mtime")),
|
||||||
|
"child_archive_time_relation": "different",
|
||||||
|
"child_archive_time_diff_seconds": None,
|
||||||
|
"json": group,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def empty_import_record(category: str, source_folder: str, word_doc: str, row_index: int, raw_json) -> Dict:
|
||||||
|
record = {column: None for column in INSERT_COLUMNS}
|
||||||
|
record.update({
|
||||||
|
"category": category,
|
||||||
|
"source_folder": source_folder,
|
||||||
|
"word_doc": word_doc,
|
||||||
|
"row_index": row_index,
|
||||||
|
"raw_json": json_dumps(raw_json),
|
||||||
|
})
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def apply_side(record: Dict, site: str, side: Dict) -> None:
|
||||||
|
prefix = "twole" if site == "twole" else "zxxk"
|
||||||
|
for key in (
|
||||||
|
"title", "url", "source_time", "id", "file_kind", "file_name", "file_mtime", "file_datetime",
|
||||||
|
"archive_mtime", "archive_datetime", "latest_child_mtime", "latest_child_datetime",
|
||||||
|
"child_archive_time_relation", "child_archive_time_diff_seconds",
|
||||||
|
):
|
||||||
|
record[f"{prefix}_{key}"] = side.get(key)
|
||||||
|
record[f"{prefix}_json"] = json_dumps(side.get("json"))
|
||||||
|
|
||||||
|
|
||||||
|
def row_key(record: Dict) -> Tuple[str, str, int]:
|
||||||
|
return (record.get("source_folder") or "", record.get("word_doc") or "", int(record.get("row_index") or 0))
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_individual_category(category: str, records: Iterable[Dict]) -> List[Dict]:
|
||||||
|
grouped: Dict[Tuple[str, str, int], Dict] = {}
|
||||||
|
raw_records: Dict[Tuple[str, str, int], List[Dict]] = {}
|
||||||
|
|
||||||
|
for source_record in records:
|
||||||
|
key = row_key(source_record)
|
||||||
|
raw_records.setdefault(key, []).append(source_record)
|
||||||
|
if key not in grouped:
|
||||||
|
grouped[key] = empty_import_record(category, key[0], key[1], key[2], raw_records[key])
|
||||||
|
grouped[key]["raw_json"] = json_dumps(raw_records[key])
|
||||||
|
|
||||||
|
site = source_record.get("site")
|
||||||
|
if site in ("twole", "zxxk"):
|
||||||
|
apply_side(grouped[key], site, build_side_from_individual(source_record))
|
||||||
|
|
||||||
|
return [grouped[key] for key in sorted(grouped)]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_pair_category(category: str, records: Iterable[Dict]) -> List[Dict]:
|
||||||
|
normalized = []
|
||||||
|
for source_record in records:
|
||||||
|
key = row_key(source_record)
|
||||||
|
record = empty_import_record(category, key[0], key[1], key[2], source_record)
|
||||||
|
record["earlier_site"] = source_record.get("earlier_site")
|
||||||
|
record["earlier_site_key"] = source_record.get("earlier_site_key")
|
||||||
|
record["child_time_diff_seconds"] = source_record.get("child_time_diff_seconds")
|
||||||
|
apply_side(record, "twole", build_side_from_pair(source_record.get("twole") or {}, "twole", source_record))
|
||||||
|
apply_side(record, "zxxk", build_side_from_pair(source_record.get("zxxk") or {}, "zxxk", source_record))
|
||||||
|
normalized.append(record)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_records_for_import(data: Dict) -> List[Dict]:
|
||||||
|
normalized: List[Dict] = []
|
||||||
|
normalized.extend(normalize_individual_category("single_files", data.get("single_files", [])))
|
||||||
|
normalized.extend(normalize_individual_category("same_child_archive_time", data.get("same_child_archive_time", [])))
|
||||||
|
normalized.extend(normalize_pair_category("zxxk_child_earlier", data.get("zxxk_child_earlier", [])))
|
||||||
|
normalized.extend(normalize_pair_category("twole_child_earlier", data.get("twole_child_earlier", [])))
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_data(file_path: Path) -> Dict:
|
||||||
|
with file_path.open("r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("当前导入脚本只支持新版分类字典格式 JSON")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def create_connection():
|
||||||
|
return mysql.connector.connect(**DB_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
def create_table(connection, table_name: str) -> None:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(CREATE_TABLE_TEMPLATE.format(table_name=mysql_identifier(table_name)))
|
||||||
|
connection.commit()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_table(connection, table_name: str) -> None:
|
||||||
|
cursor = connection.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(f"TRUNCATE TABLE `{mysql_identifier(table_name)}`")
|
||||||
|
connection.commit()
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
|
||||||
|
def build_insert_sql(table_name: str) -> str:
|
||||||
|
columns = ", ".join(f"`{column}`" for column in INSERT_COLUMNS)
|
||||||
|
placeholders = ", ".join(["%s"] * len(INSERT_COLUMNS))
|
||||||
|
updates = ", ".join(
|
||||||
|
f"`{column}` = VALUES(`{column}`)"
|
||||||
|
for column in INSERT_COLUMNS
|
||||||
|
if column not in {"category", "source_folder", "word_doc", "row_index"}
|
||||||
)
|
)
|
||||||
|
return f"""
|
||||||
|
INSERT INTO `{mysql_identifier(table_name)}` ({columns})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
ON DUPLICATE KEY UPDATE {updates}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
|
||||||
cursor = connection.cursor()
|
|
||||||
inserted_count = 0
|
|
||||||
|
|
||||||
for record in data:
|
def insert_records(connection, table_name: str, records: List[Dict], batch_size: int = 500) -> int:
|
||||||
flattened = flatten_record(record)
|
if not records:
|
||||||
|
|
||||||
values = (
|
|
||||||
flattened['row_index'],
|
|
||||||
flattened['group1_latest_extracted_mtime'],
|
|
||||||
flattened['group1_title'],
|
|
||||||
flattened['group1_url'],
|
|
||||||
flattened['group1_time'],
|
|
||||||
flattened['group1_file_name'],
|
|
||||||
flattened['group1_file_mtime'],
|
|
||||||
flattened['group1_file_datetime'],
|
|
||||||
flattened['group2_latest_extracted_mtime'],
|
|
||||||
flattened['group2_title'],
|
|
||||||
flattened['group2_url'],
|
|
||||||
flattened['group2_time'],
|
|
||||||
flattened['group2_file_name'],
|
|
||||||
flattened['group2_file_mtime'],
|
|
||||||
flattened['group2_file_datetime'],
|
|
||||||
flattened['source_folder'],
|
|
||||||
flattened['word_doc'],
|
|
||||||
flattened['21cn_son_file_datetime'],
|
|
||||||
flattened['xkw_son_file_datetime'],
|
|
||||||
flattened['21ID'],
|
|
||||||
flattened['xkwID']
|
|
||||||
)
|
|
||||||
|
|
||||||
cursor.execute(insert_sql, values)
|
|
||||||
inserted_count += 1
|
|
||||||
|
|
||||||
if inserted_count % 50 == 0:
|
|
||||||
print(f"已插入 {inserted_count} 条记录...")
|
|
||||||
|
|
||||||
connection.commit()
|
|
||||||
print(f"成功插入 {inserted_count} 条记录!")
|
|
||||||
cursor.close()
|
|
||||||
return inserted_count
|
|
||||||
except Error as e:
|
|
||||||
print(f"插入数据失败:{e}")
|
|
||||||
connection.rollback()
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
insert_sql = build_insert_sql(table_name)
|
||||||
def verify_data(connection):
|
cursor = connection.cursor()
|
||||||
"""验证导入的数据"""
|
inserted = 0
|
||||||
try:
|
try:
|
||||||
cursor = connection.cursor()
|
for start in range(0, len(records), batch_size):
|
||||||
|
batch = records[start:start + batch_size]
|
||||||
# 查询总记录数
|
values = [tuple(record.get(column) for column in INSERT_COLUMNS) for record in batch]
|
||||||
cursor.execute("SELECT COUNT(*) FROM compressed_group_time_issues")
|
cursor.executemany(insert_sql, values)
|
||||||
count = cursor.fetchone()[0]
|
connection.commit()
|
||||||
print(f"\n验证:表中总记录数 = {count}")
|
inserted += len(batch)
|
||||||
|
print(f"已写入/更新 {inserted}/{len(records)} 条记录")
|
||||||
# 查询前 2 条记录作为示例
|
finally:
|
||||||
cursor.execute("""
|
|
||||||
SELECT row_index, group1_title, group2_title, source_folder,
|
|
||||||
21cn_son_file_datetime, xkw_son_file_datetime, 21ID, xkwID
|
|
||||||
FROM compressed_group_time_issues
|
|
||||||
LIMIT 2
|
|
||||||
""")
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
print("\n示例数据:")
|
|
||||||
for row in rows:
|
|
||||||
print(f" row_index={row[0]}")
|
|
||||||
print(f" group1_title={row[1][:50]}...")
|
|
||||||
print(f" group2_title={row[2][:50]}...")
|
|
||||||
print(f" source_folder={row[3]}")
|
|
||||||
print(f" 21cn_son_file_datetime={row[4]}")
|
|
||||||
print(f" xkw_son_file_datetime={row[5]}")
|
|
||||||
print(f" 21ID={row[6]}")
|
|
||||||
print(f" xkwID={row[7]}")
|
|
||||||
print("-" * 50)
|
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
except Error as e:
|
return inserted
|
||||||
print(f"验证数据失败:{e}")
|
|
||||||
|
|
||||||
|
|
||||||
def export_table_to_excel(connection, output_path: Path):
|
def print_category_summary(records: List[Dict]) -> None:
|
||||||
"""将数据库表导出为 Excel 文件"""
|
counts = {category: 0 for category in SELECTED_CATEGORIES}
|
||||||
try:
|
for record in records:
|
||||||
import pandas as pd
|
counts[record["category"]] = counts.get(record["category"], 0) + 1
|
||||||
except ImportError:
|
print("待导入分类统计:")
|
||||||
print("请先安装 pandas 和 openpyxl:pip install pandas openpyxl")
|
for category in SELECTED_CATEGORIES:
|
||||||
return False
|
print(f" {category}: {counts.get(category, 0)}")
|
||||||
|
print(f" total: {len(records)}")
|
||||||
|
|
||||||
try:
|
|
||||||
cursor = connection.cursor()
|
|
||||||
cursor.execute("SELECT * FROM compressed_group_time_issues")
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
columns = cursor.column_names
|
|
||||||
cursor.close()
|
|
||||||
|
|
||||||
df = pd.DataFrame(rows, columns=columns)
|
def parse_args():
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
parser = argparse.ArgumentParser(description="导入 compressed_group_time_issues.json 到 MySQL")
|
||||||
df.to_excel(output_path, index=False)
|
parser.add_argument("--json-file", type=Path, default=DEFAULT_JSON_FILE_PATH, help="JSON 文件路径")
|
||||||
print(f"成功导出 Excel 文件:{output_path}")
|
parser.add_argument("--table", default=DEFAULT_TABLE_NAME, help="MySQL 表名")
|
||||||
return True
|
parser.add_argument("--truncate", action="store_true", help="导入前清空目标表")
|
||||||
except Exception as e:
|
parser.add_argument("--dry-run", action="store_true", help="只解析和统计,不连接 MySQL")
|
||||||
print(f"导出 Excel 失败:{e}")
|
return parser.parse_args()
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""主函数"""
|
args = parse_args()
|
||||||
print("=" * 60)
|
data = load_json_data(args.json_file)
|
||||||
print("开始导入 compressed_group_time_issues.json 到 MySQL")
|
records = normalize_records_for_import(data)
|
||||||
print("=" * 60)
|
print_category_summary(records)
|
||||||
|
|
||||||
# 1. 创建数据库连接
|
if args.dry_run:
|
||||||
connection = create_connection()
|
print("dry-run 模式:未连接 MySQL,未写入数据")
|
||||||
if not connection:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. 创建表
|
connection = None
|
||||||
create_table(connection)
|
try:
|
||||||
|
connection = create_connection()
|
||||||
# 3. 加载 JSON 数据
|
print("成功连接到 MySQL")
|
||||||
data = load_json_data(JSON_FILE_PATH)
|
create_table(connection, args.table)
|
||||||
if not data:
|
print(f"已确认目标表:{args.table}")
|
||||||
connection.close()
|
if args.truncate:
|
||||||
return
|
truncate_table(connection, args.table)
|
||||||
|
print(f"已清空目标表:{args.table}")
|
||||||
# 4. 插入数据
|
inserted = insert_records(connection, args.table, records)
|
||||||
insert_data(connection, data)
|
print(f"导入完成:写入/更新 {inserted} 条记录")
|
||||||
|
except Error as exc:
|
||||||
# 5. 验证数据
|
print(f"MySQL 操作失败:{exc}")
|
||||||
verify_data(connection)
|
raise
|
||||||
# 6. 导出 Excel
|
finally:
|
||||||
excel_path = Path('server/jsons/compressed_group_time_issues.xlsx')
|
if connection and connection.is_connected():
|
||||||
export_table_to_excel(connection, excel_path)
|
connection.close()
|
||||||
# 关闭连接
|
print("MySQL 连接已关闭")
|
||||||
connection.close()
|
|
||||||
print("\n数据库连接已关闭")
|
|
||||||
print("=" * 60)
|
|
||||||
print("导入完成!")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user