# Comment Restoration from Original File When comments are lost during docx editing (e.g., paragraph clear operations that remove commentRangeStart/End/Reference elements), restore them from the original file. ## Scenario - Original file has N comments (e.g., Alice×2, 法务, 杜律 = 4 comments, ids 0-3) - Edited file lost some/all original comments and may have added new ones (e.g., 华诚-Z comment id=0, Alice id=2) - Goal: merge all comments — original ones preserved + new ones added, with non-conflicting IDs ## Recovery Technique ### Step 1: Extract original comments ```python WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' z_orig = zipfile.ZipFile('original.docx') with z_orig.open('word/comments.xml') as f: ctree_orig = etree.parse(f) orig_comments = [] for c in ctree_orig.getroot().findall(f'{WNS}comment'): orig_comments.append({ 'id': c.get(f'{WNS}id'), 'author': c.get(f'{WNS}author'), 'date': c.get(f'{WNS}date'), 'text': ''.join(t.text for t in c.iter(f'{WNS}t') if t.text), 'element': copy.deepcopy(c) }) z_orig.close() ``` ### Step 2: Identify which comments survived in the edited file ```python z_edit = zipfile.ZipFile('edited.docx') with z_edit.open('word/comments.xml') as f: ctree_edit = etree.parse(f) edit_comment_ids = set() for c in ctree_edit.getroot().findall(f'{WNS}comment'): edit_comment_ids.add(c.get(f'{WNS}id')) ``` ### Step 3: Find new comments (non-original authors) ```python new_comments = [] for c in ctree_edit.getroot().findall(f'{WNS}comment'): if c.get(f'{WNS}author') not in [oc['author'] for oc in orig_comments]: new_comments.append({ 'old_id': c.get(f'{WNS}id'), 'author': c.get(f'{WNS}author'), 'element': copy.deepcopy(c) }) ``` ### Step 4: Rebuild comments.xml with all comments Assign non-conflicting IDs: - Original comments keep their original IDs (0, 1, 2, 3) - New comments get IDs starting from max(original_ids) + 1 ```python new_comments_xml = etree.Element(f'{WNS}comments') new_comments_xml.set('xmlns:w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main') # ... add other namespaces as needed max_id = max(int(oc['id']) for oc in orig_comments) # Add original comments for oc in orig_comments: new_comments_xml.append(oc['element']) # Add new comments with renumbered IDs for nc in new_comments: max_id += 1 nc['new_id'] = str(max_id) nc['element'].set(f'{WNS}id', nc['new_id']) new_comments_xml.append(nc['element']) ``` ### Step 5: Update document.xml comment references For each new comment, find its commentRangeStart, commentRangeEnd, and commentReference in document.xml and update the ID from old to new: ```python for nc in new_comments: old_id = nc['old_id'] new_id = nc['new_id'] # Update commentRangeStart for elem in root.iter(f'{WNS}commentRangeStart'): if elem.get(f'{WNS}id') == old_id: elem.set(f'{WNS}id', new_id) # Update commentRangeEnd for elem in root.iter(f'{WNS}commentRangeEnd'): if elem.get(f'{WNS}id') == old_id: elem.set(f'{WNS}id', new_id) # Update commentReference (inside w:r) for elem in root.iter(f'{WNS}commentReference'): if elem.get(f'{WNS}id') == old_id: elem.set(f'{WNS}id', new_id) ``` ### Step 6: Write back to docx ```python z_out = zipfile.ZipFile('output.docx', 'w') # Copy all files from edited.docx except comments.xml and document.xml for item in z_edit.namelist(): if item not in ('word/comments.xml', 'word/document.xml'): z_out.writestr(item, z_edit.read(item)) # Write updated comments.xml z_out.writestr('word/comments.xml', etree.tostring(new_comments_xml, encoding='UTF-8', xml_declaration=True, standalone=True)) # Write updated document.xml z_out.writestr('word/document.xml', etree.tostring(tree, encoding='UTF-8', xml_declaration=True, standalone=True)) z_edit.close() z_out.close() ``` ## Verification ```python z = zipfile.ZipFile('output.docx') with z.open('word/comments.xml') as f: ctree = etree.parse(f) for c in ctree.getroot().findall(f'{WNS}comment'): print(f" id={c.get(f'{WNS}id')} author={c.get(f'{WNS}author')}: {text[:80]}") # Check all IDs referenced in document.xml exist in comments.xml content = z.read('word/document.xml').decode('utf-8') doc_ids = set(re.findall(r'commentRangeStart[^>]*w:id="(\d+)"', content)) doc_ids |= set(re.findall(r'commentRangeEnd[^>]*w:id="(\d+)"', content)) doc_ids |= set(re.findall(r'commentReference[^>]*w:id="(\d+)"', content)) comment_ids = set(c.get(f'{WNS}id') for c in ctree.getroot().findall(f'{WNS}comment')) assert doc_ids == comment_ids, f"ID mismatch: doc={doc_ids} comments={comment_ids}" ``` ## Key Pitfall: Comment Text Extraction When extracting comment text for comparison, comments may have nested `` elements (multi-paragraph comments). Use `.iter()` not `.findall()` to get all text nodes. ## Empirical Case (2026-07-01 反委托代发工资协议) - Original: 4 comments (Alice id=0, Alice id=1, 法务 id=2, 杜律 id=3) - v1_doro_updated: 2 comments (华诚-Z id=0, Alice id=2) — lost Alice id=0/1, 法务, 杜律 - Final: 5 comments (Alice id=0, Alice id=1, 法务 id=2, 杜律 id=3, 华诚-Z id=4) - 华诚-Z's comment was id=0 in v1_doro_updated, renumbered to id=4 in final - All commentRangeStart/End/Reference IDs updated in document.xml accordingly