Word (.docx/.doc) 文档全量解析。覆盖:正文/段落文本提取、表格数据提取、高亮/颜色格式读取、多文件汇总对比、嵌入图片转 caption。
npx skills add https://github.com/OpenSenseNova/SenseNova-Skills --skill word-analysis
from docx import Document
import os
# python-docx is available; for .doc (old format) convert via libreoffice first
def load_doc(path):
"""Load .docx directly; convert .doc to .docx first if needed."""
if path.lower().endswith('.doc'):
import subprocess
out_dir = os.path.dirname(path)
subprocess.run(
['libreoffice', '--headless', '--convert-to', 'docx', '--outdir', out_dir, path],
check=True, capture_output=True
)
path = path.rsplit('.', 1)[0] + '.docx'
return Document(path)
def extract_full_text(doc_path):
"""Extract all text: paragraphs + table cells, in document order."""
doc = load_doc(doc_path)
lines = []
# Iterate paragraphs and tables in body order
from docx.oxml.ns import qn
for block in doc.element.body:
tag = block.tag.split('}')[-1]
if tag == 'p':
# Paragraph
from docx.text.paragraph import Paragraph
para = Paragraph(block, doc)
text = para.text.strip()
if text:
lines.append(text)
elif tag == 'tbl':
# Table
from docx.table import Table
tbl = Table(block, doc)
for row in tbl.rows:
row_text = '\t'.join(cell.text.strip() for cell in row.cells)
if row_text.strip():
lines.append(row_text)
return '\n'.join(lines)
# Usage
text = extract_full_text("/mnt/data/doc.docx")
print(text[:2000]) # preview first 2000 chars
import pandas as pd
def extract_all_tables(doc_path):
"""Extract all tables from a Word document as list of DataFrames."""
doc = load_doc(doc_path)
tables = []
for i, tbl in enumerate(doc.tables):
rows = []
for row in tbl.rows:
rows.append([cell.text.strip() for cell in row.cells])
if not rows:
continue
# Use first row as header if it looks like a header
df = pd.DataFrame(rows[1:], columns=rows[0]) if rows else pd.DataFrame()
tables.append((i, df))
print(f"Table {i}: {df.shape[0]} rows × {df.shape[1]} cols")
print(df.head(3))
return tables
# Usage
tables = extract_all_tables("/mnt/data/doc.docx")
Some questions require reading cell background color or text highlight color
(e.g., "标黄的行", "红色文字"). Use XML-level access:
from docx import Document
from docx.oxml.ns import qn
from lxml import etree
def get_paragraph_highlight(para):
"""Return highlight color name of first run, or None."""
for run in para.runs:
rPr = run._r.find(qn('w:rPr'))
if rPr is not None:
hl = rPr.find(qn('w:highlight'))
if hl is not None:
return hl.get(qn('w:val')) # e.g. 'yellow', 'cyan', 'red'
return None
def get_table_cell_shading(cell):
"""Return background color hex of a table cell, or None."""
tcPr = cell._tc.find(qn('w:tcPr'))
if tcPr is not None:
shd = tcPr.find(qn('w:shd'))
if shd is not None:
return shd.get(qn('w:fill')) # hex color, e.g. 'FFFF00'
return None
# Example: find all highlighted paragraphs
def find_highlighted_rows(doc_path, color='yellow'):
doc = load_doc(doc_path)
highlighted = []
for i, para in enumerate(doc.paragraphs):
hl = get_paragraph_highlight(para)
if hl == color or (color == 'yellow' and hl in ('yellow', 'FFFF00')):
highlighted.append((i, para.text))
return highlighted
# For table cells with yellow background:
def find_highlighted_table_cells(doc_path, fill_colors=('FFFF00', 'FFD700')):
doc = load_doc(doc_path)
results = []
for t_idx, tbl in enumerate(doc.tables):
for r_idx, row in enumerate(tbl.rows):
for c_idx, cell in enumerate(row.cells):
color = get_table_cell_shading(cell)
if color and color.upper() in fill_colors:
results.append({
'table': t_idx, 'row': r_idx, 'col': c_idx,
'color': color, 'text': cell.text.strip()
})
return results
When the user asks about "these files" or the input is a directory:
def process_all_docs(file_list, extractor_fn):
"""Apply extractor to all files and aggregate results."""
all_results = []
for path in file_list:
print(f"\n=== Processing: {os.path.basename(path)} ===")
try:
result = extractor_fn(path)
all_results.append({'file': os.path.basename(path), 'data': result})
except Exception as e:
print(f" ERROR: {e}")
return all_results
# Example: extract text from all .docx in a directory
doc_files = [f for f in all_files if f.lower().endswith(('.docx', '.doc'))]
results = process_all_docs(doc_files, extract_full_text)
When a Word doc contains embedded images (charts, screenshots):
import zipfile, io, subprocess, json
CAPTION = "/path/to/skills/sn-da-image-caption/scripts/caption.py"
def extract_and_caption_images(doc_path, prompt=None):
"""Extract all images from .docx and caption each one."""
# .docx is a ZIP archive; images are in word/media/
results = []
with zipfile.ZipFile(doc_path, 'r') as z:
media_files = [n for n in z.namelist() if n.startswith('word/media/')]
for media in media_files:
ext = os.path.splitext(media)[-1].lower()
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.wmf', '.emf'):
continue
# Save to temp
tmp_path = f"/tmp/{os.path.basename(media)}"
with z.open(media) as src, open(tmp_path, 'wb') as dst:
dst.write(src.read())
# Caption
cmd = ["python3", CAPTION, tmp_path, "--json"]
if prompt:
cmd += ["--prompt", prompt]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if r.returncode == 0:
desc = json.loads(r.stdout).get("description", "")
results.append({'image': media, 'caption': desc})
print(f" {media}: {desc[:100]}...")
else:
print(f" {media}: caption failed — {r.stderr[:80]}")
return results
from docx.shared import Pt
def check_font_sizes(doc_path):
doc = load_doc(doc_path)
issues = []
for i, para in enumerate(doc.paragraphs):
for run in para.runs:
size = run.font.size
size_pt = size.pt if size else None
# Also check style-level font
if size_pt is None:
style_size = run.style.font.size if run.style else None
size_pt = style_size.pt if style_size else None
issues.append({'para': i, 'text': run.text[:30], 'size_pt': size_pt})
return issues
def find_keyword(doc_path, keyword):
text = extract_full_text(doc_path)
idx = text.find(keyword)
if idx >= 0:
context = text[max(0, idx-100):idx+200]
print(f"Found '{keyword}' at pos {idx}:\n{context}")
else:
print(f"'{keyword}' not found. Try broader search.")
# Try case-insensitive or partial match
for kw in keyword.split():
if kw in text:
print(f" Partial match for '{kw}'")
| Pitfall | Fix |
|---------|-----|
| Only read doc.paragraphs, miss tables | Use the body-order iterator in Method 1 |
| Single file when input is multi-file | Check os.path.isdir(), iterate all |
| Highlighted cells not detected | Use XML-level w:shd / w:highlight (Method 3) |
| .doc format fails to open | Convert to .docx via libreoffice (Method 0) |
| Embedded charts look empty | Extract images from ZIP, caption each (Method 5) |
| Font size is None | Check both run-level and style-level (Method for font check) |
Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks
Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.
Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation.
Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill.
Create and edit Obsidian Flavored Markdown with wikilinks, embeds, callouts, properties, and other Obsidian-specific syntax. Use when working with .md files in Obsidian, or when the user mentions wikilinks, callouts, frontmatter, tags, embeds, or Obsidian notes.
Take opensensenova/word-analysis from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.