乱码三千 – 分享实用IT技术

乱码三千 – 码出一个新世界


  • 首页

  • 归档

  • 搜索

编写一个hexo插件 实现将博客中的原创文章同步到微信公众号

发表于 2024-04-19

前言

这个需求很早以前就有了 由于各种原因耽搁了 这次准备把这个功能搞定

我的大本营在博客上 微信公众号只是附带 不过考虑到微信搜索流量以及公众号的广告收益 所以 还等什么呢 自动化地搞起来 嘿嘿😜

功能实现流程

流程如下:

  1. 在执行hexo d指令是 获取所有文章html
  2. 将内容中带有本文为作者原创的文章过滤出来 并排除wx_pushed.txt中已经同步的文章
  3. 替换文章中的图片
  4. 增加封面图
  5. 替换内链样式
  6. 推送文章到公众号
  7. 推送成功后将文章文件名称记录到wx_pushed.txt中 防止后面重复推送

实现代码

插件代码如下:

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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// hexo-wx-sync.js 自动将hexo博客中的原创文章同步到微信公众号

const axios = require('axios');
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
var req = require('request-promise');
const { marked } = require('marked')
const juice = require('juice')


//1. 获取文章html 将内容中带有'本文由作者原创'的文章过滤出来 排除wx_pushed.txt中已经同步的文章
//2. 替换文章中的图片
//3. 替换内链样式
//4. 增加封面图
//5. 推送文章到公众号
//6. 记录已经推送的文章名称到wx_pushed.txt中

const PLUGIN_NAME = 'hexo-wx-sync';
const config = hexo.config.wx_sync
const parentDir = path.dirname(__dirname);//当前文件的父目录 也就是hexo
const PUSHED_ARTICLES_FILE = `${parentDir}/${PLUGIN_NAME}/wx_pushed.txt`;

let accessToken = ""

let pushedArticles = [];
// 加载已发布的文章列表
function loadPushedArticles() {
if (fs.existsSync(PUSHED_ARTICLES_FILE)) {
pushedArticles = fs.readFileSync(PUSHED_ARTICLES_FILE, 'utf8').split('\n').filter(Boolean);
}
}

// 保存已发布的文章列表
function savePushedArticles() {
fs.writeFileSync(PUSHED_ARTICLES_FILE, pushedArticles.join('\n'));
}





function filterPost(post) {
loadPushedArticles()
// 获取文章路径
const postPath = parentDir + "/source/" + post.source;
// 读取markdown文章内容
const mdContent = fs.readFileSync(postPath, 'utf8');
// 选择还未上传的原创文章
if (!pushedArticles.includes(post.source) && mdContent.includes("本文为作者原创 转载时请注明出处")) {
//将markdown转成html格式
return marked(removeMetaData(mdContent,post))
} else {
return null
}
}

function removeMetaData(content,post) {
// 删除标题
content = content.replace(/^title:.*$/m, "");

// 删除日期
content = content.replace(/^date:.*$/m,`原文地址: ${getPostSourceUrl(post)}<br>如果本文有相关附件下载 请将上面地址复制到浏览器打开` );

// 删除标签
content = content.replace(/^tags:.*$/m, '');

// 删除空行
content = content.replace(/^\s*$/gm, '');



return content;
}

function publishToWechat(parsedContent, post, coverImageId) {
// 发布文章: 'https://api.weixin.qq.com/cgi-bin/material/add_news'

axios.post(`https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${accessToken}`, {
articles: [{
title: `${post.title} `,
thumb_media_id: coverImageId, // 刚才取到的封面图素材 id
author: hexo.config.author,
digest: getDigest(post.content),//文章摘要
content: parsedContent, // 刚才处理好的文章
content_source_url: getPostSourceUrl(post), // 非必填,这里我写的是我博客这篇文章的 URL
need_open_comment: 1, // 是否打开留言功能
show_cover_pic: 0, // 是否把封面图添加到文章开头
}],
}).then(response => {

//推送成功会返回一个media_id
da = response.data
if ('media_id' in da) {
console.log(`${post.title} successfully pushed to WeChat.`);
// 记录已推送的文章名称到 wx_pushed.txt 文件中
// fs.appendFileSync(PUSHED_ARTICLES_FILE, `${post.source}\n`);
pushedArticles.push(post.source);
} else {
console.log(da)
}

}).catch(error => {
console.error(`Failed to push ${post.title} to WeChat: ${error}`);
});;
}
function getDigest(content) {
// 获取文章的摘要信息
const $ = cheerio.load(content);
const summary = $('p').first().text();
return summary
}
function getPostSourceUrl(post) {
// 这里我们使用网站的地址+文件名.html
const domain = hexo.config.url
const urlRegex = /(\d+)[.]md/g;
const match = urlRegex.exec(post.source);
const suffix = match ? match[1] : ''
return domain + "/" + suffix + ".html"
}
// 替换文章中的图片
async function replaceImages(content) {
console.log("开始替换图片")
const $ = cheerio.load(content);
// 获取所有的img元素
const imgs = $('img');
// 使用for循环遍历所有的img元素
for (let i = 0; i < imgs.length; i++) {
const newUrl = await uploadImg($(imgs[i]).attr('src'));
// 修改img标签的src属性
$(imgs[i]).attr('src', newUrl);
}
console.log("图片替换完成")
return $.html();
}


