Java实现Html保存为.mhtml文件

功能需求

将html字符串保存为.mhtml文件

代码实现

  • 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字符串内容工具类
@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,无权限访问"); }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转换.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的元素 → 侧边栏/左侧面板 等冗余区域 doc.select(".acs-side-bar .ia-scrollable-section").remove(); doc.select(".ia-splitter-left").remove(); // 2. 删除指定id的元素 → 点赞标签区、页脚、评论区 等无用模块 // 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>标签内的【指定CSS选择器】及其对应的所有样式 * @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标签,处理内部的css内容 for (Element styleTag : styleTags) { String cssContent = styleTag.html(); if (cssContent.isEmpty()) continue; // 3. 精准匹配【选择器 { 任意样式内容 }】 完整块,含换行/空格/制表符,匹配规则全覆盖 // 匹配规则:匹配 .ia-splitter-left { ... } 或 #main { ... } 完整的样式块 String regex = selector + "\\s*\\{[^}]*\\}"; // 替换匹配到的内容为空,即删除该选择器及对应样式 String newCssContent = cssContent.replaceAll(regex, "").trim(); // 处理替换后多余的空行/空格,让css内容更整洁 newCssContent = newCssContent.replaceAll("\\n+", "\n").replaceAll("\\s+", " "); // 4. 将处理后的css内容重新写入style标签 styleTag.html(newCssContent); } } // ========== 图片/CSS/JS都复用这个方法 ========== private static void parseResource(String cookie,String resourceSrc,String resourceType,String baseUrl, Map<String, String> resourceMap) { try { // 拼接完整URL(兼容:绝对路径/相对路径) String fullResourceUrl = getFullUrl(baseUrl, resourceSrc); // 下载资源文件,转成【带MIME头的Base64编码】 String base64Resource = downloadResourceToBase64(fullResourceUrl,resourceType, cookie); resourceMap.put(resourceSrc, base64Resource); } catch (Exception e) { log.error("资源解析失败,跳过该资源:" + resourceSrc, e); } } // 拼接完整URL:处理相对路径/绝对路径 (原有方法,复用) private static String getFullUrl(String baseUrl, String src) { if (src.startsWith("http://") || src.startsWith("https://")) { return src; // 绝对路径,直接返回 } else if(src.startsWith("//")){ return "https:" + src; // 兼容 //xxx.com/xxx.css 这种无协议路径 } 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); // 解决部分网站的反爬/跨域问题 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是压缩质量,可调整 } // 获取资源的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; } /** * 核心图片压缩工具方法:图片质量压缩(核心无坑) * @param imageBytes 原图字节流 * @param quality 压缩质量 0.1~1.0 ,推荐0.6~0.8 (数值越大越清晰,体积越大) * @return 压缩后的图片字节流 */ private static byte[] compressImage(byte[] imageBytes, float quality) throws Exception { // 质量值兜底,防止传参错误 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(); // 质量压缩,尺寸不变,清晰度无损,体积减小 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("&", "&amp;"); 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标准协议头 writer.write("MIME-Version: 1.0"); writer.newLine(); writer.write("Content-Type: multipart/related; boundary=\"boundary\""); writer.newLine(); writer.newLine(); // 写入内容边界开始标识 writer.write("--boundary"); writer.newLine(); writer.write("Content-Type: text/html; charset=UTF-8"); writer.newLine(); writer.newLine(); // 写入核心的、已嵌入所有资源的HTML内容 writer.write(html); writer.newLine(); writer.newLine(); // 写入MHTML结束边界标识(必须写,否则文件格式不完整) writer.write("--boundary--"); writer.flush(); }catch (IOException e){ log.error("保存MHTML文件失败:" + filePath, e); } }

逻辑调用:

  1. 通过url和cookie免密获取html字符串
  2. 获取html中的图片、CSS、JS转成base64的字符串,因为.mhtml文件中超链接类型的样式无法渲染
  3. 删除html中不需要的布局和内容
  4. 使用2. 中获取的图片、CSS、JS转成base64的字符串 替换html字符串中的超链接
  5. 保存为.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");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1155004.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3款好玩的台球游戏,玩过的人都说很上头

