markdown中图片url替换
约 2583 字大约 9 分钟
本地md文件默认路径为: E:\笔记\自己整理\typora-user-images。如果要在非本地访问,需要将其保存到 gitee 等图床上。
在保存到云床之前,需要对文件重命名并进行分组,类似如下效果:
重命名(根据创建日期):2267589-d469f4d79081582a.png -> 202006252052369002.png
日期分组:img/2020/06/25/202006252052369002.png
为了能替换 md 文件中图片地址,必须知道文件的旧名字和新名字,保存在 name.txt 中
具体实现分如下几步:
1、将原有图片文件重命名复制到新路径,方便操作和恢复
private static final String OLD_PIC_PATH = "E:\\笔记\\自己整理\\test-md\\old-pic\\";
private static final String MID_PIC_PATH = "E:\\笔记\\自己整理\\test-md\\old-pic2\\";
private static final String NEW_PIC_PATH = "E:\\笔记\\自己整理\\test-md\\new-pic\\";
private static final String OLD_MD_PATH = "E:\\笔记\\自己整理\\test-md\\old-md-path\\";
private static final String NEW_MD_PATH = "E:\\笔记\\自己整理\\test-md\\new-md-path\\";
private static final String GITEE_PATH_PREFIX = "https://vcoqrkpdhs.oss-cn-shenzhen.aliyuncs.com/img/";
private static final String MD_FILE_EXT = "md";
private static final String PIC_MAP_FILE = "name.txt";
/**
* 测试复制图片,重命名并复制到一个新路径
*/
@Test
public void test_copy_pic() {
// 判断字符串是否为纯数字的正则表达式
Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
// 列出所有文件
List<File> loopFiles = FileUtil.loopFiles(OLD_PIC_PATH);
List<String> nameLines = new ArrayList<>();
Assert.isTrue(CollUtil.isNotEmpty(loopFiles));
// 遍历
for (File lp : loopFiles) {
// 获取文件的创建日期
LocalDateTime createdTime = getCreatedTime(lp);
// 原始文件名
String fileName = FileNameUtil.getPrefix(lp.getName());
// 后缀
String ext = FileNameUtil.getSuffix(lp.getName());
// 新的文件名
String newName = fileName;
// 新路径: 加上创建日期
String newPath = MID_PIC_PATH + createdTime.format(formatter);
FileUtil.mkdir(newPath);
if (pattern.matcher(fileName).matches()) {
// 文件名是纯数字
newName = fileName;
} else {
// 需要重命名
if (fileName.startsWith("image-")) {
newName = StrUtil.replace(fileName, "image-", "");
} else {
newName = IdCreator.getId(createdTime);
}
nameLines.add(fileName + "." + ext + "," + newName + "." + ext);
}
FileUtil.copy(lp, new File(newPath + File.separator + newName + "." + ext), false);
}
FileUtil.writeUtf8Lines(nameLines, new File(MID_PIC_PATH + PIC_MAP_FILE));
}
private static LocalDateTime getCreatedTime(File f) {
try {
BasicFileAttributes fileAttributes = Files.readAttributes(Paths.get(f.toURI()), BasicFileAttributes.class);
long millis = fileAttributes.creationTime().toMillis();
return LocalDateTimeUtil.of(millis);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("get time occurs error", e);
}
}
中间使用到 IdCreator
public class IdCreator {
private static int addPart = 1;
private static String result = "";
private static DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
private static String lastDate = "";
private static AtomicInteger atomicInteger = new AtomicInteger();
private static Map<String, Integer> map = new HashMap<String, Integer>();
/**
* 获取主键
* @param length 长度
* @return 返回17位时间戳+3位递增数
*/
public synchronized static String getId(int length) {
//获取时间部分字符串
LocalDateTime now = LocalDateTime.now();
String nowStr = now.format(format);
//获取数字后缀值部分
if (IdCreator.lastDate.equals(nowStr)) {
addPart += 1;
} else {
addPart = 1;
lastDate = nowStr;
}
if (length > 17) {
length -= 17;
for (int i = 0; i < length - ((addPart + "").length()); i++) {
nowStr += "0";
}
nowStr += addPart;
result = nowStr;
} else {
result = nowStr;
}
return result;
}
public synchronized static String getId(LocalDateTime date) {
String nowStr = date.format(format);
Integer seq = map.get(nowStr);
if (null == seq) {
map.put(nowStr, 1);
}else{
map.put(nowStr, ++seq);
}
return nowStr + map.get(nowStr);
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
System.out.println(getId(LocalDateTime.now()));
}
// System.out.println(IdCreator.getId(20));
}
}
2、对文件进行分组,按照日期分组
/**
* 测试组文件,对文件进行分组,按照日期分组
*/
@Test
public void test_group_file() {
List<File> loopFiles = FileUtil.loopFiles(MID_PIC_PATH);
Assert.isTrue(CollUtil.isNotEmpty(loopFiles));
for (File file : loopFiles) {
String ext = FileNameUtil.getSuffix(file);
String name = file.getName();
if ("txt".equals(ext)) {
FileUtil.copy(file, new File(NEW_PIC_PATH + File.separator + name), false);
continue;
}
String path = file.getPath();
String dir = path.replace("\\" + name, "");
// 20190715 -> 2019/07/15
String parentDir = dir.substring(dir.lastIndexOf("\\") + 1);
String newDir = StrUtil.sub(parentDir, 0, 4) + "/" + StrUtil.sub(parentDir, 4, 6) + "/" + StrUtil.sub(parentDir, 6, 8);
FileUtil.mkdir(NEW_PIC_PATH + newDir);
FileUtil.copy(file, new File(NEW_PIC_PATH + newDir + "\\" + name), false);
}
}
3、替换md文件中图片路径
/**
* 测试改变md url,改变md中的图片地址
*/
@Test
public void test_change_md_url() {
// 列出所有md文件
List<File> loopMds = FileUtil.loopFiles(OLD_MD_PATH);
// 列出所有图片,新文件(已重命名)
List<File> newPics = FileUtil.loopFiles(NEW_PIC_PATH);
Assert.isTrue(CollUtil.isNotEmpty(loopMds) && CollUtil.isNotEmpty(newPics));
// 读取 name.txt 文件,获取映射关系
List<String> nameMapList = FileUtil.readUtf8Lines(new File(NEW_PIC_PATH + PIC_MAP_FILE));
// Map<newName, oldName>
Map<String, String> newOldNameMap = nameMapList.stream().collect(Collectors.toMap(l -> l.split(",")[1], l -> l.split(",")[0]));
// Map<oldName, newName>
Map<String, String> oldNewNameMap = nameMapList.stream().collect(Collectors.toMap(l -> l.split(",")[0], l -> l.split(",")[1]));
// 根据创建日期将图片关系进行映射,Map<fileName, yyyy/MM/dd> -> <image-20200419121824237.png, 2020/04/19>
Map<String, String> fileDayMap = getNameDayMap(newPics, newOldNameMap);
for (File md : loopMds) {
// 获取文件后缀
String suffix = FileNameUtil.getSuffix(md);
// 如果不是md文件,不处理
if (!MD_FILE_EXT.equals(suffix)) {
continue;
}
// 读取md文件的内容
List<String> lines = FileUtil.readUtf8Lines(md);
// 替换url,生成新的文件内容
//  -> 
List<String> newLines = getNewLines(fileDayMap, lines, oldNewNameMap, OLD_PIC_PATH, GITEE_PATH_PREFIX);
// 写入替换后的内容
FileUtil.writeUtf8Lines(newLines, new File(NEW_MD_PATH + md.getName()));
}
}
private Map<String, String> getNameDayMap(List<File> newPics, Map<String, String> newOldNameMap) {
Map<String, String> map = new HashMap<>();
for (File pic : newPics) {
String newPicName = pic.getName();
String newPicPath = pic.getPath();
// 获取图片所在路径 天,E:\笔记\自己整理\typora-user-images\2020\11\29\202011292139028981.png -> 2020\11\29\
String day = StrUtil.sub(newPicPath, newPicPath.indexOf(NEW_PIC_PATH) + NEW_PIC_PATH.length(), newPicPath.indexOf(newPicName) - 1);
// 新文件的路径
if (newOldNameMap.containsKey(newPicName)) {
// 获取old名字,如果有重命名的话
newPicName = newOldNameMap.get(newPicName);
}
// <202011292139028981.png, 2020/11/29>
map.put(newPicName, day.replace("\\", "/"));
}
return map;
}
private static List<String> getNewLines(Map<String, String> fileDayMap, List<String> lines, Map<String, String> oldNewNameMap, String oldPrefix, String newPrefix) {
List<String> newLines = new ArrayList<>(lines.size());
// 判断是否包含图片
String picLine = lines.stream().filter(l -> l.contains(oldPrefix)).findFirst().orElse("");
// 内容不包含图片,不处理
if (StrUtil.isBlank(picLine)) {
return lines;
}
for (String line : lines) {
// 是否包含指定前缀
if (!line.contains(oldPrefix)) {
newLines.add(line);
continue;
}
// 获取图片名称
String oldPicName = StrUtil.sub(line, line.indexOf(oldPrefix) + oldPrefix.length(), line.indexOf(")"));
if (!fileDayMap.containsKey(oldPicName)) {
newLines.add(line);
continue;
}
// 获取图片创建日期
String day = fileDayMap.get(oldPicName);
String newPicName = oldPicName;
if (oldNewNameMap.containsKey(oldPicName)) {
newPicName = oldNewNameMap.get(oldPicName);
}
// 替换图片地址url
String newLine = StrUtil.replace(line, oldPrefix + oldPicName, newPrefix + day + "/" + newPicName);
newLines.add(newLine);
}
return newLines;
}
后面需要手动将命名分组后的图片文件复制到本地 Gitee 路径下,然后提交 Gitee,md文件中的图片才能正常显示(这二步也可以考虑使用代码操作)。
完整代码如下:
package com.soulballad.book.mdr;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.soulballad.book.netty.IdCreator;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author :soulballad
* @version : v1.0 * @since :2021/9/21 21:41
*/public class MdCopyTest {
private static final String OLD_PIC_PATH = "E:\\笔记\\自己整理\\test-md\\old-pic\\";
private static final String MID_PIC_PATH = "E:\\笔记\\自己整理\\test-md\\old-pic2\\";
private static final String NEW_PIC_PATH = "E:\\笔记\\自己整理\\typora-user-images\\";
private static final String OLD_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\3.笔记\\1-1.Java学习-2\\";
private static final String NEW_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\3.笔记\\1-3.Java学习-4\\";
private static final String GITEE_PATH_PREFIX = "https://vcoqrkpdhs.oss-cn-shenzhen.aliyuncs.com/img/";
private static final String MD_FILE_EXT = "md";
private static final String PIC_MAP_FILE = "name.txt";
private static final String MD_PIC_PREFIX = "![";
private static final String MD_PIC_LINK_REGEX = ".*!\\[.*]\\(";
/**
* 测试复制图片,重命名并复制到一个新路径
*/
@Test
public void test_copy_pic() {
// 判断字符串是否为纯数字的正则表达式
Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
// 列出所有文件
List<File> loopFiles = FileUtil.loopFiles(OLD_PIC_PATH);
List<String> nameLines = new ArrayList<>();
Assert.isTrue(CollUtil.isNotEmpty(loopFiles));
// 遍历
for (File lp : loopFiles) {
// 获取文件的创建日期
LocalDateTime createdTime = getCreatedTime(lp);
// 原始文件名
String fileName = FileNameUtil.getPrefix(lp.getName());
// 后缀
String ext = FileNameUtil.getSuffix(lp.getName());
// 新的文件名
String newName = fileName;
// 新路径: 加上创建日期
String newPath = MID_PIC_PATH + createdTime.format(formatter);
FileUtil.mkdir(newPath);
if (pattern.matcher(fileName).matches()) {
// 文件名是纯数字
newName = fileName;
} else {
// 需要重命名
if (fileName.startsWith("image-")) {
newName = StrUtil.replace(fileName, "image-", "");
} else {
newName = IdCreator.getId(createdTime);
}
nameLines.add(fileName + "." + ext + "," + newName + "." + ext);
}
FileUtil.copy(lp, new File(newPath + File.separator + newName + "." + ext), false);
}
FileUtil.writeUtf8Lines(nameLines, new File(MID_PIC_PATH + PIC_MAP_FILE));
}
/**
* 测试组文件,对文件进行分组,按照日期分组
*/
@Test
public void test_group_file() {
List<File> loopFiles = FileUtil.loopFiles(MID_PIC_PATH);
Assert.isTrue(CollUtil.isNotEmpty(loopFiles));
for (File file : loopFiles) {
String ext = FileNameUtil.getSuffix(file);
String name = file.getName();
if ("txt".equals(ext)) {
FileUtil.copy(file, new File(NEW_PIC_PATH + File.separator + name), false);
continue;
}
String path = file.getPath();
String dir = path.replace("\\" + name, "");
// 20190715 -> 2019/07/15
String parentDir = dir.substring(dir.lastIndexOf("\\") + 1);
String newDir = StrUtil.sub(parentDir, 0, 4) + "/" + StrUtil.sub(parentDir, 4, 6) + "/" + StrUtil.sub(parentDir, 6, 8);
FileUtil.mkdir(NEW_PIC_PATH + newDir);
FileUtil.copy(file, new File(NEW_PIC_PATH + newDir + "\\" + name), false);
}
}
/**
* 测试改变md url,改变md中的图片地址
*/
@Test
public void test_change_md_url() {
// 列出所有md文件
List<File> loopMds = FileUtil.loopFiles(OLD_MD_PATH);
// 列出所有图片,新文件(已重命名)
List<File> newPics = FileUtil.loopFiles(NEW_PIC_PATH);
Assert.isTrue(CollUtil.isNotEmpty(loopMds) && CollUtil.isNotEmpty(newPics));
// 读取 name.txt 文件,获取映射关系
List<String> nameMapList = FileUtil.readUtf8Lines(new File(NEW_PIC_PATH + PIC_MAP_FILE));
// Map<newName, oldName>
Map<String, String> newOldNameMap = nameMapList.stream().collect(Collectors.toMap(l -> l.split(",")[1], l -> l.split(",")[0]));
// Map<oldName, newName>
Map<String, String> oldNewNameMap = nameMapList.stream().collect(Collectors.toMap(l -> l.split(",")[0], l -> l.split(",")[1]));
// 根据创建日期将图片关系进行映射,Map<fileName, yyyy/MM/dd> -> <image-20200419121824237.png, 2020/04/19>
Map<String, String> fileDayMap = getNameDayMap(newPics, newOldNameMap);
for (File md : loopMds) {
// 获取文件后缀
String suffix = FileNameUtil.getSuffix(md);
// 如果不是md文件,不处理
if (!MD_FILE_EXT.equals(suffix)) {
continue;
}
// 读取md文件的内容
List<String> lines = FileUtil.readUtf8Lines(md);
// 替换url,生成新的文件内容
//  -> 
List<String> newLines = getNewLines(fileDayMap, lines, oldNewNameMap, MD_PIC_PREFIX, GITEE_PATH_PREFIX);
// 写入替换后的内容
String sepPath = StrUtil.replace(md.getPath(), OLD_MD_PATH, "");
FileUtil.writeUtf8Lines(newLines, new File(NEW_MD_PATH + sepPath));
}
}
/**
* 根据实际文件地址改变图片地址
*/
@Test
public void test_change_md_url2() {
// 列出所有md文件
List<File> loopMds = FileUtil.loopFiles(OLD_MD_PATH);
// 列出所有图片,新文件(已重命名)
List<File> newPics = FileUtil.loopFiles(NEW_PIC_PATH);
Assert.isTrue(CollUtil.isNotEmpty(loopMds) && CollUtil.isNotEmpty(newPics));
// 根据实际文件路径,获取映射关系
Map<String, String> picPathMap = newPics.stream().collect(Collectors.toMap(File::getName, f -> f.getPath().replace(NEW_PIC_PATH, "").replaceAll("\\\\", "/"), (k, v) -> v));
for (File md : loopMds) {
// 获取文件后缀
String suffix = FileNameUtil.getSuffix(md);
// 如果不是md文件,不处理
if (!MD_FILE_EXT.equals(suffix)) {
continue;
}
// 读取md文件的内容
List<String> lines = FileUtil.readUtf8Lines(md);
// 替换url,生成新的文件内容
//  -> 
List<String> newLines = getNewLines(picPathMap, lines, Collections.emptyMap(), MD_PIC_PREFIX, GITEE_PATH_PREFIX);
// 写入替换后的内容
String sepPath = StrUtil.replace(md.getPath(), OLD_MD_PATH, "");
FileUtil.writeUtf8Lines(newLines, new File(NEW_MD_PATH + sepPath));
}
}
private Map<String, String> getNameDayMap(List<File> newPics, Map<String, String> newOldNameMap) {
Map<String, String> map = new HashMap<>();
for (File pic : newPics) {
String newPicName = pic.getName();
String newPicPath = pic.getPath();
// 获取图片所在路径 天,E:\笔记\自己整理\typora-user-images\2020\11\29\202011292139028981.png -> 2020\11\29\
String day = StrUtil.sub(newPicPath, newPicPath.indexOf(NEW_PIC_PATH) + NEW_PIC_PATH.length(), newPicPath.indexOf(newPicName) - 1);
// 新文件的路径
if (newOldNameMap.containsKey(newPicName)) {
// 获取old名字,如果有重命名的话
newPicName = newOldNameMap.get(newPicName);
}
// <202011292139028981.png, 2020/11/29>
map.put(newPicName, day.replace("\\", "/"));
}
return map;
}
private static List<String> getNewLines(Map<String, String> fileDayMap, List<String> lines, Map<String, String> oldNewNameMap, String oldPrefix, String newPrefix) {
List<String> newLines = new ArrayList<>(lines.size());
// 判断是否包含图片
String picLine = lines.stream().filter(l -> l.contains(oldPrefix)).findFirst().orElse("");
// 内容不包含图片,不处理
if (StrUtil.isBlank(picLine)) {
return lines;
}
Pattern pattern = Pattern.compile(MD_PIC_LINK_REGEX);
for (String line : lines) {
// 是否包含指定前缀
if (!line.contains(oldPrefix)) {
newLines.add(line);
continue;
}
// 获取图片名称
// String oldPicName = StrUtil.sub(line, line.indexOf(oldPrefix) + oldPrefix.length(), line.indexOf(")"));
String oldPicName = StrUtil.sub(line, line.lastIndexOf("/") + 1, line.indexOf(")"));
if (!fileDayMap.containsKey(oldPicName)) {
newLines.add(line);
continue;
}
// 获取图片创建日期
String day = fileDayMap.get(oldPicName);
String newPicName = oldPicName;
if (oldNewNameMap.containsKey(oldPicName)) {
newPicName = oldNewNameMap.get(oldPicName);
}
// 替换图片地址url
Matcher matcher = pattern.matcher(line);
if (!matcher.find()) {
System.err.println(line);
continue;
}
String newLine = matcher.group() + newPrefix + day + ")";
//String newLine = StrUtil.replace(line, oldPrefix + oldPicName, newPrefix + day + "/" + newPicName);
newLines.add(newLine);
}
return newLines;
}
private static LocalDateTime getCreatedTime(File f) {
try {
BasicFileAttributes fileAttributes = Files.readAttributes(Paths.get(f.toURI()), BasicFileAttributes.class);
long millis = fileAttributes.creationTime().toMillis();
return LocalDateTimeUtil.of(millis);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("get time occurs error", e);
}
}
}