#!/usr/bin/env python3 """ TI PDF datasheet → Markdown 转换工具(双栏 + 表格 + 书签标题) 用法: python3 pdf2md.py [output.md] 特性: - 双栏页面按 左栏→右栏 顺序重组正文(word 坐标 + 页面中线) - 表格用 pdfplumber extract_tables() 转 Markdown 表格 - 标题层级取自 PDF 原始书签 (outline),1-6 级嵌套 - 自动清理 TI 页眉页脚噪音行 依赖: pip3 install pdfplumber --break-system-packages """ import sys, re, os import pdfplumber def table_to_md(tbl): rows = [] for r in tbl: cells = [(c or "").replace("\n", " ").strip() for c in r] rows.append(cells) rows = [r for r in rows if any(c for c in r)] if not rows: return "" ncols = max(len(r) for r in rows) norm = [r + [""] * (ncols - len(r)) for r in rows] md = ["| " + " | ".join(norm[0]) + " |", "|" + "---|" * ncols] for r in norm[1:]: md.append("| " + " | ".join(r) + " |") return "\n".join(md) def in_bbox(w, bbox): x0, top, x1, bot = bbox return (w["x0"] >= x0 - 2 and w["x1"] <= x1 + 2 and w["top"] >= top - 2 and w["bottom"] <= bot + 3) def is_noise(s): if not s.strip(): return True if "Copyright ©" in s or s.strip() == "Submit Document Feedback": return True if "Product Folder Links:" in s: return True if re.match(r"^SLUSEN\d? –", s) or re.match(r"^www\.ti\.com$", s): return True if re.match(r"^\d{1,3}$", s.strip()): return True return False def convert(src, out): pdf = pdfplumber.open(src) # ---- outlines -> headers (page_no, title, level) ---- try: outlines = list(pdf.doc.get_outlines()) except Exception: outlines = [] page_by_objid = {p.page_obj.pageid: n for n, p in enumerate(pdf.pages, 1)} headers = [] for item in outlines: try: lvl, title, dest = item[0], item[1], item[2] if dest is None: continue pno = page_by_objid.get(dest[0].objid) if pno: headers.append((pno, title, lvl)) except Exception: continue # ---- per page: text lines (column-aware) + tables ---- page_data = {} for pno, page in enumerate(pdf.pages, 1): tables = page.extract_tables() or [] tboxes = [tb.bbox for tb in (page.find_tables() or [])] words = page.extract_words(x_tolerance=1.5) mid = page.width / 2.0 left, right = [], [] for w in words: if any(in_bbox(w, tb) for tb in tboxes): continue cx = (w["x0"] + w["x1"]) / 2.0 (left if cx < mid else right).append(w) def col_lines(col): by_top = {} for w in col: by_top.setdefault(round(w["top"], 1), []).append(w) res = [] for k in sorted(by_top): ws = sorted(by_top[k], key=lambda w: w["x0"]) res.append(" ".join(w["text"] for w in ws)) return res lines = [] for col in (left, right): lines.extend(col_lines(col)) lines = [l for l in lines if not is_noise(l)] tbl_mds = [table_to_md(t) for t in tables if table_to_md(t)] page_data[pno] = {"lines": lines, "tables": tbl_mds} # ---- assemble: headers inserted at matching text lines ---- header_lookup = {} for pno, title, lvl in headers: header_lookup.setdefault(pno, []).append((title, lvl)) doc_lines = [] for pno in range(1, len(pdf.pages) + 1): data = page_data[pno] page_hs = header_lookup.get(pno, []) emitted = set() for l in data["lines"]: ls = l.strip() matched = None for title, lvl in page_hs: if title in emitted: continue if ls == title: matched = (title, lvl) break if matched: title, lvl = matched doc_lines.append("") doc_lines.append("#" * min(lvl + 1, 6) + " " + title) doc_lines.append("") emitted.add(title) else: doc_lines.append(l) for title, lvl in page_hs: if title not in emitted: doc_lines.append("") doc_lines.append("#" * min(lvl + 1, 6) + " " + title) doc_lines.append("") for t in data["tables"]: doc_lines.append("") doc_lines.extend(t.split("\n")) doc_lines.append("") doc = "\n".join(doc_lines) # final cleanup of TI page noise for p in [r"^www\.ti\.com\s*$", r"^Product Folder\s*$", r"^Links: .+\s*$", r"^SLUSEN\d? – .+\s*$", r"^Submit Document Feedback\s*$", r"^Copyright © .+\s*$", r"^\d{1,3}$"]: doc = "\n".join(l for l in doc.split("\n") if not re.match(p, l.strip())) doc = re.sub(r"\n{4,}", "\n\n\n", doc) header = (f"# {os.path.basename(src)} - Markdown\n\n" f"> Auto-converted from {src}\n" f"> 转换日期:{__import__('datetime').date.today().isoformat()}。" f"标题层级取自 PDF 书签;正文双栏按左→右输出;表格已转 Markdown;" f"下标参数名平铺(如 IQ_BAT),关键参数请对照原 PDF 核对。\n\n") with open(out, "w", encoding="utf-8") as f: f.write(header + doc) return len(doc), len(headers), doc.count("\n| ") if __name__ == "__main__": if len(sys.argv) < 2: print(__doc__) sys.exit(1) src = sys.argv[1] out = sys.argv[2] if len(sys.argv) > 2 else re.sub(r"\.pdf$", ".md", src, flags=re.I) chars, nhdr, ntbl = convert(src, out) print(f"OK: {out} | chars={chars} | headers={nhdr} | table_rows={ntbl}")