整个事情的起源是这样的。
六月底,我打算重新开始更我停了很久的公众号,因为域名到期和图片自动上传不够便利的原因,我弃用了之前的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-<html> <body> <!--StartFragment--><h2 style="box-sizing: border-box; margin: 80px 10px 40px; padding: 0px; font-weight: normal; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; text-align: center; color: rgb(63, 63, 63); line-height: 1.5; font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif; font-size: 22.4px;">99岁,生日快乐</h2><!--EndFragment--> </body> </html> 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-<PyQt5.QtGui.QImage object at 0x000001C8D1944C88>而复制Typora中的html时:
text-<!doctype html> <html> <head> <meta charset='UTF-8'><meta name='viewport' content='width=device-width initial-scale=1'> ------------------------很显然,在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多了第一行<!doctype html>,以及wechat-format.html多了<!--StartFragment-->和<!--EndFragment-->的配对注释。
让我们照葫芦画瓢抄一下.
再次运行试试:
成功输出了typora.html里的内容,但是没有样式,考虑到typora.html的样式定义主要在<head>中,而wechat-format.html的样式定义在各个标签中,公众号后台应该直接忽略了<head>。
稍微改一下typora.html看看效果。把<head>部分删掉,没用的class删掉,然后添加一个样式color:red;font-size:30px。
<html> <body> <!--StartFragment--> <div id='write'> <h1 style="color:red;font-size:30px"><a name="99岁生日快乐"></a><span>99岁,生日快乐</span> </h1> </div> </body> <!--EndFragment--> </html>运行,看看效果:
果然改了html文件就好了。
现在就很清楚了,公众号后台会首先读取html的内容,如果html内容不符合他的要求,那么他就读取text内容。
那么这个要求,到底是什么呢,之前我们主要修改了两部分。把第一部分添加上试一下。
<!doctype html> <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>撤掉试一下。
<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的开头不是<html>,那么它就会使用text中的内容,这也解释了之前为什么如何复制在粘贴后都是纯文本的问题。
既然都搞了剪贴板,不如来测试下QQ、Wechat。
运行之前的代码,然后粘贴。
what?QQ和Wechat居然不一样,QQ用的是text内容,Wechat用的是html,这就是宇宙大厂腾讯吗???
这些研究花了我一晚上的时间,其结果实在是有趣。能够自由设定内容后,未来看有没有python写的markdown转化工具,也自己搞个公众号推文转化工具出来。
今天是建党节,九十九年风雨兼程,生日快乐!