async function request(option) {
return await req(option).then((response) => {
console.log("请求成功: " + response);
return response
}).catch( async (err) => {
console.log("请求错误: " + err);
//如果请求失败 重新请求
return await request(option)
return '{"media_id":"fTLNXU-IBCWKkfWOlCRS17gXwNG5_75-7aBJQalLz1BBBE5UJ43cj3JfxCyYiTN-","url":""}'
});
}



// 上传图片到素材库并获取新的图片地址
async function uploadImg(url) {
console.log("需要上传的图片地址:" + url)
const imageStream = (await axios.get(url, { responseType: 'stream' })).data;
const formData = { media: imageStream };
const res = await request({
url: `https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${accessToken}`,
method: 'POST',
formData,
});
return JSON.parse(res).url;
}

// 将内联样式改为行内样式 否则微信平台不识别
function replaceInternalLinks(content) {
//实现内链样式替换逻辑
const themeStr = fs.readFileSync(`${parentDir}/${PLUGIN_NAME}/html-component/theme6.css`, 'utf-8')
let parsedContent = juice(`
<!--插入主题样式-->
<style>
${themeStr}
</style>
<!--插入marked转换后的html-->
<div class="markdown-body">
${content}
`)
//后面这个div可以不用加 否则会多出一个

return removeWhitespaceExceptCodeBlocks(parsedContent);
}
//移除代码块以外的回车换行符
function removeWhitespaceExceptCodeBlocks(parsedContent) {
const $ = cheerio.load(parsedContent);
//预先存储未替换空格的pre标签
const origin = $('pre')
// 删除回车换行
parsedContent = parsedContent.replace(/[\r\n]+/g, '')
const $2 = cheerio.load(parsedContent);
const after = $2('pre')
for (let i = 0; i < after.length; i++) {
// 进行标签替换
$(after[i]).replaceWith($(origin[i]));
}
return $2.html();
}

function addCoverImage() {
const data = fs.readFileSync(`${parentDir}/${PLUGIN_NAME}/coverJson.json`, 'utf8');
const jsonData = JSON.parse(data);
coverList = jsonData["images"]
//随机获取封面图media_id
const randomIndex = Math.floor(Math.random() * coverList.length);
const randomMediaId = coverList[randomIndex]["media_id"];
return randomMediaId
}
async function getAccessToken() {
const res = await request({
url: `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${config.appid}&secret=${config.secret}`,
method: 'GET',
})
return JSON.parse(res).access_token
}



// 在执行 hexo d 时触发
hexo.extend.generator.register('wechat', async function (locals) {
if (!config.enable) {
console.log('关闭hexo-wx-sync插件');
return;
}
//获取微信公众号accesToken
accessToken = await getAccessToken()
console.log("accessToken" + accessToken)
// 遍历所有文章
locals.posts.forEach(async post => {//post.content 获取的是html格式内容
//过滤出未上传且为原创的文章
const content = filterPost(post)
if (content == null) {
return
}
// 替换文章中的图片
// const replacedContent = await replaceContentImage(content);
const replacedContent = await replaceImages(content);
//将样式注入到标签中
const finalContent = replaceInternalLinks(replacedContent);
// 增加封面图
const coverImageId = addCoverImage();
console.log("finalContent" + finalContent)
// 调用微信接口发布文章
publishToWechat(finalContent, post, coverImageId);
});
});

使用方法:

在script目录下新建一个自定义名称.js文件, 注意名称不要使用index.js, 将上面代码粘贴过去, 然后在config配置文件中配置AppID和AppSecret 如下:

1
2
3
4
5
# 将文章同步到微信公众号
wx_sync:
enable: true
appid: 你的appid
secret: 你的secret

关于AppID和AppSecret的获取

开发者ID(AppID)和开发者密码(AppSecret)可以在公众号后台基本配置中获取:

image-20240419133336814

image-20240419133636776

为了确保接口能正常访问, 还需要设置接口请求所在地的ip地址, 如果你在阿里云服务器调用这些接口 则填入服务器的ip , 如果是本地电脑 则在百度搜索IP获取当前公网IP:

image-20240419134212591

image-20240419133544001

相关问题

在进行文章同步过程中 出现很多文章同步不上 接口提示AxiosError: Request failed with status code 501 一般情况下是文章中有些关键字无法通过微信公众号的审核 目前我所知道的关键字有以下几个:

1
2
wget
sql

解决办法也很简单 将这些关键字删除或者拆开 比如wget写成w get这样就能上传成功了 上传完后在公众号后台手动修改回来

虽然有些繁琐 但我暂时还没有想到更好的解决方案 大家如果有好的点子的话 可以 滴滴一下我😜

文章参考 :

  • 《博客文章自动同步微信公众号实践》
  • 《Hexo官方文档》

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

博客文章自动同步微信公众号实践

发表于 2024-04-17

本文来自 zhiyi 的个人博客实践,可以通过开放能力将其他平台的文章同步到微信公众号上,对刚学前端的同学(有自己的博客就更好了)来说,是对后台接口链路的一个不错的探索。

整体思路

微信官方提供了素材管理的 API,通过 API 可以很方便地进行同步。在使用 API 之前需要进行鉴权,所以需要先获取 access token。微信公众号中不允许出现外域图片,因此需要把文章里的图片全部使用微信的图片上传接口处理后替换。此外,微信公众号支持 HTML 标签但是只支持内联样式,所以必须把外联样式全部转换为内联样式。

