-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscan_quality_issues.py
More file actions
294 lines (252 loc) · 10.1 KB
/
scan_quality_issues.py
File metadata and controls
294 lines (252 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
#!/usr/bin/env python3
"""
블로그 포스트 품질 이슈 스캐너
스캔 항목:
1. front matter에 title/date/description 누락 또는 빈 값
2. "Prop-Logic™" 본문 노출
3. 깨진 내부 링크 (/posts/... 링크의 실제 파일 존재 여부)
4. 닫히지 않은 HTML 태그 (열린/닫힌 태그 수 불일치)
"""
import os
import re
import glob
from collections import defaultdict
from pathlib import Path
POSTS_DIR = "/Users/suhun/Desktop/document/mdeeno.github.io/content/posts"
REPORT_PATH = "/Users/suhun/Desktop/document/mdeeno.github.io/quality_issues_report.txt"
# 모든 md 파일 수집
all_md_files = glob.glob(os.path.join(POSTS_DIR, "**", "*.md"), recursive=True)
# _index.md 제외
all_md_files = [f for f in all_md_files if not f.endswith("_index.md")]
# 포스트 slug 목록 생성 (내부 링크 검증용)
# /posts/category/slug/ 형태의 링크를 검증하기 위해 상대 경로 매핑
existing_slugs = set()
for f in all_md_files:
rel = os.path.relpath(f, POSTS_DIR)
# category/filename.md -> category/filename (확장자 제거)
slug = os.path.splitext(rel)[0]
existing_slugs.add(slug)
# 파일명만으로도 매칭 시도
basename = os.path.splitext(os.path.basename(f))[0]
existing_slugs.add(basename)
# 카테고리 디렉토리 목록
existing_dirs = set()
for f in all_md_files:
rel = os.path.relpath(f, POSTS_DIR)
parts = rel.split(os.sep)
if len(parts) > 1:
existing_dirs.add(parts[0])
# 결과 저장
issues_frontmatter = []
issues_proplogic = []
issues_broken_links = []
issues_unclosed_tags = []
# 검사할 HTML 태그 목록
CHECK_TAGS = ["div", "span", "section", "article", "header", "footer",
"nav", "main", "aside", "table", "thead", "tbody", "tr",
"td", "th", "ul", "ol", "li", "p", "blockquote", "details",
"summary", "figure", "figcaption", "a", "strong", "em",
"b", "i", "u", "h1", "h2", "h3", "h4", "h5", "h6"]
def parse_frontmatter(content):
"""YAML front matter 파싱 (간단 버전)"""
if not content.startswith("---"):
return None
end = content.find("---", 3)
if end == -1:
return None
fm_text = content[3:end]
result = {}
for line in fm_text.split("\n"):
line = line.strip()
if ":" in line:
key, _, val = line.partition(":")
key = key.strip()
val = val.strip().strip('"').strip("'")
result[key] = val
return result
def check_frontmatter(filepath, content):
"""front matter에서 title, date, description 누락/빈 값 확인"""
fm = parse_frontmatter(content)
if fm is None:
issues_frontmatter.append((filepath, "front matter 자체가 없음"))
return
missing = []
for field in ["title", "date", "description"]:
if field not in fm or not fm[field]:
missing.append(field)
if missing:
issues_frontmatter.append((filepath, f"누락/빈 값: {', '.join(missing)}"))
def check_proplogic(filepath, content):
"""Prop-Logic™ 노출 확인 (front matter 제외, 본문만)"""
# front matter 이후 본문 추출
if content.startswith("---"):
end = content.find("---", 3)
if end != -1:
body = content[end + 3:]
else:
body = content
else:
body = content
# 대소문자 무시, 다양한 변형 포함
patterns = [r"Prop-Logic™", r"Prop-Logic", r"PropLogic", r"prop-logic"]
for pat in patterns:
matches = re.findall(pat, body, re.IGNORECASE)
if matches:
issues_proplogic.append((filepath, f"'{matches[0]}' 발견 ({len(matches)}회)"))
break
def check_internal_links(filepath, content):
"""내부 링크 (/posts/...) 검증"""
# 마크다운 링크와 HTML href 모두 검사
link_patterns = [
r'\[.*?\]\((/posts/[^)]+)\)', # [text](/posts/...)
r'href=["\'](/posts/[^"\']+)["\']', # href="/posts/..."
]
for pat in link_patterns:
links = re.findall(pat, content)
for link in links:
# 앵커 제거
link_clean = link.split("#")[0].rstrip("/")
# /posts/ 이후 경로 추출
post_path = link_clean.replace("/posts/", "", 1)
# 실제 파일 존재 확인
# 1) 정확한 경로로 md 파일 찾기
candidate_paths = [
os.path.join(POSTS_DIR, post_path + ".md"),
os.path.join(POSTS_DIR, post_path, "index.md"),
os.path.join(POSTS_DIR, post_path, "_index.md"),
]
# 2) slug 기반 매칭
found = False
for cp in candidate_paths:
if os.path.exists(cp):
found = True
break
if not found:
# slug 부분 매칭 시도
if post_path in existing_slugs:
found = True
if not found:
# glob으로 부분 매칭
glob_results = glob.glob(
os.path.join(POSTS_DIR, "**", f"*{os.path.basename(post_path)}*"),
recursive=True,
)
if glob_results:
found = True
if not found:
issues_broken_links.append((filepath, link))
def check_unclosed_tags(filepath, content):
"""닫히지 않은 HTML 태그 간단 체크"""
# front matter 이후 본문만
if content.startswith("---"):
end = content.find("---", 3)
if end != -1:
body = content[end + 3:]
else:
body = content
else:
body = content
tag_mismatches = []
for tag in CHECK_TAGS:
# 열린 태그 (<tag ...> 또는 <tag>), self-closing 제외
open_pattern = rf"<{tag}(?:\s[^>]*)?>(?!.*/>)"
close_pattern = rf"</{tag}\s*>"
self_closing = rf"<{tag}[^>]*/>"
open_count = len(re.findall(open_pattern, body, re.IGNORECASE))
close_count = len(re.findall(close_pattern, body, re.IGNORECASE))
sc_count = len(re.findall(self_closing, body, re.IGNORECASE))
# self-closing은 열린 태그에서 제외
effective_open = open_count - sc_count
if effective_open < 0:
effective_open = 0
diff = effective_open - close_count
if diff != 0:
tag_mismatches.append(f"<{tag}>: 열림 {effective_open}, 닫힘 {close_count} (차이: {diff:+d})")
if tag_mismatches:
issues_unclosed_tags.append((filepath, tag_mismatches))
# 전체 스캔 실행
print(f"총 {len(all_md_files)}개 포스트 스캔 시작...")
for i, filepath in enumerate(all_md_files):
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
except Exception as e:
issues_frontmatter.append((filepath, f"파일 읽기 실패: {e}"))
continue
check_frontmatter(filepath, content)
check_proplogic(filepath, content)
check_internal_links(filepath, content)
check_unclosed_tags(filepath, content)
if (i + 1) % 100 == 0:
print(f" {i + 1}/{len(all_md_files)} 완료...")
print(f"스캔 완료. 리포트 생성 중...")
# 상대 경로 변환 헬퍼
def rel(path):
return os.path.relpath(path, POSTS_DIR)
# 리포트 생성
with open(REPORT_PATH, "w", encoding="utf-8") as f:
f.write("=" * 80 + "\n")
f.write(" 블로그 포스트 품질 이슈 리포트\n")
f.write(f" 스캔 대상: {len(all_md_files)}개 포스트\n")
f.write(f" 경로: {POSTS_DIR}\n")
f.write("=" * 80 + "\n\n")
# 1. Front matter 이슈
f.write("-" * 80 + "\n")
f.write(f"[1] Front Matter 누락/빈 값 ({len(issues_frontmatter)}건)\n")
f.write("-" * 80 + "\n")
if issues_frontmatter:
for path, issue in issues_frontmatter:
f.write(f" - {rel(path)}\n → {issue}\n")
else:
f.write(" (이슈 없음)\n")
f.write("\n")
# 2. Prop-Logic 노출
f.write("-" * 80 + "\n")
f.write(f"[2] Prop-Logic™ 본문 노출 ({len(issues_proplogic)}건)\n")
f.write(" → 'M-DEENO 분석 엔진'으로 교체 필요\n")
f.write("-" * 80 + "\n")
if issues_proplogic:
for path, issue in issues_proplogic:
f.write(f" - {rel(path)}\n → {issue}\n")
else:
f.write(" (이슈 없음)\n")
f.write("\n")
# 3. 깨진 내부 링크
f.write("-" * 80 + "\n")
f.write(f"[3] 깨진 내부 링크 ({len(issues_broken_links)}건)\n")
f.write("-" * 80 + "\n")
if issues_broken_links:
for path, link in issues_broken_links:
f.write(f" - {rel(path)}\n → 깨진 링크: {link}\n")
else:
f.write(" (이슈 없음)\n")
f.write("\n")
# 4. 닫히지 않은 HTML 태그
f.write("-" * 80 + "\n")
f.write(f"[4] HTML 태그 불일치 ({len(issues_unclosed_tags)}건)\n")
f.write("-" * 80 + "\n")
if issues_unclosed_tags:
for path, mismatches in issues_unclosed_tags:
f.write(f" - {rel(path)}\n")
for m in mismatches:
f.write(f" → {m}\n")
else:
f.write(" (이슈 없음)\n")
f.write("\n")
# 요약
f.write("=" * 80 + "\n")
f.write(" 요약\n")
f.write("=" * 80 + "\n")
total = len(issues_frontmatter) + len(issues_proplogic) + len(issues_broken_links) + len(issues_unclosed_tags)
f.write(f" 총 이슈: {total}건\n")
f.write(f" [1] Front Matter 이슈: {len(issues_frontmatter)}건\n")
f.write(f" [2] Prop-Logic™ 노출: {len(issues_proplogic)}건\n")
f.write(f" [3] 깨진 내부 링크: {len(issues_broken_links)}건\n")
f.write(f" [4] HTML 태그 불일치: {len(issues_unclosed_tags)}건\n")
print(f"리포트 저장 완료: {REPORT_PATH}")
print(f"\n=== 요약 ===")
print(f"[1] Front Matter 이슈: {len(issues_frontmatter)}건")
print(f"[2] Prop-Logic™ 노출: {len(issues_proplogic)}건")
print(f"[3] 깨진 내부 링크: {len(issues_broken_links)}건")
print(f"[4] HTML 태그 불일치: {len(issues_unclosed_tags)}건")
print(f"총 이슈: {len(issues_frontmatter) + len(issues_proplogic) + len(issues_broken_links) + len(issues_unclosed_tags)}건")