在移动游戏市场中&#xff0c;台球品类长期被少数热门产品占据流量高地&#xff0c;但许多玩家反馈这些"爆款"存在氪金碾压、广告泛滥、体验割裂等问题。事实上&#xff0c;一些低调运营却用心打磨的台球手游&#xff0c;凭借真实的物理体验、丰富的玩法设计和友好的…

IP 地址解析

“IP 地址解析 / IP 地址详解” —IP地址的基础信息 一、什么是 IP 地址&#xff1f; IP 地址 网络中设备的唯一编号 就像&#xff1a; 手机号 → 找到一个人IP 地址 → 找到一台设备 常见格式&#xff08;IPv4&#xff09;&#xff1a; 192.168.1.100由 **4 个字节&#xff0…

Google DeepMind :RAG 已死,无限上下文是伪命题?RLM 如何用“代码思维”终结 AI 的记忆焦虑

不久前 DeepMind 发布了一篇论文&#xff0c;内容简单说是&#xff1a; RLM&#xff08;Recursive Language Models&#xff09; 不是让模型“硬记”所有内容&#xff0c;而是赋予模型像程序员一样操作数据的能力&#xff0c;让模型在不把超长 prompt 直接塞进 Transformer 的…

AI Agent企业落地避坑指南:7大致命错误,收藏级干货

企业落地AI Agent常面临七大陷阱&#xff1a;需求误判&#xff08;未先优化流程&#xff09;、目标输入不明确、数据处理难题、业务模式局限&#xff08;盲目追求全自动&#xff09;、项目管理缺失、预期与成本误区。AI Agent本质是效率工具&#xff0c;需明确边界&#xff0c;…

制造业企业数据采集系统选型指南:从技术挑战到架构实践

在工业4.0和智能制造浪潮的推动下&#xff0c;数据已成为制造业企业的新型生产要素。然而&#xff0c;许多制造企业仍面临“数据孤岛”困境&#xff1a;生产设备产生的海量数据沉睡在异构系统中&#xff0c;无法转化为有效的决策支持。根据业界调研&#xff0c;超过60%的制造业…

跨境远控无忧,开启高效跨国连接新时代

为什么需要【全球节点】&#xff1f;当您需要进行跨国、跨地区远程控制时&#xff0c;普通网络连接往往面临高延迟、易丢包、速度慢等问题。ToDesk全球节点插件专为跨境高速连接设计&#xff0c;通过覆盖全球200高速专用节点&#xff0c;构建出稳定、低延迟的传输通道&#xff…

语音识别噪声抑制优化实战

&#x1f493; 博客主页&#xff1a;借口的CSDN主页 ⏩ 文章专栏&#xff1a;《热点资讯》 语音识别噪声抑制优化实战&#xff1a;轻量化策略与边缘计算应用目录语音识别噪声抑制优化实战&#xff1a;轻量化策略与边缘计算应用 引言&#xff1a;噪声抑制——语音识别的“隐形瓶…

8款全场景CRM系统横向对比:从获客闭环到供应链协同的能力矩阵

在数字化转型浪潮中&#xff0c;企业对CRM的需求早已从“销售流程管理”升级为“全场景业务闭环”——既要覆盖获客-跟单-订单-售后的客户全生命周期&#xff0c;也要实现订单-采购-生产-委外的供应链协同&#xff0c;最终通过数据驱动构建业务增长闭环。本文选取8款主流CRM/一…

企业级AI客服Agent架构设计实战:风险分层、状态跟踪与模糊意图处理(建议收藏)

本文详细介绍了企业级AI客服Agent系统架构设计&#xff0c;强调"拒绝闲聊&#xff0c;追求收敛"的设计哲学。文章从风险分层架构、后端权威数据源、多轮控制环设计、三层状态管理模型、模糊意图处理策略到工程化交付标准&#xff0c;全面阐述了如何构建一个严谨、可靠…

AI是如何让DDoS变智能,如何防御智能的DDOS!

AI增强DDoS攻击的智能化方式动态流量模式学习 攻击者利用AI分析目标网络流量模式&#xff0c;动态调整攻击流量特征&#xff0c;绕过基于静态规则的防御系统。例如&#xff0c;通过强化学习模拟合法用户行为&#xff0c;使攻击流量更难被检测。自适应攻击策略 AI模型实时监控防…

基于 RPA 的企微外部群自动化架构实现

