Files
xuanheng-website/_tools/check_html.py
T
dsh f81d41c5ca 初始化:炫恒网络官网(五个页面)
站点:纯静态 HTML + CSS + 原生 JS,零框架零构建
页面:首页 / 合作品牌 / 荣誉资质 / 客户案例 / 关于我们

内容:
- site/          站点本体(含已压缩 webp 图片,约 4.8 MB)
- deploy/        Docker Compose / Caddy / Apache 部署配置
- _tools/        开发自检脚本与部署脚本
- Dockerfile     自建 nginx 镜像方案
- README.md      项目文档

设计:品牌黄 #ffc107 + 白,深蓝仅用于页脚;菜单长期置顶;
      交互动效只加在关键词与区块级元素上。

素材说明:原始素材(背景图/老网站/方案/荣誉资质/合作品牌,约 290 MB)
         未纳入版本控制,站点使用的是 site/assets 内已处理压缩的图片。
2026-10-13 16:12:41 +08:00

148 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""HTML 结构校验:标签嵌套是否合法、必需元素是否齐全、无障碍属性是否到位。"""
import os
import re
import sys
from html.parser import HTMLParser
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SITE = os.path.join(ROOT, "site")
PAGES = ["index.html", "brands.html", "honors.html", "cases.html", "about.html"]
VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input", "link",
"meta", "param", "source", "track", "wbr"}
OPTIONAL_END = {"li", "p", "td", "th", "tr", "option", "thead", "tbody"}
class Checker(HTMLParser):
def __init__(self):
super().__init__(convert_charrefs=True)
self.stack = []
self.errors = []
self.tags = {}
self.attrs = []
self.texts = []
def handle_starttag(self, tag, attrs):
self.tags[tag] = self.tags.get(tag, 0) + 1
self.attrs.append((tag, dict(attrs)))
if tag not in VOID:
self.stack.append((tag, self.getpos()))
def handle_startendtag(self, tag, attrs):
self.tags[tag] = self.tags.get(tag, 0) + 1
self.attrs.append((tag, dict(attrs)))
def handle_endtag(self, tag):
if tag in VOID:
self.errors.append("闭合了自闭合标签 </%s> 行 %d" % (tag, self.getpos()[0]))
return
if not self.stack:
self.errors.append("多余的闭合标签 </%s> 行 %d" % (tag, self.getpos()[0]))
return
if self.stack[-1][0] == tag:
self.stack.pop()
return
# 容忍可省略的闭合标签
names = [t for t, _ in self.stack]
if tag in names:
idx = len(names) - 1 - names[::-1].index(tag)
skipped = names[idx + 1:]
if all(s in OPTIONAL_END for s in skipped):
del self.stack[idx:]
return
self.errors.append(
"</%s> 行 %d 与最近未闭合标签 <%s> 行 %d 交叉嵌套(中间有 %s)"
% (tag, self.getpos()[0], self.stack[-1][0], self.stack[-1][1][0],
", ".join(skipped)))
del self.stack[idx:]
return
self.errors.append("未匹配的闭合标签 </%s> 行 %d" % (tag, self.getpos()[0]))
def handle_data(self, data):
t = data.strip()
if t:
self.texts.append(t)
def main():
problems = 0
for page in PAGES:
path = os.path.join(SITE, page)
html = open(path, encoding="utf-8").read()
c = Checker()
c.feed(html)
c.close()
errs = list(c.errors)
for tag, pos in c.stack:
errs.append("标签 <%s> 行 %d 未闭合" % (tag, pos[0]))
# 必需结构
need = {
"html": "html 标签",
"head": "head 标签",
"body": "body 标签",
"main": "main 标签",
"h1": "h1 标题",
"title": "title",
}
for tag, label in need.items():
if c.tags.get(tag, 0) == 0:
errs.append("缺少 %s" % label)
if c.tags.get("h1", 0) > 1:
errs.append("存在 %d 个 h1(应为 1 个)" % c.tags["h1"])
# 图片必须有 alt
for tag, at in c.attrs:
if tag == "img" and "alt" not in at:
errs.append("<img src=%s> 缺少 alt" % at.get("src", "?"))
if tag == "html" and at.get("lang") != "zh-CN":
errs.append("<html> 缺少 lang=\"zh-CN\"")
if tag == "a":
if at.get("target") == "_blank" and "noopener" not in (at.get("rel") or ""):
errs.append("外链 %s 缺少 rel=noopener" % at.get("href", "?"))
# 图片尺寸提示(避免布局抖动)
#
# 例外:品牌 logo 由 CSS 用「固定高度 + 宽度自动」控制大小
# (见 main.css 的 .brandstrip .marquee__item img)。
# 这类图片不能写 width/height 属性 —— logo 源图自带大量透明留白,
# 属性声明的宽高比(360/240 = 1.5)与实际内容(锐捷 320/54 = 5.9)不符,
# 浏览器会按错误比例算宽度并把 logo 压扁。
# 它们位于固定高度的卡片内居中显示,不会造成布局抖动。
def css_controls_size(at):
if "img/brands/" in (at.get("src") or ""):
return True # 品牌 logo:由 CSS 统一控制尺寸
if "brand__mark" in (at.get("class") or ""):
return True # 站点 Logo:CSS 固定 40px
return False
imgs = [at for tag, at in c.attrs if tag == "img"]
no_dim = [i for i in imgs
if not (i.get("width") and i.get("height"))
and not css_controls_size(i)]
if no_dim:
errs.append("%d 张图片缺少 width/height(会抖动布局):%s"
% (len(no_dim), ", ".join(i.get("src", "?") for i in no_dim[:3])))
# 按钮必须有可读文字或 aria-label
for tag, at in c.attrs:
if tag == "button" and not at.get("aria-label") and at.get("data-lightbox") is None:
pass # 由子元素文字提供
if errs:
problems += len(errs)
print("[%s]" % page)
for e in errs:
print(" x " + e)
else:
print("[%s] 结构 OK 标签 %d 个 / 图片 %d 张 / 文本块 %d 段"
% (page, sum(c.tags.values()), len(imgs), len(c.texts)))
print("\nHTML 结构校验:" + ("全部通过 OK" if problems == 0 else "%d 个问题" % problems))
return 0 if problems == 0 else 1
if __name__ == "__main__":
sys.exit(main())