99 行
4.0 KiB
Python
99 行
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
u"""把「从原理图导入变更」对话框的变更表**按 DOM 行结构**读出来(不是 innerText 拼接)。
|
||
|
||
/c/Python314/python .claude/p1_import_rows2.py [--board2]
|
||
|
||
## 为什么要再写一个
|
||
|
||
`pcb1_import.py --probe` 读的是滚动容器的 `innerText`。那样拿到的是一大坨文本,
|
||
**表头行(位号)和明细行(动作/旧值/新值)之间的归属关系是靠换行猜的**,
|
||
一旦表格是虚拟滚动 / 分列渲染,就会把 A 的表头和 B 的明细拼到一起 ——
|
||
2026-09-08 就是这么读出一份「整个风扇块按位号错位一格」的吓人清单,
|
||
而 `pcb1_import.py --verify`(PCB 元件表 vs 原理图 BOM)说 301 个位号全对、
|
||
封装只差 J1 一处。两者必有一个是假的。
|
||
|
||
这里改成逐 `<tr>` 读、每行自己带着单元格,谁属于谁不用猜。
|
||
每行还带上 `rowTop`,行序一目了然。
|
||
"""
|
||
import io
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, HERE)
|
||
from eda import run
|
||
|
||
if '--base' in sys.argv: # R2 底板
|
||
PCB = '7b9524b6aaf6f235'
|
||
OUT = os.path.join(HERE, 'r2', '_base_import_rows2.json')
|
||
else:
|
||
PCB = '942ed1560d81c1e7' if '--board2' in sys.argv else '7e9415a16a909c51'
|
||
OUT = os.path.join(HERE, '_p1_import_rows2.json')
|
||
|
||
JS = u"""
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||
const propsOf = el => { for (const k of Object.keys(el)) if (k.startsWith('__reactProps$')) return el[k]; return null; };
|
||
const fire = (el) => { let t = el; for (let i=0;i<8&&t;i++){const p=propsOf(t); if(p&&typeof p.onClick==='function'){p.onClick({target:t,currentTarget:t,stopPropagation(){},preventDefault(){},nativeEvent:{},button:0,detail:1});return true;} t=t.parentElement;} return false; };
|
||
const leaf = (txt) => { for (const el of document.querySelectorAll('*')) if (el.children.length===0 && (el.textContent||'').trim()===txt && el.offsetParent!==null) return el; return null; };
|
||
|
||
await eda.dmt_EditorControl.openDocument('%s');
|
||
await sleep(1800);
|
||
const out = {rows: [], note: ''};
|
||
fire(leaf('设计')); await sleep(900);
|
||
out.opened = fire(leaf('从原理图导入变更'));
|
||
await sleep(9000);
|
||
|
||
// 找滚动容器(和 pcb1_import 一样的判据,取最紧的那个)
|
||
let sc = null;
|
||
for (const el of document.querySelectorAll('div'))
|
||
if (el.scrollHeight > el.clientHeight + 20 && /移除元件|增加元件|修改/.test(el.innerText || ''))
|
||
if (!sc || el.innerText.length < sc.innerText.length) sc = el;
|
||
if (!sc) { out.note = 'no scroll container'; return out; }
|
||
|
||
// 逐行收集:key = 该行在文档流里的 offsetTop,避免虚拟滚动重复
|
||
const seen = {};
|
||
const grab = () => {
|
||
for (const tr of sc.querySelectorAll('tr')) {
|
||
if (tr.offsetParent === null) continue;
|
||
const cells = [...tr.querySelectorAll('td,th')].map(td => (td.innerText || '').trim());
|
||
if (!cells.length) continue;
|
||
const key = tr.offsetTop + '|' + cells.join('\\u0001');
|
||
if (seen[key]) continue;
|
||
seen[key] = 1;
|
||
out.rows.push({top: tr.offsetTop, cells});
|
||
}
|
||
};
|
||
sc.scrollTop = 0; await sleep(500);
|
||
let last = -1;
|
||
for (let i = 0; i < 300; i++) {
|
||
grab();
|
||
if (sc.scrollTop === last) break;
|
||
last = sc.scrollTop;
|
||
if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 2) break;
|
||
sc.scrollTop += Math.max(30, sc.clientHeight / 3);
|
||
await sleep(240);
|
||
}
|
||
grab();
|
||
out.rows.sort((a, b) => a.top - b.top);
|
||
out.nTr = sc.querySelectorAll('tr').length;
|
||
return out;
|
||
""" % PCB
|
||
|
||
|
||
def main():
|
||
r = run(JS, timeout=300)
|
||
if not r.get('success'):
|
||
print(json.dumps(r, ensure_ascii=False)[:900]); sys.exit(1)
|
||
res = r['result']
|
||
if res.get('note'):
|
||
print(res['note']); sys.exit(1)
|
||
io.open(OUT, 'w', encoding='utf-8').write(json.dumps(res, ensure_ascii=False, indent=1))
|
||
print(u'opened=%s 行数 %d -> %s' % (res.get('opened'), len(res['rows']), OUT))
|
||
for row in res['rows']:
|
||
print(u' top=%-6s %s' % (row['top'], ' | '.join(row['cells'])))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|