一、 背景与设计初衷 在企业私域流量的精细化运营中&#xff0c;外部群&#xff08;包含组织外成员的群聊&#xff09;是触达客户的关键节点。由于业务场景的多样性&#xff0c;标准的官方 API 在某些特定管理动作上存在权限边界。 ​ QiWe开放平台提供了后台直登功能&#xf…

RPA赋能:外部群自动化管理新纪元

QiWe开放平台提供了后台直登功能&#xff0c;登录成功后获取相关参数&#xff0c;快速Apifox在线测试&#xff0c;所有登录功能都是基于QiWe平台API自定义开发。 一、 RPA 外部群自动化的核心架构 RPA 充当了“数字化员工”的角色&#xff0c;通过模拟人工在桌面端或移动端的…

深度解析LLM训练革命:从GPT到DeepSeek R1的技术演进与架构创新,技术人必看!

本文解析了大语言模型训练范式的技术演进&#xff0c;对比了传统监督训练与GPT自监督学习的根本差异&#xff0c;揭示了"下一个词元预测"核心机制。重点探讨人类介入的对齐阶段重要性&#xff0c;介绍思考链、DeepSeek R1模型等创新技术&#xff0c;以及混合专家(MoE…

中国温室气体排放因子数据库

1793中国温室气体排放因子数据库数据简介本数据是2025年1月最新更新的《国家温室气体数据库》&#xff0c;该数据原始数据来源于国家温室气体排放因子数据库&#xff0c;发布年份为2024年&#xff0c;由数据皮皮侠团队人工整理。本数据记录了不同温室气体排放源的详细信息&…

带货主播记不住卖点台词?一键提词让直播更专业

在带货直播间&#xff0c;无论是新品推荐、产品测评&#xff0c;还是福利秒杀&#xff0c;流程紧凑、信息量大&#xff0c;对主播的临场记忆和表达提出了极高要求。你是不是经常遇到这些场景——产品卖点说一半突然忘词&#xff0c;话术顺序搞混&#xff0c;直播节奏“翻车”&a…

阿里一面直接挂!我用 CompletableFuture 优化代码,面试官:你这是在生产环境埋雷!

上周有个粉丝阿强哭丧着脸来找我&#xff0c;说阿里一面被“秒杀”了。 起因很简单&#xff0c;面试官问他&#xff1a;“有一个核心接口响应很慢&#xff0c;里面串行调用了用户信息、积分查询、优惠券三个服务&#xff0c;你会怎么优化&#xff1f;” 阿强自信满满&#xff…

浏览器秒变 VS Code!Code-Server+cpolar,异地开发再也不用带电脑

Code-Server 是一款能将 VS Code 完整部署到服务器的工具&#xff0c;通过浏览器就能实现远程编码&#xff0c;保留了原编辑器的插件安装、代码调试、终端操作等所有核心功能。它特别适合三类人群&#xff1a;笔记本性能有限的开发者、需要跨设备协作的团队、经常出差的职场人&…

VisionPro二开之显示OK和NG渲染图

VisionPro二开之显示OK和NG渲染图ICogRecord cogRecord null;double width AlgorithmService.Instance.DetectWidth(info.Image,out cogRecord);public double DetectWidth(ICogImage img,out ICogRecord cogRecord){return vpAlgo.DetectWidth(img, out cogRecord);}public …

【技术干货】必藏!2025年AI智能体元年:从命令执行到协作解决,全面解析AI智能体的核心技术架构

2025年被称为AI智能体元年&#xff0c;AI智能体正将人机交互从命令执行转向协作式问题解决。与普通AI工作流不同&#xff0c;AI智能体具备推理、规划、工具使用和记忆能力&#xff0c;能通过反思、工具使用、规划和多智能体协作模式处理复杂任务。智能体式工作流具有灵活性强、…

走进腾讯|MoonBit Codebuddy AI 编程实践交流会回顾

随着大模型能力持续跃迁&#xff0c;AI 正加速进入生产级软件开发场景&#xff0c;软件工程正站在从“人主导编程”迈向“人机协作开发”的关键节点。1 月 10 日&#xff0c;由 腾讯云 IDEA 研究院 MoonBit 联合举办的 「IDEA研究院MoonBit 走进腾讯&#xff5c;腾讯云 Codeb…