학술 논문 docx 형식 재정리
사용 시기
- Markdown 분석 보고서/표를 투고 규격에 맞는 docx로 변환해야 할 때
- 중서문 혼합 조판, 삼선표, 다단계 제목 글꼴 크기 및 간격을 일괄 설정해야 할 때
- 단락별 API 삽입으로 삼선표 테두리, run 레벨 rFonts, 이탤릭 분할을 제어할 수 없을 때 이 스킬을 사용합니다.
중문 호 → 포인트 값
| 호 | 포인트(pt) |
|---|---|
| 3호 | 16 |
| 소3호 | 15 |
| 4호 | 14 |
| 소4호 | 12 |
| 5호 | 10.5 |
기본 규범 (대상 학술지/학교 요구 사항에 따라 덮어쓰기 가능): 1단계 제목 소3호 흑체, 2단계 4호 흑체, 본문 소4호 송체 1.5배 줄 간격, 서양어는 모두 Times New Roman, 표는 5호 송체.
주요 구현 포인트 (겪은 함정)
1. 삼선표 테두리
w:tblBorders 의 w:sz 단위는 1/8 포인트입니다:
- 상/하단 1.5pt →
sz=12 - 중간선(표 머리글 아래) 1.0pt →
sz=8 - 세로선은 명시적으로
val="none"을 설정해야 합니다. 그렇지 않으면 Word에서 기본 세로선을 그립니다.
표 머리글 아래 선은 row.cells[i] 로 셀별로 w:tcBorders/w:bottom 을 설정해야 하며,
tblBorders 의 insideH 만 설정하면 일부 렌더러에서 신뢰할 수 없습니다.
2. cell.text = '' 는 서식 없는 빈 run을 남깁니다.
셀을 비우려면 다음을 사용하세요:
p = cell.paragraphs[0]
for r in list(p.runs):
r._element.getparent().remove(r._element)
그렇지 않으면 runs[0] 가 rPr 없는 빈 run이 되어, 이후 글꼴 검증 시 오판하게 됩니다.
3. 중서문 혼합 조판은 반드시 4개의 글꼴 슬롯을 동시에 설정해야 합니다.
rFonts.set(qn('w:ascii'), 'Times New Roman')
rFonts.set(qn('w:hAnsi'), 'Times New Roman')
rFonts.set(qn('w:eastAsia'), '宋体') # 중문 글꼴
rFonts.set(qn('w:cs'), 'Times New Roman')
eastAsia 누락 시 중문이 기본 글꼴로 대체됩니다.
4. 이탤릭은 run을 분할해야 합니다.
통계량 (P<0.001, OR=2.17, CI, β, P 값) 은 반드시 독립적인 run으로 분할하여 개별적으로 italic을 설정해야 합니다.
정규식 순서에 주의: P값 은 \bP(\s*값)\b 로 전체 매칭해야 합니다;
먼저 P\s*[<>=]\s*[\d.]+ 를 매칭한 후 고립된 P를 매칭해야 합니다. 그렇지 않으면 P=0.06 이 P + =0.06 으로 분할됩니다.
5. "한 줄 띄우기" = 단락 간격, 빈 단락이 아닙니다.
절대 Enter(빈 단락)를 사용하여 "한 줄 띄우기"를 구현하지 마세요. 빈 단락은 Word에서 다음과 같은 문제를 일으킵니다:
- 후속 편집 시 불필요한 빈 줄이 생겨 깔끔하게 삭제 불가
- 인쇄/PDF 변환 시 불확실한 공백 발생
- 단락 앞/뒤 간격과 중첩되어 간격 제어 불가
올바른 방법—— space_before / space_after (단위 포인트) 사용:
| 시나리오 | 구현 |
|---|---|
| 1단계 제목 "상하 각 한 줄 띄우기" | space_before = Pt(소4호 크기×1.5), space_after = 동일 값 |
| 2단계 제목 "위에 한 줄 띄우기" | space_before = Pt(소4호 크기×1.5), space_after = Pt(0) |
| 3단계 이하 제목 | 간격 설정 안 함 |
| 본문 | space_before = Pt(0), space_after = Pt(0) |
| 그림/표와 본문 사이 한 줄 띄우기 | 그림/표가 있는 단락 에 space_before/after 설정, 빈 단락 추가 안 함 |
참고값: 소4호(12pt) 한 줄 띄우기 ≈ 18pt; 5호(10.5pt) 한 줄 띄우기 ≈ 15.75pt.
검증 방법: sum(1 for p in doc.paragraphs if not p.text.strip()) == 0
——정상 문서는 빈 단락이 없어야 합니다.
6. 자동 굵게 금지
어떤 경우에도 텍스트를 굵게 하지 마세요. 사용자가 명확히 요청하지 않는 한.
이것은 매우 흔한 함정입니다: Markdown 의 **텍스트** 는 원본 파일의 강조 표시이지, 조판 명령이 아닙니다.
이것을 Word 굵게 렌더링하는 것은 원문에 임의로 강조점을 추가하는 것과 같습니다.
올바른 방법:
**텍스트**→ 텍스트 내용만 가져오고, 굵게 하지 않음- 소제목이 필요할 경우, 독립적인 단락 + 대상 글꼴/크기 사용, 굵게로 계층 구현 금지
- 계층감은 오직 다음에서만 비롯됩니다: 글꼴(흑체/해서체), 크기, 단락 앞/뒤 간격, 번호
마찬가지로 금지되는 것: 이탤릭은 지정된 통계량/기호 외에는 사용하지 않음; 밑줄, 강조점, 컬러 텍스트, 문자 확대/축소/자간——사용자가 요청하지 않은 것은 절대 건드리지 않음.
변환 완료 후 자체 점검:
# 사용자가 요청하지 않은 bold는 나타나지 않아야 함
bold = [r.text for p in doc.paragraphs for r in p.runs if r.font.bold]
표 머리글 굵게는 사용자 요구 사항에 따름; 본 스킬은 기본적으로 표 머리글을 굵게 하지 않음(크기+삼선표로 구분).
7. 3단계 이하 제목은 줄 띄우기 없음
1, 2단계 제목에만 단락 앞/뒤 간격 설정; 3단계 이하는 간격 없음, 굵게 없음.
스크립트
| 스크립트 | 용도 |
|---|---|
scripts/tri_line_table.py | 삼선표 테두리 (verify_table_borders() 자체 점검 포함) |
scripts/fmt_lib.py | 글꼴/간격/굵게 구문 분석/셀 스타일 도구 함수 라이브러리 |
scripts/fix_italic.py | 통계량 이탤릭 분할 (원위치 수정, run 재구성 안 함) |
scripts/fix_spacing2.py | 중영문 혼합 조판 및 기호 간격 수정 (원위치 수정, run 재구성 안 함) |
의존성: pip install python-docx
fmt_lib.parse_bold_segments(text, allow_bold=False) 기본적으로 ** 표시를 버리지만 렌더링하지는 않음;
사용자가 명확히 굵게 요청할 때만 allow_bold=True 전달.
기존 docx에 대한 형식 수정 실행 순서 (순서 변경 불가):
python3 scripts/fix_italic.py 대상.docx # 먼저 이탤릭 분할
python3 scripts/fix_spacing2.py 대상.docx # 나중에 간격 수정
순서를 바꾸면 간격 규칙이 이미 분할된 기호를 다시 잘못 조합합니다.
검증 체크리스트 (생성 후 반드시 확인)
- 빈 단락 수 == 0 —— 최우선 순위,
sum(1 for p in doc.paragraphs if not p.text.strip()) - 굵게 run == 0 (사용자 요청 시 제외) ——
[r.text for p in doc.paragraphs for r in p.runs if r.font.bold]가 비어 있어야 함; 표 내부도 확인 - 1단계 제목 sz==15 및 eastAsia==흑체, space_before==space_after==18pt
- 2단계 제목 sz==14, space_before==18pt
- 본문 sz==12, eastAsia==송체, line_spacing==1.5
- 모든 run의 ascii/hAnsi==Times New Roman
- 이탤릭 run 수 >0 이고 내용이 통계량임 (이것이 유일하게 허용되는 특수 형식)
- 삼선표:
verify_table_borders(table)로 표별 검증, 모두(True, [])를 반환해야 함 - 표 제목: 5호 흑체 가운데 정렬, 문장 끝에 구두점 없음
- 표 내 모든 run: 송체 + 10.5pt
- 본문 단락 alignment == JUSTIFY, 제목과 표 제목은 원래 상태 유지
- 백분율 기호 앞에 공백:
33 %가 아닌33% - 기호와 한자 사이 공백 없음:
30余次; 기호와 숫자 사이 공백 있음:≥ 10 小时,P < 0.05
일회성 자체 점검 스크립트:
import sys; sys.path.insert(0, '<skill>/scripts')
from tri_line_table import verify_table_borders
from docx import Document
d = Document('out.docx'); ps = d.paragraphs
assert sum(1 for x in ps if not x.text.strip()) == 0, '빈 단락이 있습니다'
assert not [r for x in ps for r in x.runs if r.font.bold], '굵게가 있습니다'
assert not [r for t in d.tables for row in t.rows for c in row.cells
for r in c.paragraphs[0].runs if r.font.bold], '표에 굵게가 있습니다'
for i, t in enumerate(d.tables, 1):
good, prob = verify_table_borders(t)
assert good, f'표{i} 삼선표 규격 미달: {prob}'
print('모두 통과')
시각적 검증 환경이 없을 때
LibreOffice / Word 없이 PDF로 변환하여 삼선표를 시각적으로 검증할 수 없을 때는, XML 레벨 검증(체크리스트 1–9항)을 사용하여 글꼴 크기, 글꼴, 테두리, 간격을 신뢰성 있게 커버합니다.
보충: 삼선표의 정확한 규격
3개의 선만 있음:
| 위치 | 포인트 값 | w:sz |
|---|---|---|
| 표 머리글 행 (상단 테두리) | 1.5 | 12 |
| 표 머리글 행 (하단 테두리) | 1.0 | 8 |
| 마지막 행 (하단 테두리) | 1.5 | 12 |
| 나머지 모든 테두리 (모든 중간 행, 세로선 포함) | 빈 값 | val="none" |
핵심: tblBorders 에서 insideH 만 설정하면 안 됩니다. 표 레벨 테두리는 셀 레벨 tcBorders 에 의해 덮어쓰여질 수 있으며,
일단 특정 셀에 bottom 이 작성되면 셀 레벨이 우선합니다. 반드시 셀별로 명시적으로 설정해야 합니다:
- 표 레벨
tblBorders6개 항목 모두none설정 - 모든 행
tcBorders네 면을 먼저 모두none설정 - 표 머리글 행 셀:
top=('single',12)+bottom=('single',8) - 마지막 행 셀:
bottom=('single',12)
clear_row_borders() 는 모든 행에 대해 호출해야 하며, 표 머리글과 마지막 행만 지우면 안 됩니다. 그렇지 않으면 중간 행에 선이 남습니다.
보충: 중문 따옴표 대체
Markdown 의 " 는 곧은 따옴표이므로, docx로 변환 후 반드시 쌍으로 “ / ” 로 대체해야 합니다:
dq_index = 0 # 반드시 run을 넘어 연속적으로 카운트해야 하며, 각 run을 0부터 시작하면 안 됨
for r in p.runs:
for ch in r.text:
if ch == '"':
out.append('“' if dq_index % 2 == 0 else '”')
dq_index += 1
r.text 만 수정하고, rPr 는 건드리지 않음 → 글꼴/크기/이탤릭 모두 유지.
대체 후 반드시 검증: 전체 " 개수 == 0, “ 개수 == ” 개수, 단락별 좌우 쌍 매칭
(전체 문서 균형이 단락별 균형을 의미하지는 않음).
보충: Word 편집은 조각 run을 생성합니다.
사용자가 Word에서 수동으로 편집하면 단락이 여러 run으로 분할되며, 그중 일부(분할된 -, 단일 문자)는
rFonts 가 없어 기본 글꼴로 대체됩니다. 따옴표/테두리 수정 후 한 번 스캔해야 합니다:
rf = r._element.rPr.rFonts if r._element.rPr is not None else None
if rf is None or rf.get(qn('w:eastAsia')) is None: # → 보충 필요
보충 방법: 단락 내에서 이미 글꼴이 설정된 첫 번째 run을 참조하여 rFonts/sz/b를 상속합니다.
중요 작업 흐름 주의 사항
사용자가 Word에서 문서를 수동으로 수정한 경우, 절대 전체 문서를 다시 생성하지 마십시오——사용자 수정 사항을 덮어씁니다.
올바른 방법: 기존 docx 읽기 → 원위치에서 XML 수정 → 저장.
수정 전 백업: cp xxx.docx /tmp/backup.docx.
이탤릭 규범
유일한 판단 기준: 해당 문자가 수학/통계학에서 변수 또는 기호인지 여부
| 범주 | 이탤릭 여부 | 예 |
|---|---|---|
| 통계적 유의성 P | 이탤릭 | P = 0.04, P < 0.001 |
| 검정 수준 α | 이탤릭 | α = 0.05 |
| 검정 통계량 F, Z, H, χ², t, r | 이탤릭 | F = 4.52, χ² = 8.31 |
| 특수 문자 (유전자 기호, 메틸화 β 값 등) | 이탤릭 | β |
| OR, CI, RR, HR, SD, SE, AUC | 이탤릭 안 함 | OR = 3.14, 95% CI |
| 여러 문자 약어 (BMI, OGTT, FPG…) | 이탤릭 안 함 | BMI, OGTT |
| 한자 텍스트 | 이탤릭 안 함 | 값, 위 |
⚠️ 가장 쉬운 실수: "P값" 에서 P만 이탤릭
✗ 이탤릭 'P값' ← "값" 도 함께 이탤릭
✓ 이탤릭 'P' + 정체 '값'
✓ 이탤릭 'P' + 정체 '값이' + ...
P 값, P값, P 값이 0.07 ——모두 P 만 이탤릭, 뒤따르는 한자는 모두 정체.
이탤릭 분할의 세 가지 불변의 법칙 (겪은 피눈물)
- 절대 run을 재구성하지 않음. "문자 스트림 재구성 run"으로 형식을 수정하면 rFonts/sz/b가 모두 사라지고,
삼선표 테두리가 지워지며, 이탤릭 표시가 조각나서 잘못 정렬됩니다.
run.text만 수정하고, run 요소를 절대 삭제/생성하지 않습니다. - 분할은 반드시
copy.deepcopy(run._element)를 사용하여 모든 형식 속성을 유지해야 합니다; 삽입 위치parent.insert(index+1, el)(원래 run 바로 옆).append를 사용하면 단락 끝으로 이동하여 순서가 어긋나PP가 나타납니다. 복사본 텍스트 =lead + rest + trail(stat 제외), 원래 run 텍스트 =lead + stat. run.text = x는 기존<w:t>노드를 지우지 않으며, deepcopy 복사본에서 추가됩니다. 반드시 먼저el.remove(child)로 모든w:t/w:br/w:tab/w:cr을 삭제해야 합니다; 또한 먼저w:i/w:iCs를 제거한 후 텍스트를 설정해야 하며, 순서가 반대여도 오류가 발생합니다.
참고 구현은 scripts/fix_italic.py 에 있습니다.
중영문 혼합 조판 및 기호 간격
기본 규칙
| 시나리오 | 규칙 | 예 |
|---|---|---|
| 중문 ↔ 숫자/문자/구두점 | 공백 없음 | 在30余次未校正检验的背景下, 但Table 1显示 |
| 한자 ↔ 한자 | 공백 없음 | 研究目的 |
| 영어 단어 사이 | 공백 있음 | Table 1 |
| 수학 표현식 내부 | 문자/숫자와 연산자 사이 공백 있음 | P = 0.04, BMI < 18.5 |
| 수학 표현식 ↔ 중문 | 공백 있음 | P < 0.05 为阈值 |
기호와 숫자/문자/한자의 간격 (실수하기 쉬움)
| 조합 | 규칙 | 예 |
|---|---|---|
| 기호 ↔ 숫자 | 공백 있음 | 33 %, 87.3 %, P = 0.04 |
| 기호 ↔ 영어 단어/문자 | 공백 있음 | Table 1; α = 0.05 |
| 기호 ↔ 한자 | 공백 없음 | 30余次, ≥10小时 (기호가 한자에 붙음) |
실수하기 쉬운 점: 30 余次 (오류, 기호와 한자 사이 공백 있음) vs 30余次 (정답).
연산자 유형 기호(≥ ≤ = < >)와 숫자 사이에는 공백이 있어야 하지만, 기호와 바로 뒤의 한자 사이에는 공백을 넣지 않습니다.
백분율 기호
33 % 숫자와 % 사이에 공백을 넣습니다. 이것은 과학 논문 규범의 요구 사항입니다. 33% 로 쓰지 마십시오.
참고 구현은 scripts/fix_spacing2.py (fix_spacing_in_run 포함) 에 있습니다.
본문 단락 정렬
본문 단락은 양쪽 정렬을 사용합니다——즉 Word의 JUSTIFY (Cmd+J / Ctrl+J),
DISTRIBUTE 가 아님에 주의하십시오.
from docx.enum.text import WD_ALIGN_PARAGRAPH
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
본문 단락에만 적용하며, 다음은 각각 원래 정렬을 유지하고 변경하지 않습니다:
- 각 단계 제목——보통 왼쪽 정렬
- 표 제목——가운데 정렬
- 표 내 셀——필요에 따라 (왼쪽 정렬 또는 가운데 정렬)
⚠️ 세 가지 불변의 법칙 (실행 순서 및 검증)
실행 순서
python3 scripts/fix_italic.py 대상.docx # 먼저 이탤릭 분할
python3 scripts/fix_spacing2.py 대상.docx # 나중에 간격 수정
검증 (필수, 하나라도 빠지면 안 됨)
import re
norm = lambda s: re.sub(r'\s', '', s)
# ① 텍스트 손실 없음 — 텍스트 삼킴/중복 방지
assert all(norm(a.text) == norm(b.text)
for a, b in zip(원본_문서.paragraphs, 새_문서.paragraphs))
# ② 이탤릭 run은 오직 통계 기호 자체(P/α/β/F/Z/H/χ/t/r)여야 하며, 다음을 포함해서는 안 됨:
# - 연산자 또는 숫자: [=<>≤≥0-9]
# - 한자 ('P값' 의 '값'은 정체여야 함)
# - OR/CI/RR 등의 약어
for p in 새_문서.paragraphs:
for r in p.runs:
if r.font.italic and r.text.strip():
t = r.text.strip()
assert not re.search(r'[=<>≤≥0-9\u4e00-\u9fff]', t), f'이탤릭에 연산자/숫자/한자 포함: {t!r}'
assert t.split(' ')[0] not in ('OR','CI','RR','HR','SD','SE','AUC'), f'약어는 이탤릭이 아니어야 함: {t!r}'
# ③ 본문 단락은 반드시 양쪽 정렬
from docx.enum.text import WD_ALIGN_PARAGRAPH
assert any(p.alignment == WD_ALIGN_PARAGRAPH.JUSTIFY for p in 새_문서.paragraphs)
# ④ 중문 따옴표 쌍 매칭
assert sum(p.text.count('“') for p in 새_문서.paragraphs) == \
sum(p.text.count('”') for p in 새_문서.paragraphs)
⚠️ 최우선 순위 불변의 법칙 (위반 시 재작업)
- "한 줄 띄우기"는 반드시 단락 간격(
space_before/space_after)만 사용해야 하며, 절대 Enter/빈 단락을 사용하면 안 됩니다. - 어떤 경우에도 텍스트를 굵게 하지 마십시오. 사용자가 요구 사항에서 명확히 굵게 하라고 하지 않는 한.
Markdown 의
**텍스트**는 원본 파일의 강조 표시이지 조판 명령이 아닙니다——표시만 제거하고 굵게 하지 않습니다. - 사용자가 요청하지 않은 형식(밑줄, 강조점, 컬러 텍스트, 자간, 문자 확대/축소)은 절대 건드리지 않습니다.
- 사용자가 문서를 수동으로 수정한 경우, 전체 문서를 다시 생성하는 것을 금지하며, 기존 파일을 읽어서만 원위치 XML 수정을 해야 합니다.
계층감은 오직 글꼴(흑체/해서체), 크기, 단락 앞/뒤 간격, 번호에서만 비롯됩니다——굵게가 아닙니다.
본문/표 텍스트 간소화 시 주의 사항
사용자가 이미 수정한 docx에서 텍스트 내용을 간소화할 때:
불변의 법칙: 첫 번째 run에 병합하여 쓰고, 나머지는 비워둡니다.
def set_paragraph_text(p, text):
runs = p.runs
if not runs: return False
runs[0].text = text # 형식은 첫 번째 run에 유지
for r in runs[1:]:
r.text = '' # ⚠️ 반드시 비워야 함, 그렇지 않으면 이전 텍스트가 남아 중복됨
return True
마찬가지로 "run 재구성 금지" 원칙을 따릅니다——run.text 만 수정하고, <w:r> 를 절대 삭제/생성하지 않습니다.
표 위치 찾기: 문서 흐름 순서대로, doc.paragraphs 인덱스를 사용할 수 없습니다.
단락 내에 표가 포함된 경우, doc.paragraphs[i] 의 인덱스는 표와 일치하지 않습니다. 반드시 body 흐름을 따라야 합니다:
from docx.table import Table
from docx.text.paragraph import Paragraph
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
seq = []
for c in doc.element.body.iterchildren():
if c.tag == W+'p': seq.append(('p', Paragraph(c, doc)))
elif c.tag == W+'tbl': seq.append(('t', Table(c, doc)))
행 추가/삭제의 두 가지 함정
table.rows는 접근할 때마다 XML을 다시 구문 분석합니다 —— 행 삭제 후rows[ri]를 사용하면 인덱스가 초과됩니다. 먼저rows = list(table.rows)로 스냅샷을 찍고, 뒤에서부터 삭제합니다.- 복제된 행은 마지막 행의 하단 테두리를 잃습니다 —— 복제된 마지막 행을 보충하면 삼선표의 하단선이
nil이 됩니다. 마무리 단계에서 반드시 마지막 행bottom테두리(sz=12)를 다시 보충해야 합니다.
⚠️ 테두리 감지 시 none 만 인식하지 마십시오.
Word 자체 삼선표에서 중간 행은 w:val="nil" 이지 "none" 이 아닙니다.
"선 없음" 판단 시 둘 다 수용해야 합니다:
assert tv in ('nil','none') and bv in ('nil','none')
none 만 인식하면 "테두리 손상"으로 오보되며, 실제로는 감지 로직 오류입니다.