353 lines
16 KiB
C#
353 lines
16 KiB
C#
// 元件库数据工具:按厂家勾选抓取数据(树/系列/价格),打包成数据包 zip 供新服务读取
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Windows.Forms;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace yjktool
|
|
{
|
|
public partial class MainForm : Form
|
|
{
|
|
private List<厂家项> 全部厂家 = new List<厂家项>();
|
|
private string 工作根; // 抓取落盘根(data 布局)
|
|
private 抓取器 抓;
|
|
private volatile bool 运行中;
|
|
|
|
private class 厂家项
|
|
{
|
|
public string 树id;
|
|
public string 名称;
|
|
public override string ToString() { return 名称; }
|
|
}
|
|
|
|
public MainForm()
|
|
{
|
|
InitializeComponent();
|
|
工作根 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "抓取输出");
|
|
txt输出.Text = 工作根;
|
|
加载厂家清单();
|
|
chk代理.Checked = true; // 默认走旧代理(抓取时会自动启动工具内置的旧代理)
|
|
检测旧代理(); // 启动时显示旧代理在线状态
|
|
FormClosing += (s, e) => 关闭内置旧代理();
|
|
}
|
|
|
|
// ---------- 旧代理(工具内置副本,8081;node 新服务占 8080 不能同端口) ----------
|
|
private const int 代理端口 = 8081;
|
|
private System.Diagnostics.Process _代理进程; // 本工具拉起的内置旧代理(退出时收尾)
|
|
|
|
private static bool 端口通(int 端口)
|
|
{
|
|
try
|
|
{
|
|
using (var c = new System.Net.Sockets.TcpClient())
|
|
{
|
|
var ar = c.BeginConnect("127.0.0.1", 端口, null, null);
|
|
return ar.AsyncWaitHandle.WaitOne(600) && c.Connected;
|
|
}
|
|
}
|
|
catch { return false; }
|
|
}
|
|
|
|
/// <summary>8081 不通时自动启动工具旁的内置旧代理(旧代理\python\python.exe -u proxy_server.py --port 8081)。
|
|
/// 阻塞最多约 15 秒等端口就绪,应在后台线程调用。</summary>
|
|
private bool 确保旧代理运行()
|
|
{
|
|
if (端口通(代理端口)) return true;
|
|
string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "旧代理");
|
|
string py = Path.Combine(dir, "python", "python.exe");
|
|
if (!File.Exists(py) || !File.Exists(Path.Combine(dir, "proxy_server.py")))
|
|
{
|
|
UI日志("! 工具旁没有内置旧代理目录(旧代理\\python\\python.exe),价格将全部落空");
|
|
return false;
|
|
}
|
|
UI日志("正在启动内置旧代理(端口 " + 代理端口 + ")…");
|
|
try
|
|
{
|
|
_代理进程 = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = py,
|
|
Arguments = "-u proxy_server.py --port " + 代理端口,
|
|
WorkingDirectory = dir,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
});
|
|
}
|
|
catch (Exception ex) { UI日志("! 启动旧代理失败: " + ex.Message); return false; }
|
|
for (int i = 0; i < 30 && !端口通(代理端口); i++) Thread.Sleep(500);
|
|
bool ok = 端口通(代理端口);
|
|
UI日志(ok ? "✅ 内置旧代理已启动(端口 " + 代理端口 + ")" : "! 内置旧代理 15 秒内未就绪(可能启动报错)");
|
|
检测旧代理();
|
|
return ok;
|
|
}
|
|
|
|
/// <summary>工具退出时结束自己拉起的内置旧代理,避免留孤儿进程</summary>
|
|
private void 关闭内置旧代理()
|
|
{
|
|
try
|
|
{
|
|
if (_代理进程 != null && !_代理进程.HasExited) _代理进程.Kill();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>所有可用的种子目录:工具内置副本+根目录原件(预热缓存)+元件库网页 data(现成真价,含原始 t)。</summary>
|
|
private static string[] 旧代理缓存目录们()
|
|
{
|
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
string[] 候选 = {
|
|
Path.Combine(baseDir, @"旧代理\proxy_cache"), // 工具内置副本
|
|
Path.Combine(baseDir, @"..\..\..\旧服务-python代理\src\proxy_cache"), // 根目录原件(预热流程)
|
|
Path.Combine(baseDir, @"..\..\..\成套报价软件\bin\Debug\元件库网页\data\dx2_webGetPrice"), // 新服务现成真价(开发机)
|
|
Path.Combine(baseDir, @"元件库网页\data\dx2_webGetPrice"), // 安装布局
|
|
};
|
|
return Array.FindAll(候选, p => Directory.Exists(p));
|
|
}
|
|
|
|
/// <summary>清空旧数据:抓取输出两目录(树/系列包+价格)+两处旧代理缓存(proxy_cache)。
|
|
/// 旧代理缓存会被直接回放成旧响应(等于抓到的还是旧价),要拿全新数据就得先清。在启动代理前执行。</summary>
|
|
private int 清空旧数据()
|
|
{
|
|
int n = 0;
|
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
string[] 目录们 = {
|
|
Path.Combine(工作根, @"dian\electricData_zips"),
|
|
Path.Combine(工作根, @"dx2_webGetPrice"),
|
|
Path.Combine(baseDir, @"旧代理\proxy_cache"), // 工具内置副本
|
|
Path.Combine(baseDir, @"..\..\..\旧服务-python代理\src\proxy_cache"), // 根目录原件
|
|
};
|
|
foreach (string d in 目录们)
|
|
{
|
|
try
|
|
{
|
|
if (!Directory.Exists(d)) continue;
|
|
int 个数 = Directory.GetFiles(d, "*", SearchOption.AllDirectories).Length;
|
|
Directory.Delete(d, true);
|
|
Directory.CreateDirectory(d);
|
|
n++;
|
|
UI日志(" 已清空 " + 个数 + " 个文件: " + d);
|
|
}
|
|
catch (Exception ex) { UI日志(" ! 清空失败 " + d + ": " + ex.Message); }
|
|
}
|
|
return n;
|
|
}
|
|
|
|
private void 检测旧代理()
|
|
{
|
|
if (InvokeRequired) { BeginInvoke((Action)(检测旧代理)); return; }
|
|
bool 在线 = 端口通(代理端口);
|
|
lbl代理.Text = 在线 ? "旧代理:在线 ✓" : "旧代理:离线";
|
|
lbl代理.ForeColor = 在线 ? System.Drawing.Color.Green : System.Drawing.Color.Firebrick;
|
|
}
|
|
|
|
private void chk代理_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (chk代理.Checked) 检测旧代理();
|
|
else lbl代理.Text = "旧代理:未使用";
|
|
}
|
|
|
|
// ---------- 厂家清单:元件库网页\data(开发/安装布局)→exe旁→内置 f.zip 都无则提示 ----------
|
|
private void 加载厂家清单()
|
|
{
|
|
string fzip = null;
|
|
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
string[] 候选 = {
|
|
Path.Combine(baseDir, @"..\..\..\成套报价软件\bin\Debug\元件库网页\data\dian\electricData_zips\f.zip"),
|
|
Path.Combine(baseDir, @"..\..\..\..\..\..\成套报价软件\bin\Debug\元件库网页\data\dian\electricData_zips\f.zip"),
|
|
Path.Combine(baseDir, @"元件库网页\data\dian\electricData_zips\f.zip"),
|
|
Path.Combine(baseDir, "f.zip"),
|
|
};
|
|
foreach (string p in 候选)
|
|
if (File.Exists(p)) { fzip = p; break; }
|
|
if (fzip == null)
|
|
{
|
|
MessageBox.Show(this, "找不到厂家清单 f.zip。\r\n请把工具放在 元件库网页\\ 目录旁,或把 f.zip 拷到工具旁。",
|
|
"元件库数据工具", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
string 文;
|
|
using (var z = ZipFile.OpenRead(fzip))
|
|
using (var r = new StreamReader(z.Entries[0].Open(), System.Text.Encoding.UTF8))
|
|
文 = r.ReadToEnd();
|
|
var 见过 = new HashSet<string>();
|
|
foreach (Match m in Regex.Matches(文, @"\{[^{}]*?""f""\s*:\s*(\d+)[^{}]*?""name""\s*:\s*""([^""]+)""[^{}]*?\}"))
|
|
{
|
|
string id = m.Groups[1].Value;
|
|
if (id == "0" || 见过.Contains(id)) continue;
|
|
见过.Add(id);
|
|
全部厂家.Add(new 厂家项 { 树id = id, 名称 = m.Groups[2].Value });
|
|
}
|
|
// f 在 name 前的变体
|
|
if (全部厂家.Count == 0)
|
|
{
|
|
foreach (Match m in Regex.Matches(文, @"\{[^{}]*?""name""\s*:\s*""([^""]+)""[^{}]*?""f""\s*:\s*(\d+)[^{}]*?\}"))
|
|
{
|
|
string id = m.Groups[2].Value;
|
|
if (id == "0" || 见过.Contains(id)) continue;
|
|
见过.Add(id);
|
|
全部厂家.Add(new 厂家项 { 树id = id, 名称 = m.Groups[1].Value });
|
|
}
|
|
}
|
|
填列表("");
|
|
lbl统计.Text = "厂家 " + 全部厂家.Count + " 家";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, "读取 f.zip 失败: " + ex.Message, "元件库数据工具");
|
|
}
|
|
}
|
|
|
|
private void 填列表(string 过滤)
|
|
{
|
|
lst厂家.BeginUpdate();
|
|
lst厂家.Items.Clear();
|
|
foreach (var f in 全部厂家)
|
|
{
|
|
if (过滤 != "" && f.名称.IndexOf(过滤, StringComparison.OrdinalIgnoreCase) < 0) continue;
|
|
var it = lst厂家.Items.Add(f.名称);
|
|
it.Checked = false;
|
|
it.Tag = f;
|
|
}
|
|
lst厂家.EndUpdate();
|
|
}
|
|
|
|
private void txt搜索_TextChanged(object sender, EventArgs e)
|
|
{
|
|
填列表(txt搜索.Text.Trim());
|
|
}
|
|
|
|
private void btn全选_Click(object sender, EventArgs e)
|
|
{
|
|
foreach (ListViewItem it in lst厂家.Items) it.Checked = true;
|
|
}
|
|
|
|
private void btn反选_Click(object sender, EventArgs e)
|
|
{
|
|
foreach (ListViewItem it in lst厂家.Items) it.Checked = !it.Checked;
|
|
}
|
|
|
|
// ---------- UI 线程调度 ----------
|
|
private void UI日志(string s)
|
|
{
|
|
if (InvokeRequired) { BeginInvoke((Action)(() => UI日志(s))); return; }
|
|
rtb日志.AppendText("[" + DateTime.Now.ToString("HH:mm:ss") + "] " + s + "\r\n");
|
|
}
|
|
private void UI进度(int 厂完成, int 厂总数)
|
|
{
|
|
if (InvokeRequired) { BeginInvoke((Action)(() => UI进度(厂完成, 厂总数))); return; }
|
|
if (厂总数 > 0) progress.Value = Math.Min(progress.Maximum, 厂完成 * 100 / 厂总数);
|
|
lbl进度.Text = 厂完成 + " / " + 厂总数;
|
|
}
|
|
|
|
private void btn开始_Click(object sender, EventArgs e)
|
|
{
|
|
if (运行中) { 抓.停止 = true; btn开始.Text = "正在停止…"; return; }
|
|
var 选中 = lst厂家.Items.Cast<ListViewItem>().Where(i => i.Checked).Select(i => (厂家项)i.Tag).ToList();
|
|
if (选中.Count == 0)
|
|
{
|
|
MessageBox.Show(this, "请先勾选要抓取的厂家。", "元件库数据工具");
|
|
return;
|
|
}
|
|
工作根 = txt输出.Text.Trim();
|
|
Directory.CreateDirectory(Path.Combine(工作根, "dian", "electricData_zips"));
|
|
Directory.CreateDirectory(Path.Combine(工作根, "dx2_webGetPrice"));
|
|
抓 = new 抓取器(工作根) { 日志 = UI日志 };
|
|
抓.经旧代理 = chk代理.Checked;
|
|
运行中 = true;
|
|
btn开始.Text = "停止";
|
|
progress.Maximum = 100; progress.Value = 0;
|
|
int 线程 = (int)num线程.Value;
|
|
UI日志("开始抓取 " + 选中.Count + " 家,线程 " + 线程 + ",输出 " + 工作根 + (抓.经旧代理 ? ",价格走旧代理" : ",价格直连"));
|
|
int 总系 = 0, 总价 = 0;
|
|
ThreadPool.QueueUserWorkItem(_ =>
|
|
{
|
|
// 先清旧数据(用户要求:旧代理缓存会回放旧响应、抓取输出留旧文件,要全新数据先清干净)
|
|
if (chk清理.Checked)
|
|
{
|
|
UI日志("清空旧数据…");
|
|
清空旧数据();
|
|
}
|
|
// 代理仅在勾选时启动(终版机制直连+种子 t 即可命中,不依赖代理)
|
|
if (抓.经旧代理) 确保旧代理运行();
|
|
// ★ 种子加载与代理无关,必须无条件执行:种子(系列有效时刻 t)决定价格能不能查,
|
|
// 不加载种子则所有系列按"无 t"跳过查价 → 0 价格
|
|
string[] 缓存们 = 旧代理缓存目录们();
|
|
if (缓存们.Length > 0)
|
|
{
|
|
int n种 = 0;
|
|
foreach (string 缓存 in 缓存们) n种 = 抓.加载种子(缓存); // 多处种子合并
|
|
UI日志("已加载种子: " + n种 + " 个系列的有效时刻(来自 " + 缓存们.Length + " 处目录)");
|
|
if (n种 == 0)
|
|
UI日志("! 种子为 0:价格将全部跳过。先在预热页面浏览目标厂家系列,或确认元件库网页 data 目录存在");
|
|
}
|
|
else
|
|
UI日志("! 未找到种子目录(价格将全部跳过)");
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
for (int i = 0; i < 选中.Count && !抓.停止; i++)
|
|
{
|
|
var kv = 抓.抓厂家(选中[i].树id, 选中[i].名称, 线程);
|
|
总系 += kv.Key; 总价 += kv.Value;
|
|
UI进度(i + 1, 选中.Count);
|
|
}
|
|
UI日志((抓.停止 ? "已停止: " : "抓取完成: ") + "系列 " + 总系 + ", 价格组合 " + 总价 +
|
|
", 用时 " + (int)sw.Elapsed.TotalMinutes + " 分钟。可点【打包数据包】。");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
UI日志("! 抓取中断: " + ex);
|
|
}
|
|
BeginInvoke((Action)(() => { 运行中 = false; btn开始.Text = "开始抓取"; }));
|
|
});
|
|
}
|
|
|
|
// ---------- 打包 ----------
|
|
private void btn打包_Click(object sender, EventArgs e)
|
|
{
|
|
if (运行中) { MessageBox.Show(this, "抓取进行中,请先停止。"); return; }
|
|
if (!Directory.Exists(工作根))
|
|
{
|
|
MessageBox.Show(this, "输出目录不存在,先抓取。"); return;
|
|
}
|
|
using (var dlg = new SaveFileDialog
|
|
{
|
|
Title = "打包数据包",
|
|
Filter = "元件库数据包 (*.zip)|*.zip",
|
|
FileName = "厂家数据包_" + DateTime.Now.ToString("yyyyMMdd_HHmm") + ".zip",
|
|
InitialDirectory = AppDomain.CurrentDomain.BaseDirectory,
|
|
})
|
|
{
|
|
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
|
try
|
|
{
|
|
int n = 0;
|
|
using (var zip = ZipFile.Open(dlg.FileName, ZipArchiveMode.Create))
|
|
{
|
|
foreach (string f in Directory.GetFiles(工作根, "*", SearchOption.AllDirectories))
|
|
{
|
|
if (f.EndsWith(".json")) continue; // 树缓存伴随文件不入包
|
|
string rel = f.Substring(工作根.Length + 1).Replace('\\', '/');
|
|
zip.CreateEntryFromFile(f, rel, CompressionLevel.Optimal);
|
|
n++;
|
|
}
|
|
}
|
|
MessageBox.Show(this, "打包完成: " + n + " 个文件\r\n" + dlg.FileName +
|
|
"\r\n\r\n把 zip 放进主程序 元件库网页\\ 目录,重启程序即自动导入。", "元件库数据工具");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show(this, "打包失败: " + ex.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|