markdown文章快速转微信公众号文章
我日常习惯使用typora进行文档的记录,由于优快云的骚操作,我计划将原有文档及部分本地文档慢慢更新到微信公众号,但存在以下问题:
- typora的markdown和微信公众号的格式有差别,导致直接复制会存在问题
- 当文章涉及图片,每次都要将图片手动复制到文章会格外麻烦,因此我需要设置一个图床
- 我希望保留本地图片缓存,以避免图床失效
经过一些探索,得出了以下方案:
-
微信markdown编辑器:https://github.com/doocs/md,该编辑器允许将markdown同步修改为微信公众号格式,只需一键复制然后粘贴到公众号即可
-
使用PicGo和github实现图床
-
typora仍然保持添加图片会把保存在本地,无需做任意改动
具体操作如下:
-
使用docker部署微信markdown编辑器
docker run -d -p 8085:80 doocs/md
然后访问:
http://localhost:8085/
-
安装picGo(https://github.com/Molunerfinn/PicGo)
release中下载对应的安装程序直接安装即可,安装路径下有一个叫做PicGo.exe的程序,这很重要
-
我的picGo配置如下
然后在设置中开启时间戳重命名
-
编写脚本对指定路径的markdown进行图片上传并替换其中的链接
-
将result.md中的内容复制到微信markdown编辑器
微调好格式后直接复制
-
粘贴到微信公众号的编辑器中
公众号会花一点时间将图片自动上传到
mmbiz.qlogo.cn
这个域名下,稍微等图片处理完成,如果有不行的,公众号会提示对指定图片重试,点重试就可以,等图片都处理完直接发布文章。
代码如下:
# -*- coding:utf-8 -*-
import csv
import json
import re
import os
import argparse
from pathlib import Path
from time import sleep
from typing import List, Dict, Optional, Any
import subprocess
import requests
def save_mapping(list1: List[Any], list2: List[Any], file_path: str, format: str = 'json') -> None:
"""
将两个等长列表进行一对一映射并保存到本地文件
参数:
list1: 第一个列表
list2: 第二个列表(与list1长度相同)
file_path: 保存的文件路径
format: 文件格式,支持'json'或'csv',默认为'json'
异常:
ValueError: 如果两个列表长度不一致
ValueError: 如果指定的格式不支持
"""
# 检查列表长度是否一致
if len(list1) != len(list2):
raise ValueError("两个列表的长度必须一致")
# 创建映射字典
mapping = dict(zip(list1, list2))
# 根据指定格式保存文件
if format.lower() == 'json':
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(mapping, f, ensure_ascii=False, indent=2)
elif format.lower() == 'csv':
with open(file_path, 'w', encoding='utf-8', newline='') as f:
writer = csv.writer(f)
writer.writerow(['key', 'value']) # 表头
writer.writerows(mapping.items())
else:
raise ValueError("不支持的格式,仅支持'json'或'csv'")
def open_command_in_new_window(command, title=None, timeout=None):
try:
# 构建启动命令
start_cmd = "start"
# 如果指定了标题,添加到命令中
if title:
start_cmd += f' "{title}"'
# 添加命令
start_cmd += f' cmd /c "{command}"'
# 执行命令
process = subprocess.Popen(start_cmd, shell=True)
# 如果指定了超时时间,等待命令执行完成
if timeout is not None:
try:
process.wait(timeout=timeout)
return process.returncode == 0
except subprocess.TimeoutExpired:
print(f"命令执行超时 ({timeout}秒)")
return False
return True
except FileNotFoundError:
print(f"错误: 找不到命令或程序 - {command}")
raise FileNotFoundError
except PermissionError:
print("错误: 没有执行命令的权限")
raise PermissionError
except subprocess.SubprocessError as e:
print(f"子进程错误: {e}")
raise subprocess.SubprocessError
except Exception as e:
print(f"未知错误: {e}")
raise Exception
def extract_image_links(content: str) -> List[Dict[str, str]]:
"""从 Markdown 内容中提取本地图片链接"""
image_pattern = r'\!\[(.*?)\]\((.*?)\)'
# 匹配 HTML 图片语法:
html_image_pattern = r'<img\s+[^>]*src="([^"]+)"[^>]*>'
image_links = []
# 提取 Markdown 格式的图片
for match in re.finditer(image_pattern, content):
alt_text, image_path = match.groups()
# 检查是否为本地文件(非 http/https 开头)
if not image_path.startswith(('http:', 'https:')):
image_links.append({
'type': 'markdown',
'alt': alt_text,
'path': image_path,
'match': match
})
# 提取 HTML 格式的图片
for match in re.finditer(html_image_pattern, content):
image_path = match.group(1)
# 检查是否为本地文件
if not image_path.startswith(('http:', 'https:')):
image_links.append({
'type': 'html',
'alt': '', # HTML 中的 alt 可能在其他属性中,这里简化处理
'path': image_path,
'match': match
})
return image_links
def replace_image_links(content: str, old_links: list, new_links: list) -> str:
"""替换Markdown内容中的图片链接"""
updated_content = content
for old_link, new_link in zip(old_links, new_links):
# 转义特殊字符,避免正则表达式错误
escaped_old_link = re.escape(old_link)
# 构建匹配模式,确保只替换完整的图片链接
pattern = fr'(!\[(.*?)\]\()({escaped_old_link})(\))'
# 使用lambda函数处理替换,确保只替换路径部分
updated_content = re.sub(pattern, lambda m: f"{m.group(1)}{new_link}{m.group(4)}", updated_content)
return updated_content
class MarkdownImageUploader:
def __init__(self, input_file: str, picgo_path, image_dir, output_file: str = None, ):
"""初始化 Markdown 图片上传器"""
self.input_file = Path(input_file)
self.image_dir = image_dir
self.output_file = Path(output_file) if output_file else self.input_file.with_name(
f"{self.input_file.stem}_new{self.input_file.suffix}")
self.picgo_path = picgo_path
self.image_mapping = {} # 存储原始图片路径与图床链接的映射
self.storage_path = "image_mapping.json"
# 尝试启动picGO,如果已经启动就啥问题没有
open_command_in_new_window(command=picgo_path, title="pico server", timeout=None)
self.load_image_mapping()
def load_image_mapping(self):
"""
尝试加载本地映射表 origin_image_path:new_image_url
:return:
"""
if os.path.exists(self.storage_path):
try:
with open(self.storage_path, 'r') as f:
self.image_mapping = json.load(f)
print(f"数据已成功从 {self.storage_path} 加载")
except Exception as e:
print(f"加载数据时出错: {e}")
else:
print(f"存储文件 {self.storage_path} 不存在,将使用空字典")
def save_mapping(self):
"""将image_mapping保存到本地JSON文件"""
try:
with open(self.storage_path, 'w') as f:
json.dump(self.image_mapping, f, indent=4)
print(f"数据已成功保存到 {self.storage_path}")
except Exception as e:
print(f"保存数据时出错: {e}")
def upload_images(self, image_origin_path: str or list[str]):
headers = {
'Content-Type': 'application/json'
}
url = "http://127.0.0.1:36677/upload"
image_list = []
if isinstance(image_origin_path, str):
image_list.append(image_origin_path)
elif isinstance(image_origin_path, list):
image_list = image_origin_path
else:
print("image_origin_path格式错误,请选择str or list[str]")
return None
for image_link in image_list:
try:
path = os.path.abspath(image_link)
# 如果不存在,尝试获取图片路径
if not os.path.exists(path):
image_filename = os.path.basename(image_link)
path= os.path.normpath(self.image_dir +"/"+ image_filename)
# 如果本地映射表存在该映射就循环下一个
if self.image_mapping.get(image_link):
continue
# 重复上传直到当前url成功
count = 0
while 1:
payload = json.dumps({"list": [path]})
response = requests.post(url, headers=headers, data=payload)
result = json.loads(response.text)
if "result" in result and isinstance(result["result"], list):
if not self.image_mapping.get(image_link):
self.image_mapping[image_link] = result["result"][0]
# 上传成功停止1秒
sleep(1)
break
count += 1
if count > 5:
print("当前图片重复上传次数超过5次,检查图片路径。原始路径:%s, 请求路径:%s"%(image_link, path))
except Exception as e:
print("处理%s时出错"%image_link)
print(e)
return None
def process_markdown(self) -> None:
"""处理 Markdown 文件并替换图片链接"""
try:
# 读取 Markdown 文件内容
with open(self.input_file, 'r', encoding='utf-8') as f:
content = f.read()
# 提取本地图片链接
image_links = extract_image_links(content)
print(image_links)
if not image_links:
print("没有找到需要处理的本地图片链接")
return
print(f"找到 {len(image_links)} 个本地图片链接")
origin_image_list = []
for item in image_links:
path = item.get('path')
origin_image_list.append(path)
print(origin_image_list)
self.upload_images(origin_image_list)
# 替换文档中的url之前需要保存图片映射表
self.save_mapping()
for origin_image_path, url in self.image_mapping.items():
content = re.sub(re.escape(origin_image_path), url, content)
# 写入更新后的内容
with open(self.output_file, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Markdown文件已更新并保存至: {self.output_file}")
except Exception as e:
print(f"错误: 处理 Markdown 文件时发生意外错误: {e}")
def main():
parser = argparse.ArgumentParser(description='Markdown 图片上传并替换链接工具')
parser.add_argument('input', help='输入的 Markdown 文件路径')
parser.add_argument('-o', '--output', help='输出的 Markdown 文件路径,默认为原文件名添加 "_new" 后缀')
parser.add_argument('-p', '--picgo', default='picgo', help='PicGo 可执行文件路径,默认为 "picgo"')
parser.add_argument('-i', '--image_dir', required=True, help='图片所在目录')
args = parser.parse_args()
# 检查输入文件是否存在
if not os.path.exists(args.input):
print(f"错误: 输入文件不存在: {args.input}")
return
# 检查输入文件是否为 Markdown 文件
if not args.input.lower().endswith(('.md', '.markdown')):
print(f"错误: 输入文件不是 Markdown 文件: {args.input}")
return
# 创建并运行 Markdown 图片上传器
uploader = MarkdownImageUploader(args.input, picgo_path=args.picgo, image_dir=args.image_dir, output_file=args.output)
uploader.process_markdown()
if __name__ == "__main__":
# python md_image_uploader.py E:\document\笔记\网络安全\(4)无线安全\无线局域网\无线局域网技术分析及攻击实战.md -p D:\picgo\PicGo.exe -o result.md -i E:\document\笔记\网络安全\(4)无线安全\无线局域网\assets
main()
文章已同步至公众号,求关注: