Java實(shí)現(xiàn)Markdown圖片批量本地化處理工具
一、工具介紹
在日常使用Markdown時(shí),我們常通過遠(yuǎn)程URL引用圖片,但這種方式存在依賴網(wǎng)絡(luò)、圖片易失效、離線無法查看等問題。MarkdownImageProcessor 正是為解決這些問題而生的Java工具,其核心功能是批量識(shí)別Markdown文件中的遠(yuǎn)程圖片URL,自動(dòng)下載圖片到本地目錄,并生成替換后的新Markdown文件(圖片路徑改為本地相對(duì)路徑),讓文檔徹底擺脫對(duì)遠(yuǎn)程資源的依賴。
核心功能
多文件/目錄支持:可處理單個(gè)Markdown文件或目錄(遞歸查找所有.md/.markdown文件);
遠(yuǎn)程圖片下載:自動(dòng)識(shí)別http:///https://開頭的遠(yuǎn)程圖片URL,下載至本地images目錄;
路徑自動(dòng)替換:生成的新Markdown文件中,圖片路徑會(huì)替換為本地相對(duì)路徑(如images/xxx.jpg);
安全處理機(jī)制:下載失敗時(shí)保留原始URL,避免文檔損壞;文件名自動(dòng)去重(同名文件加序號(hào)),防止覆蓋;
清晰的進(jìn)度反饋:實(shí)時(shí)輸出處理進(jìn)度(成功/失敗的文件/圖片數(shù)量),生成處理總結(jié)。
使用流程
運(yùn)行程序后,輸入Markdown文件路徑或目錄路徑(每行一個(gè));
輸入空行開始處理,程序會(huì)遞歸掃描目錄中的所有Markdown文件;
處理完成后,在原文件同目錄生成帶_processed后綴的新文件(如doc.md→doc_processed.md),圖片保存至同目錄的images文件夾。
二、代碼優(yōu)化方案
原代碼功能完整,但在靈活性、健壯性和現(xiàn)代Java特性使用上有優(yōu)化空間。以下是優(yōu)化后的代碼及關(guān)鍵改進(jìn)點(diǎn):
優(yōu)化后的代碼
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 {
// 可配置參數(shù)(默認(rèn)值)
private final String imageRegex;
private String imageDir;
private String processedSuffix;
private int connectionTimeout; // 連接超時(shí)(毫秒)
private int readTimeout; // 讀取超時(shí)(毫秒)
private String userAgent;
private static final Scanner scanner = new Scanner(System.in);
// 構(gòu)造函數(shù):支持自定義配置
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("程序運(yùn)行出錯(cuò): " + e.getMessage());
e.printStackTrace();
} finally {
scanner.close();
}
}
// 從控制臺(tái)收集待處理文件
private List<Path> collectFilesFromConsole() {
List<Path> files = new ArrayList<>();
System.out.println("Markdown圖片處理工具 - 批量模式");
System.out.println("--------------------------------");
System.out.println("請(qǐng)輸入Markdown文件或目錄路徑(每行一個(gè),輸入空行開始處理)");
System.out.println("支持:");
System.out.println(" - 單個(gè)文件路徑");
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("錯(cuò)誤: 文件/目錄不存在 - " + pathStr);
continue;
}
processedPaths.add(pathStr);
if (Files.isDirectory(path)) {
List<Path> dirFiles = getMdFilesRecursively(path);
files.addAll(dirFiles);
System.out.printf("已添加目錄: %s (%d個(gè)MD文件)%n", path, dirFiles.size());
} else {
if (isMarkdownFile(path)) {
files.add(path);
System.out.println("已添加文件: " + path);
} else {
System.out.println("錯(cuò)誤: 不是Markdown文件 - " + pathStr);
}
}
}
return files;
}
// 遞歸獲取目錄中所有Markdown文件(使用NIO簡(jiǎn)化代碼)
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 個(gè)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;
}
// 處理單個(gè)Markdown文件
private void processMarkdownFile(Path mdFile) throws IOException {
// 讀取文件內(nèi)容
String content = readFileContent(mdFile);
// 創(chuàng)建圖片保存目錄(基于原文件目錄)
Path imageDirPath = mdFile.getParent().resolve(imageDir);
Files.createDirectories(imageDirPath); // 自動(dòng)創(chuàng)建父目錄
// 處理圖片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(" 未發(fā)現(xiàn)需要處理的遠(yuǎn)程圖片URL");
}
}
// 使用Java 8兼容的方式讀取文件內(nèi)容
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兼容的方式寫入文件內(nèi)容
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();
// 處理遠(yuǎn)程圖片URL(http/https)
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
totalImages++;
try {
// 下載圖片并返回本地文件名
String fileName = downloadImage(imageUrl, imageDirPath);
// 構(gòu)建相對(duì)路徑(原文件到images目錄的相對(duì)路徑)
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 {
// 創(chuàng)建HTTP連接并設(shè)置超時(shí)
HttpURLConnection connection = createHttpConnection(imageUrl);
// 獲取響應(yīng)狀態(tài)
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP請(qǐng)求失敗,狀態(tài)碼: " + responseCode);
}
// 根據(jù)Content-Type獲取正確的文件擴(kuò)展名
String contentType = connection.getContentType();
String extension = getExtensionFromContentType(contentType);
// 提取基礎(chǔ)文件名(不含擴(kuò)展名)
String baseFileName = extractBaseFileName(imageUrl);
// 拼接完整文件名(基礎(chǔ)名+擴(kuò)展名)
String originalFileName = baseFileName + "." + extension;
// 生成唯一文件名(避免重復(fù))
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;
}
// 創(chuàng)建HTTP連接(統(tǒng)一配置超時(shí)和請(qǐng)求頭)
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); // 連接超時(shí)
connection.setReadTimeout(readTimeout); // 讀取超時(shí)
connection.setInstanceFollowRedirects(true); // 自動(dòng)跟隨重定向
return connection;
}
// 從Content-Type獲取正確的文件擴(kuò)展名(兼容Java 8)
private String getExtensionFromContentType(String contentType) {
if (contentType == null) {
return "jpg"; // 默認(rèn) fallback
}
// 傳統(tǒng)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"; // 未知類型默認(rèn)jpg
}
}
// 提取基礎(chǔ)文件名(不含擴(kuò)展名)
private String extractBaseFileName(String url) {
// 移除URL查詢參數(shù)和錨點(diǎn)
String path = url.split("[?#]")[0];
// 提取最后一個(gè)路徑段
String fileName = path.substring(path.lastIndexOf('/') + 1);
// 移除可能的擴(kuò)展名(避免重復(fù))
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;
}
// 打印處理總結(jié)
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("--------------------------------");
}
// 配置參數(shù)設(shè)置方法(提高靈活性)
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;
}
// 處理總結(jié)內(nèi)部類
static class ProcessSummary {
int successfulFiles = 0;
int failedFiles = 0;
}
// 圖片處理結(jié)果內(nèi)部類
static class ImageProcessingResult {
String processedContent;
int totalImages;
int successfulImages;
int failedImages;
}
}
主要優(yōu)化點(diǎn)
增強(qiáng)靈活性:將images目錄、輸出文件后綴(_processed)、HTTP超時(shí)等硬編碼參數(shù)改為可配置字段(通過setter調(diào)整),適應(yīng)不同場(chǎng)景。
現(xiàn)代IO API:用java.nio.file(Path、Files)替代File類,簡(jiǎn)化路徑處理(如resolve拼接路徑、createDirectories自動(dòng)創(chuàng)建目錄),代碼更簡(jiǎn)潔。
準(zhǔn)確的文件擴(kuò)展名:原代碼默認(rèn)用jpg,優(yōu)化后通過Content-Type動(dòng)態(tài)獲?。ㄈ?code>image/png對(duì)應(yīng)png),避免文件格式錯(cuò)誤。
HTTP超時(shí)控制:為HttpURLConnection設(shè)置連接超時(shí)和讀取超時(shí),防止因網(wǎng)絡(luò)問題導(dǎo)致程序卡死。
遞歸查找優(yōu)化:用Files.walk替代手動(dòng)遞歸,代碼更簡(jiǎn)潔,且自動(dòng)處理異常。
路徑處理優(yōu)化:生成新文件路徑時(shí)通過Path API操作,避免跨平臺(tái)路徑分隔符問題(如Windows的\和Linux的/)。
代碼復(fù)用:抽離createHttpConnection、getExtensionFromContentType等方法,減少重復(fù)代碼,提高可維護(hù)性。
錯(cuò)誤處理增強(qiáng):細(xì)化異常信息(如區(qū)分連接超時(shí)、HTTP錯(cuò)誤碼),用戶可更清晰定位問題。
通過這些優(yōu)化,工具不僅保留了原有的核心功能,還在靈活性、健壯性和易用性上有顯著提升,更適合實(shí)際場(chǎng)景使用。
到此這篇關(guān)于Java實(shí)現(xiàn)Markdown圖片批量本地化處理工具的文章就介紹到這了,更多相關(guān)Java圖片批處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于@JsonProperty,@NotNull,@JsonIgnore的具體使用
這篇文章主要介紹了關(guān)于@JsonProperty,@NotNull,@JsonIgnore的具體使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
解決SpringCloud Gateway配置自定義路由404的坑
這篇文章主要介紹了解決SpringCloud Gateway配置自定義路由404的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
一文詳解Springboot中filter的原理與注冊(cè)
這篇文章主要為大家詳細(xì)介紹了Springboot中filter的原理與注冊(cè)的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),對(duì)我們掌握SpringBoot有一定的幫助,需要的可以參考一下2023-02-02
淺談SpringCloud feign的http請(qǐng)求組件優(yōu)化方案
這篇文章主要介紹了淺談SpringCloud feign的http請(qǐng)求組件優(yōu)化方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02
Spring框架 引入@Resource注解報(bào)空指針的解決
這篇文章主要介紹了Spring框架 引入@Resource注解報(bào)空指針的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
IDEA中Java出現(xiàn)無效的源發(fā)行版錯(cuò)誤的解決辦法
這篇文章主要給大家介紹了關(guān)于IDEA中Java出現(xiàn)無效的源發(fā)行版錯(cuò)誤的解決辦法,IDEA中Java出現(xiàn)?效的源發(fā)?版解決辦法出現(xiàn)該問題的原因是項(xiàng)?Project當(dāng)中的jdk與電腦當(dāng)中的jdk版本不?致造成的,需要的朋友可以參考下2023-10-10
SpringBoot實(shí)現(xiàn)消息推送的實(shí)戰(zhàn)(讓服務(wù)器學(xué)會(huì)"主動(dòng)搭訕")
本文介紹了如何使用SSE技術(shù)實(shí)現(xiàn)SpringBoot應(yīng)用的消息推送,并提供了詳細(xì)的實(shí)戰(zhàn)步驟和優(yōu)化建議,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2026-03-03
SpringCloud中的Feign遠(yuǎn)程調(diào)用最佳實(shí)踐方案
本文介紹了Feign作為聲明式HTTP客戶端的優(yōu)勢(shì),包括提升代碼可讀性、簡(jiǎn)化URL維護(hù)及支持自定義配置(日志級(jí)別、連接池),通過模塊化抽取Feign客戶端和公共類,實(shí)現(xiàn)代碼復(fù)用并解決掃描包問題,優(yōu)化微服務(wù)間調(diào)用效率,對(duì)SpringCloud Feign遠(yuǎn)程調(diào)用相關(guān)知識(shí)感興趣的朋友一起看看吧2025-07-07

