| New file |
| | |
| | | <# |
| | | 22 所上位机文件侧只读探查:工步方案(含可能的工步名称)在不在软件目录的文件里。 |
| | | |
| | | 为什么走文件侧:该机 SQL Server 的 5141 个用户库**全部**是 DB_<时间> 测试库、其他库 0 个, |
| | | 所以工步方案不可能在库里。上一轮现场已在运行目录见到 VariableMap.xml、UnitConvert.xml、 |
| | | DeviceConfiguration.xml、TaskModel.xml 等文件,另有遗留的 .sto 工步方案(gzip 压缩的 XML)。 |
| | | |
| | | 只读纪律:只列目录、只读文件内容;不写、不改厂商目录与文件;报告只写到本脚本所在目录。 |
| | | |
| | | 用法(在那台机器上): |
| | | powershell -ExecutionPolicy Bypass -File .\j22-introspect-files.ps1 |
| | | powershell -ExecutionPolicy Bypass -File .\j22-introspect-files.ps1 -Roots 'D:\22所5v10A测试柜' |
| | | powershell -ExecutionPolicy Bypass -File .\j22-introspect-files.ps1 -SearchRoot 'D:\' -MaxDepth 4 |
| | | #> |
| | | param( |
| | | # 要探查的目录;留空则自动找(见下)。 |
| | | [string[]]$Roots = @(), |
| | | [string]$SearchRoot = 'D:\', |
| | | [int]$MaxDepth = 3, |
| | | # 最多深入解析几个方案文件(只打印结构,不倒内容)。 |
| | | [int]$MaxFiles = 3, |
| | | [int]$MaxHeadChars = 400, |
| | | [int]$MaxNames = 25, |
| | | [string]$ReportFile = '' |
| | | ) |
| | | |
| | | $ErrorActionPreference = 'Stop' |
| | | |
| | | if (-not $ReportFile) { |
| | | $base = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } |
| | | $ReportFile = Join-Path $base 'j22-introspect-files-report.txt' |
| | | } |
| | | $report = New-Object System.Collections.ArrayList |
| | | function Say([string]$text) { |
| | | Write-Host $text |
| | | [void]$report.Add($text) |
| | | } |
| | | |
| | | # 方案文件的判据:.sto 一定是方案;名字里带 step/plan/task/recipe/scheme/program/工步/方案的也算。 |
| | | # 其余文件(设备配置、单位换算表等)不看——它们存不下工步名称。 |
| | | function Test-PlanFile([string]$name) { |
| | | $extension = [System.IO.Path]::GetExtension($name) |
| | | if ($extension -ieq '.sto') { return $true } |
| | | return $name -imatch '(step|plan|task|recipe|scheme|program|工步|方案)' |
| | | } |
| | | |
| | | # 读文件文本:gzip(.sto 是 gzip 压缩的 XML)自动解压;只读前若干字符,避免把大文件全读进来。 |
| | | # 编码:有 BOM 按 BOM,没有 BOM 按 GBK(Windows 上的国产上位机多半是 GBK),否则中文名称会读成乱码。 |
| | | function Read-PlanText([string]$path, [int]$maxChars = 200000) { |
| | | $stream = [System.IO.File]::OpenRead($path) |
| | | try { |
| | | $head = New-Object byte[] 2 |
| | | $read = $stream.Read($head, 0, 2) |
| | | $stream.Position = 0 |
| | | $maxBytes = [Math]::Max(4096, $maxChars * 3) |
| | | if ($read -eq 2 -and $head[0] -eq 0x1F -and $head[1] -eq 0x8B) { |
| | | $gzip = New-Object System.IO.Compression.GZipStream($stream, [System.IO.Compression.CompressionMode]::Decompress) |
| | | try { $bytes = Read-Bytes $gzip $maxBytes } |
| | | finally { $gzip.Dispose() } |
| | | } |
| | | else { $bytes = Read-Bytes $stream $maxBytes } |
| | | return Convert-PlanBytes $bytes |
| | | } |
| | | finally { $stream.Dispose() } |
| | | } |
| | | |
| | | # 最多读 $maxBytes 个字节;读到多少算多少(文件可能比上限大,也可能很小)。非文本文件也照读, |
| | | # 由调用方按"开头是不是 <"判断它是不是 XML,不在这里猜。 |
| | | function Read-Bytes($stream, [int]$maxBytes) { |
| | | $buffer = New-Object byte[] 8192 |
| | | $memory = New-Object System.IO.MemoryStream |
| | | try { |
| | | while ($memory.Length -lt $maxBytes) { |
| | | $wanted = [Math]::Min($buffer.Length, $maxBytes - $memory.Length) |
| | | $count = $stream.Read($buffer, 0, $wanted) |
| | | if ($count -le 0) { break } |
| | | $memory.Write($buffer, 0, $count) |
| | | } |
| | | return $memory.ToArray() |
| | | } |
| | | finally { $memory.Dispose() } |
| | | } |
| | | |
| | | # 编码判定:先看 BOM;再看 XML 声明里的 encoding;都没有按 GBK 兜底——国产上位机在中文 Windows 上 |
| | | # 多半写 GBK,按 UTF-8 硬解会把中文工步名称读成乱码(.sto 是 gzip,流不可回退,所以必须先读再判)。 |
| | | function Convert-PlanBytes([byte[]]$bytes) { |
| | | if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { |
| | | return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) |
| | | } |
| | | if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { |
| | | return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) |
| | | } |
| | | $asciiHead = [System.Text.Encoding]::ASCII.GetString($bytes, 0, [Math]::Min(200, $bytes.Length)) |
| | | if ($asciiHead -match '(?i)encoding\s*=\s*"([\w\-]+)"') { |
| | | try { return [System.Text.Encoding]::GetEncoding($Matches[1]).GetString($bytes) } |
| | | catch { } |
| | | } |
| | | return [System.Text.Encoding]::GetEncoding(936).GetString($bytes) |
| | | } |
| | | |
| | | # XML 里出现过哪些元素名:只看结构,不打印内容。判断"有没有名称字段"靠这个。 |
| | | function Get-ElementNames([string]$text) { |
| | | $names = New-Object System.Collections.ArrayList |
| | | foreach ($match in [regex]::Matches($text, '<\s*([A-Za-z_][\w.\-]*)')) { |
| | | $name = [string]$match.Groups[1].Value |
| | | if (-not $names.Contains($name)) { [void]$names.Add($name) } |
| | | } |
| | | return , $names |
| | | } |
| | | |
| | | # 名称类元素或属性及其取值:这是"方案里到底有没有工步名称"的直接证据。 |
| | | # 先取名字再筛关键字——不能把关键字塞进正则的主体里:属性名就叫 Name 时关键字前面没有字符, |
| | | # 那种写法一条也匹配不到(演练里踩过)。 |
| | | function Get-NameEvidence([string]$text) { |
| | | $evidence = New-Object System.Collections.ArrayList |
| | | $keyword = '(?i)(name|desc|title|remark|说明|名称|备注)' |
| | | foreach ($match in [regex]::Matches($text, '<\s*([A-Za-z_][\w.\-]*)\s*>([^<]{1,200})</')) { |
| | | $name = [string]$match.Groups[1].Value |
| | | if ($name -match $keyword) { |
| | | [void]$evidence.Add('<' + $name + '> = ' + [string]$match.Groups[2].Value) |
| | | } |
| | | } |
| | | foreach ($match in [regex]::Matches($text, '([A-Za-z_][\w.\-]*)\s*=\s*"([^"]{1,200})"')) { |
| | | $name = [string]$match.Groups[1].Value |
| | | if ($name -match $keyword) { |
| | | [void]$evidence.Add($name + ' = ' + [string]$match.Groups[2].Value) |
| | | } |
| | | } |
| | | return , $evidence |
| | | } |
| | | |
| | | try { |
| | | # ---------------------------------------------------------------- 1. 要找哪些目录 |
| | | if (-not $Roots -or $Roots.Count -eq 0) { |
| | | Say '=== 1. 自动查找目录 ===' |
| | | $found = New-Object System.Collections.ArrayList |
| | | # 运行目录的特征文件:VariableMap.xml / TaskModel.xml / config.ini。 |
| | | foreach ($marker in @('VariableMap.xml', 'TaskModel.xml', 'config.ini')) { |
| | | try { |
| | | $hits = Get-ChildItem -LiteralPath $SearchRoot -Recurse -Depth $MaxDepth -Filter $marker -File ` |
| | | -ErrorAction SilentlyContinue | Select-Object -First 5 |
| | | } |
| | | catch { $hits = @() } |
| | | foreach ($hit in $hits) { |
| | | $directory = $hit.DirectoryName |
| | | if (-not $found.Contains($directory)) { [void]$found.Add($directory) } |
| | | } |
| | | } |
| | | # 现场的遗留方案目录(存在就一起看)。 |
| | | foreach ($legacy in @('D:\22所5v10A测试柜', 'D:\22所5v10A测试柜\')) { |
| | | if (Test-Path -LiteralPath $legacy) { if (-not $found.Contains($legacy)) { [void]$found.Add($legacy) } } |
| | | } |
| | | foreach ($directory in $found) { |
| | | Say (" 候选目录:" + $directory) |
| | | [void]$Roots.Add($directory) |
| | | } |
| | | if ($Roots.Count -eq 0) { |
| | | Say ' 没有找到任何候选目录。请手工指定,例如:-Roots ''D:\<软件目录>''' |
| | | } |
| | | } |
| | | else { Say '=== 1. 指定目录 ===' } |
| | | |
| | | # ---------------------------------------------------------------- 2. 方案候选文件 |
| | | $candidates = New-Object System.Collections.ArrayList |
| | | foreach ($root in $Roots) { |
| | | if (-not (Test-Path -LiteralPath $root)) { Say (" 目录不存在:" + $root); continue } |
| | | Say '' |
| | | Say ("=== 2. 方案候选文件:" + $root + " ===") |
| | | $files = Get-ChildItem -LiteralPath $root -Recurse -Depth 1 -File -ErrorAction SilentlyContinue | |
| | | Where-Object { Test-PlanFile $_.Name } | Sort-Object LastWriteTime -Descending |
| | | $shown = 0 |
| | | foreach ($file in $files) { |
| | | if ($shown -lt 30) { Say (" " + $file.Name + " " + [math]::Round($file.Length / 1KB, 1) + " KB " + $file.LastWriteTime) } |
| | | $shown++ |
| | | [void]$candidates.Add($file) |
| | | } |
| | | if ($shown -eq 0) { Say ' (没有匹配到方案候选文件)' } |
| | | elseif ($shown -gt 30) { Say (" …… 共 " + $shown + " 个,只列了前 30 个") } |
| | | } |
| | | |
| | | # ---------------------------------------------------------------- 3. 解析最像方案的几个 |
| | | Say '' |
| | | Say ("=== 3. 结构解析(最多 " + $MaxFiles + " 个,按修改时间倒序) ===") |
| | | $parsed = 0 |
| | | foreach ($file in ($candidates | Sort-Object LastWriteTime -Descending)) { |
| | | if ($parsed -ge $MaxFiles) { break } |
| | | $parsed++ |
| | | Say '' |
| | | Say (" " + $file.FullName + " (" + [math]::Round($file.Length / 1KB, 1) + " KB)") |
| | | try { $text = Read-PlanText $file.FullName } |
| | | catch { Say (' 读取失败:' + $_.Exception.Message); continue } |
| | | if ([string]::IsNullOrWhiteSpace($text)) { Say ' (空文件或解压后为空)'; continue } |
| | | if (-not $text.TrimStart().StartsWith('<')) { Say ' (不是 XML 文本,可能不是方案文件)' } |
| | | $names = Get-ElementNames $text |
| | | $limited = @($names | Select-Object -First $MaxNames) |
| | | Say (' 元素名(前 ' + $limited.Count + ' / 共 ' + $names.Count + '):' + ($limited -join ', ')) |
| | | $evidence = Get-NameEvidence $text |
| | | if ($evidence.Count -gt 0) { |
| | | Say (' 名称类元素或属性(最多 5 条)——**有名称就直接抄下来**:') |
| | | foreach ($line in @($evidence | Select-Object -First 5)) { Say (' ' + $line) } |
| | | } |
| | | else { Say ' 没有发现名称类元素或属性。' } |
| | | $head = $text.Trim() |
| | | if ($head.Length -gt $MaxHeadChars) { $head = $head.Substring(0, $MaxHeadChars) + ' …' } |
| | | Say (' 文本开头:' + $head) |
| | | } |
| | | |
| | | Say '' |
| | | if ($parsed -eq 0) { Say '=== 结论:没有可解析的方案候选文件 ===' } |
| | | else { Say '=== 结论:见上面各文件的"名称类元素或属性"与"元素名"两行 ===' } |
| | | Say ' 如果方案里根本没有名称,就只剩两条路:① 用推导方向当名称(标注"推导");' |
| | | Say ' ② 在来源配置里声明「工步号 → 名称」,把甲方界面上对得上的名字配上。' |
| | | Say ('报告同时写在:' + $ReportFile) |
| | | } |
| | | finally { |
| | | # 中途出错也把已有内容落盘:控制台刷不完,文件可以慢慢复制。 |
| | | $report | Set-Content -LiteralPath $ReportFile -Encoding UTF8 |
| | | } |