Java實現(xiàn)Html保存為.mhtml文件的代碼邏輯
更新時間:2026年01月19日 10:10:43 作者:知秋正在996
文章介紹了實現(xiàn)將HTML字符串保存為.mhtml文件的代碼邏輯,包括通過URL和Cookie免密獲取HTML字符串,將HTML中的圖片、CSS、JS轉(zhuǎn)換為base64字符串,刪除不需要的布局和內(nèi)容,最終將替換后的HTML保存為.mhtml文件,感興趣的朋友跟隨小編一起看看吧
功能需求
將html字符串保存為.mhtml文件
代碼實現(xiàn)
- pom.xml依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.43</version>
</dependency>
<!-- Jsoup:解析HTML標簽、提取圖片/樣式資源,必備 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
<!-- Apache工具包:Base64編碼圖片資源、IO流處理,必備 -->
<!-- Source: https://mvnrepository.com/artifact/commons-codec/commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
<scope>compile</scope>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
<scope>compile</scope>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>compile</scope>
</dependency>
</dependencies>- 獲取通過訪問url獲取html字符串內(nèi)容工具類
@Slf4j
public class WikiUtils {
/**
* 獲取wiki 頁面html
*/
public static String getConfluencePageHtml(String url,String cookie) {
String value = "";
HttpResponse httpResponse = HttpClient.httpGetResponse(url, cookie);
if (httpResponse.isOk()){
value = httpResponse.body();
}else if (httpResponse.getStatus() == 403|| httpResponse.getStatus() == 302){
log.error("無效的cookie,無權(quán)限訪問");
}else {
log.error("獲取html頁面失敗");
}
return value;
}
/**
* 在請求頭中放入cookie,避免登錄攔截
*/
public static HttpResponse httpGetResponse(String url,String cookie) {
Map<String, String> headers = new HashMap<>();
headers.put("Cookie", cookie);
//登錄
HttpResponse response = HttpRequest.get(url).headerMap(headers, true).execute();
return response;
}
}- Html轉(zhuǎn)換.mhtml核心類
@Slf4j
public class Html2MHTCompiler {
public static String parseTittle(String html) {
Document doc = Jsoup.parse(html);
Element titleElement = doc.selectFirst("title");
if (titleElement != null) {
String text = titleElement.text();
int i = text.indexOf("-");
if (i > 0) {
return text.substring(0, i).trim();
}
return text.trim();
}
return null;
}
// 原資源URL -> 資源的Base64編碼(帶MIME頭)
public static Map<String, String> parseHtmlPage(String cookie,String html, String baseUrl) {
Map<String, String> resourceMap = new HashMap<>();
Document doc = Jsoup.parse(html);
// ========== 1. 提取所有 img 圖片資源 ==========
Elements imgElements = doc.select("img[src]");
for (Element imgElement : imgElements) {
String imgSrc = imgElement.attr("src");
parseResource(cookie,imgSrc,"image",baseUrl, resourceMap);
}
// ========== 2. 提取所有 link 外鏈CSS樣式表資源==========
Elements cssElements = doc.select("link[rel=stylesheet][href]");
for (Element cssElement : cssElements) {
String cssHref = cssElement.attr("href");
parseResource(cookie,cssHref, "CSS",baseUrl, resourceMap);
}
// ========== 3. 提取所有 script 外鏈JS腳本資源 ==========
Elements jsElements = doc.select("script[src]");
for (Element jsElement : jsElements) {
String jsSrc = jsElement.attr("src");
parseResource(cookie,jsSrc,"javascript",baseUrl, resourceMap);
}
return resourceMap;
}
// ========== 刪除部分元素class="acs-side-bar ia-scrollable-section" 、
// class="ia-splitter-left"、
// id="header"
// id="navigation"
// id="likes-and-labels-container"、
// id="footer" 、
// id="comments-section"
// id="page-metadata-banner"
// id="breadcrumb-section"
// 、id="main"的style="margin-left: 285px;" ==========
public static String removeUnwantedElements(String html) {
Document doc = Jsoup.parse(html);
//刪除head標簽下的style標簽的屬性中的.ia-splitter-left #main 這兩個選擇器
removeCssSelectorFromStyleTag(doc, ".ia-splitter-left");
removeCssSelectorFromStyleTag(doc, "#main");
// 1. 刪除指定class的元素 → 側(cè)邊欄/左側(cè)面板 等冗余區(qū)域
doc.select(".acs-side-bar .ia-scrollable-section").remove();
doc.select(".ia-splitter-left").remove();
// 2. 刪除指定id的元素 → 點贊標簽區(qū)、頁腳、評論區(qū) 等無用模塊
// doc.getElementById("likes-and-labels-container").remove();
doc.getElementById("footer").remove();
doc.getElementById("header").remove();
doc.getElementById("navigation").remove();
doc.getElementById("comments-section").remove();
doc.getElementById("page-metadata-banner").remove();
doc.getElementById("breadcrumb-section").remove();
// 3. 精準移除 id="main" 標簽中【指定的style樣式:margin-left: 285px;】,保留其他style樣式
Element mainElement = doc.getElementById("main");
if (mainElement != null && mainElement.hasAttr("style")) {
// 獲取原style屬性值
String oldStyle = mainElement.attr("style");
// 移除指定的樣式段,保留其他樣式
String newStyle = oldStyle.replace("margin-left: 285px;", "").trim();
// 處理移除后style為空的情況,避免殘留空的style=""屬性
if (newStyle.isEmpty()) {
mainElement.removeAttr("style");
} else {
mainElement.attr("style", newStyle);
}
}
return doc.html();
}
/**
* 核心工具方法:刪除<head>標簽下所有<style>標簽內(nèi)的【指定CSS選擇器】及其對應(yīng)的所有樣式
* @param doc jsoup解析后的文檔對象
* @param selector 要刪除的css選擇器,如:.ia-splitter-left 、 #main
*/
private static void removeCssSelectorFromStyleTag(Document doc, String selector) {
// 1. 獲取head標簽下所有的style樣式標簽
Elements styleTags = doc.head().select("style");
if (styleTags.isEmpty()) {
return; // 沒有style標簽,直接返回
}
// 2. 遍歷每一個style標簽,處理內(nèi)部的css內(nèi)容
for (Element styleTag : styleTags) {
String cssContent = styleTag.html();
if (cssContent.isEmpty()) continue;
// 3. 精準匹配【選擇器 { 任意樣式內(nèi)容 }】 完整塊,含換行/空格/制表符,匹配規(guī)則全覆蓋
// 匹配規(guī)則:匹配 .ia-splitter-left { ... } 或 #main { ... } 完整的樣式塊
String regex = selector + "\\s*\\{[^}]*\\}";
// 替換匹配到的內(nèi)容為空,即刪除該選擇器及對應(yīng)樣式
String newCssContent = cssContent.replaceAll(regex, "").trim();
// 處理替換后多余的空行/空格,讓css內(nèi)容更整潔
newCssContent = newCssContent.replaceAll("\\n+", "\n").replaceAll("\\s+", " ");
// 4. 將處理后的css內(nèi)容重新寫入style標簽
styleTag.html(newCssContent);
}
}
// ========== 圖片/CSS/JS都復(fù)用這個方法 ==========
private static void parseResource(String cookie,String resourceSrc,String resourceType,String baseUrl, Map<String, String> resourceMap) {
try {
// 拼接完整URL(兼容:絕對路徑/相對路徑)
String fullResourceUrl = getFullUrl(baseUrl, resourceSrc);
// 下載資源文件,轉(zhuǎn)成【帶MIME頭的Base64編碼】
String base64Resource = downloadResourceToBase64(fullResourceUrl,resourceType, cookie);
resourceMap.put(resourceSrc, base64Resource);
} catch (Exception e) {
log.error("資源解析失敗,跳過該資源:" + resourceSrc, e);
}
}
// 拼接完整URL:處理相對路徑/絕對路徑 (原有方法,復(fù)用)
private static String getFullUrl(String baseUrl, String src) {
if (src.startsWith("http://") || src.startsWith("https://")) {
return src; // 絕對路徑,直接返回
} else if(src.startsWith("http://")){
return "https:" + src; // 兼容 //xxx.com/xxx.css 這種無協(xié)議路徑
} else {
return src.startsWith("/") ? baseUrl + src : baseUrl + "/" + src; // 相對路徑,拼接根路徑
}
}
// ========== 通用資源下載+Base64編碼方法,支持【圖片/CSS/JS】所有類型 ==========
private static String downloadResourceToBase64(String resourceUrl,String resourceType,String cookie) throws Exception {
URL url = new URL(resourceUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Cookie",cookie);
// 解決部分網(wǎng)站的反爬/跨域問題
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0");
conn.setRequestProperty("Connection", "keep-alive");
conn.setRequestProperty("Accept", "*/*");
if (resourceType.equals("image")){
conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
}
if (conn.getResponseCode() == 200) {
InputStream in = conn.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
byte[] resourceBytes = out.toByteArray();
// 對圖片類型做【體積壓縮+無損渲染】處理
if ("image".equalsIgnoreCase(resourceType) && resourceBytes.length > 0) {
resourceBytes = compressImage(resourceBytes, 0.7f); // 0.7是壓縮質(zhì)量,可調(diào)整
}
// 獲取資源的MIME類型 + Base64編碼,自動適配圖片/CSS/JS
String mimeType = conn.getContentType();
String base64 = Base64.encodeBase64String(resourceBytes);
in.close();
out.close();
conn.disconnect();
// 返回標準的data-url格式,可直接嵌入HTML替換原URL
return "data:" + mimeType + ";base64," + base64;
}
return null;
}
/**
* 核心圖片壓縮工具方法:圖片質(zhì)量壓縮(核心無坑)
* @param imageBytes 原圖字節(jié)流
* @param quality 壓縮質(zhì)量 0.1~1.0 ,推薦0.6~0.8 (數(shù)值越大越清晰,體積越大)
* @return 壓縮后的圖片字節(jié)流
*/
private static byte[] compressImage(byte[] imageBytes, float quality) throws Exception {
// 質(zhì)量值兜底,防止傳參錯誤
if (quality < 0.1f) quality = 0.1f;
if (quality > 1.0f) quality = 1.0f;
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
BufferedImage bufferedImage = ImageIO.read(bais);
if (bufferedImage == null) {
return imageBytes; // 非標準圖片,返回原圖
}
// 獲取圖片格式(png/jpg等)
String format = getImageFormat(imageBytes);
if (format == null) {
format = "jpeg";
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質(zhì)量壓縮,尺寸不變,清晰度無損,體積減小
ImageIO.write(bufferedImage, format, new MemoryCacheImageOutputStream(baos) {
@Override
public void write(byte[] b, int off, int len) {
try {
super.write(b, off, len);
} catch (Exception e) {
// 異常時直接寫入原圖,不影響
}
}
});
// 如果壓縮后體積變大,返回原圖
byte[] compressedBytes = baos.toByteArray();
bais.close();
baos.close();
return compressedBytes.length < imageBytes.length ? compressedBytes : imageBytes;
}
/**
* 獲取圖片真實格式
*/
private static String getImageFormat(byte[] imageBytes) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
ImageInputStream iis = ImageIO.createImageInputStream(bais);
Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
if (readers.hasNext()) {
ImageReader reader = readers.next();
String format = reader.getFormatName();
iis.close();
bais.close();
return format;
}
iis.close();
bais.close();
return null;
}
public static String embedResources(String html, Map<String, String> resources) {
String embeddedHtml = html;
// 遍歷所有資源,替換原URL為Base64編碼
for (Map.Entry<String, String> entry : resources.entrySet()) {
String resourceUrl = entry.getKey();
String resourceUrlEscape = resourceUrl.replace("&", "&");
String embeddedUrl = entry.getValue();
embeddedHtml = embeddedHtml.replace(resourceUrlEscape, embeddedUrl);
}
return embeddedHtml;
}
public static void saveAsMhtml(String html, String filePath) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(filePath), StandardCharsets.UTF_8)
)) {
// 寫入MHTML標準協(xié)議頭
writer.write("MIME-Version: 1.0");
writer.newLine();
writer.write("Content-Type: multipart/related; boundary=\"boundary\"");
writer.newLine();
writer.newLine();
// 寫入內(nèi)容邊界開始標識
writer.write("--boundary");
writer.newLine();
writer.write("Content-Type: text/html; charset=UTF-8");
writer.newLine();
writer.newLine();
// 寫入核心的、已嵌入所有資源的HTML內(nèi)容
writer.write(html);
writer.newLine();
writer.newLine();
// 寫入MHTML結(jié)束邊界標識(必須寫,否則文件格式不完整)
writer.write("--boundary--");
writer.flush();
}catch (IOException e){
log.error("保存MHTML文件失?。? + filePath, e);
}
}邏輯調(diào)用:
- 通過url和cookie免密獲取html字符串
- 獲取html中的圖片、CSS、JS轉(zhuǎn)成base64的字符串,因為.mhtml文件中超鏈接類型的樣式無法渲染
- 刪除html中不需要的布局和內(nèi)容
- 使用2. 中獲取的圖片、CSS、JS轉(zhuǎn)成base64的字符串 替換html字符串中的超鏈接
- 保存為.mhtml文件
String html = WikiUtils.getConfluencePageHtml(link, cookie);
if (html.isEmpty()){
log.error("獲取html頁面失敗");
return;
}
Map<String, String> htmlMap = Html2MHTCompiler.parseHtmlPage(cookie, html, properties.baseURL);
String tittle = Html2MHTCompiler.parseTittle(html);
String html2 = Html2MHTCompiler.removeUnwantedElements(html);
String parseHtml = Html2MHTCompiler.embedResources(html2, htmlMap);
Html2MHTCompiler.saveAsMhtml(parseHtml, currentDir+File.separator + tittle + ".mhtml");到此這篇關(guān)于Java實現(xiàn)Html保存為.mhtml文件的代碼邏輯的文章就介紹到這了,更多相關(guān)java html保存為.mhtml文件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?component-scan?XML配置與@ComponentScan注解配置
這篇文章主要介紹了Spring?component-scan?XML配置與@ComponentScan注解配置,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-09-09
SpringBoot如何使用MyBatis-Plus實現(xiàn)高效的數(shù)據(jù)訪問層
在開發(fā) Spring Boot 應(yīng)用時,數(shù)據(jù)訪問是不可或缺的部分,本文將詳細介紹如何在 Spring Boot 中使用 MyBatis-Plus,并結(jié)合具體代碼示例來講解它的使用方法和常見配置,希望對大家有一定的幫助2025-04-04
Java final static abstract關(guān)鍵字概述
這篇文章主要介紹了Java final static abstract關(guān)鍵字的相關(guān)資料,需要的朋友可以參考下2016-05-05
Java如何根據(jù)不同系統(tǒng)動態(tài)獲取換行符和盤分割符
這篇文章主要介紹了Java如何根據(jù)不同系統(tǒng)動態(tài)獲取換行符和盤分割符,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12

