Java實(shí)現(xiàn)Html保存為.mhtml文件的代碼邏輯
功能需求
將html字符串保存為.mhtml文件
代碼實(shí)現(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標(biāo)簽、提取圖片/樣式資源,必備 -->
<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>- 獲取通過(guò)訪問(wèn)url獲取html字符串內(nèi)容工具類
@Slf4j
public class WikiUtils {
/**
* 獲取wiki 頁(yè)面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("無(wú)效的cookie,無(wú)權(quán)限訪問(wèn)");
}else {
log.error("獲取html頁(yè)面失敗");
}
return value;
}
/**
* 在請(qǐng)求頭中放入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標(biāo)簽下的style標(biāo)簽的屬性中的.ia-splitter-left #main 這兩個(gè)選擇器
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的元素 → 點(diǎn)贊標(biāo)簽區(qū)、頁(yè)腳、評(píng)論區(qū) 等無(wú)用模塊
// 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. 精準(zhǔn)移除 id="main" 標(biāo)簽中【指定的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>標(biāo)簽下所有<style>標(biāo)簽內(nèi)的【指定CSS選擇器】及其對(duì)應(yīng)的所有樣式
* @param doc jsoup解析后的文檔對(duì)象
* @param selector 要?jiǎng)h除的css選擇器,如:.ia-splitter-left 、 #main
*/
private static void removeCssSelectorFromStyleTag(Document doc, String selector) {
// 1. 獲取head標(biāo)簽下所有的style樣式標(biāo)簽
Elements styleTags = doc.head().select("style");
if (styleTags.isEmpty()) {
return; // 沒有style標(biāo)簽,直接返回
}
// 2. 遍歷每一個(gè)style標(biāo)簽,處理內(nèi)部的css內(nèi)容
for (Element styleTag : styleTags) {
String cssContent = styleTag.html();
if (cssContent.isEmpty()) continue;
// 3. 精準(zhǔn)匹配【選擇器 { 任意樣式內(nèi)容 }】 完整塊,含換行/空格/制表符,匹配規(guī)則全覆蓋
// 匹配規(guī)則:匹配 .ia-splitter-left { ... } 或 #main { ... } 完整的樣式塊
String regex = selector + "\\s*\\{[^}]*\\}";
// 替換匹配到的內(nèi)容為空,即刪除該選擇器及對(duì)應(yīng)樣式
String newCssContent = cssContent.replaceAll(regex, "").trim();
// 處理替換后多余的空行/空格,讓css內(nèi)容更整潔
newCssContent = newCssContent.replaceAll("\\n+", "\n").replaceAll("\\s+", " ");
// 4. 將處理后的css內(nèi)容重新寫入style標(biāo)簽
styleTag.html(newCssContent);
}
}
// ========== 圖片/CSS/JS都復(fù)用這個(gè)方法 ==========
private static void parseResource(String cookie,String resourceSrc,String resourceType,String baseUrl, Map<String, String> resourceMap) {
try {
// 拼接完整URL(兼容:絕對(duì)路徑/相對(duì)路徑)
String fullResourceUrl = getFullUrl(baseUrl, resourceSrc);
// 下載資源文件,轉(zhuǎn)成【帶MIME頭的Base64編碼】
String base64Resource = downloadResourceToBase64(fullResourceUrl,resourceType, cookie);
resourceMap.put(resourceSrc, base64Resource);
} catch (Exception e) {
log.error("資源解析失敗,跳過(guò)該資源:" + resourceSrc, e);
}
}
// 拼接完整URL:處理相對(duì)路徑/絕對(duì)路徑 (原有方法,復(fù)用)
private static String getFullUrl(String baseUrl, String src) {
if (src.startsWith("http://") || src.startsWith("https://")) {
return src; // 絕對(duì)路徑,直接返回
} else if(src.startsWith("http://")){
return "https:" + src; // 兼容 //xxx.com/xxx.css 這種無(wú)協(xié)議路徑
} else {
return src.startsWith("/") ? baseUrl + src : baseUrl + "/" + src; // 相對(duì)路徑,拼接根路徑
}
}
// ========== 通用資源下載+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)站的反爬/跨域問(wèn)題
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();
// 對(duì)圖片類型做【體積壓縮+無(wú)損渲染】處理
if ("image".equalsIgnoreCase(resourceType) && resourceBytes.length > 0) {
resourceBytes = compressImage(resourceBytes, 0.7f); // 0.7是壓縮質(zhì)量,可調(diào)整
}
// 獲取資源的MIME類型 + Base64編碼,自動(dòng)適配圖片/CSS/JS
String mimeType = conn.getContentType();
String base64 = Base64.encodeBase64String(resourceBytes);
in.close();
out.close();
conn.disconnect();
// 返回標(biāo)準(zhǔn)的data-url格式,可直接嵌入HTML替換原URL
return "data:" + mimeType + ";base64," + base64;
}
return null;
}
/**
* 核心圖片壓縮工具方法:圖片質(zhì)量壓縮(核心無(wú)坑)
* @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ì)量值兜底,防止傳參錯(cuò)誤
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; // 非標(biāo)準(zhǔn)圖片,返回原圖
}
// 獲取圖片格式(png/jpg等)
String format = getImageFormat(imageBytes);
if (format == null) {
format = "jpeg";
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 質(zhì)量壓縮,尺寸不變,清晰度無(wú)損,體積減小
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) {
// 異常時(shí)直接寫入原圖,不影響
}
}
});
// 如果壓縮后體積變大,返回原圖
byte[] compressedBytes = baos.toByteArray();
bais.close();
baos.close();
return compressedBytes.length < imageBytes.length ? compressedBytes : imageBytes;
}
/**
* 獲取圖片真實(shí)格式
*/
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標(biāo)準(zhǔn)協(xié)議頭
writer.write("MIME-Version: 1.0");
writer.newLine();
writer.write("Content-Type: multipart/related; boundary=\"boundary\"");
writer.newLine();
writer.newLine();
// 寫入內(nèi)容邊界開始標(biāo)識(shí)
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é)束邊界標(biāo)識(shí)(必須寫,否則文件格式不完整)
writer.write("--boundary--");
writer.flush();
}catch (IOException e){
log.error("保存MHTML文件失?。? + filePath, e);
}
}邏輯調(diào)用:
- 通過(guò)url和cookie免密獲取html字符串
- 獲取html中的圖片、CSS、JS轉(zhuǎn)成base64的字符串,因?yàn)?mhtml文件中超鏈接類型的樣式無(wú)法渲染
- 刪除html中不需要的布局和內(nèi)容
- 使用2. 中獲取的圖片、CSS、JS轉(zhuǎn)成base64的字符串 替換html字符串中的超鏈接
- 保存為.mhtml文件
String html = WikiUtils.getConfluencePageHtml(link, cookie);
if (html.isEmpty()){
log.error("獲取html頁(yè)面失敗");
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實(shí)現(xiàn)Html保存為.mhtml文件的代碼邏輯的文章就介紹到這了,更多相關(guān)java html保存為.mhtml文件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot項(xiàng)目中定時(shí)任務(wù)的四種實(shí)現(xiàn)方式詳解
Spring的@Scheduled注解是一種非常簡(jiǎn)單和便捷的實(shí)現(xiàn)定時(shí)任務(wù)的方式,通過(guò)在方法上添加@Scheduled注解,我們可以指定方法在特定的時(shí)間間隔或固定的時(shí)間點(diǎn)執(zhí)行,本文給大家介紹Springboot項(xiàng)目中定時(shí)任務(wù)的四種實(shí)現(xiàn)方式,感興趣的的朋友一起看看b2024-02-02
詳解JAVA中ListIterator和Iterator的辨析
這篇文章主要為大家詳細(xì)介紹了JAVAListIterator和Iterator的辨析,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-02-02
SpringBoot整合Canal與RabbitMQ監(jiān)聽數(shù)據(jù)變更記錄
這篇文章主要介紹了SpringBoot整合Canal與RabbitMQ監(jiān)聽數(shù)據(jù)變更記錄,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
SpringBoot利用@Validated注解優(yōu)雅實(shí)現(xiàn)參數(shù)校驗(yàn)
在開發(fā) Web 應(yīng)用時(shí),用戶輸入的合法性校驗(yàn)是保障系統(tǒng)穩(wěn)定性的基礎(chǔ),?Spring Boot 的 @Validated 注解 提供了一種更優(yōu)雅的解決方案,下面就跟隨小編一起學(xué)習(xí)一下吧2025-04-04
java?Comparable和Comparator的區(qū)別及作用面試精講
這篇文章主要為大家介紹了java?Comparable和Comparator的區(qū)別及作用面試精講,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
帶你了解如何使用Spring基于ProxyFactoryBean創(chuàng)建AOP代理
這篇文章主要介紹了Spring基于ProxyFactoryBean創(chuàng)建AOP代理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2021-08-08
Springboot配置Swagger2登錄密碼的實(shí)現(xiàn)
本文主要介紹了Springboot配置Swagger2登錄密碼的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
Java實(shí)現(xiàn)動(dòng)態(tài)創(chuàng)建類操作示例
這篇文章主要介紹了Java實(shí)現(xiàn)動(dòng)態(tài)創(chuàng)建類操作,結(jié)合完整示例形式分析了Java動(dòng)態(tài)創(chuàng)建類的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2020-02-02

