Compare commits

..

13 Commits

Author SHA1 Message Date
f5c0ecc31f modified pppp 2026-07-01 15:52:29 +08:00
44a9c5a5d5 revers smart 2026-07-01 12:58:13 +08:00
be4cabbb23 fff 2026-05-30 14:04:40 +08:00
6e669ac098 adjust again 2026-05-12 08:40:53 +08:00
ebf59e15d1 adjust again 2026-05-12 08:38:12 +08:00
c41074b6f6 adjust some width 2026-05-12 08:32:09 +08:00
6dad547dcd simplified server/association_detail_smart.py,maybe need reverse 2026-05-12 08:26:41 +08:00
6b0a9a5bdb ajust width 2026-05-12 08:26:06 +08:00
9a7fad6538 modified url format 2026-05-01 10:21:01 +08:00
32881e661b update databases 2026-04-29 10:50:22 +08:00
deca0ed4f5 优化暗码读取器 2026-04-29 10:46:50 +08:00
e13f9e775d add database 2026-04-28 16:15:38 +08:00
430e586e82 added get hidden code 2026-04-28 09:55:09 +08:00
6 changed files with 2095 additions and 895 deletions

View File

@@ -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 ;
} }
} }

View File

@@ -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

File diff suppressed because it is too large Load Diff

BIN
server/hiddencode.db Normal file

Binary file not shown.

View File

@@ -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 和 openpyxlpip 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()