← 解説「壊れた HTML を直す — 閉じ忘れ、迷子の終了タグ、無い html / head / body」に戻る
サンプルコードで身につける: 壊れた HTML を直す — 閉じ忘れ、迷子の終了タグ、無い html / head / body
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1<p> の閉じ忘れは次の <p> で閉じる
HTML Standard 13.2.6.4.7 "in body" は、<p> の開始タグが来たとき開いている <p> があれば先に閉じると定めます(close a p )。だから <p>one<p>two の 2 つの <p> は入れ子ではなく兄弟になります。木を作らずスタックの深さで字下げして形を見せており、実行すると閉じ忘れの入力と正しく書いた入力が同じ形になり、自動で閉じた箇所に矢印の注記が付きます。
Rust
// <p> の閉じ忘れを直す。HTML Standard 13.2.6.4.7 "in body": <p> の開始タグが来たとき、
// 開いている <p> があれば先に閉じる("close a p element")。だから "<p>one<p>two" の 2 つの <p> は
// 入れ子ではなく兄弟になる。ここでは木を作らず、スタックの深さで字下げして形を見せる。
#[derive(Debug)]
enum Token<'a> {
Start(&'a str),
End(&'a str),
Text(&'a str),
}
/// "<p>one<p>two" を素朴にトークンにする(属性なし)
fn tokenize<'a>(html: &'a str) -> Vec<Token<'a>> {
let mut out = Vec::new();
let mut rest = html;
while !rest.is_empty() {
if let Some(after_lt) = rest.strip_prefix('<') {
let gt = after_lt.find('>').unwrap_or(after_lt.len());
let name = &after_lt[..gt];
out.push(match name.strip_prefix('/') {
Some(n) => Token::End(n),
None => Token::Start(name),
});
rest = &after_lt[(gt + 1).min(after_lt.len())..];
} else {
let lt = rest.find('<').unwrap_or(rest.len());
out.push(Token::Text(&rest[..lt]));
rest = &rest[lt..];
}
}
out
}
fn build(html: &str) {
let mut open: Vec<&str> = vec!["body"];
println!("<body>");
for t in tokenize(html) {
match t {
Token::Start(tag) => {
if tag == "p" && open.contains(&"p") {
// 開いている <p> をそこで閉じる(閉じ忘れの修復)
while open.pop() != Some("p") {}
println!("{}</p> ← 次の <p> が来たので自動で閉じた", " ".repeat(open.len()));
}
println!("{}<{}>", " ".repeat(open.len()), tag);
open.push(tag);
}
Token::End(tag) => {
if open.contains(&tag) {
while open.pop() != Some(tag) {}
println!("{}</{}>", " ".repeat(open.len()), tag);
}
}
Token::Text(s) => println!("{}#text {:?}", " ".repeat(open.len()), s),
}
}
while open.len() > 1 {
let tag = open.pop().unwrap();
println!("{}</{}> ← 入力の終わりで自動で閉じた", " ".repeat(open.len()), tag);
}
println!("</body>\n");
}
fn main() {
println!("入力: <p>one<p>two");
build("<p>one<p>two");
println!("入力: <p>one</p><p>two</p> (正しく書いた場合。結果は同じ形になる)");
build("<p>one</p><p>two</p>");
}
2迷子の終了タグは無視・閉じ忘れは一緒に閉じる・html/head/body は暗黙に作る
「止まらずに直す」3 つの規則を 1 つの構築器にまとめ、直した箇所を errors に記録します。HTML Standard は を「報告してよいが処理は仕様どおり続ける」ものと定めるので、1 か所の壊れで全体が止まることはありません。実行すると <div><p>a</b>b</div><span>c</span> から # > html > head, body > div > p(a, b), span(c) の木ができ、</b> は無視、</div> の前に <p> を一緒に閉じた、という 2 件の parse error が出ます。
Rust
// 「止まらずに直す」3 つの規則を 1 つの構築器で見せる。
// (1) 迷子の終了タグ(対応する開始タグが開いていない)は無視する(parse error だが処理は続く)
// (2) <div> の中で <p> を閉じ忘れても、</div> が来たら p も一緒に下ろす
// (3) html / head / body を書かなくても作られる(本文は暗黙の body に入る)
// 直した箇所は errors に記録する。HTML Standard は parse error を「報告してよいが、処理は仕様どおり続ける」と定める。
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
tag: String,
children: RefCell<Vec<Rc<Node>>>,
}
fn node(tag: &str) -> Rc<Node> {
Rc::new(Node { tag: tag.to_string(), children: RefCell::new(Vec::new()) })
}
fn parse(html: &str) -> (Rc<Node>, Vec<String>) {
let mut errors = Vec::new();
let document = node("#document");
let mut open: Vec<Rc<Node>> = Vec::new();
// (3) 暗黙の html / head / body
for tag in ["html", "head", "body"] {
let el = node(tag);
let parent = open.last().cloned().unwrap_or_else(|| document.clone());
parent.children.borrow_mut().push(el.clone());
if tag == "head" {
continue; // head はすぐ閉じる(中身を書いていないので)
}
open.push(el);
}
let mut rest = html;
while !rest.is_empty() {
if let Some(after) = rest.strip_prefix('<') {
let gt = after.find('>').unwrap_or(after.len());
let name = &after[..gt];
rest = &after[(gt + 1).min(after.len())..];
if let Some(end) = name.strip_prefix('/') {
if open.iter().any(|n| n.tag == end) {
// (2) その要素まで下ろす。間に閉じ忘れがあれば一緒に閉じる
while let Some(n) = open.pop() {
if n.tag == end {
break;
}
errors.push(format!("</{}> の前に <{}> が閉じられていない → 一緒に閉じた", end, n.tag));
}
} else {
errors.push(format!("</{}> は対応する開始タグが無い → 無視", end)); // (1)
}
} else {
let el = node(name);
open.last().unwrap().children.borrow_mut().push(el.clone());
open.push(el);
}
} else {
let lt = rest.find('<').unwrap_or(rest.len());
open.last().unwrap().children.borrow_mut().push(node(&format!("#text {:?}", &rest[..lt])));
rest = &rest[lt..];
}
}
(document, errors)
}
fn dump(n: &Rc<Node>, depth: usize) {
println!("{}{}", " ".repeat(depth), if n.tag.starts_with('#') { n.tag.clone() } else { format!("<{}>", n.tag) });
for c in n.children.borrow().iter() {
dump(c, depth + 1);
}
}
fn main() {
let html = "<div><p>a</b>b</div><span>c</span>";
println!("入力: {}\n", html);
let (doc, errors) = parse(html);
dump(&doc, 0);
println!();
for e in &errors {
println!("parse error: {}", e);
}
}
3実務に写す: 壊れた CSV を止まらずに読む
HTML のと同じ設計を CSV に写しました。列が足りない行は空文字で埋め、多い行は切り捨て、数値にならない値は None にして、どう直したかを errors に記録して続けます。実行すると 4 行すべてが読め、3 行目は列不足、4 行目は qty が数値でない、6 行目は列が多すぎる、という 3 件の記録が行番号つきで出ます。
Rust
// 実務に写す: 壊れた CSV を止まらずに読む。列が足りない行は空文字で埋め、多い行は切り捨て、
// 数値にならない値は None にして、どう直したかを errors に記録する。
// HTML の構文解析と同じ設計: 「最初の壊れた行で全部を捨てる」のではなく、「直して続け、記録を残す」。
#[derive(Debug, PartialEq)]
struct Row {
name: String,
qty: Option<u32>,
unit: String,
}
fn parse_csv(text: &str) -> (Vec<Row>, Vec<String>) {
let mut rows = Vec::new();
let mut errors = Vec::new();
for (i, line) in text.lines().enumerate().skip(1) {
let lineno = i + 1;
if line.trim().is_empty() {
continue; // 空行は読み飛ばす(エラーにもしない)
}
let mut cols: Vec<&str> = line.split(',').map(|c| c.trim()).collect();
if cols.len() < 3 {
errors.push(format!("{} 行目: 列が {} 個しかない → 空文字で埋めた", lineno, cols.len()));
cols.resize(3, "");
} else if cols.len() > 3 {
errors.push(format!("{} 行目: 列が {} 個ある → 4 列目以降を捨てた", lineno, cols.len()));
cols.truncate(3);
}
let qty = match cols[1].parse::<u32>() {
Ok(n) => Some(n),
Err(_) => {
errors.push(format!("{} 行目: qty {:?} が数値でない → None にした", lineno, cols[1]));
None
}
};
rows.push(Row { name: cols[0].to_string(), qty, unit: cols[2].to_string() });
}
(rows, errors)
}
fn main() {
let csv = "\
name,qty,unit
apple,3,kg
banana,2
cherry,x,box
durian,1,pc,extra,columns
";
let (rows, errors) = parse_csv(csv);
println!("読めた行: {}", rows.len());
for r in &rows {
println!(" {:<8} qty={:<8} unit={:?}", r.name, format!("{:?}", r.qty), r.unit);
}
println!("直した箇所: {}", errors.len());
for e in &errors {
println!(" {}", e);
}
}