# -*- 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 一处。两者必有一个是假的。 这里改成逐 `` 读、每行自己带着单元格,谁属于谁不用猜。 每行还带上 `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()