Java实现Markdown图片批量本地化处理工具
作者:zk_xyb
一、工具介绍
在日常使用Markdown时,我们常通过远程URL引用图片,但这种方式存在依赖网络、图片易失效、离线无法查看等问题。MarkdownImageProcessor 正是为解决这些问题而生的Java工具,其核心功能是批量识别Markdown文件中的远程图片URL,自动下载图片到本地目录,并生成替换后的新Markdown文件(图片路径改为本地相对路径),让文档彻底摆脱对远程资源的依赖。
核心功能
多文件/目录支持:可处理单个Markdown文件或目录(递归查找所有.md/.markdown文件);
远程图片下载:自动识别http:///https://开头的远程图片URL,下载至本地images目录;
路径自动替换:生成的新Markdown文件中,图片路径会替换为本地相对路径(如images/xxx.jpg);
安全处理机制:下载失败时保留原始URL,避免文档损坏;文件名自动去重(同名文件加序号),防止覆盖;
清晰的进度反馈:实时输出处理进度(成功/失败的文件/图片数量),生成处理总结。
使用流程
运行程序后,输入Markdown文件路径或目录路径(每行一个);
输入空行开始处理,程序会递归扫描目录中的所有Markdown文件;
处理完成后,在原文件同目录生成带_processed后缀的新文件(如doc.md→doc_processed.md),图片保存至同目录的images文件夹。
二、代码优化方案
原代码功能完整,但在灵活性、健壮性和现代Java特性使用上有优化空间。以下是优化后的代码及关键改进点:
优化后的代码
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class MarkdownImageProcessor {
// 可配置参数(默认值)
private final String imageRegex;
private String imageDir;
private String processedSuffix;
private int connectionTimeout; // 连接超时(毫秒)
private int readTimeout; // 读取超时(毫秒)
private String userAgent;
private static final Scanner scanner = new Scanner(System.in);
// 构造函数:支持自定义配置
public MarkdownImageProcessor() {
this.imageRegex = "!\\[(.*?)\\]\\((.*?)\\)";
this.imageDir = "images";
this.processedSuffix = "_processed";
this.connectionTimeout = 5000; // 5秒
this.readTimeout = 10000; // 10秒
this.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
}
public static void main(String[] args) {
try {
MarkdownImageProcessor processor = new MarkdownImageProcessor();
List<Path> filesToProcess = processor.collectFilesFromConsole();
if (filesToProcess.isEmpty()) {
System.out.println("没有需要处理的文件。");
return;
}
ProcessSummary summary = processor.processFiles(filesToProcess);
processor.printSummary(summary);
} catch (Exception e) {
System.err.println("程序运行出错: " + e.getMessage());
e.printStackTrace();
} finally {
scanner.close();
}
}
// 从控制台收集待处理文件
private List<Path> collectFilesFromConsole() {
List<Path> files = new ArrayList<>();
System.out.println("Markdown图片处理工具 - 批量模式");
System.out.println("--------------------------------");
System.out.println("请输入Markdown文件或目录路径(每行一个,输入空行开始处理)");
System.out.println("支持:");
System.out.println(" - 单个文件路径");
System.out.println(" - 目录路径(将递归处理所有.md文件)");
System.out.println("--------------------------------");
Set<String> processedPaths = new HashSet<>();
while (true) {
System.out.print("输入路径: ");
String pathStr = scanner.nextLine().trim();
if (pathStr.isEmpty()) {
break;
}
if (processedPaths.contains(pathStr)) {
System.out.println("警告: 路径已添加 - " + pathStr);
continue;
}
Path path = Paths.get(pathStr);
if (!Files.exists(path)) {
System.out.println("错误: 文件/目录不存在 - " + pathStr);
continue;
}
processedPaths.add(pathStr);
if (Files.isDirectory(path)) {
List<Path> dirFiles = getMdFilesRecursively(path);
files.addAll(dirFiles);
System.out.printf("已添加目录: %s (%d个MD文件)%n", path, dirFiles.size());
} else {
if (isMarkdownFile(path)) {
files.add(path);
System.out.println("已添加文件: " + path);
} else {
System.out.println("错误: 不是Markdown文件 - " + pathStr);
}
}
}
return files;
}
// 递归获取目录中所有Markdown文件(使用NIO简化代码)
private List<Path> getMdFilesRecursively(Path directory) {
List<Path> result = new ArrayList<>();
try (Stream<Path> stream = Files.walk(directory)) {
stream.filter(Files::isRegularFile)
.filter(this::isMarkdownFile)
.forEach(result::add);
} catch (IOException e) {
System.err.printf("警告: 读取目录失败 %s - %s%n", directory, e.getMessage());
}
return result;
}
// 判断是否为Markdown文件
private boolean isMarkdownFile(Path path) {
String fileName = path.getFileName().toString().toLowerCase();
return fileName.endsWith(".md") || fileName.endsWith(".markdown");
}
// 处理所有文件
private ProcessSummary processFiles(List<Path> files) {
ProcessSummary summary = new ProcessSummary();
int totalFiles = files.size();
System.out.printf("%n开始处理 %d 个Markdown文件...%n%n", totalFiles);
for (int i = 0; i < totalFiles; i++) {
Path file = files.get(i);
System.out.printf("处理文件 %d/%d: %s%n", i + 1, totalFiles, file.toAbsolutePath());
try {
processMarkdownFile(file);
summary.successfulFiles++;
} catch (Exception e) {
System.err.printf(" 处理失败: %s%n", e.getMessage());
summary.failedFiles++;
}
System.out.println();
}
return summary;
}
// 处理单个Markdown文件
private void processMarkdownFile(Path mdFile) throws IOException {
// 读取文件内容
String content = readFileContent(mdFile);
// 创建图片保存目录(基于原文件目录)
Path imageDirPath = mdFile.getParent().resolve(imageDir);
Files.createDirectories(imageDirPath); // 自动创建父目录
// 处理图片URL并下载
ImageProcessingResult result = processImages(content, imageDirPath);
if (result.totalImages > 0) {
// 生成新的MD文件路径
Path newMdFile = getProcessedFilePath(mdFile);
writeFileContent(newMdFile, result.processedContent);
System.out.printf(" 已处理 %d 张图片,生成新文件: %s%n",
result.totalImages, newMdFile.getFileName());
} else {
System.out.println(" 未发现需要处理的远程图片URL");
}
}
// 使用Java 8兼容的方式读取文件内容
private String readFileContent(Path path) throws IOException {
StringBuilder content = new StringBuilder();
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
return content.toString();
}
// 使用Java 8兼容的方式写入文件内容
private void writeFileContent(Path path, String content) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write(content);
}
}
// 获取处理后的文件路径(可自定义后缀)
private Path getProcessedFilePath(Path originalPath) {
String fileName = originalPath.getFileName().toString();
int dotIndex = fileName.lastIndexOf('.');
String baseName = (dotIndex > 0) ? fileName.substring(0, dotIndex) : fileName;
String extension = (dotIndex > 0) ? fileName.substring(dotIndex) : "";
String newFileName = baseName + processedSuffix + extension;
return originalPath.getParent().resolve(newFileName);
}
// 处理图片URL并替换为本地路径
private ImageProcessingResult processImages(String content, Path imageDirPath) throws IOException {
Pattern pattern = Pattern.compile(imageRegex);
Matcher matcher = pattern.matcher(content);
StringBuffer sb = new StringBuffer();
int totalImages = 0;
int failedImages = 0;
while (matcher.find()) {
String altText = matcher.group(1);
String imageUrl = matcher.group(2).trim();
// 处理远程图片URL(http/https)
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
totalImages++;
try {
// 下载图片并返回本地文件名
String fileName = downloadImage(imageUrl, imageDirPath);
// 构建相对路径(原文件到images目录的相对路径)
Path relativePath = imageDirPath.getParent().relativize(imageDirPath).resolve(fileName);
String replacement = "";
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
System.out.printf(" ✔ 已下载: %s -> %s%n", imageUrl, relativePath);
} catch (Exception e) {
failedImages++;
System.err.printf(" ✘ 下载失败 (%s): %s%n", e.getMessage(), imageUrl);
// 保留原始URL
matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group(0)));
}
} else {
// 本地图片路径不处理
matcher.appendReplacement(sb, Matcher.quoteReplacement(matcher.group(0)));
}
}
matcher.appendTail(sb);
ImageProcessingResult result = new ImageProcessingResult();
result.processedContent = sb.toString();
result.totalImages = totalImages;
result.successfulImages = totalImages - failedImages;
result.failedImages = failedImages;
return result;
}
// 下载图片并保存到本地目录
private String downloadImage(String imageUrl, Path imageDirPath) throws IOException {
// 创建HTTP连接并设置超时
HttpURLConnection connection = createHttpConnection(imageUrl);
// 获取响应状态
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP请求失败,状态码: " + responseCode);
}
// 根据Content-Type获取正确的文件扩展名
String contentType = connection.getContentType();
String extension = getExtensionFromContentType(contentType);
// 提取基础文件名(不含扩展名)
String baseFileName = extractBaseFileName(imageUrl);
// 拼接完整文件名(基础名+扩展名)
String originalFileName = baseFileName + "." + extension;
// 生成唯一文件名(避免重复)
String fileName = generateUniqueFileName(imageDirPath, originalFileName);
// 下载并保存文件
try (InputStream in = connection.getInputStream();
OutputStream out = Files.newOutputStream(imageDirPath.resolve(fileName))) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
return fileName;
}
// 创建HTTP连接(统一配置超时和请求头)
private HttpURLConnection createHttpConnection(String urlStr) throws IOException {
URL url = new URL(urlStr);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", userAgent);
connection.setConnectTimeout(connectionTimeout); // 连接超时
connection.setReadTimeout(readTimeout); // 读取超时
connection.setInstanceFollowRedirects(true); // 自动跟随重定向
return connection;
}
// 从Content-Type获取正确的文件扩展名(兼容Java 8)
private String getExtensionFromContentType(String contentType) {
if (contentType == null) {
return "jpg"; // 默认 fallback
}
// 传统switch语句(Java 8支持)
switch (contentType) {
case "image/jpeg":
return "jpg";
case "image/png":
return "png";
case "image/gif":
return "gif";
case "image/bmp":
return "bmp";
case "image/webp":
return "webp";
default:
return "jpg"; // 未知类型默认jpg
}
}
// 提取基础文件名(不含扩展名)
private String extractBaseFileName(String url) {
// 移除URL查询参数和锚点
String path = url.split("[?#]")[0];
// 提取最后一个路径段
String fileName = path.substring(path.lastIndexOf('/') + 1);
// 移除可能的扩展名(避免重复)
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0) {
fileName = fileName.substring(0, dotIndex);
}
// sanitize文件名(移除非法字符)
return fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
}
// 生成唯一文件名(避免覆盖)
private String generateUniqueFileName(Path directory, String originalName) {
String baseName = originalName;
String extension = "";
int dotIndex = originalName.lastIndexOf('.');
if (dotIndex > 0) {
baseName = originalName.substring(0, dotIndex);
extension = originalName.substring(dotIndex);
}
String fileName = originalName;
int counter = 1;
while (Files.exists(directory.resolve(fileName))) {
fileName = baseName + "_" + counter + extension;
counter++;
}
return fileName;
}
// 打印处理总结
private void printSummary(ProcessSummary summary) {
System.out.println("--------------------------------");
System.out.println("处理完成!");
System.out.printf("成功处理文件: %d%n", summary.successfulFiles);
System.out.printf("处理失败文件: %d%n", summary.failedFiles);
System.out.println("--------------------------------");
}
// 配置参数设置方法(提高灵活性)
public void setImageDir(String imageDir) {
this.imageDir = imageDir;
}
public void setProcessedSuffix(String processedSuffix) {
this.processedSuffix = processedSuffix;
}
public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public void setReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
}
public void setUserAgent(String userAgent) {
this.userAgent = userAgent;
}
// 处理总结内部类
static class ProcessSummary {
int successfulFiles = 0;
int failedFiles = 0;
}
// 图片处理结果内部类
static class ImageProcessingResult {
String processedContent;
int totalImages;
int successfulImages;
int failedImages;
}
}
主要优化点
增强灵活性:将images目录、输出文件后缀(_processed)、HTTP超时等硬编码参数改为可配置字段(通过setter调整),适应不同场景。
现代IO API:用java.nio.file(Path、Files)替代File类,简化路径处理(如resolve拼接路径、createDirectories自动创建目录),代码更简洁。
准确的文件扩展名:原代码默认用jpg,优化后通过Content-Type动态获取(如image/png对应png),避免文件格式错误。
HTTP超时控制:为HttpURLConnection设置连接超时和读取超时,防止因网络问题导致程序卡死。
递归查找优化:用Files.walk替代手动递归,代码更简洁,且自动处理异常。
路径处理优化:生成新文件路径时通过Path API操作,避免跨平台路径分隔符问题(如Windows的\和Linux的/)。
代码复用:抽离createHttpConnection、getExtensionFromContentType等方法,减少重复代码,提高可维护性。
错误处理增强:细化异常信息(如区分连接超时、HTTP错误码),用户可更清晰定位问题。
通过这些优化,工具不仅保留了原有的核心功能,还在灵活性、健壮性和易用性上有显著提升,更适合实际场景使用。
到此这篇关于Java实现Markdown图片批量本地化处理工具的文章就介绍到这了,更多相关Java图片批处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