所以,同步到微信公众号的操作,需要按照以下步骤:

  1. 使用公众号的 appid 和 secret 换取 access token。
  2. 把文章中的所有图片用微信图片上传接口上传,并替换文章里的 URL。这一步需要使用 access token 鉴权。
  3. 将文章中的所有外联 css 转为内联样式。
  4. 调用微信素材管理接口,同步文章。这里需要使用 access token 鉴权。

获取 access token

获取 access token 本身没什么难度,使用微信公众号的 appid 和 secret 就可以从接口获取到。需要注意的是,这个接口有调用频率限制,短时间内调用次数不能过多。

所以我们从微信的接口获取 access token 之后应该将它缓存,之后直接从缓存中获取,缓存失效了再重新从接口获取。这里的缓存机制使用了 Redis,因为 Redis 提供的过期失效机制正好满足我们的需求。

首先我们在 Koa 的全局变量里注册 Redis,以便在各种场景调用。这里,我们把它写成 Koa 中间件的形式,并把几个 Redis 常用操作 Promise 化。

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
module.exports = (redisConfig) =>async (ctx, next) => {
const client = redis.createClient(redisConfig.port, redisConfig.host);
client.auth(redisConfig.password);
ctx.redis = {
get: (key) => new Promise((resolve, reject) => {
client.get(key, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
}),
set: (key, value) => new Promise((resolve, reject) => {
client.set(key, value, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
}),
expire: (key, expire) => new Promise((resolve, reject) => {
client.expire(key, expire, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
}),
del: (key) => new Promise((resolve, reject) => {
client.del(key, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
}),
};
await next();
};

之后,在获取access token 时,先尝试从Redis中取。如果获取到了,就直接返回结果;如果没取到,就向微信接口请求并写入 Redis。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = async (ctx) => {
try {
let accessToken = await ctx.redis.get('wechatAccessToken');
if (!accessToken) {
const res = await axios.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${wechatConfig.appid}&secret=${wechatConfig.secret}`);
if (res?.status !== 200) {
throw new Error('get access token error');
}
accessToken = res?.data?.access_token;
const expiresIn = res?.data?.expires_in;
if (!accessToken) {
throw new Error('get access token error');
} else {
await ctx.redis.set('wechatAccessToken', accessToken);
await ctx.redis.expire('wechatAccessToken', expiresIn);
}
}
return Promise.resolve(accessToken);
} catch (err) {
return Promise.reject(err);
}
};

这样,我们就实现了获取并缓存access token。

上传并替换图片

我的文章内容是一段 HTML 代码字符串,这是由前端传入的。不管前端使用什么编辑器,这一步都需要先转为 HTML 字符串再操作。

首先需要把文章内容中的图片全部找出来,这里直接用正则即可:

1
2
3
4
5
6
7
8
const images = parsedContent.match(/<img.*?(?:>|\/>)/gi);
if (images) {
for (const image of images) {
const src = image.match(/src=['"]?([^'"]*)['"]?/i);
const url = src?.[1];
// 对取出的 URL 做处理
}
}

首先匹配所有的 <img /> 标签,之后针对每个标签再做一次匹配,取到其中的 src 值(也就是图片的 URL)。

对匹配到的图片 URL 依次下载为 stream 并上传到微信公众号图片上传接口,之后使用返回的微信域内 URL 替换原文中的 URL。

1
2
3
4
5
6
7
8
9
10
if (url) {
const imageStream = (await axios.get(url, { responseType: 'stream' })).data;
const formData = { media: imageStream };
const res = await request({
url: `https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token=${accessToken}`,
method: 'POST',
formData,
});
parsedContent = parsedContent.replace(url, JSON.parse(res).url);
}

上传图片的接口当然需要鉴权,这里的 access token 是直接使用上一步封装好的方法获取的。

1
const accessToken = await getWechatAccessToken(ctx);

需要注意的是,上传到微信公众号必须使用 request 或者基于 request 的 request-promise。因为 node 环境的 axios 对 form-data 格式发送文件的 POST 并不能很好地支持。

不要忘记了对封面图也做一样的处理,因为使用 API 编辑公众号图文必须添加封面图,封面图也必须是微信域内的。

1
2
3
4
5
6
7
const coverImageStream = (await axios.get(coverImage, { responseType: 'stream' })).data;
const coverImageRes = await request({
url: `https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=${accessToken}&type=image`,
method: 'POST',
formData: { media: coverImageStream },
});
const coverImageId = JSON.parse(coverImageRes).media_id;

这里需要注意的是,上传封面图的接口和上传图片的接口是不一样的,要注意区分。最后我们拿到的是封面图的素材 id,这个 id 我们后面创建图文素材时会用到。

把封面图和文章中的图片都替换一遍后,我们就完成了这一步。

外联 CSS 转为内联

这一步我本来以为会很麻烦,但是幸运的是,在 node 上(前端浏览器等环境不可以用这个包,会报错)有一个名为 juice 的 npm 包可以帮我们一行代码完成任务:

1
2
3
4
5
6
7
8
parsedContent = juice(`
<style>
${cssString}
</style>
<div>
${parsedContent}
</div>
`);

这里的 cssString 是定义好的字符串,事先把需要应用的 CSS 代码定义好一个字符串变量即可;第一个参数则是需要处理的 HTML 代码,也就是上一步替换图片 URL 的结果。

调用接口创建素材

到这一步已经没什么问题了,按照微信开发文档调用接口即可。

1
2
3
4
5
6
7
8
9
10
11
12
await axios.post(`https://api.weixin.qq.com/cgi-bin/draft/add?access_token=${accessToken}`, {
articles: [{
title: '你的文章标题',
thumb_media_id: coverImageId, // 刚才取到的封面图素材 id
author: '文章作者',
digest: '文章摘要',
content: parsedContent, // 刚才处理好的文章
content_source_url: '原文链接', // 非必填,这里我写的是我博客这篇文章的 URL
need_open_comment: 1, // 是否打开留言功能
show_cover_pic: 0, // 是否把封面图添加到文章开头
}],
});

这样就可以成功把文章同步到微信公众号后台的素材库中。最后在微信公众号官方客户端 “订阅号助手” 操作一下,就可以成功把文章发布出去了。当然,最后的发布操作也可以调用 API 解决,不过官方客户端本身就有这个功能,而且官方客户端的 “预览” 功能可以让我提前看到效果,所以我就不必多此一举了。

尚未解决的小问题

虽然同步到微信公众号这个功能帮我打通了在手机上创作到发布的整个链路,但是还是有两个小问题暂时没法解决:

  1. 微信公众号未提供声明原创的接口,官方客户端也没有这个功能,因此想要声明原创文章还是必须在电脑上操作。
  2. 微信公众号网页版管理后台支持对封面图进行自定义裁剪,而通过 API 指定封面图则只能使用图片中间部分。

参考链接

微信公众号开发文档

本文转载自: 微信公众号

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

2024年关于新浪图床失效的解决方法

发表于 2024-04-17

前言

很多早期的站长基本使用新浪的图床, 新浪图床免费又稳定, 受到很多人的青睐, 我本人不怎么用新浪图床, 但是手上有一批素材, 引用的是新浪服务器中的图片, 目前这些图片无论是在浏览器还是在Markdown编辑器中都无法显示

image-20240417181012252

image-20240417181029759

这已经不是简单的防盗链了 应该是新浪服务器做了某种屏蔽处理

检测服务器和链接是否正常

首先我们要确保新浪服务器中有该图片文件, 可以通过以下几种方式检测:

  1. 用VScode编辑器中打开文档 然后鼠标停留在图片链接上, 如果能显示图片, 说明该链接有效

    2024-04-17 18-02-18.2024-04-17 18_02_58

  2. 用PostMan或者ApiPost进行请求访问
    image-20240417181652619

  3. 使用下载工具进行下载

    image-20240417181820927

值得庆幸的是 新浪服务器还没有挂 我们还有足够的时间来对图片进行转移和备份

防盗链临时解决方法

为了使图片迅速恢复访问, 我们可以使用第三方缓存服务来解决防盗链问题, 有以下四种方式可行:

  1. WordPress

    1
    https://i0.wp.com/图片地址(图片地址要掉 https://)
  2. Weserv.nl

    1
    https://images.weserv.nl/?url=图片地址
  3. 百度 1:

    1
    https://image.baidu.com/search/down?url=图片地址
  4. 百度 2

    1
    https://gimg2.baidu.com/image_search/&app=2020&src=图片地址(图片地址要去掉 https://)

使用百度的速度相对快一些

Wordpress批量替换

如果你的wordpress博客大量的图片都在新浪中, 那么可以通过以下两种方式进行批量替换:

  1. 静态替换 修改数据库

    修改数据库之前记得先备份数据库 以防止误操作无法还原

    sql命令如下:

    1
    UPDATE wp_posts SET post_content = REPLACE( post_content,  'https://tvax1.sinaimg.cn/',  'https://image.baidu.com/search/down?url=https://tvax1.sinaimg.cn/' )
  2. 动态替换 修改主题代码

    进入wordpress主题编辑器:

    image-20240417185327043

    将以下代码添加到主题页脚foot.php中:

    1
    2
    3
    4
    5
    6
    7
    <script>
    let context = Array.prototype.map.call(document.images, (event) => {
    event.src = event.src.replace("tvax1.sinaimg.cn/", "image.baidu.com/search/down?url=https://tvax1.sinaimg.cn/")

    });

    </script>

    image-20240417185632599

推荐使用动态的方式自动批量替换, 能将降低数据库操作风险, 又方便后期再次更改, 动态的方式唯一不足之处就是图片加载有延迟

如果你追求加载速度和用户体验, 那么建议采用静态替换的方式

图片批量备份

上面这种临时替换方式始终不是长久之计, 我们需要尽快对图片进行备份和迁移 防止新浪图床哪天不再提供服务了

我个人的做法是通过python批量将图片下载到本地 然后进行图床更换, 核心代码如下:

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
# -*- coding:utf-8 -*-

import os
import sys
from pyquery import PyQuery as pq
import urllib
# 当前文件路径
current_path = os.path.abspath(__file__)
# 父目录
father_path = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")

#----- 用于图片下载------
opener=urllib.request.build_opener()
opener.addheaders=[('User-Agent','Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1941.0 Safari/537.36'),('Accept' , 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'),(
'Accept-Encoding' , 'gzip,deflate,sdch'),
('Accept-Language' , 'zh-CN,zh;q=0.8')]
req=urllib.request
req.install_opener(opener)
#----- 用于图片下载------

# 图片输出目录
pic_dir = 'img'
if not os.path.exists(pic_dir):
os.makedirs(pic_dir)


#将网络图片下载到本地
def download_img(url,dir,image_name):
try:
req.urlretrieve(url, dir+image_name)
except Exception as e:
raise e


#采用原图片地址的名称
def get_pic_name_by_url(url):
# 我只需要末尾xxx部分 使用正则替换
image_name = url.split('/')[-1]
return image_name

def img2url(url):
fir="https://cdn.jsdelivr.net/gh/xxx/xxx/img/"

if "sinaimg.cn" in url:
url="https://image.baidu.com/search/down?url="+url
new_name=get_pic_name_by_url(url)
try:
download_img(url,"{parent}/img/".format(parent=father_path),new_name)
except Exception as e:
print(f"图片下载异常: {e}")
return url
return fir+new_name

# 遍历每篇文章
for idx, post in enumerate(posts):
content, lastmodifytime = post

d=pq(content)
for item in d("img").items():
old_url=item.attr("src")
# 将图片下载到本地
new_url=img2url(old_url)
print(new_url)
#替换图片地址
item.attr("src",new_url)

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

内容多平台分发工具汇总

发表于 2024-04-16

前言

文章撰写一次 到处分发 无需繁琐的复制粘贴 节省大量的重复机械劳动力 追求极致的自动化 一键分发工具 无疑是解决了很多创作者的痛点

接下来给大家汇总几个可一键分发的网站

一键分发平台

  • mdnice

    image-20240418194825613

  • openwrite

    image-20240417225346730

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

使用python3将wordpress博客数据迁移到hexo

发表于 2024-04-16

前言

为了追求网站访问的稳定性 准备将wordpress博客迁移到hexo上 考虑到数据量比较多 我直接将数据库导出成压缩包 然后下载到本地电脑 然后使用python批量将里面的文章转成markdown格式的文档

准备工作

  1. 导出数据库

    1
    mysqldump -u用户名 -p密码 数据库名 | gzip > 数据库名.sql.gz
  2. 将导出的数据库文件下载到本地 然后双击解压该数据库

    1
    scp -r root@服务器IP:刚导出的数据库所在路径 本地目录
  3. 数据库导入进本地mysql中 方便python连接

    首先创建新数据库:

    1
    mysql>create database 数据库名;

    然后将sql文件导入到该库中:

    1
    mysql -u用户名 -p密码 数据库名 < 数据库名.sql

Python批量转换

python代码如下:

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
# coding:utf-8

##########################
#
# 链接本地mysql数据库将wordpress中的文章提取出来并转成hexo博客格式
#
##########################

import os
import sys
from markdownify import markdownify as md
import pymysql.cursors
import sys
sys.setrecursionlimit(1000000)


# 连接数据库
conn = pymysql.Connect(
host='127.0.0.1',
port=3306,
user='数据库用户名',
passwd='数据库密码',
db='wordpress_it',
charset='utf8mb4'
)
# 获取游标
cursor = conn.cursor()


# 获取所有文章
cursor.execute("SELECT post_title, post_content, post_date FROM wp_posts WHERE post_type='post'")
posts = cursor.fetchall()

# 确保输出目录存在
output_dir = 'hexo_posts'
if not os.path.exists(output_dir):
os.makedirs(output_dir)

tags=["Android","IOS","服务器","Docker","Mysql","Flutter","Java","Hexo"]
postTag="技术文章" #默认分类为

# 遍历每篇文章
for idx, post in enumerate(posts):
title, content,date = post
for tag in tags:
if tag in title:
postTag=tag
break


# 将HTML内容转换为Markdown
markdown_content = md(content)
# 写入Hexo格式的Markdown文件
with open(os.path.join(output_dir, f"{idx+1}.md"), 'w', encoding='utf-8') as f:
f.write("---\n")
f.write(f"title: {title}\n") # 如果要嵌入变量 需要加f
f.write(f"date: {date}\n") # 请将日期替换为实际发布日期
f.write(f"tags: {postTag}\n")
f.write("---\n")
f.write("\n")
f.write(markdown_content)


# 关闭游标和连接
cursor.close()
conn.close()

想要正常运行该代码 需要安装pymysql和markdownify两个库:

1
2
pip3 install markdownify
pip3 install pymysql

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

编写一个hexo插件 实现给文章中的外部超链接增加一个中间跳转询问页面

发表于 2024-04-16

需求

我本来想让AI给我写一个hexo插件 如下:

1
2
3
4
5
6
7
8
9
10
11
请写一个hexo插件 要求功能如下:

1. 在执行hexo -g指令时自动将文章中的非本站合法超链接提取替换出来 为每个合法的链接生成独立的跳转页面 独立页面的地址和链接形成固定的对应关系 不会因为新增文章而发生对应链接改变的问题
2. 当用户浏览博客时点击超链接会跳转到一个单独的网页 在这个网页中显示文字和该链接地址并询问用户是否需要跳转到该链接 当用户点击文字下方的跳转按钮则自动跳转到该链接
3. 文章中的链接应该替换成对应已经生成的跳转页面的链接
4. 要求能在`config`文件中自定义需要进行排除的域名 不对排除的域名下的所有链接进行跳转页面的生成 保留原始的直接跳转即可
5. 在`config`文件中增加插件的开启开关
6. 需要过滤掉非合法的超链接 非合法的链接不做处理
7. 跳转页面的UI希望能美观一些 内容居中显示

请给出插件的完整代码

然后AI给我的答案是:

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
以下是一个满足您需求的Hexo插件的完整代码:

const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const { URL } = require('url');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

const config = hexo.config.external_link_redirect || {};
const excludedDomains = config.excluded_domains || [];
const redirectPageDir = path.join(hexo.public_dir, 'redirect_pages');
const redirectPageTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirect Page</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
}
.container {
margin-top: 100px;
}
.link {
font-size: 18px;
font-weight: bold;
}
.button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<p class="link">%LINK%</p>
<button class="button" onclick="redirectToLink()">Go to Link</button>
</div>
<script>
function redirectToLink() {
window.location.href = '%URL%';
}
</script>
</body>
</html>
`;

hexo.extend.filter.register('before_generate', async function () {
if (!config.enable) {
console.log('External link redirect plugin is disabled.');
return;
}

await createRedirectPages();
});

async function createRedirectPages() {
const posts = hexo.locals.get('posts');
if (!posts || !posts.length) {
return;
}

for (const post of posts.data) {
const content = post.content;
const updatedContent = await processContent(content);
post.content = updatedContent;
}
}

async function processContent(content) {
const updatedContent = content.replace(/<a href="([^"]+)"[^>]*>(.*?)<\/a>/gi, async (match, url, text) => {
if (!isLinkValid(url)) {
return match; // Skip invalid links
}

const redirectPagePath = await generateRedirectPage(url);
const redirectLink = `<a href="${redirectPagePath}">${text}</a>`;
return redirectLink;
});

return updatedContent;
}

function isLinkValid(url) {
if (!url || typeof url !== 'string') {
return false;
}

const parsedUrl = new URL(url);
const domain = parsedUrl.hostname.replace('www.', '');
return !excludedDomains.includes(domain);
}

async function generateRedirectPage(url) {
const redirectUrl = new URL(url);
const domain = redirectUrl.hostname.replace('www.', '');
const redirectPagePath = path.join(redirectPageDir, `${domain}_${Date.now()}.html`);
const pageContent = redirectPageTemplate.replace('%LINK%', url).replace('%URL%', url);

await writeFile(redirectPagePath, pageContent);
return redirectPagePath.replace(hexo.public_dir, '');
}
在这个插件中,我们做了以下几件事情:

在执行hexo -g指令之前,通过before_generate过滤器来自动生成跳转页面。
通过正则表达式遍历文章内容,找到所有超链接,并检查链接是否有效。
对于有效的链接,创建一个跳转页面,并将原始链接替换为跳转页面的链接。
生成的跳转页面是一个HTML文件,包含跳转链接和一个按钮,点击按钮可以跳转到原始链接。
用户可以在Hexo配置文件中设置插件的开启状态、需要排除的域名以及跳转页面模板。
最终生成的跳转页面保存在public/redirect_pages目录下,每个页面都有固定的对应关系,不会因为新增文章而改变链接。

看了一下AI给我写的代码, 可以说是秀的我头疼, 代码可读性实在太差了, 难道是我问的方式不对?🤔

由于ChatGPT免费次数有限 我也懒得去搜寻免费的AIChat, 干脆就自己手动写一个吧

最后调试完成后的插件代码如下:

完整代码

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
'use strict';

const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const crypto = require('crypto');
const { log } = require('console');

const DEFAULT_EXCLUDE_DOMAINS = ['example.com']; // 默认排除的域名列表

hexo.extend.filter.register('before_generate', function () {
const config = hexo.config.external_links || {};
const excludeDomains = config.exclude_domains || DEFAULT_EXCLUDE_DOMAINS;
if (!config.enable) {
console.log('关闭external_links插件');
return;
}
hexo.extend.generator.register('external_links', function (locals) {
const posts = locals.posts;
const author = hexo.config.author;
const outputDir = hexo.config.external_links_output || 'external_links';
const linkMap = new Map(); // 存储链接和其对应的哈希值的映射

posts.forEach(post => {
const $ = cheerio.load(post.content);
$('a').each(function () {
const href = filterUrl($(this).attr('href'));
const hostname = getHostname(href);

if (hostname!=""&&!excludeDomains.includes(hostname)) {
const hash = generateHash(href);
linkMap.set(href, hash); // 存储链接和哈希值的映射关系

//替换文章中的原始链接
const newHref = `/${outputDir}/${hash}.html`;
$(this).attr('href', newHref);
$(this).attr('target', "blank");//在新标签页中打开超链接
}
});

post.content = $.html();
});

const outputPath = path.join(hexo.public_dir, outputDir);
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath);
}

// 生成每个链接对应的页面
linkMap.forEach((hash, href) => {
const htmlContent = generateLinkPage(href, hash,author);
const outputFile = path.join(outputPath, `${hash}.html`);
fs.writeFileSync(outputFile, htmlContent);
});


});
});

function generateLinkPage(href, hash,author) {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>访问外部网站-{author}</title>
<style>
body {
margin: 20px;
padding-top: 100px;
color: #222;
font-size: 13px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.5;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.wrapper {
margin: auto;
padding-left: 30px;
padding-right: 30px;
max-width: 540px;
padding-top: 25px;
padding-bottom: 25px;
background-color: #f7f7f7;
border: 1px solid #babbbc;
border-radius: 5px;
}



.button {
padding: 0;
font-family: inherit;
background: none;
border: none;
outline: none;
cursor: pointer;
}
.button:hover {
background-color: #0070cd;
}
a{
text-decoration: none
}

.button:active {
background-color: #0077d9;
}


.link {
margin-bottom: 10px;
}

.button {
display: inline-block;
padding: 10px 16px;
color: #fff;
font-size: 14px;
line-height: 1;
background-color: #0077d9;
border-radius: 3px;
}
.actions {
margin-top: 15px;
padding-top: 30px;
text-align: right;
border-top: 1px solid #d8d8d8;
}
</style>
</head>
<body>

<div class="wrapper">
<div class="content">
<h1>即将离开${author}</h1>
<p class="info">您即将离开${author},前往外部网站。</p>
<p class="link">${href}</p>
</div>
<div class="actions">
<a class="button" href="${href}" one-link-mark="yes">继续访问</a>
</div>
</div>
</body>
</html>
`;
return html;
}

function getHostname(url) {
try {
return new URL(url).hostname;
} catch (error) {
return '';
}
}
function filterUrl(url) {
const urlRegex = /http[s]?:\/\/[\u4e00-\u9fa5\w.-\/:]+[\u4e00-\u9fa5\w.?&\/=-]+/g;
const match = urlRegex.exec(url);
return match ? match[0] : ''
}

function generateHash(data) {
return crypto.createHash('md5').update(data).digest('hex');
}

使用方法:

在script目录下新建一个js文件, 注意名称不要使用index.js, 将上面代码粘贴过去, 然后在config配置文件中指定输出的目录以及需要过滤的域名 如下:

1
2
3
4
5
6
7
8
9
10
#需要排除的域名 以及是否开启该插件
external_links:
exclude_domains:
- 'acg.newban.cn'
- 'newban.cn'
- 'code.newban.cn'
- 'audio.newban.cn'
enable: true
#输出目录
external_links_output: external_links

效果展示

模仿的知乎效果:

image-20240416141911249

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

数据库常用操作指令

发表于 2024-04-15

前言

数据库操作指令涵盖了创建、查询、修改和删除数据库及表的过程,以及如何在这些数据库和表中进行数据操作。以下是一些常用的数据库操作指令:

指令

创建数据库

1
create database 数据库名;

删除数据库

1
drop database 数据库名;

创建表

1
create table 表名 (列名 数据类型);

删除表

1
drop table 表名;

显示所有数据库

1
show databases;

进入数据库

1
use 数据库名;

显示所有表

1
show tables;

插入数据

1
insert into 表名 values(值1,值2,...);

修改数据

1
update 表名 set 字段=新值 where 条件;

删除数据

1
delete from 表名 where 条件;

修改表名

1
alter table 旧表名 rename to 新表名;

查看表所有的字段

1
describe 表名

或者

1
show columns from 表名

添加字段

1
alter table 表名 add 字段名 数据类型;

修改字段

1
alter table modify 字段名 新数据类型;

删除字段

1
alter table 表名 drop 字段名;

这些指令提供了对数据库和表的基本操作,包括创建、修改、删除和查询数据 根据具体需求,可以组合使用这些指令来完成更复杂的数据库管理任务

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

Mac平台如何下载ffmpeg并对m3u8视频进行下载转换成MP4

发表于 2024-04-13

ffmpeg安装

在Mac平台上 本人多次使用brew工具进行ffmpeg安装都未成功

于是决定直接去ffmpeg官网下载执行包

点击进入官网

由于是国外的网站 下载速度会相对较慢 文末已经给大家准备了网盘下载链接

这里我们选择Mac :

image-20240413173518406

image-20240413173539785

image-20240413173602675

将执行压缩包解压后得到一个独立的ffmpeg执行文件:

image-20240413173929604

为了方便我们接下来的使用 需要对其进行环境变量的配置:

  1. 在终端中输入以下命令 打开环境变量配置文件:

    1
    vim ~/.bash_profile
  2. 在该文件中加入以下内容

    1
    export PATH="/path/to/ffmpeg:$PATH"

    注意: 这里需要将 /path/to/ffmpeg 替换为你的ffmpeg所在路径

  1. 使用source命令使新的环境变量生效

    1
    source ~/.bash_profile
  2. 验证是否配置成功

    在任意一个非ffmpeg所在目录执行以下命令, 如果正常打印内容 则代表配置成功:

    1
    ffmpeg -version

注意: 如果你懒得配置环境变量 还可以直接将ffmpeg执行文件移动至/usr/local/bin 此目录下的所有执行文件 全局可运行

下载m3u8视频并转换成mp4

ffmpeg支持直接访问m3u8链接并将其内容转成mp4格式, 执行命令如下:

1
ffmpeg -i {m3u8链接} -c copy -bsf:a aac_adtstoasc {文件名}.mp4

同样的该ffmpeg指令也适用于直播流的下载和转换 比如http://xxx.flv:

1
ffmpeg -i {直播链接} -c copy -bsf:a aac_adtstoasc {文件名}.mp4

ffmpeg国内下载

网盘下载 (访问密码: 312306)

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

使用ffmpeg将本地的flv视频压制转换成小体积的MP4

发表于 2024-04-13

前言

关于ffmpeg的下载, 可查阅上一篇文章《Mac平台如何下载ffmpeg并对m3u8视频进行下载转换成MP4》

本文将给大家介绍如何使用ffmpeg将本地的flv视频压制转换成小体积的MP4

格式转换

如果只是单纯的进行格式转换 可以直接使用以下指令来实现:

1
ffmpeg -i input.flv -vcodec copy -acodec copy output.mp4

这种方式通常体积不会有太大的改变

转换格式并压缩视频体积

如果需要将视频进行压缩 以减少空间的占用 可以参考以下指令:

1
ffmpeg -i input.flv -c:v libx264 -crf 26 -c:a aac -strict experimental -b:a 128k output.mp4

解释各选项含义:

  • -i input.flv:指定输入文件名为input.flv。
  • -c:v libx264:使用H.264编码来压缩视频。
  • -crf 28:设置CRF(常量速率因子)的值为23。低值表示更好的质量,但文件会更大;高值会降低质量。范围是从0(无损)到51(最糟),通常使用18到28, 27通常是用于MP4格式的视频质量很好的平衡
  • -c:a aac:使用AAC编码来压缩音频。
  • -strict experimental:允许使用实验性的编码器。
  • -b:a 128k:设置音频比特率为128k。根据需求可以调整大小。

我们需要找到一个合适的参数平衡点 确保视频画面的质量的同时尽可能地压缩视频体积

如果我们想要快速压制 减少等待时间 还可以指定编码器预设 如下:

1
ffmpeg -i input.flv -c:v libx264 -preset veryfast -crf 24 -c:a aac -b:a 128k output.mp4

解释选项含义:

  • -preset veryfast:指定编码器预设,veryfast提供快速但质量较低的编码, 除此之外 还有fast、HQ、 SuperHQ等预设。

ffmpeg 截取视频

要使用FFmpeg截取视频 可以使用以下指令:

1
ffmpeg -i input.mp4 -ss 00:00:10 -t 00:00:30 -c copy output.mp4

解释各选项含义:

  • -i input.mp4 指定输入视频文件。
  • -ss 00:00:10 表示从视频的指定时间点开始截取,这里是从10秒处开始。
  • -t 00:00:30 表示截取的时长,这里是30秒。
  • -c copy 表示复制编码器,即不重新编码,直接复制视频流和音频流。
  • output.mp4 是输出文件的名称。

请根据实际需求调整时间参数。如果需要不同的起始时间或时长,只需修改-ss和-t参数即可。如果需要对视频进行重编码,可以去掉-c copy参数或指定其他编码器选项。

本文为作者原创 转载时请注明出处 谢谢

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

如何开发hexo插件

发表于 2024-04-13

前言

Hexo 有强大的插件系统,使您能轻松扩展功能而不用修改核心模块的源码。在 Hexo 中有两种形式的插件:

脚本(Scripts)

如果您的代码很简单,建议您编写脚本,您只需要把 JavaScript 文件放到 scripts 文件夹,在启动时就会自动加载。

插件(Packages)

如果您的代码较复杂,或是您想要发布到 NPM 上,建议您编写插件。首先,在 node_modules 文件夹中建立文件夹,文件夹名称开头必须为 hexo-,如此一来 Hexo 才会在启动时加载;否则 Hexo 将会忽略它。

文件夹内至少要包含 2 个文件:一个是主程序,另一个是 package.json,描述插件的用途和所依赖的插件。

1
2
3
.
├── index.js
└── package.json

package.json 中至少要包含 name, version, main 属性,例如:

1
2
3
4
5
package.json{
"name": "hexo-my-plugin",
"version": "0.0.1",
"main": "index"
}

工具

您可以使用 Hexo 提供的官方工具插件来加速开发:

  • hexo-fs:文件 IO
  • hexo-util:工具程式
  • hexo-i18n:本地化(i18n)
  • hexo-pagination:生成分页数据

发布

当您完成插件后,可以考虑将它发布到 插件列表,让更多人能够使用您的插件。发布插件的步骤和 更新文档 非常类似。

  1. Fork hexojs/site

  2. 把库(repository)复制到电脑上,并安装所依赖的插件。

    1
    2
    3
    $ git clone https://github.com/<username>/site.git
    $ cd site
    $ npm install
  3. 在 source/_data/plugins/ 中创建一个新的 yaml 文件,使用您的插件名称作为文件名。

  4. 编辑 source/_data/plugins/<your-plugin-name>.yml 并添加您的插件。例如:

    1
    2
    3
    4
    5
    6
    description: Server module for Hexo.
    link: https://github.com/hexojs/hexo-server
    tags:
    - official
    - server
    - console
  5. 推送(push)分支。

  6. 建立一个新的合并申请(pull request)并描述改动。

本文转载自 : Hexo中文网

乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站

123…48

乱码三千

android程序员一枚,擅长java,kotlin,python,金融投资,欢迎交流~

479 日志
139 标签
RSS
© 2024 乱码三千
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%