在這篇文言,
我想紀錄一下並分享一個用 Java 檢查 HTML unclosed (或
unpaired) tag,並嘗試將其補上缺少的 開頭 tag(openTag)/結尾 tag (closeTag)
的方法。
Java 並不是重點,只要了解邏輯,用什麼程式語言都可以,只是我習慣 Java 所以用 Java 來做例子。
在 HTML 中,除了一些不用成對的 tag ,例如 <img/>, <b/> 等,
大多數的
tag 都是需要成對的,例如 <div> , <p>, <span>
等,需要配合使用 </div>, </p>, </span>
來使之頭尾成對才是合法的 HTML tag。
我們希望能用程式來幫助我們成出一串 HTML 中不成對的非法 tag,
並嘗試將缺少的
openTag 或 closeTag 補上將其修正成合法的 HTML,
要注意到的是,沒有一定正確的修正方法,因為我們不會知道原來寫這串
HTML 的原作者是哪裡忘記少加 openTag/closeTag 了,
例如現在有一串 HTML:
<div> xxx <div> xxx
原作者到底原本是想寫
<div> xxx </div> <div> xxx </div>
還是
<div>
xxx
<div>
xxx
</div>
</div>
呢?
我們不得而知,而兩種修正方法最後修完的 HTML 也確實都是有成對 tag 的合法 HTML。
在這篇文章裡,我的修法邏輯是採用上述第二種,將缺少的 tag 從外圍補上的方法。
先來直接看最後成品的 Java 程式碼例子,說明及最後的輸出結果都注釋在程式碼中了:
package test;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JustT {
public static void main(String[] args) {
String htmlStr = "<div><div id='kk'>xxx</p></p><div><div>\n" + "<div></p><div>";
// 在這裡我們只修正 div 和 p 這兩個 tag 做例子
// 如果還要修正其他 tag 的話,可以補在 tagToFix 變數中,用 | 符號來分隔
String tagsToFix = "div|p";
String regularExpression = "<(\\/?(?:" + tagsToFix + "))(?:\\s+?(?:.*?))?>";
System.out.println("===========================");
System.out.println("測試測試的句子:\n" + htmlStr);
System.out.println("正規表示法: " + regularExpression);
Pattern pattern = Pattern.compile(regularExpression, Pattern.MULTILINE);
Matcher matcher = pattern.matcher(htmlStr);
Deque<String> foundOpenTagStack = new ArrayDeque<>();
int nextOffset = 0;
int currentOffset = 0;
for (int matchCount = 1; matcher.find(); matchCount++) {
// groupCount不包括匹配的字串,即matcher.group(0)
System.out.println("---------------------------");
System.out.println("第" + matchCount + "次匹配,找到" + matcher.groupCount() + "個Group:");
System.out.println("匹配的字串為: " + matcher.group(0));
System.out.println("start: " + matcher.start() + ", end: " + matcher.end());
for (int groupCount = 1; groupCount <= matcher.groupCount(); groupCount++) {
System.out.println("第" + groupCount + "個Group: " + matcher.group(groupCount));
String tag = matcher.group(groupCount);
boolean isOpenTag = !tag.contains("/");
System.out.println("isOpenTag: " + isOpenTag);
if (isOpenTag) {
currentOffset = nextOffset;
System.out.println("是 openTag, 將其放進堆疊中");
foundOpenTagStack.push(tag);
showStackContent(foundOpenTagStack);
} else {
while (true) {
if (foundOpenTagStack.isEmpty()) {
// add missing open tag
System.out.println("最後還是找不到與 closeTag(" + tag + ") 相對的 openTag,\n"
+ "代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正");
String strNeedToAdd = "<" + tag.replace("/", "") + ">";
htmlStr = strNeedToAdd + htmlStr;
nextOffset += strNeedToAdd.length();
System.out.println("修正後的 HTML: " + htmlStr);
break;
}
String openTag = foundOpenTagStack.pop();
System.out.println("取出堆疊中的 openTag(" + openTag + ")");
showStackContent(foundOpenTagStack);
if (openTag.equalsIgnoreCase(tag.replace("/", ""))) {
// found matched open tag
System.out.println("找到與 closeTag(" + tag + ")相對應的 openTag,此tag不需修正");
System.out.println("目前 HTML: " + htmlStr);
break;
}
// add missing end tag
String strNeedToAdd = "</" + openTag + ">";
StringBuilder sb = new StringBuilder(htmlStr);
sb.insert(matcher.start() + currentOffset, strNeedToAdd);
htmlStr = sb.toString();
nextOffset += strNeedToAdd.length();
System.out.println("找到與 closeTag(" + tag + ")不相對應的 openTag(" + openTag + "),\n"
+ "代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正");
System.out.println("修正後的 HTML: " + htmlStr);
}
}
}
}
// handle left unclosed open tags, add missing close tag
while (!foundOpenTagStack.isEmpty()) {
System.out.println("發現堆疊中留有缺少 closeTag 的 openTag 未修正");
String openTag = foundOpenTagStack.pop();
System.out.println("取出堆疊中的 openTag");
showStackContent(foundOpenTagStack);
htmlStr = htmlStr + "</" + openTag + ">";
System.out.println("補上 openTag 缺少的相對應 closeTag");
System.out.println("修正後的 HTML: " + htmlStr);
}
System.out.println("===========================");
System.out.println();
System.out.println("最後修正完後的 HTML: " + htmlStr);
//輸出的結果:
/*
===========================
測試測試的句子:
<div><div id='kk'>xxx</p></p><div><div>
<div></p><div>
正規表示法: <(\/?(?:div|p))(?:\s+?(?:.*?))?>
---------------------------
第1次匹配,找到1個Group:
匹配的字串為: <div>
start: 0, end: 5
第1個Group: div
isOpenTag: true
是 openTag, 將其放進堆疊中
目前堆疊成員:
<==>|div|
---------------------------
第2次匹配,找到1個Group:
匹配的字串為: <div id='kk'>
start: 5, end: 18
第1個Group: div
isOpenTag: true
是 openTag, 將其放進堆疊中
目前堆疊成員:
<==>|div|div|
---------------------------
第3次匹配,找到1個Group:
匹配的字串為: </p>
start: 21, end: 25
第1個Group: /p
isOpenTag: false
取出堆疊中的 openTag(div)
目前堆疊成員:
<==>|div|
找到與 closeTag(/p)不相對應的 openTag(div),
代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
修正後的 HTML: <div><div id='kk'>xxx</div></p></p><div><div>
<div></p><div>
取出堆疊中的 openTag(div)
目前堆疊成員:
<==>|
找到與 closeTag(/p)不相對應的 openTag(div),
代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
修正後的 HTML: <div><div id='kk'>xxx</div></div></p></p><div><div>
<div></p><div>
最後還是找不到與 closeTag(/p) 相對的 openTag,
代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正
修正後的 HTML: <p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></p><div>
---------------------------
第4次匹配,找到1個Group:
匹配的字串為: </p>
start: 25, end: 29
第1個Group: /p
isOpenTag: false
最後還是找不到與 closeTag(/p) 相對的 openTag,
代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正
修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></p><div>
---------------------------
第5次匹配,找到1個Group:
匹配的字串為: <div>
start: 29, end: 34
第1個Group: div
isOpenTag: true
是 openTag, 將其放進堆疊中
目前堆疊成員:
<==>|div|
---------------------------
第6次匹配,找到1個Group:
匹配的字串為: <div>
start: 34, end: 39
第1個Group: div
isOpenTag: true
是 openTag, 將其放進堆疊中
目前堆疊成員:
<==>|div|div|
---------------------------
第7次匹配,找到1個Group:
匹配的字串為: <div>
start: 40, end: 45
第1個Group: div
isOpenTag: true
是 openTag, 將其放進堆疊中
目前堆疊成員:
<==>|div|div|div|
---------------------------
第8次匹配,找到1個Group:
匹配的字串為: </p>
start: 45, end: 49
第1個Group: /p
isOpenTag: false
取出堆疊中的 openTag(div)
目前堆疊成員:
<==>|div|div|
找到與 closeTag(/p)不相對應的 openTag(div),
代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></div></p><div>
取出堆疊中的 openTag(div)
目前堆疊成員:
<==>|div|
找到與 closeTag(/p)不相對應的 openTag(div),
代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></div></div></p><div>
取出堆疊中的 openTag(div)
目前堆疊成員:
<==>|
找到與 closeTag(/p)不相對應的 openTag(div),
代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></div></div></div></p><div>
最後還是找不到與 closeTag(/p) 相對的 openTag,
代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正
修正後的 HTML: <p><p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></div></div></div></p><div>
---------------------------
第9次匹配,找到1個Group:
匹配的字串為: <div>
start: 49, end: 54
第1個Group: div
isOpenTag: true
是 openTag, 將其放進堆疊中
目前堆疊成員:
<==>|div|
發現堆疊中留有缺少 closeTag 的 openTag 未修正
取出堆疊中的 openTag
目前堆疊成員:
<==>|
補上 openTag 缺少的相對應 closeTag
修正後的 HTML: <p><p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></div></div></div></p><div></div>
===========================
最後修正完後的 HTML: <p><p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
<div></div></div></div></p><div></div>
*/
}
public static <T> void showStackContent(Deque<T> stack) {
System.out.println("目前堆疊成員:");
System.out.print("<==>|");
stack.forEach((T member) -> {
System.out.print(member + "|");
});
System.out.println("");
}
}
說明:
在這次的例子中,先以只修正 div 和 p 為例,有需要修正其他的 tag 的話可自行補充到 tagsToFix 變數中 (用 | 符號分隔)。
實現的邏輯很簡單,首先我們要找出所有要修正的 tag 的 openTag 和 closeTag,
尋找 tag 使用了如下的正規表達示:
<(\/?(?:div|p))(?:\s+?(?:.*?))?>
找到了以後,
1. 我們依序看找到的 tag,如果每個 tag 都看過了,就進到第2步,否則進到第1步。
1-1. 如果是屬於 openTag 就放到一個名為 foundOpenTagStack 的堆疊 (Stack) 中,否則到第1-2步。
1-2. 如果是屬於 closeTag,就從堆疊中取出 openTag 來作比較。
1-2-1. 如果被取出的 openTag 和 closeTag 是成對的,那即是代表這兩個 tag 是一對的,不用對 HTML 做修正,並且因為這個 closeTag 已經處理完成,我們可以繼續處理下一個找到的 closeTag,回到第1步。
1-2-2. 如果被取出的 openTag 和 closeTag 是不成對的,那即是代表這個 openTag 缺少了對應的 closeTag,我們便必需對 HTML 做修正,在 HTML 中找到 closeTag 的位置前面補上 openTag 缺少的結尾 tag ,
因為 closeTag 還沒被處理好,可能堆疊中還有與其成對的 openTag,所以回到 1-2 步繼續取出堆疊中的 openTag 做檢查。
1-2-3. 如果堆疊中的 openTag 都拿出來處理過並看過了 (此時堆疊中應該是已經沒有任何東西了),
但還是找不到與 closeTag 相應的 openTag 的話,代表 closeTag 缺少相對應的開頭 Tag,
所以我們必須要修正 HTML,在 HTML 的開頭加上對應的開頭 tag,處理完 closeTag 後,回到第1步繼續處理下一個 closeTag。
2. 當每個 tag 都看過了,此時 closeTag 都處理完畢了,但堆疊中可能還留有未處理的 openTag,例如如果 HTML 是 <div><div> 的話,因為沒有 closeTag,所以上述步驟執行完後會發現堆疊中還留有未處理的 openTag,而此時堆疊中的 openTag 都是缺少成對結尾 tag 的,
所以我們可以一個個把 openTag 取出,並對其修正 HTML,在 HTML 的尾端補上缺少的結尾 tag 。
當上述步驟都做完了以後,我們就能得到被修正好的 HTML 了。
另外補充一點,在 1-2-2 補上 openTag 的 closeTag 時,我們可以利用 matcher.start() 來得知要在 HTML 句子中的哪個位置補上 closeTag,但是因為在每個步驟補上 closeTag 和 openTag 後會改變 HTML 句子的長度,造成 matcher.start() 還要在位移一下才能正確地指到現在 htmlStr 要補上 closeTag 的位置,所以我們使用 currentOffset 和 nextOffset 進行累加要偏移的字數進行修正。
而當第一個 closeTag 處理完畢後,堆疊中的所有 openTag 都會被處理完並被取出堆疊的關係,
所以其他的 closeTag 在 1-2-3 補上對應的開頭 tag 時,只要把開頭 tag 加在 HTML 開頭就行了。
在第2步,只有一開始 HTML 裡都沒有 closeTag 時才會發生堆疊中留有 openTag,此時對應的缺少結尾 tag 只要加在 HTML 尾端就行了。
沒有留言 :
張貼留言