在這篇文言,
我想紀錄一下並分享一個用 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 尾端就行了。