2024年4月14日 星期日

檢查 unclodsed(unpaired) html tag 並將其修正 - 使用 Java

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

沒有留言 :

張貼留言