整个事情的起源是这样的。
六月底,我打算重新开始更我停了很久的公众号,因为域名到期和图片自动上传不够便利的原因,我弃用了之前的vscode+markdown preview enhanced插件+qiniu-upload-image插件的写文方案。同时,vscode写markdown的换行总有问题,每次都要到网页转化工具进行大量重调,十分不爽。
在不停的搜索过后,我采用了来自KrisTM博客的Typora+PicGo的方案,解决了markdown编写和粘贴、拖拽图片自动上传(图片存储在Gitee仓库)的问题。但是转化问题还是出现了,博客里对于转化到公众号推文的部分是这样说的。
打开HTML,复制网页上的所有内容,直接粘贴到微信公众号编辑框里即可。
而我在实际操作中发现,不管是复制还是生成html,在粘贴到公众号后台时,均会出现如下情况。
这是什么鬼,说好的直接粘贴就行,结果,就这,就这?猜测应该是公众号后台改版了,这个博客写于2020年3月,才6月底就不能用了,可怕。
于是只能继续使用网页转化工具,Md2All和WeChat Format来进行markdown到公众号推文的转化。在网页上点击复制,然后到公众号后台粘贴,就有了内容。
问题似乎已经解决,但是我的好奇心属实被勾起来了。为什么在网页转化工具上点击复制,粘贴到公众号后台就有样式,而在Typora上复制,或者从其他地方复制,粘贴后都是纯文本呢?
在实践中,我发现,在网页上点击复制后,不管是粘贴到QQ、Wechat,还是Vscode、Pycharm,都呈现的是纯文本形式,只有复制到公众号后台时,才有样式。我顿时对两个网站的复制位置背后的行为产生了好奇,认为这里面肯定有玄学操作。
为了探寻复制的奥秘,我找到了WeChat Format项目的源码,clone后进行查看。
整个项目基于vue,我在写主vue项目的editor.js
找到了比较核心的copy
、refresh
、renderWeChat
等函数,在对应到主页面index.html
之后,可以发现,点击复制运行的就是copy
,copy
主要使用的是output
区域的内容。
copy: function () {
var clipboardDiv = document.getElementById('output')
clipboardDiv.focus();
window.getSelection().removeAllRanges();
var range = document.createRange();
range.setStartBefore(clipboardDiv.firstChild);
range.setEndAfter(clipboardDiv.lastChild);
window.getSelection().addRange(range);
try {
if (document.execCommand('copy')) {
this.$message({
message: '已复制到剪贴板', type: 'success'
})
} else {
this.$message({
message: '未能复制到剪贴板,请全选后右键复制', type: 'warning'
})
}
} catch (err) {
this.$message({
message: '未能复制到剪贴板,请全选后右键复制', type: 'warning'
})
}
}
其中document.execCommand('copy')
是最主要的一行内容,搜索后得知,这一行实现了Copies the current selection to the clipboard
。也就是说,第4至8行实现了window.getSelection()
区域的清空,添加clipboardDiv
区域的首子节点到尾子节点的所有内容到一个新的range
,将这个range
添加到window.getSelection()
等操作。最后第10行完成复制。
output
区域的原始内容为空。
<div id="output" v-html="output">
在选项更改后触发的refresh
函数中,output
值得到更新,v-html
将output
的内容作为html展现,其值来自renderWeChat
函数。
fontChanged: function (fonts) {
this.wxRenderer.setOptions({
fonts: fonts
})
this.refresh()
},
sizeChanged: function(size){
this.wxRenderer.setOptions({
size: size
})
this.refresh()
},
themeChanged: function(themeName){
var themeName = themeName;
var themeObject = this.styleThemes[themeName];
this.wxRenderer.setOptions({
theme: themeObject
})
this.refresh()
},
refresh: function () {
this.output = this.renderWeChat(this.editor.getValue())
}
在refresh
后,document.getElementById('output')
也就有了内容。
产生output
值的renderWeChat
函数,则使用了marked.js
实现了从markdown到html的渲染,同时自定义了一个函数来根据样式进行渲染,之后添加脚注。
renderWeChat: function (source) {
var output = marked(source, { renderer: this.wxRenderer.getRenderer() })
if (this.wxRenderer.hasFootnotes()) {
output += this.wxRenderer.buildFootnotes()
}
return output
}
到这已经非常清楚了,送进剪贴板的内容是html,这个结果并不amazing,我原以为公众号后台定义了新的html标准,而这两个网站可以根据标准进行对应的渲染。但是,结果还是html。那为什么我从其他地方复制的html在粘贴到公众号后台后还是纯文本呢。问题,一定出在剪贴板身上。
对微软剪贴板的实现稍加搜索。
官方解释称,在剪贴板可以放置超过一个对象,每个代表不同格式的同样数据。联想到剪贴板也可以复制图片、复制文件,那么大概率,html和text,在剪贴板中也是作为不同类型存储的。
接下来,便是要找到一个接口,将剪贴板里的数据拿出来,看看是否和我想的一样。
在搜索中,我发现pyqt
可以与剪贴板进行交互,并且支持Html、Text、Image、Url等类型。Python如何获取Windows剪贴板内容并判断类型?-施Sugar的回答-知乎
稍加修改后,写出如下代码。
from PyQt5.QtWidgets import QApplication
app = QApplication([])
clipboard = app.clipboard()
def on_clipboard_change():
data = clipboard.mimeData()
if data.hasHtml():
print(f'html-{data.html()}')
if data.hasText():
print(f'text-{data.text()}')
if data.hasUrls():
print(f'urls-{data.urls()}')
if data.hasImage():
print(f'image-{data.imageData()}')
if data.hasFormat():
print(f'format-{data.formats()}')
clipboard.dataChanged.connect(on_clipboard_change)
app.exec()
该函数检测五种类型的数据是否存在,存在的时候进行相应输出。
运行后,当点击WeChat Format网页上的复制时,出现如下内容:
html-
99岁,生日快乐
text-99岁,生日快乐
当选择复制一个文件夹时:
text-file:///C:/sssimonyang/projects
urls-[PyQt5.QtCore.QUrl('file:///C:/sssimonyang/projects')]
复制图片时:
text-file:///C:/Users/sssimonyang/Pictures/日用类/头像.jpg
urls-[PyQt5.QtCore.QUrl('file:///C:/Users/sssimonyang/Pictures/日用类/头像.jpg')]
image-
而复制Typora中的html时:
text-
------------------------
很显然,在Typora中的复制只添加了剪贴板的text内容,html内容为空,所以在复制到公众号后台时呈现的也是text中的内容。
那,如果我将Typora复制的text强行添加到剪贴板的html里会是什么情况呢。
首先试一下,强行添加html到剪贴板是否能够成功。
我将在wechat-format点击复制的html写入wechat-format.html
,然后用程序读取这个文件添加到剪贴板的html,同时,为了区分html和text,我在两者添加了显然不同的内容。注意,在程序运行前,复制一个无关内容更新掉剪贴板,同时程序运行后不要复制其他内容。
from PyQt5.QtCore import QMimeData
from PyQt5.QtWidgets import QApplication
app = QApplication([])
clipboard = QApplication.clipboard()
with open('wechat-format.html', 'r', encoding='utf-8') as f:
html = f.read()
data = QMimeData()
data.setHtml(html)
data.setText('庆祝中国gongchandang成立九十九周年,初心不改,99如一') #原话不能通过审核,故修改
clipboard.setMimeData(data)
app.exec()
复制到公众号后台后:
成功了!我第一次实现了自己添加的内容被公众号后台成功解析。
下一步,很显然,把wechat-format.html
替换成Typora导出的typora.html
。
替换过后的运行结果:
???这就非常有意思了,居然粘贴的是text里的内容。
两次运行的唯一区别就是html文件,让我们来看看两个html文件之间有什么区别。
wechat-format.html
与typora.html
的区别主要在于,typora.html
多了第一行,以及
wechat-format.html
多了和
的配对注释。
让我们照葫芦画瓢抄一下.
再次运行试试:
成功输出了typora.html
里的内容,但是没有样式,考虑到typora.html
的样式定义主要在中,而
wechat-format.html
的样式定义在各个标签中,公众号后台应该直接忽略了。
稍微改一下typora.html
看看效果。把部分删掉,没用的
class
删掉,然后添加一个样式color:red;font-size:30px
。
<html>
<body>
<div id='write'>
<h1 style="color:red;font-size:30px"><a name="99岁生日快乐">a><span>99岁,生日快乐span>
h1>
div>
body>
html>
运行,看看效果:
果然改了html文件就好了。
现在就很清楚了,公众号后台会首先读取html的内容,如果html内容不符合他的要求,那么他就读取text内容。
那么这个要求,到底是什么呢,之前我们主要修改了两部分。把第一部分添加上试一下。
<html>
<body>
<div id='write'>
<h1 style="color:red;font-size:30px"><a name="99岁生日快乐">a><span>99岁,生日快乐span>
h1>
div>
body>
html>
不行,所以识别大概率第一个标签必须是,我们把
撤掉试一下。
<body>
<div id='write'>
<h1 style="color:red;font-size:30px"><a name="99岁生日快乐">a><span>99岁,生日快乐span>
h1>
div>
body>
不行,加上。
<html>
<body>
<div id='write'>
<h1 style="color:red;font-size:30px"><a name="99岁生日快乐">a><span>99岁,生日快乐span>
h1>
div>
body>
html>
OK了!
公众号识别读取的是剪贴板中的html内容,如果html的开头不是,那么它就会使用text中的内容,这也解释了之前为什么如何复制在粘贴后都是纯文本的问题。
既然都搞了剪贴板,不如来测试下QQ、Wechat。
运行之前的代码,然后粘贴。
what?QQ和Wechat居然不一样,QQ用的是text内容,Wechat用的是html,这就是宇宙大厂腾讯吗???
这些研究花了我一晚上的时间,其结果实在是有趣。能够自由设定内容后,未来看有没有python写的markdown转化工具,也自己搞个公众号推文转化工具出来。
今天是建党节,九十九年风雨兼程,生日快乐!