基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)
This commit is contained in:
Generated
+269
@@ -0,0 +1,269 @@
|
||||
namespace yjktool
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null)) components.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lbl搜索 = new System.Windows.Forms.Label();
|
||||
this.txt搜索 = new System.Windows.Forms.TextBox();
|
||||
this.lst厂家 = new System.Windows.Forms.ListView();
|
||||
this.btn全选 = new System.Windows.Forms.Button();
|
||||
this.btn反选 = new System.Windows.Forms.Button();
|
||||
this.lbl线程 = new System.Windows.Forms.Label();
|
||||
this.num线程 = new System.Windows.Forms.NumericUpDown();
|
||||
this.lbl输出 = new System.Windows.Forms.Label();
|
||||
this.txt输出 = new System.Windows.Forms.TextBox();
|
||||
this.btn开始 = new System.Windows.Forms.Button();
|
||||
this.btn打包 = new System.Windows.Forms.Button();
|
||||
this.chk代理 = new System.Windows.Forms.CheckBox();
|
||||
this.lbl代理 = new System.Windows.Forms.Label();
|
||||
this.chk清理 = new System.Windows.Forms.CheckBox();
|
||||
this.progress = new System.Windows.Forms.ProgressBar();
|
||||
this.lbl进度 = new System.Windows.Forms.Label();
|
||||
this.rtb日志 = new System.Windows.Forms.RichTextBox();
|
||||
this.lbl统计 = new System.Windows.Forms.Label();
|
||||
((System.ComponentModel.ISupportInitialize)(this.num线程)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lbl搜索
|
||||
//
|
||||
this.lbl搜索.AutoSize = true;
|
||||
this.lbl搜索.Location = new System.Drawing.Point(12, 15);
|
||||
this.lbl搜索.Name = "lbl搜索";
|
||||
this.lbl搜索.Size = new System.Drawing.Size(37, 17);
|
||||
this.lbl搜索.TabIndex = 0;
|
||||
this.lbl搜索.Text = "搜索:";
|
||||
//
|
||||
// txt搜索
|
||||
//
|
||||
this.txt搜索.Location = new System.Drawing.Point(55, 12);
|
||||
this.txt搜索.Name = "txt搜索";
|
||||
this.txt搜索.Size = new System.Drawing.Size(200, 25);
|
||||
this.txt搜索.TabIndex = 1;
|
||||
this.txt搜索.TextChanged += new System.EventHandler(this.txt搜索_TextChanged);
|
||||
//
|
||||
// lst厂家
|
||||
//
|
||||
this.lst厂家.CheckBoxes = true;
|
||||
this.lst厂家.Columns.Add("厂家");
|
||||
this.lst厂家.Columns[0].Width = 240;
|
||||
this.lst厂家.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable;
|
||||
this.lst厂家.HideSelection = true;
|
||||
this.lst厂家.Location = new System.Drawing.Point(12, 44);
|
||||
this.lst厂家.MultiSelect = false;
|
||||
this.lst厂家.Name = "lst厂家";
|
||||
this.lst厂家.ShowGroups = false;
|
||||
this.lst厂家.Size = new System.Drawing.Size(262, 470);
|
||||
this.lst厂家.TabIndex = 2;
|
||||
this.lst厂家.View = System.Windows.Forms.View.Details;
|
||||
//
|
||||
// btn全选
|
||||
//
|
||||
this.btn全选.Location = new System.Drawing.Point(12, 520);
|
||||
this.btn全选.Name = "btn全选";
|
||||
this.btn全选.Size = new System.Drawing.Size(75, 28);
|
||||
this.btn全选.TabIndex = 3;
|
||||
this.btn全选.Text = "全选";
|
||||
this.btn全选.UseVisualStyleBackColor = true;
|
||||
this.btn全选.Click += new System.EventHandler(this.btn全选_Click);
|
||||
//
|
||||
// btn反选
|
||||
//
|
||||
this.btn反选.Location = new System.Drawing.Point(96, 520);
|
||||
this.btn反选.Name = "btn反选";
|
||||
this.btn反选.Size = new System.Drawing.Size(75, 28);
|
||||
this.btn反选.TabIndex = 4;
|
||||
this.btn反选.Text = "反选";
|
||||
this.btn反选.UseVisualStyleBackColor = true;
|
||||
this.btn反选.Click += new System.EventHandler(this.btn反选_Click);
|
||||
//
|
||||
// lbl统计
|
||||
//
|
||||
this.lbl统计.AutoSize = true;
|
||||
this.lbl统计.ForeColor = System.Drawing.Color.DimGray;
|
||||
this.lbl统计.Location = new System.Drawing.Point(180, 525);
|
||||
this.lbl统计.Name = "lbl统计";
|
||||
this.lbl统计.Size = new System.Drawing.Size(0, 17);
|
||||
this.lbl统计.TabIndex = 5;
|
||||
//
|
||||
// lbl线程
|
||||
//
|
||||
this.lbl线程.AutoSize = true;
|
||||
this.lbl线程.Location = new System.Drawing.Point(290, 15);
|
||||
this.lbl线程.Name = "lbl线程";
|
||||
this.lbl线程.Size = new System.Drawing.Size(52, 17);
|
||||
this.lbl线程.TabIndex = 6;
|
||||
this.lbl线程.Text = "线程数:";
|
||||
//
|
||||
// num线程
|
||||
//
|
||||
this.num线程.Location = new System.Drawing.Point(348, 12);
|
||||
this.num线程.Maximum = new decimal(new int[] { 16, 0, 0, 0 });
|
||||
this.num线程.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
|
||||
this.num线程.Name = "num线程";
|
||||
this.num线程.Size = new System.Drawing.Size(52, 25);
|
||||
this.num线程.TabIndex = 7;
|
||||
this.num线程.Value = new decimal(new int[] { 4, 0, 0, 0 });
|
||||
//
|
||||
// lbl输出
|
||||
//
|
||||
this.lbl输出.AutoSize = true;
|
||||
this.lbl输出.Location = new System.Drawing.Point(418, 15);
|
||||
this.lbl输出.Name = "lbl输出";
|
||||
this.lbl输出.Size = new System.Drawing.Size(67, 17);
|
||||
this.lbl输出.TabIndex = 8;
|
||||
this.lbl输出.Text = "输出目录:";
|
||||
//
|
||||
// txt输出
|
||||
//
|
||||
this.txt输出.Location = new System.Drawing.Point(487, 12);
|
||||
this.txt输出.Name = "txt输出";
|
||||
this.txt输出.Size = new System.Drawing.Size(340, 25);
|
||||
this.txt输出.TabIndex = 9;
|
||||
//
|
||||
// btn开始
|
||||
//
|
||||
this.btn开始.Location = new System.Drawing.Point(290, 44);
|
||||
this.btn开始.Name = "btn开始";
|
||||
this.btn开始.Size = new System.Drawing.Size(120, 32);
|
||||
this.btn开始.TabIndex = 10;
|
||||
this.btn开始.Text = "开始抓取";
|
||||
this.btn开始.UseVisualStyleBackColor = true;
|
||||
this.btn开始.Click += new System.EventHandler(this.btn开始_Click);
|
||||
//
|
||||
// btn打包
|
||||
//
|
||||
this.btn打包.Location = new System.Drawing.Point(420, 44);
|
||||
this.btn打包.Name = "btn打包";
|
||||
this.btn打包.Size = new System.Drawing.Size(120, 32);
|
||||
this.btn打包.TabIndex = 11;
|
||||
this.btn打包.Text = "打包数据包";
|
||||
this.btn打包.UseVisualStyleBackColor = true;
|
||||
this.btn打包.Click += new System.EventHandler(this.btn打包_Click);
|
||||
//
|
||||
// progress
|
||||
//
|
||||
this.progress.Location = new System.Drawing.Point(560, 50);
|
||||
this.progress.Name = "progress";
|
||||
this.progress.Size = new System.Drawing.Size(180, 20);
|
||||
this.progress.TabIndex = 12;
|
||||
//
|
||||
// lbl进度
|
||||
//
|
||||
this.lbl进度.AutoSize = true;
|
||||
this.lbl进度.Location = new System.Drawing.Point(750, 54);
|
||||
this.lbl进度.Name = "lbl进度";
|
||||
this.lbl进度.Size = new System.Drawing.Size(0, 17);
|
||||
this.lbl进度.TabIndex = 13;
|
||||
//
|
||||
// chk代理
|
||||
//
|
||||
this.chk代理.AutoSize = true;
|
||||
this.chk代理.Location = new System.Drawing.Point(290, 82);
|
||||
this.chk代理.Name = "chk代理";
|
||||
this.chk代理.Size = new System.Drawing.Size(204, 21);
|
||||
this.chk代理.TabIndex = 15;
|
||||
this.chk代理.Text = "价格走旧代理(8081,自动启动)";
|
||||
this.chk代理.UseVisualStyleBackColor = true;
|
||||
this.chk代理.CheckedChanged += new System.EventHandler(this.chk代理_CheckedChanged);
|
||||
//
|
||||
// lbl代理
|
||||
//
|
||||
this.lbl代理.AutoSize = true;
|
||||
this.lbl代理.ForeColor = System.Drawing.Color.Firebrick;
|
||||
this.lbl代理.Location = new System.Drawing.Point(500, 85);
|
||||
this.lbl代理.Name = "lbl代理";
|
||||
this.lbl代理.Size = new System.Drawing.Size(103, 17);
|
||||
this.lbl代理.TabIndex = 16;
|
||||
this.lbl代理.Text = "旧代理:未检测";
|
||||
//
|
||||
// chk清理
|
||||
//
|
||||
this.chk清理.AutoSize = true;
|
||||
this.chk清理.Checked = true;
|
||||
this.chk清理.CheckState = System.Windows.Forms.CheckState.Checked;
|
||||
this.chk清理.Location = new System.Drawing.Point(620, 82);
|
||||
this.chk清理.Name = "chk清理";
|
||||
this.chk清理.Size = new System.Drawing.Size(163, 21);
|
||||
this.chk清理.TabIndex = 17;
|
||||
this.chk清理.Text = "抓取前清空旧数据(缓存+输出)";
|
||||
this.chk清理.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// rtb日志
|
||||
//
|
||||
this.rtb日志.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(30)))), ((int)(((byte)(30)))), ((int)(((byte)(30)))));
|
||||
this.rtb日志.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
||||
this.rtb日志.ForeColor = System.Drawing.Color.Gainsboro;
|
||||
this.rtb日志.Font = new System.Drawing.Font("Consolas", 9F);
|
||||
this.rtb日志.Location = new System.Drawing.Point(290, 110);
|
||||
this.rtb日志.Name = "rtb日志";
|
||||
this.rtb日志.ReadOnly = true;
|
||||
this.rtb日志.Size = new System.Drawing.Size(537, 404);
|
||||
this.rtb日志.TabIndex = 14;
|
||||
this.rtb日志.Text = "";
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(842, 561);
|
||||
this.Controls.Add(this.lbl代理);
|
||||
this.Controls.Add(this.chk代理);
|
||||
this.Controls.Add(this.chk清理);
|
||||
this.Controls.Add(this.rtb日志);
|
||||
this.Controls.Add(this.lbl进度);
|
||||
this.Controls.Add(this.progress);
|
||||
this.Controls.Add(this.btn打包);
|
||||
this.Controls.Add(this.btn开始);
|
||||
this.Controls.Add(this.txt输出);
|
||||
this.Controls.Add(this.lbl输出);
|
||||
this.Controls.Add(this.num线程);
|
||||
this.Controls.Add(this.lbl线程);
|
||||
this.Controls.Add(this.lbl统计);
|
||||
this.Controls.Add(this.btn反选);
|
||||
this.Controls.Add(this.btn全选);
|
||||
this.Controls.Add(this.lst厂家);
|
||||
this.Controls.Add(this.txt搜索);
|
||||
this.Controls.Add(this.lbl搜索);
|
||||
this.MaximizeBox = false;
|
||||
this.Name = "MainForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "元件库厂家数据工具";
|
||||
((System.ComponentModel.ISupportInitialize)(this.num线程)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label lbl搜索;
|
||||
private System.Windows.Forms.TextBox txt搜索;
|
||||
private System.Windows.Forms.ListView lst厂家;
|
||||
private System.Windows.Forms.Button btn全选;
|
||||
private System.Windows.Forms.Button btn反选;
|
||||
private System.Windows.Forms.Label lbl统计;
|
||||
private System.Windows.Forms.Label lbl线程;
|
||||
private System.Windows.Forms.NumericUpDown num线程;
|
||||
private System.Windows.Forms.Label lbl输出;
|
||||
private System.Windows.Forms.TextBox txt输出;
|
||||
private System.Windows.Forms.Button btn开始;
|
||||
private System.Windows.Forms.Button btn打包;
|
||||
private System.Windows.Forms.ProgressBar progress;
|
||||
private System.Windows.Forms.Label lbl进度;
|
||||
private System.Windows.Forms.RichTextBox rtb日志;
|
||||
private System.Windows.Forms.CheckBox chk代理;
|
||||
private System.Windows.Forms.Label lbl代理;
|
||||
private System.Windows.Forms.CheckBox chk清理;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
// 元件库数据工具:按厂家勾选抓取数据(树/系列/价格),打包成数据包 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace yjktool
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
// 全局异常兜底:任何未捕获异常弹窗而不是闪退
|
||||
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
|
||||
Application.ThreadException += (s, e) => 弹(e.Exception);
|
||||
AppDomain.CurrentDomain.UnhandledException += (s, e) => 弹(e.ExceptionObject as Exception);
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
|
||||
private static void 弹(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
MessageBox.Show("程序异常(可继续):\n\n" + (ex == null ? "?" : ex.ToString()),
|
||||
"元件库数据工具", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{8A1E5C72-9D3B-4E7A-A0C1-5F6E7D8B9E01}</ProjectGuid>
|
||||
<ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>yjktool</RootNamespace>
|
||||
<AssemblyName>元件库数据工具</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DebugSymbols>True</DebugSymbols>
|
||||
<DebugType>Full</DebugType>
|
||||
<Optimize>False</Optimize>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<DebugType>None</DebugType>
|
||||
<Optimize>True</Optimize>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="MainForm.Designer.cs">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="抓取器.cs" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
// 抓取器:抓厂家树/系列规格/价格,按新服务布局落盘(断点续抓,多线程安全)
|
||||
// 两种价格通道:直连 dq123(默认) / 经内置旧代理 localhost:8081(带会话伪装,命中率高——工具自动启动它)
|
||||
// 8081 是工具专用端口:node 新服务(元件库网页)占了 8080,两者须能同时跑
|
||||
// 种子模式:从旧代理 proxy_cache 提取真实查询过的组合(sid→bg 列表),优先按种子形状抓价(重放+单变化)
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace yjktool
|
||||
{
|
||||
internal class 抓取器
|
||||
{
|
||||
// 域名映射照旧 python 代理
|
||||
private const string 电数据 = "https://dian.dq123.com/electricData?v=";
|
||||
private const string 价格口 = "https://dx2.dhub.com.cn/pcprice/default/dx2/interface/webGetPrice?infoJson=";
|
||||
// 经旧代理(路径同源,代理按前缀转发到真实域名并注入会话头)
|
||||
private const string 代电数据 = "http://127.0.0.1:8081/electricData?v=";
|
||||
private const string 代价格口 = "http://127.0.0.1:8081/dx2/pcprice/default/dx2/interface/webGetPrice?infoJson=";
|
||||
|
||||
public string 输出根 { get; private set; }
|
||||
private string ZipDir { get { return Path.Combine(输出根, "dian", "electricData_zips"); } }
|
||||
private string 价Dir { get { return Path.Combine(输出根, "dx2_webGetPrice"); } }
|
||||
|
||||
/// <summary>价格请求是否经旧代理(8081)。树/系列包恒直连(静态资源无会话要求)。</summary>
|
||||
public bool 经旧代理;
|
||||
|
||||
/// <summary>种子组合:旧代理缓存里真实查询过的 (系列→组合串列表),价格抓取优先按种子形状。</summary>
|
||||
/// <summary>种子表:sid → [bg组合串, t历史时间戳] 列表。
|
||||
/// 终版机制(实测):命中 = 系列在有效时刻 t 查过(服务器按 sid+t 记录) AND 组合是真实型号——
|
||||
/// 系列只要有一个种子的 t,即可解锁它全部规格组组合的价格查询;t 用当前时间必返空。</summary>
|
||||
private Dictionary<string, List<string[]>> 种子 = new Dictionary<string, List<string[]>>();
|
||||
private int 无t系列数;
|
||||
private int 无t提示过;
|
||||
|
||||
// 进度/日志回调(UI 线程调度由调用方处理)
|
||||
public Action<string> 日志 { get; set; }
|
||||
public Action<int, int> 进度 { get; set; } // (当前,总数)
|
||||
public volatile bool 停止;
|
||||
|
||||
public 抓取器(string 输出目录)
|
||||
{
|
||||
输出根 = 输出目录;
|
||||
Directory.CreateDirectory(ZipDir);
|
||||
Directory.CreateDirectory(价Dir);
|
||||
// net48 下显式启用 TLS1.2(dq123 全站 https,不设会握手失败)
|
||||
try { ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; }
|
||||
catch { }
|
||||
ServicePointManager.DefaultConnectionLimit = 32;
|
||||
}
|
||||
|
||||
/// <summary>从缓存/数据目录加载种子(webGetPrice 响应里的 bg 组合串+原始 t 时间戳)。
|
||||
/// 可多处调用合并(旧代理缓存+元件库网页 data 的现成真价都吃)。返回种子系列数。</summary>
|
||||
public int 加载种子(string 目录)
|
||||
{
|
||||
if (种子 == null) 种子 = new Dictionary<string, List<string[]>>();
|
||||
try
|
||||
{
|
||||
foreach (string f in Directory.GetFiles(目录, "*webGetPrice*.data"))
|
||||
{
|
||||
try
|
||||
{
|
||||
string body = File.ReadAllText(f);
|
||||
foreach (System.Text.RegularExpressions.Match m in
|
||||
System.Text.RegularExpressions.Regex.Matches(body, "\"content\"\\s*:\\s*\"([A-Za-z0-9+/=]+)\""))
|
||||
{
|
||||
string decoded;
|
||||
try { decoded = Encoding.UTF8.GetString(Convert.FromBase64String(m.Groups[1].Value)); }
|
||||
catch { continue; }
|
||||
if (string.IsNullOrEmpty(decoded) || decoded.Trim() == "[]") continue;
|
||||
// ★ JavaScriptSerializer.Deserialize<object> 对顶层数组返回 Object[](不是 ArrayList),
|
||||
// 按 IEnumerable 遍历两者通吃——之前 as ArrayList 恒 null,种子从来没加载成功过
|
||||
System.Collections.IEnumerable 条集 = 解析器.Deserialize<object>(decoded) as System.Collections.IEnumerable;
|
||||
if (条集 == null) continue;
|
||||
foreach (object o in 条集)
|
||||
{
|
||||
var it = o as Dictionary<string, object>;
|
||||
if (it == null || !it.ContainsKey("sid")) continue;
|
||||
string sid = Convert.ToString(it["sid"]);
|
||||
string bg = it.ContainsKey("bg") ? Convert.ToString(it["bg"]) : "";
|
||||
// ★ t 是 13 位毫秒时间戳:JavaScriptSerializer 把 JSON 整数解析成 double,
|
||||
// Convert.ToString(double) 会变成科学计数法(1.785401168E+12)拼进 time 服务器不认——
|
||||
// 必须经 ToDouble→ToInt64 还原成纯数字串
|
||||
string t = "";
|
||||
object tv;
|
||||
if (it.TryGetValue("t", out tv) && tv != null)
|
||||
{
|
||||
try { t = Convert.ToInt64(Convert.ToDouble(tv)).ToString(); }
|
||||
catch { }
|
||||
}
|
||||
if (string.IsNullOrEmpty(sid) || string.IsNullOrEmpty(bg) || bg.IndexOf('-') < 0) continue;
|
||||
if (string.IsNullOrEmpty(t)) continue; // 没 t 的种子重放不了(当前时间必空),跳过
|
||||
List<string[]> l;
|
||||
if (!种子.TryGetValue(sid, out l)) { l = new List<string[]>(); 种子[sid] = l; }
|
||||
if (!l.Any(e => e[0] == bg)) l.Add(new[] { bg, t });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return 种子.Count;
|
||||
}
|
||||
|
||||
private void L(string s) { if (日志 != null) 日志(s); }
|
||||
|
||||
// ---------- HTTP(简单重试) ----------
|
||||
private byte[] Get(string url, int timeoutMs)
|
||||
{
|
||||
for (int 试 = 0; 试 < 3 && !停止; 试++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = (HttpWebRequest)WebRequest.Create(url);
|
||||
req.Method = "GET";
|
||||
req.Timeout = timeoutMs;
|
||||
req.ReadWriteTimeout = timeoutMs;
|
||||
req.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0";
|
||||
req.Referer = "https://www.dq123.com/";
|
||||
req.Proxy = null;
|
||||
using (var r = (HttpWebResponse)req.GetResponse())
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
r.GetResponseStream().CopyTo(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
catch (WebException we)
|
||||
{
|
||||
var hr = we.Response as HttpWebResponse;
|
||||
if (hr != null && (int)hr.StatusCode >= 400 && (int)hr.StatusCode < 500)
|
||||
return null; // 4xx(404 无此包)不重试,快速跳过
|
||||
Thread.Sleep(600);
|
||||
}
|
||||
catch (Exception) { return null; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- v 参数(base64 逻辑名去 padding) ----------
|
||||
private static string V参数(string 逻辑名)
|
||||
{
|
||||
string b = Convert.ToBase64String(Encoding.UTF8.GetBytes(逻辑名));
|
||||
return b.TrimEnd('=');
|
||||
}
|
||||
|
||||
/// <summary>读 ZIP 字节数组里的首个文件文本(JSON)。失败返回 null。</summary>
|
||||
private static string 读Zip内文(byte[] zip)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var ms = new MemoryStream(zip))
|
||||
using (var z = new ZipArchive(ms))
|
||||
{
|
||||
var e = z.Entries[0];
|
||||
using (var r = new StreamReader(e.Open(), Encoding.UTF8))
|
||||
return r.ReadToEnd();
|
||||
}
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// ---------- JSON 反序列化(JavaScriptSerializer) ----------
|
||||
private static readonly System.Web.Script.Serialization.JavaScriptSerializer 解析器 =
|
||||
new System.Web.Script.Serialization.JavaScriptSerializer { MaxJsonLength = 64 * 1024 * 1024 };
|
||||
|
||||
/// <summary>系列包 → 主规格组有序表(aq顺序号→as选项列表)。
|
||||
/// ★ 实测规则(1122 对照页面真实组合 337-350-15-9-29-30-37 破解):
|
||||
/// 参与组合的组 = 顶层 c 键里 bc==0 且 p==2 的项;段顺序 = aq 字段升序;
|
||||
/// bc!=0 / p!=2 的组(附件名称、延时类型等)不参与组合。
|
||||
/// 系列包名 = 4/z_p/{系列id}.jss(不是 z_seri)。</summary>
|
||||
private static SortedDictionary<int, List<int>> 规格组(string 系文)
|
||||
{
|
||||
var g = new SortedDictionary<int, List<int>>();
|
||||
try
|
||||
{
|
||||
var root = 解析器.Deserialize<Dictionary<string, object>>(系文);
|
||||
object co;
|
||||
if (root == null || !root.TryGetValue("c", out co)) return g;
|
||||
var cArr = co as System.Collections.ArrayList;
|
||||
if (cArr == null) return g;
|
||||
foreach (object o in cArr)
|
||||
{
|
||||
var 组 = o as Dictionary<string, object>;
|
||||
if (组 == null) continue;
|
||||
// ★ 终版规则(SDK Ke 过滤器:z 数组=an==1;两厂实测 725/1122 真实组合段 100% 命中):
|
||||
// p 各家不同(正泰=1 德力西=2),不能加 p 条件
|
||||
if (Convert.ToInt32(组.ContainsKey("an") ? 组["an"] : 0) != 1) continue;
|
||||
int aq = Convert.ToInt32(组.ContainsKey("aq") ? 组["aq"] : 0);
|
||||
var ar = 组.ContainsKey("ar") ? 组["ar"] as System.Collections.ArrayList : null;
|
||||
if (ar == null || ar.Count == 0) continue;
|
||||
var l = new List<int>();
|
||||
foreach (object oo in ar)
|
||||
{
|
||||
var 选项 = oo as Dictionary<string, object>;
|
||||
if (选项 == null || !选项.ContainsKey("as")) continue;
|
||||
l.Add(Convert.ToInt32(选项["as"]));
|
||||
}
|
||||
if (l.Count > 0 && !g.ContainsKey(aq)) g[aq] = l;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return g;
|
||||
}
|
||||
|
||||
// ---------- 最小 JSON 取值(避免引第三方库;仅取固定结构字段) ----------
|
||||
private static System.Text.RegularExpressions.MatchCollection 正则匹配(string json, string pattern)
|
||||
{
|
||||
return System.Text.RegularExpressions.Regex.Matches(json, pattern);
|
||||
}
|
||||
|
||||
/// <summary>解析厂家树 ZIP 文本 → 叶子系列号(s 字段非 0)列表。</summary>
|
||||
public static List<string> 树系列(string 树文)
|
||||
{
|
||||
var r = new List<string>();
|
||||
foreach (System.Text.RegularExpressions.Match m in 正则匹配(树文, "\"s\"\\s*:\\s*(\\d+)"))
|
||||
{
|
||||
string v = m.Groups[1].Value;
|
||||
if (v != "0" && !r.Contains(v)) r.Add(v);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>组合策略(照 preheat 脚本):默认组合 + 每组每选项单独变化。</summary>
|
||||
private static List<string> 组合(SortedDictionary<int, List<int>> g)
|
||||
{
|
||||
if (g.Count == 0) return new List<string>();
|
||||
var 默认 = g.Values.Select(l => l[0].ToString()).ToList();
|
||||
var r = new List<string> { string.Join("-", 默认) };
|
||||
for (int i = 0; i < g.Count; i++)
|
||||
{
|
||||
foreach (int ab in g.Values.ElementAt(i).Skip(1))
|
||||
{
|
||||
var c = new List<string>(默认) { };
|
||||
c[i] = ab.ToString();
|
||||
r.Add(string.Join("-", c));
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---------- webGetPrice 哈希(复刻 python json.dumps 分隔符) ----------
|
||||
private static string PyDumps(string json)
|
||||
{
|
||||
// 本场景 JSON 只含 ASCII 字符串/数字/数组/对象,手工重排分隔符即可:
|
||||
// 把 "," 与 ":" 后补空格(仅结构符,字符串内部不会出现裸的这些符号——base64 内容安全)
|
||||
var sb = new StringBuilder(json.Length * 2);
|
||||
bool 引号内 = false;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
sb.Append(c);
|
||||
if (c == '"') 引号内 = !引号内;
|
||||
else if (!引号内 && (c == ',' || c == ':'))
|
||||
{
|
||||
char 后 = i + 1 < json.Length ? json[i + 1] : ' ';
|
||||
if (后 != ' ') sb.Append(' ');
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string 价格哈希(string infoJsonB64)
|
||||
{
|
||||
string json = Encoding.UTF8.GetString(Convert.FromBase64String(infoJsonB64));
|
||||
// 剥 time 字段("time":123,) —— 手工正则
|
||||
json = System.Text.RegularExpressions.Regex.Replace(json, "\"time\"\\s*:\\s*\\d+\\s*,?\\s*", "");
|
||||
json = json.Replace(",}", "}").Replace(",]", "]");
|
||||
string 稳定 = "infoJson=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(PyDumps(json)));
|
||||
using (var md5 = MD5.Create())
|
||||
return BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(稳定))).Replace("-", "").ToLower().Substring(0, 8);
|
||||
}
|
||||
|
||||
// ---------- 抓取主流程 ----------
|
||||
/// <summary>抓一个厂家:树+全部系列+全部组合价格。返回 (系列数, 价格数)。任何异常不外抛(记日志返回 0)。</summary>
|
||||
public KeyValuePair<int, int> 抓厂家(string 树id, string 厂名, int 线程数)
|
||||
{
|
||||
try
|
||||
{
|
||||
return 抓厂家内部(树id, 厂名, 线程数);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
L("! 厂家 " + 厂名 + " 抓取异常: " + ex.Message);
|
||||
return new KeyValuePair<int, int>(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyValuePair<int, int> 抓厂家内部(string 树id, string 厂名, int 线程数)
|
||||
{
|
||||
// 1) 厂家树(断点:已存在直接读)
|
||||
string 树zip路径 = Path.Combine(ZipDir, 树id + ".zip");
|
||||
string 树文 = null;
|
||||
if (File.Exists(树zip路径))
|
||||
{
|
||||
树文 = 读Zip内文(File.ReadAllBytes(树zip路径));
|
||||
}
|
||||
if (树文 == null)
|
||||
{
|
||||
byte[] z = Get(电数据 + V参数("z_seri/" + 树id + ".jss") + "&t=" + 时间戳(), 20000);
|
||||
if (z == null || z.Length < 100) { L("× 厂家 " + 厂名 + " 树拉取失败"); return new KeyValuePair<int, int>(0, 0); }
|
||||
File.WriteAllBytes(树zip路径, z);
|
||||
树文 = 读Zip内文(z);
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
if (树文 == null) { L("× 厂家 " + 厂名 + " 树解析失败"); return new KeyValuePair<int, int>(0, 0); }
|
||||
var 系列 = 树系列(树文);
|
||||
L("▶ " + 厂名 + ": " + 系列.Count + " 个系列");
|
||||
无t系列数 = 0; 无t提示过 = 0;
|
||||
|
||||
// 2) 并发抓系列
|
||||
var 队列 = new ConcurrentQueue<string>(系列);
|
||||
var 计数 = new int[2];
|
||||
var 线程 = new List<Thread>();
|
||||
for (int i = 0; i < Math.Max(1, 线程数); i++)
|
||||
{
|
||||
var t = new Thread(delegate()
|
||||
{
|
||||
string sid;
|
||||
while (!停止 && 队列.TryDequeue(out sid))
|
||||
{
|
||||
int 局系 = 0, 局价 = 0;
|
||||
string szPath = Path.Combine(ZipDir, sid + ".zip");
|
||||
string 系文 = null;
|
||||
try
|
||||
{
|
||||
系文 = File.Exists(szPath) ? 读Zip内文(File.ReadAllBytes(szPath)) : null;
|
||||
if (系文 == null)
|
||||
{
|
||||
byte[] z = Get(电数据 + V参数("4/z_p/" + sid + ".jss") + "&t=" + 时间戳(), 20000);
|
||||
if (z != null && z.Length > 100) { File.WriteAllBytes(szPath, z); 系文 = 读Zip内文(z); Thread.Sleep(20); }
|
||||
}
|
||||
if (系文 != null && 系文.IndexOf("\"c\"") >= 0)
|
||||
{
|
||||
局系 = 1;
|
||||
// ★ 终版机制(实测):命中 = 系列在有效时刻 t 查过 AND 组合是真实型号。
|
||||
// 每个系列只要有一个种子的 t,就能解锁它全部规格组组合;无 t 的系列必空,跳过查价。
|
||||
List<string[]> sd;
|
||||
bool 有种子 = 种子.TryGetValue(sid, out sd) && sd.Count > 0;
|
||||
if (!有种子)
|
||||
{
|
||||
Interlocked.Increment(ref 无t系列数);
|
||||
if (无t提示过 == 0)
|
||||
{
|
||||
int n = Interlocked.Increment(ref 无t提示过);
|
||||
if (n == 1)
|
||||
L("· 提示: 无有效时刻(t)的系列跳过价格查询(服务器对没查过的系列+当前时间必返空)——已跳过 " + sid + " 等");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string t种 = sd[0][1]; // 系列任一种子的 t(系列级会话标识,只配自己系列)
|
||||
// ★ 组合恒用当前系列包规格组生成(默认+单变化):编号与页面渲染同源,查到的价页面必能命中。
|
||||
// 种子 bg(旧版本编号)不再作基底——跨版本编号混合的组合页面永远点不出来(725 教训:75条仅1条可用)。
|
||||
var cs = 组合(规格组(系文));
|
||||
foreach (string c in cs)
|
||||
{
|
||||
string info = 构造infoJson(sid, c, t种);
|
||||
string h = 价格哈希(info);
|
||||
string 价path = Path.Combine(价Dir, "dx2_pcprice_default_dx2_interface_webGetPrice_" + h + ".data");
|
||||
if (!File.Exists(价path))
|
||||
{
|
||||
string 口 = 经旧代理 ? 代价格口 : 价格口;
|
||||
byte[] body = Get(口 + info, 20000);
|
||||
// ★ 空结果不落盘:content="W10="(base64("[]"))或 content="" 都表示该组合厂家无报价,
|
||||
// 存了也是废数据(旧代理靠运行时合成兜底,新服务只需要真价格)
|
||||
string 文 = body == null ? null : Encoding.UTF8.GetString(body);
|
||||
bool 有价 = 文 != null && 文.Length > 30 && 文.Length < 300000
|
||||
&& 文.IndexOf("\"content\":\"W10=\"") < 0
|
||||
&& 文.IndexOf("\"content\": \"W10=\"") < 0
|
||||
&& 文.IndexOf("\"content\":\"\"") < 0
|
||||
&& 文.IndexOf("\"content\": \"\"") < 0;
|
||||
if (有价)
|
||||
{
|
||||
File.WriteAllBytes(价path, body);
|
||||
var mb = System.Text.RegularExpressions.Regex.Match(文, "\"bh\"\\s*:\\s*\"?([0-9.]+)");
|
||||
L(" ¥ 系列" + sid + " 命中真价" + (mb.Success ? " " + mb.Groups[1].Value + " 元" : "") + ": " + c);
|
||||
}
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
局价++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex_series)
|
||||
{
|
||||
L("· 系列 " + sid + " 异常: " + ex_series.Message);
|
||||
}
|
||||
Interlocked.Add(ref 计数[0], 局系);
|
||||
Interlocked.Add(ref 计数[1], 局价);
|
||||
if (进度 != null) 进度(0, 0);
|
||||
}
|
||||
});
|
||||
t.IsBackground = true;
|
||||
线程.Add(t); t.Start();
|
||||
}
|
||||
foreach (var t in 线程) t.Join();
|
||||
if (无t系列数 > 0)
|
||||
L("· " + 厂名 + ": " + 无t系列数 + " 个系列无有效时刻(t),已跳过价格查询(可先在预热页面浏览这些系列再抓)");
|
||||
return new KeyValuePair<int, int>(计数[0], 计数[1]);
|
||||
}
|
||||
|
||||
private static long 时间戳() { return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds; }
|
||||
|
||||
/// <summary>构造 infoJson 的 base64。t 必须传历史时间戳(种子的原始 t 或兜底 t):
|
||||
/// 实测服务器只对「真实查过的组合+历史时间戳」返回真价,当前时间必空。</summary>
|
||||
private static string 构造infoJson(string 系列, string 组合串, string t)
|
||||
{
|
||||
// {"s": "1122", "options": ["337-350"], "time": 123, "from": "dx2jsapi"} —— 键序照 SDK
|
||||
string json = "{\"s\": \"" + 系列 + "\", \"options\": [\"" + 组合串 + "\"], \"time\": " + t + ", \"from\": \"dx2jsapi\"}";
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace yjktool
|
||||
{
|
||||
static class 测试入口
|
||||
{
|
||||
[STAThread]
|
||||
static int Main(string[] args)
|
||||
{
|
||||
string 树id = args.Length > 0 ? args[0] : "10";
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
var g = new 抓取器(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "抓取输出"))
|
||||
{ 日志 = s => Console.WriteLine(s) };
|
||||
var kv = g.抓厂家(树id, "(测试厂" + 树id + ")", 4);
|
||||
Console.WriteLine("== 结果: 系列 {0}, 价格 {1}", kv.Key, kv.Value);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"37141": [
|
||||
"5-10-14-15-19-21-23"
|
||||
],
|
||||
"1506": [
|
||||
"10-15-19-21-460-26-44",
|
||||
"8-15-19-21-460-26-44",
|
||||
"230-15-19-21-460-26-39",
|
||||
"230-331-19-21-460-446-39",
|
||||
"11-15-19-21-460-26-48",
|
||||
"182-313",
|
||||
"11-17-19-21-460-27-50",
|
||||
"11-17-19-21-460-27-48",
|
||||
"9-12-19-21-460-26-28",
|
||||
"7-15-19-21-460-26-39",
|
||||
"10-15-19-21-460-26-48",
|
||||
"11-17-19-21-460-26-48",
|
||||
"11-329-19-21-460-26-48",
|
||||
"159-285",
|
||||
"230-14-19-21-460-26-39",
|
||||
"119-279",
|
||||
"7-331-19-21-460-446-28",
|
||||
"114-115-267",
|
||||
"11-330-19-21-460-26-48",
|
||||
"8-15-19-21-460-26-39",
|
||||
"382-384"
|
||||
],
|
||||
"46805": [
|
||||
"5-7-14"
|
||||
],
|
||||
"725": [
|
||||
"7-13-22-15-17-18-19-25-74-20-46-27-82-83-96-84-353",
|
||||
"8-13-22-15-17-18-19-25-74-20-46-27-82-83-96-84-353",
|
||||
"56-24-22-15-17-18-19-25-74-20-33-27-82-83-96-84-353",
|
||||
"23-24-22-15-17-18-19-25-74-20-85-27-82-83-96-84-353",
|
||||
"7-13-22-15-17-18-19-25-74-20-41-27-82-83-96-84-353",
|
||||
"6-12-22-15-17-18-19-25-74-20-39-27-82-83-96-84-353",
|
||||
"341-13-22-15-17-18-19-25-74-20-48-27-82-83-96-84-352",
|
||||
"56-24-22-28-17-18-19-25-74-20-85-27-82-83-96-84-353",
|
||||
"23-24-22-15-17-18-19-25-74-20-35-27-82-83-96-84-353",
|
||||
"56-24-22-15-17-18-59-25-75-20-29-27-82-83-96-84-353",
|
||||
"319-12-22-28-17-18-19-25-74-20-29-27-82-83-96-84-352",
|
||||
"5-12-22-28-17-18-19-25-74-20-29-27-82-83-96-84-353",
|
||||
"56-24-22-15-17-18-59-25-74-20-29-27-82-83-96-84-353",
|
||||
"6-12-22-15-17-18-19-25-74-20-37-27-82-83-96-84-353",
|
||||
"56-24-22-15-17-18-19-25-74-20-85-27-82-83-96-84-353",
|
||||
"23-24-22-28-17-18-19-25-74-20-85-27-82-83-96-84-353",
|
||||
"56-24-22-28-17-18-19-25-74-20-85-27-82-83-96-84-350",
|
||||
"330-13-22-15-17-18-19-25-74-20-46-27-82-83-96-84-352",
|
||||
"6-12-22-15-17-18-19-25-74-20-36-27-82-83-96-84-353"
|
||||
],
|
||||
"47762": [
|
||||
"5-6-8-9-10"
|
||||
],
|
||||
"42825": [
|
||||
"5-11-14",
|
||||
"5-6-13",
|
||||
"5-10-14",
|
||||
"5-12-15"
|
||||
],
|
||||
"3205": [
|
||||
"5-11-15-27",
|
||||
"10-13-15-27"
|
||||
],
|
||||
"56242": [
|
||||
"5-10-14-19-20-22-29-31-32"
|
||||
],
|
||||
"1401": [
|
||||
"5-6-8-12-16"
|
||||
],
|
||||
"2486": [
|
||||
"5-8-6-936-14-46-35-33"
|
||||
],
|
||||
"1245": [
|
||||
"5-7-16-17"
|
||||
],
|
||||
"10586": [
|
||||
"6-12"
|
||||
],
|
||||
"48269": [
|
||||
"5-807-22-37-58-60-17-67-810"
|
||||
],
|
||||
"47084": [
|
||||
"5-6-12-16-20"
|
||||
],
|
||||
"1122": [
|
||||
"337-350-15-9-29-30-37",
|
||||
"6-13-18-10-28-30-37"
|
||||
],
|
||||
"40": [
|
||||
"5-8"
|
||||
],
|
||||
"23641": [
|
||||
"7-163-10-12-14-164-19-21"
|
||||
],
|
||||
"757": [
|
||||
"5-11-66-17-20-21-22-23-25-27-68-67-91-90-96-92-134"
|
||||
],
|
||||
"1825": [
|
||||
"5-18-26-9-15-8"
|
||||
],
|
||||
"733": [
|
||||
"18-37-32-39"
|
||||
],
|
||||
"59163": [
|
||||
"5-11-13-16-19-21",
|
||||
"5-11-15-17-20-27"
|
||||
],
|
||||
"32339": [
|
||||
"7-124-8-34"
|
||||
],
|
||||
"1427": [
|
||||
"5-7-24-17"
|
||||
],
|
||||
"604": [
|
||||
"16-5-12-15",
|
||||
"16-5-12-14"
|
||||
],
|
||||
"71": [
|
||||
"5-15-50-154"
|
||||
],
|
||||
"47960": [
|
||||
"5-12-10-20-23-26-37-63"
|
||||
],
|
||||
"791": [
|
||||
"5-7-11-13"
|
||||
],
|
||||
"1086": [
|
||||
"10-6-30",
|
||||
"12-6-30"
|
||||
],
|
||||
"735": [
|
||||
"5-7-17-12-13-15-16-119"
|
||||
]
|
||||
}
|
||||
+1524
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user