基于Python打造超漂亮的HTML兩欄文本對(duì)比工具
在日常開(kāi)發(fā)、文檔撰寫(xiě)或代碼審查中,我們經(jīng)常需要對(duì)比兩個(gè)文本文件的差異。雖然系統(tǒng)自帶的 diff 工具或 IDE 插件可以滿足基本需求,但它們往往不夠直觀,尤其是當(dāng)需要分享給非技術(shù)人員時(shí)。
今天,我將帶你用 Python + difflib + HTML + CSS 手動(dòng)打造一個(gè)美觀、清晰、支持行內(nèi)字符級(jí)差異高亮的兩欄對(duì)比工具,并自動(dòng)生成可瀏覽器查看的 HTML 報(bào)告!
功能亮點(diǎn)
雙欄并排顯示:左右分別為兩個(gè)文件內(nèi)容,一目了然
行號(hào)標(biāo)注:每行左側(cè)顯示行號(hào),刪除/新增行特殊標(biāo)記為 -
行內(nèi)差異高亮:
- 紅色刪除線表示被刪內(nèi)容(
<span class="highlight-delete">) - 綠色加粗表示新增內(nèi)容(
<span class="highlight-insert">)
智能分組上下文:使用 difflib.SequenceMatcher 智能識(shí)別修改塊,保留上下文
自動(dòng)換行 & 響應(yīng)式布局:長(zhǎng)行自動(dòng)折行,適配不同屏幕
美觀現(xiàn)代化 UI:采用扁平化設(shè)計(jì) + 陰影 + 圓角 + 懸停效果
一鍵打開(kāi)瀏覽器預(yù)覽:生成后自動(dòng)調(diào)用本地瀏覽器打開(kāi)結(jié)果
核心技術(shù)棧
| 技術(shù) | 用途 |
|---|---|
| difflib | 計(jì)算文本差異(行級(jí) + 字符級(jí)) |
| os, urllib.parse | 文件路徑處理與 URL 構(gòu)造 |
| webbrowser | 自動(dòng)打開(kāi)瀏覽器 |
| HTML/CSS | 渲染可視化界面 |
| pre-wrap + word-break | 實(shí)現(xiàn)代碼塊自動(dòng)換行 |
實(shí)現(xiàn)思路詳解
1. 讀取文件內(nèi)容
def read_file(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
return [line.rstrip('\n') for line in f.readlines()]
注意:保留原始換行符信息用于對(duì)比,但去除 \n 避免干擾字符級(jí) diff。
2. 行內(nèi)字符級(jí)差異高亮
利用 difflib.SequenceMatcher.get_opcodes() 分析兩行字符串的差異:
def highlight_line_diff(text1, text2):
sm = difflib.SequenceMatcher(None, text1, text2)
result1 = []
result2 = []
for tag, i1, i2, j1, j2 in sm.get_opcodes():
part1 = text1[i1:i2]
part2 = text2[j1:j2]
if tag == 'equal':
result1.append(part1)
result2.append(part2)
elif tag == 'delete':
result1.append(f'<span class="highlight-delete">{part1}</span>')
elif tag == 'insert':
result2.append(f'<span class="highlight-insert">{part2}</span>')
elif tag == 'replace':
result1.append(f'<span class="highlight-delete">{part1}</span>')
result2.append(f'<span class="highlight-insert">{part2}</span>')
return ''.join(result1), ''.join(result2)
示例:
- 原文:
Hello world - 修改:
Hello Python - 輸出:
Hello <del>world</del><ins>Python</ins>
3. 使用get_grouped_opcodes(n=3)顯示上下文
matcher = difflib.SequenceMatcher(None, lines1, lines2) blocks = matcher.get_grouped_opcodes(n=3) # 每個(gè)差異塊前后保留3行相同內(nèi)容
這樣不會(huì)只展示“純差異”,而是讓用戶看到變更前后的完整邏輯上下文,極大提升可讀性。
4. 構(gòu)建 HTML 表格結(jié)構(gòu)
每一行生成如下結(jié)構(gòu):
<tr>
<td class="line-num">42</td>
<td class="content left"><pre>這是修改過(guò)的<del>舊內(nèi)容</del><ins>新內(nèi)容</ins></pre></td>
<td class="line-num ins">42</td>
<td class="content right added"><pre>這是修改過(guò)的<ins>新內(nèi)容</ins></pre></td>
</tr>
并通過(guò) CSS 控制樣式:
- 刪除行:粉紅底 + 紅邊框
- 新增行:淡綠底 + 綠邊框
- 修改行:淺黃底 + 橙邊框
- 空白占位:灰色斜體
-
5. 美化 CSS 樣式(關(guān)鍵技巧)
td.content pre {
white-space: pre-wrap; /* 保留空格和換行 */
word-wrap: break-word; /* 超長(zhǎng)單詞也能斷行 */
}
table.diff {
table-layout: fixed; /* 固定列寬,避免錯(cuò)位 */
width: 100%;
}
小貼士:如果不設(shè)置 table-layout: fixed,長(zhǎng)文本會(huì)撐破表格!
使用方法
只需調(diào)用函數(shù)即可:
generate_beautiful_side_by_side_diff(
file1_path="file1.txt",
file2_path="file2.txt",
open_in_browser=True,
output_html="【美化版】?jī)蓹趯?duì)比結(jié)果.html"
)
運(yùn)行后會(huì)自動(dòng):
- 生成 HTML 文件
- 打印路徑
- 自動(dòng)用默認(rèn)瀏覽器打開(kāi)預(yù)覽
效果截圖預(yù)覽(文字描述)



想象一下這個(gè)畫(huà)面:
- 頂部是深藍(lán)色標(biāo)題欄:“文檔兩欄對(duì)比”
- 下方是一個(gè)整潔的雙欄表格,左側(cè)是
file1.txt,右側(cè)是file2.txt - 修改過(guò)的行左側(cè)行號(hào)變橙色,背景微黃,左邊有橙色豎條
- 被刪除的行畫(huà)了刪除線,紅色提示
- 新增的行綠色高亮,還有綠色邊框
- 每一行內(nèi)的變動(dòng)字符都被精準(zhǔn)標(biāo)記
- 最下方還有說(shuō)明圖例:紅=刪除|綠=新增|黃=修改
是不是比 Git 的命令行 diff 清晰多了?
完整代碼(可直接復(fù)制運(yùn)行)
import difflib
import os
import webbrowser
from urllib.parse import urljoin, urlparse
def read_file(filepath):
"""讀取文件內(nèi)容,按行返回列表(每行保留換行符)"""
try:
with open(filepath, 'r', encoding='utf-8') as f:
lines = f.readlines()
return [line.rstrip('\n') for line in lines]
except Exception as e:
print(f"無(wú)法讀取文件: {filepath}, 錯(cuò)誤: {e}")
return []
def highlight_line_diff(text1, text2):
"""對(duì)兩個(gè)字符串進(jìn)行字符級(jí)差異高亮"""
sm = difflib.SequenceMatcher(None, text1, text2)
result1 = []
result2 = []
for tag, i1, i2, j1, j2 in sm.get_opcodes():
part1 = text1[i1:i2]
part2 = text2[j1:j2]
if tag == 'equal':
result1.append(part1)
result2.append(part2)
elif tag == 'delete':
result1.append(f'<span class="highlight-delete">{part1}</span>')
result2.append(part2)
elif tag == 'insert':
result1.append(part1)
result2.append(f'<span class="highlight-insert">{part2}</span>')
elif tag == 'replace':
result1.append(f'<span class="highlight-delete">{part1}</span>')
result2.append(f'<span class="highlight-insert">{part2}</span>')
return ''.join(result1), ''.join(result2)
def generate_beautiful_side_by_side_diff(file1_path, file2_path, open_in_browser, output_html="diff_comparison.html"):
"""生成美化版兩欄對(duì)比 HTML"""
lines1 = read_file(file1_path)
lines2 = read_file(file2_path)
filename1 = os.path.basename(file1_path)
filename2 = os.path.basename(file2_path)
matcher = difflib.SequenceMatcher(None, lines1, lines2)
blocks = matcher.get_grouped_opcodes(n=3)
table_rows = []
for group in blocks:
for tag, i1, i2, j1, j2 in group:
if tag == 'equal':
for line1, line2 in zip(lines1[i1:i2], lines2[j1:j2]):
hl_line1, hl_line2 = highlight_line_diff(line1, line2)
table_rows.append(f"""
<tr>
<td class="line-num">{i1 + 1}</td>
<td class="content left"><pre>{hl_line1 or ' '}</pre></td>
<td class="line-num">{j1 + 1}</td>
<td class="content right"><pre>{hl_line2 or ' '}</pre></td>
</tr>""")
i1 += 1; j1 += 1
elif tag == 'delete':
for line in lines1[i1:i2]:
hl_line, _ = highlight_line_diff(line, "")
table_rows.append(f"""
<tr>
<td class="line-num del">{i1 + 1}</td>
<td class="content left deleted"><pre>{hl_line or ' '}</pre></td>
<td class="line-num">-</td>
<td class="content right empty"><pre>-</pre></td>
</tr>""")
i1 += 1
elif tag == 'insert':
for line in lines2[j1:j2]:
_, hl_line = highlight_line_diff("", line)
table_rows.append(f"""
<tr>
<td class="line-num">-</td>
<td class="content left empty"><pre>-</pre></td>
<td class="line-num ins">{j1 + 1}</td>
<td class="content right added"><pre>{hl_line or ' '}</pre></td>
</tr>""")
j1 += 1
elif tag == 'replace':
max_len = max(i2 - i1, j2 - j1)
for k in range(max_len):
line1 = lines1[i1 + k] if i1 + k < i2 else ""
line2 = lines2[j1 + k] if j1 + k < j2 else ""
hl_line1, hl_line2 = highlight_line_diff(line1, line2)
lineno1 = str(i1 + k + 1) if i1 + k < i2 else "-"
lineno2 = str(j1 + k + 1) if j1 + k < j2 else "-"
cls1 = "replaced" if line1 else "empty"
cls2 = "replaced" if line2 else "empty"
table_rows.append(f"""
<tr>
<td class="line-num {cls1}">{lineno1}</td>
<td class="content left {cls1}"><pre>{hl_line1 or ' '}</pre></td>
<td class="line-num {cls2}">{lineno2}</td>
<td class="content right {cls2}"><pre>{hl_line2 or ' '}</pre></td>
</tr>""")
if not table_rows:
table_rows.append("""
<tr>
<td colspan="2" style="text-align:center; color:green;">? 兩文件內(nèi)容完全相同</td>
<td colspan="2" style="text-align:center; color:green;">? No differences found</td>
</tr>
""")
custom_css = """
<style>
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background-color: #f8f9fa;
color: #333;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
header {
background: #2c3e50;
color: white;
padding: 15px 20px;
text-align: center;
}
header h1 {
margin: 0;
font-size: 1.5em;
}
.diff-table-container {
overflow-x: auto;
}
table.diff {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 14px;
}
table.diff th {
background: #34495e;
color: white;
padding: 10px 8px;
text-align: center;
position: sticky;
top: 0;
z-index: 10;
}
td.line-num {
width: 40px;
text-align: right;
font-weight: bold;
color: #777;
background: #f8f8f8;
padding: 6px 4px;
user-select: none;
white-space: nowrap;
font-family: monospace;
font-size: 13px;
}
td.line-num.del { color: #e74c3c; }
td.line-num.ins { color: #2ecc71; }
td.content {
width: 45%;
white-space: normal;
word-wrap: break-word;
word-break: break-word;
padding: 6px 8px;
vertical-align: top;
}
td.content pre {
margin: 0;
font-family: "Consolas", "Menlo", "Monaco", monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-word;
background: none;
border: none;
padding: 0;
}
table.diff tr:hover {
background-color: #f1f9ff;
}
td.deleted {
background-color: #fff8f8;
border-left: 3px solid #e74c3c;
}
td.added {
background-color: #f8fff8;
border-left: 3px solid #2ecc71;
}
td.replaced {
background-color: #fffff0;
border-left: 3px solid #f39c12;
}
td.empty {
color: #ccc;
font-style: italic;
}
.highlight-delete {
background-color: #ffebee;
text-decoration: line-through;
color: #c62828;
padding: 0 2px;
border-radius: 3px;
}
.highlight-insert {
background-color: #e8f5e8;
color: #2e7d32;
font-weight: bold;
padding: 0 2px;
border-radius: 3px;
}
</style>
"""
full_html = f"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>文檔對(duì)比結(jié)果</title>
{custom_css}
</head>
<body>
<div class="container">
<header>
<h1>?? 文檔兩欄對(duì)比</h1>
<p>{filename1} <strong>vs</strong> {filename2}</p>
</header>
<div class="diff-table-container">
<table class="diff">
<thead>
<tr>
<th style="width:40px">行號(hào)</th>
<th style="width:45%">{filename1}</th>
<th style="width:40px">行號(hào)</th>
<th style="width:45%">{filename2}</th>
</tr>
</thead>
<tbody>
{''.join(table_rows)}
</tbody>
</table>
</div>
<div style="padding: 15px; font-size: 13px; color: #666; text-align: center; background: #f5f5f5;">
<strong>說(shuō)明:</strong>
<span style="color: #e74c3c;">刪除</span> |
<span style="color: #2ecc71;">新增</span> |
<span style="color: #f39c12;">修改</span>
? 行內(nèi)變化已高亮
</div>
</div>
</body>
</html>"""
with open(output_html, 'w', encoding='utf-8') as f:
f.write(full_html)
print(f"? 美化版兩欄對(duì)比已生成: {os.path.abspath(output_html)}")
print(f"?? 請(qǐng)用瀏覽器打開(kāi)查看優(yōu)化后的效果。")
if open_in_browser:
try:
abs_path = os.path.abspath(output_html)
file_url = 'file:///' + abs_path.replace('\\', '/') if os.name == 'nt' else urljoin('file:', urlparse(abs_path).path.replace(os.sep, '/'))
webbrowser.open(file_url)
print(f"?? 已自動(dòng)打開(kāi)瀏覽器預(yù)覽: {file_url}")
except Exception as e:
print(f"?? 瀏覽器打開(kāi)失敗,請(qǐng)手動(dòng)打開(kāi):\n {abs_path}")
# === 使用示例 ===
if __name__ == "__main__":
file1 = r"C:\Users\Administrator\Desktop\file2.txt"
file2 = r"C:\Users\Administrator\Desktop\file1.txt"
generate_beautiful_side_by_side_diff(file1, file2, True, "【美化版】?jī)蓹趯?duì)比結(jié)果.html")
結(jié)語(yǔ)
這個(gè)小工具雖然只有 300 多行代碼,但卻融合了文本處理、算法匹配、前端渲染和用戶體驗(yàn)設(shè)計(jì)。它不僅實(shí)用,還能作為學(xué)習(xí) difflib 和 HTML/CSS 布局的優(yōu)秀范例。
到此這篇關(guān)于基于Python打造超漂亮的HTML兩欄文本對(duì)比工具的文章就介紹到這了,更多相關(guān)Python兩欄文本對(duì)比內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Python針對(duì)不同文本長(zhǎng)度的處理方案總結(jié)與對(duì)比
- 在python中獲取div的文本內(nèi)容并和想定結(jié)果進(jìn)行對(duì)比詳解
- Python實(shí)現(xiàn)的文本對(duì)比報(bào)告生成工具示例
- 使用Python實(shí)現(xiàn)Word文檔的自動(dòng)化對(duì)比方案
- Python輕松實(shí)現(xiàn)Word文檔對(duì)比并生成可視化HTML報(bào)告
- Python實(shí)現(xiàn)對(duì)比Word文檔差異的三種方式小結(jié)
- Python操作Word文檔7種方法的實(shí)現(xiàn)與對(duì)比(史上最全)
相關(guān)文章
python?PyQt5(自定義)信號(hào)與槽使用及說(shuō)明
這篇文章主要介紹了python?PyQt5(自定義)信號(hào)與槽使用及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-12-12
python腳本監(jiān)控logstash進(jìn)程并郵件告警實(shí)例
這篇文章主要介紹了python腳本監(jiān)控logstash進(jìn)程并郵件告警實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04
Python數(shù)學(xué)建模學(xué)習(xí)模擬退火算法整數(shù)規(guī)劃問(wèn)題示例解析
整數(shù)規(guī)劃問(wèn)題在工業(yè)、經(jīng)濟(jì)、國(guó)防、醫(yī)療等各行各業(yè)應(yīng)用十分廣泛,是指規(guī)劃中的變量(全部或部分)限制為整數(shù),屬于離散優(yōu)化問(wèn)題Discrete Optimization2021-10-10
python如何通過(guò)pyarmor庫(kù)保護(hù)源代碼
pyarmor是一款用于加密和保護(hù)python腳本的工具,可以用于防止源代碼泄露,下面我們就來(lái)學(xué)習(xí)一下Python如何利用pyarmor庫(kù)來(lái)保護(hù)源代碼吧2024-11-11
OpenCV-Python實(shí)現(xiàn)懷舊濾鏡與連環(huán)畫(huà)濾鏡
很多時(shí)候通過(guò)ps可以做很多效果,今天我們來(lái)介紹使用OpenCV-Python實(shí)現(xiàn)懷舊濾鏡與連環(huán)畫(huà)濾鏡,具有一定的參考價(jià)值,感興趣的可以了解一下2021-06-06
python進(jìn)行TCP端口掃描的實(shí)現(xiàn)
這篇文章主要介紹了python進(jìn)行TCP端口掃描的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12

