批量上传markdown中图片到gitee图床
之前在新建上百个markdown文件,每个文件中都或多或少有一些图片,这些图片全部都在本地
md文件一旦离开本机,其中的图片就无法展示,所以需要把它们换成网络存储;
有一种方案是使用 Gitee+PicGo 来上传图片,其中 Gitee 作为图床,PicGo作为插件集成到 Typora 软件中,可以在图片上右击选中进行上传

但是这种方式每次只能上传一个文件,如果使用批量上传,就会出现错误


本地的文件夹汇总保存了很多图片,并不能一张张进行上传

所以需要有一种批量上传的方式。
这个问题困扰了很久,一直没有好的解决方案,批量上传失败的原因,之前猜测可能和 Gitee 有关,甚至考虑过换成阿里云 OSS 进行存储。
直到今天(20210724),灵机一动,有了一个想法:
既然 Gitee 保存图片是以代码仓库的形式存在,那么是否可以通过 TortoiseGit 批量 Commit/Push 图片到远程仓库,然后再替换 md 中的链接呢?
想到就做:于是找了一个md文件,然后手动将其中一个图片 push 到 Gitee仓库,再仿照 PicGo 上传后生成的链接格式,修改原来的图片地址,发现是可行的。
想法成立!!!
作为程序员,替换md中的链接地址肯定要通过代码来实现,所以根据根据已有的情况,给自己确立了几点需求:
- 批量替换 100 多个 md 文件中的图片地址;
- 之前的图片统一保存在根目录下,现在最好根据日期进行分组;
- 分组的日期选择图片文件的创建日期,一般也是 md 文件的创建日期,后面看到图片的地址就可以联想到 md 是什么时候写的;
- 之前的图片名称格式不统一,有时间戳、image+时间戳、英文字符串、随机数、特殊符号的等多种,需要将文件名格式进行统一,使用创建时间生成时间戳进行重命名;
- 为了防止出现操作失误,最好有备份;
主代码如下:
先对已有 2570 个文件进行分组,保存到其他目录
private static final String OLD_PIC_PATH = "E:\\笔记\\自己整理\\typora-user-images\\"; private static final String PIC_PATH_PREFIX = "E:\\笔记\\自己整理\\typora-user-images\\"; private static final String GITEE_PATH_PREFIX = "https://vcoqrkpdhs.oss-cn-shenzhen.aliyuncs.com/img/"; private static final Long ONE_M = 1024 * 1024L; private static final String MD_FILE_EXT = "md"; private static final String NEW_PIC_PATH = "E:\\笔记\\自己整理\\images\\"; private static final String NEW_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\3.笔记\\1-1.Java学习-new\\"; 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<>(); // 遍历 for (File lp : loopFiles) { // 获取文件的创建日期 LocalDateTime createdTime = getCreatedTime(lp); // 原始文件名前缀, 20210123102104155.png -> 20210123102104155 String fileName = FileNameUtil.getPrefix(lp.getName()); // 后缀 String ext = FileNameUtil.getSuffix(lp.getName()); // 新的文件名 String newName = fileName; // 新路径: 加上创建日期,根据日期进行分组 String newPath = NEW_PIC_PATH + createdTime.format(formatter); // 创建目录 FileUtil.mkdir(newPath); if (pattern.matcher(fileName).matches()) { // 文件名是纯数字,不作修改 newName = fileName; } else { // 不是纯数字,需要重命名 if (fileName.startsWith("image-")) { // image-xxxx格式,去掉前面的 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); } // 记录新老文件名的映射关系,保存的txt文件 FileUtil.writeUtf8Lines(nameLines, new File(NEW_PIC_PATH + "name.txt")); } // 获取文件的创建日期 private 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 DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); private static Map<String, Integer> map = new HashMap<String, Integer>(); /** * 获取主键 * @param length 长度 * @return 返回17位时间戳 */ 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())); } } }替换 md 文件中的图片地址
private static final String OLD_PIC_PATH = "E:\\笔记\\自己整理\\typora-user-images\\"; private static final String PIC_PATH_PREFIX = "E:\\笔记\\自己整理\\typora-user-images\\"; private static final String GITEE_PATH_PREFIX = "https://vcoqrkpdhs.oss-cn-shenzhen.aliyuncs.com/img/"; private static final Long ONE_M = 1024 * 1024L; private static final String MD_FILE_EXT = "md"; private static final String NEW_PIC_PATH = "E:\\笔记\\自己整理\\images\\"; private static final String NEW_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\3.笔记\\1-1.Java学习-new\\"; public static void main(String[] args) { // 列出所有md文件 List<File> loopMds = FileUtil.loopFiles(NEW_MD_PATH); // 列出所有图片,新路径文件(已重命名) List<File> loopPics = FileUtil.loopFiles(NEW_PIC_PATH); // 重命名文件映射 List<String> oldNewNameList = FileUtil.readUtf8Lines(new File(NEW_PIC_PATH + "name.txt")); // Map<newName, oldName> Map<String, String> newOldNameMap = oldNewNameList.stream().collect(Collectors.toMap(l -> l.split(",")[1], l -> l.split(",")[0])); // Map<oldName, newName> Map<String, String> oldNewNameMap = oldNewNameList.stream().collect(Collectors.toMap(l -> l.split(",")[0], l -> l.split(",")[1])); // Map<fileName, filePath>,将图片关系进行映射,根据创建日期 Map<String, String> fileDayMap = getNameDayMap(loopPics, newOldNameMap); for (File lp : loopMds) { // 获取文件后缀 String suffix = FileNameUtil.getSuffix(lp); // 如果不是md文件,不处理 if (!MD_FILE_EXT.equals(suffix)) { continue; } // 读取md文件的内容 List<String> lines = FileUtil.readUtf8Lines(lp); // 判断是否包含图片 String picLine = lines.stream().filter(l -> l.contains(PIC_PATH_PREFIX) || l.contains(GITEE_PATH_PREFIX)).findFirst().orElse(""); // 不包含图片,不处理 if (StrUtil.isBlank(picLine)) { continue; } // 新的文件内容 List<String> newLines = getNewLines(fileDayMap, lines, oldNewNameMap); // 覆盖文件原有内容 FileUtil.writeUtf8Lines(newLines, lp); } } private static List<String> getNewLines(Map<String, String> fileDayMap, List<String> lines, Map<String, String> oldNewNameMap) { List<String> newLines = new ArrayList<>(lines.size()); for (String line : lines) { // 是否包含指定前缀(本地图片路径), E:\\笔记\\自己整理\\typora-user-images\\ if (!line.contains(PIC_PATH_PREFIX)) { newLines.add(line); continue; } // 获取图片名称 String picName = StrUtil.sub(line, line.indexOf(PIC_PATH_PREFIX) + PIC_PATH_PREFIX.length(), line.indexOf(")")); if (!fileDayMap.containsKey(picName)) { newLines.add(line); continue; } // 获取图片创建日期 String day = fileDayMap.get(picName); String newPicName = picName; // 修改md中图片名为重新命名后的 if (oldNewNameMap.containsKey(picName)) { newPicName = oldNewNameMap.get(picName); } // 替换图片地址url String newLine = StrUtil.replace(line, PIC_PATH_PREFIX + picName, GITEE_PATH_PREFIX + day + "/" + newPicName); newLines.add(newLine); } return newLines; }将分好组后的文件推送到远程仓库

验证图片是否能正常显示
待优化点:
图片按照天分组,目录较多,最好能根据 yyyy/MM/dd 进行分组;
已将上传过的文件,保存在 Gitee 仓库根目录下,最好能去除;
创建日期可能不太准确,有时间戳的以时间戳为准;
经过一番调整优化后,最红解决了上述问题,按照日期进行分组

后续如果再有新文件产生,仍会保存在根目录下,需要手动处理

后面整个过程中用到的代码如下
package com.soulballad.book.netty;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.StrUtil;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author :soulballad
* @version : v1.0
* @since :2021/7/25 12:49
*/
public class PicMdUtil {
private static final String NEW_PIC_PATH = "E:\\GitRepository\\gitee\\PicGo\\img\\";
// private static final String NEW_MD_PATH = "E:\\笔记\\博客\\Hexo\\GiteePages\\Hexo\\source\\_posts2\\";
// private static final String NEW_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\3.笔记\\1-1.Java学习-new2\\5.架构师必备工具\\";
// private static final String NEW_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\8.工具\\";
private static final String NEW_MD_PATH = "E:\\笔记\\Obsidian\\我的知识\\3.笔记\\2.剑指offer\\";
private static final String GITEE_PATH_PREFIX = "https://vcoqrkpdhs.oss-cn-shenzhen.aliyuncs.com/img/";
private static final String LOCAL_PATH_PREFIX = "E:\\笔记\\自己整理\\typora-user-images\\";
private static final String MD_FILE_EXT = "md";
private static final String PIC_MAP_FILE = "name.txt";
@Test
public void test1() {
// 1.将图片移动到新目录,按照年月日分组(只移动一级目录下文件)
movePicToSubPath();
// 2.替换Md文件中图片路径, http -> http
changeUrlPath();
}
@Test
public void changeUrlPath() {
// 列出所有md文件
List<File> loopMds = FileUtil.loopFiles(NEW_MD_PATH);
// 列出所有图片,新文件(已重命名)
List<File> loopPics = FileUtil.loopFiles(NEW_PIC_PATH);
// 读取 name.txt 文件,获取映射关系
List<String> oldNewNameList = FileUtil.readUtf8Lines(new File(NEW_PIC_PATH + PIC_MAP_FILE));
// Map<newName, oldName>
Map<String, String> newOldNameMap = oldNewNameList.stream().collect(Collectors.toMap(l -> l.split(",")[1], l -> l.split(",")[0]));
// Map<oldName, newName>
Map<String, String> oldNewNameMap = oldNewNameList.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(loopPics, 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, GITEE_PATH_PREFIX, GITEE_PATH_PREFIX);
List<String> newLines = getNewLines(fileDayMap, lines, oldNewNameMap, LOCAL_PATH_PREFIX, GITEE_PATH_PREFIX);
// 写入替换后的内容
FileUtil.writeUtf8Lines(newLines, md);
}
}
@Test
public void movePicToSubPath() {
// 获取一级目录下图片文件
List<File> loopFiles = FileUtil.loopFiles(new File(NEW_PIC_PATH), 1, null);
// 新老文件名映射
List<String> nameLines = new ArrayList<>();
for (File file : loopFiles) {
// 目录不处理
if (file.isDirectory()) {
continue;
}
// name.txt 不处理
if (PIC_MAP_FILE.equals(file.getName())) {
continue;
}
// 获取创建时间, yyyyMMdd
String createdDay = getCreatedDay(file);
// 组装新目录, yyyy/MM/dd
String newDir = StrUtil.sub(createdDay, 0, 4) + "/" + StrUtil.sub(createdDay, 4, 6) + "/" + StrUtil.sub(createdDay, 6, 8);
// 创建目录
FileUtil.mkdir(NEW_PIC_PATH + newDir);
// 文件重命名
String newName = renamePic(file, nameLines);
// 移动文件到新目录
FileUtil.move(file, new File(NEW_PIC_PATH + newDir + "\\" + newName), false);
}
FileUtil.appendUtf8Lines(nameLines, new File(NEW_PIC_PATH + PIC_MAP_FILE));
}
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 picName = StrUtil.sub(line, line.indexOf(oldPrefix) + oldPrefix.length(), line.indexOf(")"));
if (!fileDayMap.containsKey(picName)) {
newLines.add(line);
continue;
}
// 获取图片创建日期
String day = fileDayMap.get(picName);
String newPicName = picName;
if (oldNewNameMap.containsKey(picName)) {
newPicName = oldNewNameMap.get(picName);
}
// 替换图片地址url
String newLine = StrUtil.replace(line, oldPrefix + picName, newPrefix + day + "/" + newPicName);
newLines.add(newLine);
}
return newLines;
}
private Map<String, String> getNameDayMap(List<File> picFiles, Map<String, String> renameMap) {
Map<String, String> map = new HashMap<>();
for (File pic : picFiles) {
String name = pic.getName();
String path = pic.getPath();
String sub = StrUtil.sub(path, path.indexOf(NEW_PIC_PATH) + NEW_PIC_PATH.length(), path.indexOf(name) - 1);
// 新文件的路径
if (renameMap.containsKey(name)) {
name = renameMap.get(name);
}
map.put(name, sub.replace("\\", "/"));
}
return map;
}
private String renamePic(File file, List<String> nameLines) {
Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
// 原始文件名
String fileName = FileNameUtil.getPrefix(file.getName());
// 后缀
String ext = FileNameUtil.getSuffix(file.getName());
// 新文件名
String newName;
if (pattern.matcher(fileName).matches()) {
// 文件名是纯数字
newName = fileName;
} else {
// 需要重命名
if (fileName.startsWith("image-")) {
newName = StrUtil.replace(fileName, "image-", "");
} else {
// 获取文件的创建日期
LocalDateTime createdTime = getCreatedTime(file);
newName = IdCreator.getId(createdTime);
}
// 记录新老文件名的映射关系
nameLines.add(fileName + "." + ext + "," + newName + "." + ext);
}
return newName + "." + ext;
}
private String getCreatedDay(File f) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDateTime createdTime = getCreatedTime(f);
return createdTime.format(formatter);
}
private 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);
}
}
}

