/ 教程  

Markdown文本中图片的整体迁移

问题复现

在markdown软件中写作时,为了使图片在所有平台都可以访问,必须要一个图床。插入图片时可以先通过iPic或者PicGO等图床软件进行转换,但是每次插图的时候都需要进行一次上传图片在粘贴markdown代码的操作,有点不方便,尤其是文章要插入很多图片的时候。

还有一个情况就是,比如在有一篇好文章你想把它保存为markdown,但是文章采用了防盗链功能,直接转换图片地址就有点行不通。

那么有什么方法,可以一键将markdown文件中的所有图片批量上传至图床并且自动更换链接呢?

使用教程

本地图片可以是电脑中任意位置的文件(使用绝对路径),也可以MD同名文件夹下的图片(使用相对路径)。插入的网络图片也不用担心出现“此图片来自微信公众平台,未经允许不可引用”之类问题,因为脚本处理之后会直接转到到个人的七牛空间。插入网络图片如果是自己的七牛空间里面的图片,脚本会自动忽略,避免重复上传。

插完图之后也可以随时修改,不用担心图片管理混乱的问题(上传的图床的图片不方便管理)。

写作完成,浏览修改,运行

1
python 脚本路径 MD文件路径 [是否图片压缩]
  • 0 - 不需要压缩
  • 1 - 需要压缩,注意tiniPNGkey

上传之后的七牛中的图片名称为上传日期-MD文件名/image序号.png or jpg,方便后期管理和备份
发表markdown文件到网络,删除备份的MD.bak和清理本地图片。

使用注意事项

Python3.x
依赖包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import re
import os
import sys
import datetime
import math
import imghdr
import shutil
import random
import string
import tinify
import urllib
import sqlite3
import operator
from hashlib import md5
from qiniu import Auth, put_file, etag, BucketManager
from datetime import date
import validators

注意使用前最好创建一个 sqlite3 数据库。

这些重要的参数不要忘了哦 ~

1
2
3
4
5
ak = ''		# ak
sk = '' # sk
domain = '' # 上传域名
bucket = '' # 空间名称
tinify.key = '' # 设置tinipng的key

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import re
import os
import sys
import time
import datetime
import math
import imghdr
import shutil
import random
import string
import tinify
import urllib
import sqlite3
import operator
from hashlib import md5
from qiniu import Auth, put_file, etag, BucketManager
import validators

# 七牛配置
ak = ' '
sk = ' '
domain = ' ' # 上传域名
bucket = ' ' # 空间名称

tinify.key = ' ' # 设置tinipng的key

q = Auth(ak, sk) # 七牛认证
#初始化BucketManager
Bucket_Manager = BucketManager(q)
md_loc = '' # md地址
need_zip = True

today = datetime.date.today()
yyyymmdd = today.strftime('%Y%m%d') #创建时间变量,用于图片起名
total, success, failure, ignore = 0, 0, 0, 0
def upload_file(upload_file_name):
'''
根据给定的图片名上传图片,并返回图片地址和一些上传信息
'''
global success , ignore , failure
#rstr = str(time.time())+''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 12))
#key = md5(rstr.encode('utf-8')).hexdigest() # 上传到七牛后的图片名
key = yyyymmdd+"_"+os.path.splitext(os.path.basename(sys.argv[1]) )[0]+"/image"+('{0}'.format(success+1))+os.path.splitext(upload_file_name)[1]
# 上传到七牛后的图片名:上传日期-MD文件名/image序号.png or jpg
mime_type = upload_file_name[upload_file_name.rfind('.') + 1:]
token = q.upload_token(bucket, key)
ret, info = put_file(token, key, upload_file_name, mime_type=mime_type, check_crc=True)
if ret['key'] == key and ret['hash'] == etag(upload_file_name):
success = success + 1
return 'http://' + domain + '/' +key, info
failure = failure + 1
return None

def transfer_online_img(old_link):
'''
根据给定的图片链接上传图片到七牛,并返回图片地址和一些上传信息
'''
global success , ignore , failure
if validators.url(old_link) is not True:
ignore = ignore + 1
print('invalid url, ignore')
return None

# maybe a url
# already from qiniu
if old_link.find(domain) != -1:
ignore = ignore + 1
print('already in qiuniu, ignore')
return None

# omit the query string section like:?arg1=val1&arg2=val2 in the url
if old_link.find('?') != -1:
old_link = old_link[: old_link.index('?')]

#key = yyyymmdd + "-" +''.join(random.sample('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 6))
key = yyyymmdd+"_"+os.path.splitext(os.path.basename(sys.argv[1]) )[0]+"/image"+('{0}'.format(success+1))
# 上传到七牛后的图片名:上传日期-MD文件名/image序号
ret, info = Bucket_Manager.fetch(old_link, bucket, key)
if ret['key'] == key :
success = success + 1
return 'http://' + domain + '/' +key
failure = failure + 1
return None

def cached_img_url(img_loc_path):
'''
根据给定的本地图片绝对路径,转换成一个网上路径。
如果本地缓存中有,则直接读取并返回,如果没有,则上传后返回。
'''
conn = sqlite3.connect(md_loc + './img_hash_cache.db')
cursor = conn.cursor()
try:
cursor.execute('''
CREATE TABLE img_cache_table (
img_hash TEXT,
real_p TEXT,
img_url TEXT,
u_info TEXT
)
''')
except Exception as e: pass
img_hash = md5(open(img_loc_path, 'rb').read()).hexdigest() # 图片的hash值,用来确定图片的唯一性,避免多次上传,浪费流量
cursor.execute("SELECT img_url FROM img_cache_table WHERE img_hash='%s'" % img_hash) #根据图片的hash值来找缓存下来的图片网址
select_res = [row for row in cursor]
img_url = (select_res[0][0] if select_res and len(select_res) > 0 and select_res[0] and len(select_res[0]) > 0 else None)

