跳至主要內容

批量上传markdown中图片到gitee图床

soulballad总结文字总结文字总结约 2716 字大约 9 分钟

之前在新建上百个markdown文件,每个文件中都或多或少有一些图片,这些图片全部都在本地

md文件一旦离开本机,其中的图片就无法展示,所以需要把它们换成网络存储;

有一种方案是使用 Gitee+PicGo 来上传图片,其中 Gitee 作为图床,PicGo作为插件集成到 Typora 软件中,可以在图片上右击选中进行上传

image-20210724212152611

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

image-20210724212243857

image-20210724212307620

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

image-20210724212416070

所以需要有一种批量上传的方式。

这个问题困扰了很久,一直没有好的解决方案,批量上传失败的原因,之前猜测可能和 Gitee 有关,甚至考虑过换成阿里云 OSS 进行存储。

直到今天(20210724),灵机一动,有了一个想法:

既然 Gitee 保存图片是以代码仓库的形式存在,那么是否可以通过 TortoiseGit 批量 Commit/Push 图片到远程仓库,然后再替换 md 中的链接呢?

想到就做:于是找了一个md文件,然后手动将其中一个图片 push 到 Gitee仓库,再仿照 PicGo 上传后生成的链接格式,修改原来的图片地址,发现是可行的。

想法成立!!!

作为程序员,替换md中的链接地址肯定要通过代码来实现,所以根据根据已有的情况,给自己确立了几点需求:

  1. 批量替换 100 多个 md 文件中的图片地址;
  2. 之前的图片统一保存在根目录下,现在最好根据日期进行分组;
  3. 分组的日期选择图片文件的创建日期,一般也是 md 文件的创建日期,后面看到图片的地址就可以联想到 md 是什么时候写的;
  4. 之前的图片名称格式不统一,有时间戳、image+时间戳、英文字符串、随机数、特殊符号的等多种,需要将文件名格式进行统一,使用创建时间生成时间戳进行重命名;
  5. 为了防止出现操作失误,最好有备份;

主代码如下:

  1. 先对已有 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()));
            }
        }
    }
    
  2. 替换 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;
    }
    
  3. 将分好组后的文件推送到远程仓库

    image-20210724214852084

  4. 验证图片是否能正常显示

待优化点:

  1. 图片按照天分组,目录较多,最好能根据 yyyy/MM/dd 进行分组;

  2. 已将上传过的文件,保存在 Gitee 仓库根目录下,最好能去除;

  3. 创建日期可能不太准确,有时间戳的以时间戳为准;

    image-20210724223727825

    image-20210724223751621

经过一番调整优化后,最红解决了上述问题,按照日期进行分组

image-20210725140419389

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

image-20210725140457521

后面整个过程中用到的代码如下

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);
        }
    }
}
上次编辑于:
贡献者: soulballad