remote_exists = False
if img_url:
try: remote_exists = urllib.request.urlopen(img_url).code == 200
except Exception as e:
print('#warning: 网址不存在 :', img_url)
remote_exists = False
if not img_url or not remote_exists: # 如果没有查到图片的网址,或者网址失效
print('上传图片 :', img_loc_path)
img_url, uinfo = upload_file(img_loc_path) # 接取上传后的图片信息
if not img_url: # 如果图片地址为空,则说明上传失败
print('#warning: 上传失败')
conn.close()
return None
else:
if not remote_exists: cursor.execute('INSERT INTO img_cache_table VALUES(?,?,?,?)', (img_hash, img_loc_path, img_url, str(uinfo))) # 如果上传成功,则直接缓存下来
else : cursor.execute("UPDATE img_cache_table SET img_url='%s', u_info='%s' WHERE img_hash='%s'" % (img_url, str(uinfo), img_hash))
conn.commit()
conn.close()

return img_url

def md_img_find(md_file):
'''
将给定的markdown文件里的图片本地路径转换成网上路径
'''
post = None # 用来存放markdown文件内容
global total , success , failure , ignore
with open(md_file, 'r',encoding='utf-8') as f: #使用utf-8 编码打开 by chalkit
post = f.read()
matches = re.compile('!\\[.*?\\]\\((.*?)\\)|<img.*?src=[\'\"](.*?)[\'\"].*?>').findall(post) # 匹配md文件中的图片
if matches and len(matches) > 0:
for sub_match in matches: # 正则里有个或,所以有分组,需要单独遍历去修改
for match in sub_match: # 遍历去修改每个图片
total = total + 1
if match and len(match) > 0:
print("match pic : ", match)
if not re.match('((http(s?))|(ftp))://.*', match): # 判断是不是已经是一个图片的网址
loc_p = match
if not os.path.exists(loc_p) or not os.path.isfile(loc_p): # 如果文件不存在,则可能这是用的一个相对路径,需要转成绝对路径
loc_p = md_file[:md_file.rfind('\\')+1] + match # Windows中 md_file的本地路径为反斜杠\\, match的相对路径为 "MD标题\图片文件名"
if os.path.exists(loc_p) and os.path.isfile(loc_p):
if imghdr.what(loc_p): # 如果是一个图片的话,才要上传,否则的话,不用管
if need_zip:
o_img = loc_p + '.ori' # 原始未压缩的图片
try:
if not os.path.isfile(o_img) or not imghdr.what(o_img): # 如果没有的话,那就需要进行压缩处理
print('压缩图片 :', loc_p)
s_img = tinify.from_file(loc_p)
s_img.to_file(loc_p + '.z')
os.rename(loc_p, loc_p + '.ori')
os.rename(loc_p + '.z', loc_p)
except Exception as e:
print('#warning: tinypng压缩出问题了,图片未压缩。')
file_url = cached_img_url(loc_p) # 获取上传后的图片地址
if file_url: # 在图片地址存在的情况下进行替换
print('图片地址是 : ', file_url)
post = post.replace(match, file_url) # 替换md文件中的地址
else:
ignore = ignore + 1
print('#warning: 不是一个图片文件 :', loc_p)
continue
else:
ignore = ignore + 1
print('#warning: 文件不存在 :', loc_p)
else:
print('markdown文件中的图片用的是网址 :', match)
file_url = transfer_online_img(match) # 获取上传后的图片地址
if file_url: # 在图片地址存在的情况下进行替换
print('图片地址是 : ', file_url)
post = post.replace(match, file_url) # 替换md文件中的地址
if post: open(md_file, 'w',encoding='utf-8').write(post) #如果有内容的话,就直接覆盖写入当前的markdown文件
#仍然注意用uft-8编码打开
print ('Complete!')
print (' total :%d' %(total))
print (' success :%d' %(success))
print (' failure :%d' %(failure))
print (' ignore :%d' %(ignore))


def find_md(folder):
'''
在给定的目录下寻找md文件
'''
if len(folder) > 3:
if folder[folder.rfind('.') + 1:] == 'md': md_img_find(folder) # 判断是否是一个md文件,如果是的话,直接开始转换
elif os.path.isdir(folder):
files = os.listdir(folder)
# 读取目录下的文件
for file in files:
curp = folder + '/' + file
if os.path.isdir(curp): find_md(curp) # 递归读取
elif file[file.rfind('.') + 1:] == 'md': md_img_find(curp)

if __name__ == '__main__':
if len(sys.argv) >= 2:
if len(sys.argv) >= 3:
need_zip = sys.argv[2] == '1'
c_p = sys.argv[0]
md_loc = c_p[:c_p.rfind('/') + 1]
bak_md = '%s.bak' % (sys.argv[1])
shutil.copyfile(sys.argv[1], bak_md) # 在执行改动之前备份原MD文件,可手动删除
print('origin markdown file backup in: %s' % (bak_md))
find_md(sys.argv[1])