冰老师很久很久以前做的友链朋友圈,功能很棒但是配置过程为了追求免费涵盖了特别特别多业务。之前部署成功过一起,但是后来担心GitHub action限额和leancloud的多个限制,在一次无法使用的情况下,彻底被我给移除了。

上周得知友链朋友圈已经支持服务器部署了,全程使用服务器不用第三方服务,那不得整一个?

从功能性上一开始用的原装的,后来发现木木的功能更多一些,所以现在用的是木木的前端结构。这个文章只作为记录整个部署过程,不作为教程使用。

前端部署文档

在前端部署用的木木的部署方式:立即访问文档

后端部署文档

在后端部署用的官方文档:立即访问官方文档

在部署方式上使用的是最简单的server+sqlite方式,推荐有服务器的小伙伴使用这个方式。

部署后端

因为用的宝塔面板,所以很轻松的安装了文档要求的python版本。

安装python

后来按照文档执行./server.sh时发现api进程始终无法运行。

只有run.py

后来与二猹探讨完后发现是因为没有安装aiohttp库,直接pip3 install aiohttp安装成功后就可以正常运行了。

友链适配

因为前端很久之前就部署好了,所以可以轻松无缝移植新api。但是出现一个问题就是因为我的友链是分成两段的,一个是我自己魔改的友链样式,专门给100文章+的友链样式,一种是butterfly自带样式。使用butterfly主题抓取规则还不能抓取我的友链。观察了一下发现本身自带通用友链规则,调了半天发现最初的通用规则是有dom结构要求的,必须按照他这个结构来,但是修改友链结构不仅会破坏原有的友链样式,而且有些样式本身就不适合这个结构(因为原有规则的css太模糊了,如果有多个img标签抓取的就有问题)。

自己动手丰衣足食,写了一个commonPro(后来被迫更名为common2的通用规则)。

规则如下:

1
2
3
4
5
6
7
def get_common2_url(self,response,queue):
avatar = response.css('.cf-friends-avatar::attr(data-lazy-src)').extract()
if not avatar:
avatar = response.css('img.cf-friends-avatar::attr(src)').extract()
link = response.css('a.cf-friends-link::attr(href)').extract()
name = response.css('.cf-friends-name::text').extract()
self.handle(avatar, link, name, queue)

只需要在需要抓取的内容添加class类名即可,不需要去改任何dom结构。

最终成功完成了友链适配。

前端部署

前端没有遇到太多的问题,在刚开始部署的时候发现点击更多按钮后,图片因为lazyload的原因没有去主动加载,翻阅lazylaod文档之后看到这个函数,执行之后就会检查图片是否加载,并加载需要加载的图片。

1
lazyLoadInstance.update();

调了一些结构,比如木木不支持点击头像查看卡片,我改了一下结构给弄支持了,还有一些小调整。

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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
/*
Last Modified time : 20220211 15:38 by https://immmmm.com
已适配 FriendCircle 公共库和主库
*/

//默认数据
var fdata = {
jsonurl: '',
apiurl: '',
apipublicurl: 'https://moments.zhheo.com/', //默认公共库
initnumber: 30, //首次加载文章数
stepnumber: 30, //更多加载文章数
article_sort: 'created', //文章排序 updated or created
error_img: 'https://sdn.geekzu.org/avatar/57d8260dfb55501c37dde588e7c3852c'
}
//可通过 var fdataUser 替换默认值
if(typeof(fdataUser) !=="undefined"){
for(var key in fdataUser) {
if(fdataUser[key]){
fdata[key] = fdataUser[key];
}
}
}
var article_num = '',sortNow='',UrlNow='',friends_num=''
var container = document.getElementById('cf-container') || document.getElementById('fcircleContainer') ;
// 获取本地 排序值、加载apiUrl,实现记忆效果
var localSortNow = localStorage.getItem("sortNow")
var localUrlNow = localStorage.getItem("urlNow")
if(localSortNow && localUrlNow){
sortNow = localSortNow
UrlNow = localUrlNow
}else{
sortNow = fdata.article_sort
if(fdata.jsonurl){
UrlNow = fdata.apipublicurl+'postjson?jsonlink='+ fdata.jsonurl+"&"
}else if(fdata.apiurl){
UrlNow = fdata.apiurl+'all?'
}else{
UrlNow = fdata.apipublicurl+'all?'
}
console.log("当前模式:"+UrlNow)
localStorage.setItem("urlNow",UrlNow)
localStorage.setItem("sortNow",sortNow)
}
// 打印基本信息
function loadStatistical(sdata){
article_num = sdata.article_num
friends_num = sdata.friends_num
var messageBoard =`
<div id="cf-state" class="cf-new-add">
<div class="cf-state-data">
<div class="cf-data-friends" onclick="openToShow()">
<span class="cf-label">订阅</span>
<span class="cf-message">${sdata.friends_num}</span>
</div>
<div class="cf-data-active" onclick="changeEgg()">
<span class="cf-label">活跃</span>
<span class="cf-message">${sdata.active_num}</span>
</div>
<div class="cf-data-article" onclick="clearLocal()">
<span class="cf-label">日志</span>
<span class="cf-message">${sdata.article_num}</span>
</div>
</div>
<div id="cf-change">
<span id="cf-change-created" data-sort="created" onclick="changeSort(event)" class="${sortNow == 'created' ? 'cf-change-now':''}">Created</span> | <span id="cf-change-updated" data-sort="updated" onclick="changeSort(event)" class="${sortNow == 'updated' ? 'cf-change-now':''}" >Updated</span>
</div>
</div>
`;
var loadMoreBtn = `
<div id="cf-more" class="cf-new-add" onclick="loadNextArticle()"><i class="fas fa-angle-double-down"></i></div>
<div id="cf-footer" class="cf-new-add">
<span id="cf-version-up" onclick="checkVersion()"></span>
<span class="cf-data-lastupdated">更新于:${sdata.last_updated_time}</span>
<span class="cf-data-lastupdated">订阅:${sdata.friends_num} 活跃:${sdata.active_num} 日志:${sdata.article_num}</span>
</div>
<div id="cf-overlay" class="cf-new-add" onclick="closeShow()"></div>
<div id="cf-overshow" class="cf-new-add"></div>
`;
if(container){
// container.insertAdjacentHTML('beforebegin', messageBoard);
container.insertAdjacentHTML('afterend', loadMoreBtn);
}
}
// 打印文章内容 cf-article
function loadArticleItem(datalist,start,end){
var articleItem = '';
var articleNum = article_num;
var endFor = end
if(end > articleNum){endFor = articleNum}
if(start < articleNum){
for (var i = start;i<endFor;i++){
var item = datalist[i];
articleItem +=`
<div class="cf-article">
<a class="cf-article-title" href="${item.link}" target="_blank" rel="noopener nofollow" data-title="${item.title}">${item.title}</a>
<span class="cf-article-floor">${item.floor}</span>
<div class="cf-article-avatar no-lightbox flink-item-icon">
<a onclick="openMeShow(event)" data-link="${item.link}" class="" target="_blank" rel="noopener nofollow" href="javascript:;"><img class="cf-img-avatar avatar" src="${item.avatar}" alt="avatar" onerror="this.src='${fdata.error_img}'; this.onerror = null;"><span class="cf-article-author">${item.author}</span></a>
<span class="cf-article-time">
<span class="cf-time-created" style="${sortNow == 'created' ? '':'display:none'}">${item.created}</span>
<span class="cf-time-updated" style="${sortNow == 'updated' ? '':'display:none'}"><i class="fas fa-history">更新于</i>${item.updated}</span>
</span>
</div>
</div>
`;
}
container.insertAdjacentHTML('beforeend', articleItem);
// 预载下一页文章
fetchNextArticle()
}else{
// 文章加载到底
document.getElementById('cf-more').outerHTML = `<div id="cf-more" class="cf-new-add" onclick="loadNoArticle()"><small>一切皆有尽头!</small></div>`
}
}
// 打印个人卡片 cf-overshow
function loadFcircleShow(userinfo,articledata){
var showHtml = `
<div class="cf-overshow">
<div class="cf-overshow-head">
<img class="cf-img-avatar avatar" src="${userinfo.avatar}" alt="avatar" onerror="this.src='${fdata.error_img}'; this.onerror = null;">
<a class="" target="_blank" rel="noopener nofollow" href="${userinfo.link}">${userinfo.author}</a>
</div>
<div class="cf-overshow-content">
`
for (var i = 0;i<userinfo.article_num;i++){
var item = articledata[i];
showHtml += `
<p><a class="cf-article-title" href="${item.link}" target="_blank" rel="noopener nofollow" data-title="${item.title}">${item.title}</a><span>${item.created}</span></p>
`
}
showHtml += '</div></div>'
document.getElementById('cf-overshow').insertAdjacentHTML('beforeend', showHtml);
document.getElementById('cf-overshow').className = 'cf-show-now';
}

// 预载下一页文章,存为本地数据 nextArticle
function fetchNextArticle(){
var start = document.getElementsByClassName('cf-article').length
var end = start + fdata.stepnumber
var articleNum = article_num;
if(end > articleNum){
end = articleNum
}
if(start < articleNum){
UrlNow = localStorage.getItem("urlNow")
var fetchUrl = UrlNow+"rule="+sortNow+"&start="+start+"&end="+end
//console.log(fetchUrl)
fetch(fetchUrl)
.then(res => res.json())
.then(json =>{
var nextArticle = eval(json.article_data);
console.log("已预载"+"?rule="+sortNow+"&start="+start+"&end="+end)
localStorage.setItem("nextArticle",JSON.stringify(nextArticle))
})
}else if(start = articleNum){
document.getElementById('cf-more').outerHTML = `<div id="cf-more" class="cf-new-add" onclick="loadNoArticle()"><small>一切皆有尽头!</small></div>`
}
}
// 显示下一页文章,从本地缓存 nextArticle 中获取
function loadNextArticle(){
var nextArticle = JSON.parse(localStorage.getItem("nextArticle"));
var articleItem = ""
for (var i = 0;i<nextArticle.length;i++){
var item = nextArticle[i];
articleItem +=`
<div class="cf-article">
<a class="cf-article-title" href="${item.link}" target="_blank" rel="noopener nofollow" data-title="${item.title}">${item.title}</a>
<span class="cf-article-floor">${item.floor}</span>
<div class="cf-article-avatar no-lightbox flink-item-icon">
<a onclick="openMeShow(event)" data-link="${item.link}" class="" target="_blank" rel="noopener nofollow" href="javascript:;"><img class="cf-img-avatar avatar" src="${item.avatar}" alt="avatar" onerror="this.src='${fdata.error_img}'; this.onerror = null;"><span class="cf-article-author">${item.author}</span></a>
<span class="cf-article-time">
<span class="cf-time-created" style="${sortNow == 'created' ? '':'display:none'}">${item.created}</span>
<span class="cf-time-updated" style="${sortNow == 'updated' ? '':'display:none'}"><i class="fas fa-history">更新于</i>${item.updated}</span>
</span>
</div>
</div>
`;
}
container.insertAdjacentHTML('beforeend', articleItem);
lazyLoadInstance.update();
// 同时预载下一页文章
fetchNextArticle()
}
// 没有更多文章
function loadNoArticle(){
var articleSortData = sortNow+"ArticleData"
localStorage.removeItem(articleSortData)
localStorage.removeItem("statisticalData")
//localStorage.removeItem("sortNow")
document.getElementById('cf-more').remove()
window.scrollTo(0,document.getElementsByClassName('cf-state').offsetTop)
}
// 清空本地数据
function clearLocal(){
localStorage.removeItem("updatedArticleData")
localStorage.removeItem("createdArticleData")
localStorage.removeItem("nextArticle")
localStorage.removeItem("statisticalData")
localStorage.removeItem("sortNow")
localStorage.removeItem("urlNow")
location.reload();
}
//
function checkVersion(){
var url = fdata.apiurl+"version"
fetch(url)
.then(res => res.json())
.then(json =>{
console.log(json)
var nowStatus = json.status,nowVersion = json.current_version,newVersion = json.latest_version
var versionID = document.getElementById('cf-version-up')
if(nowStatus == 0){
versionID.innerHTML = "当前版本:v"+ nowVersion
}else if(nowStatus == 1){
versionID.innerHTML = "发现新版本:v"+ nowVersion + " ↦ " + newVersion
}else{
versionID.innerHTML = "网络错误,检测失败!"
}
})
}
// 切换为公共全库
function changeEgg(){
//有自定义json或api执行切换
if(fdata.jsonurl || fdata.apiurl ){
document.querySelectorAll('.cf-new-add').forEach(el => el.remove());
localStorage.removeItem("updatedArticleData")
localStorage.removeItem("createdArticleData")
localStorage.removeItem("nextArticle")
localStorage.removeItem("statisticalData")
container.innerHTML = ""
UrlNow = localStorage.getItem("urlNow")
//console.log("新"+UrlNow)
var UrlNowPublic = fdata.apipublicurl+'all?'
if(UrlNow !== UrlNowPublic){ //非完整默认公开库
changeUrl = fdata.apipublicurl+'all?'
}else{
if(fdata.jsonurl){
changeUrl = fdata.apipublicurl+'postjson?jsonlink='+ fdata.jsonurl+"&"
}else if(fdata.apiurl){
changeUrl = fdata.apiurl+'all?'
}
}
localStorage.setItem("urlNow",changeUrl)
FetchFriendCircle(sortNow,changeUrl)
}else{
clearLocal()
}
}
// 首次加载文章
function FetchFriendCircle(sortNow,changeUrl){
var end = fdata.initnumber
var fetchUrl = UrlNow + "rule="+sortNow+"&start=0&end="+end
if(changeUrl){
fetchUrl = changeUrl + "rule="+sortNow+"&start=0&end="+end
}
//console.log(fetchUrl)
fetch(fetchUrl)
.then(res => res.json())
.then(json =>{
var statisticalData = json.statistical_data;
var articleData = eval(json.article_data);
var articleSortData = sortNow+"ArticleData";
loadStatistical(statisticalData);
loadArticleItem(articleData ,0,end)
localStorage.setItem("statisticalData",JSON.stringify(statisticalData))
localStorage.setItem(articleSortData,JSON.stringify(articleData))
})
}
// 点击切换排序
function changeSort(event){
sortNow = event.currentTarget.dataset.sort
localStorage.setItem("sortNow",sortNow)
document.querySelectorAll('.cf-new-add').forEach(el => el.remove());
container.innerHTML = "";
changeUrl = localStorage.getItem("urlNow")
//console.log(changeUrl)
initFriendCircle(sortNow,changeUrl)
if(fdata.apiurl){
checkVersion()
}
}
//查询个人文章列表
function openMeShow(event){
event.preventDefault()
var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
var meLink = event.currentTarget.dataset.link.replace(parse_url, '$1:$2$3')
console.log(meLink)
var fetchUrl = ''
if(fdata.apiurl){
fetchUrl = fdata.apiurl + "post?link="+meLink
}else{
fetchUrl = fdata.apipublicurl + "post?link="+meLink
}
//console.log(fetchUrl)
if(noClick == 'ok'){
noClick = 'no'
fetchShow(fetchUrl)
}
}
// 关闭 show
function closeShow(){
document.getElementById('cf-overlay').className -= 'cf-show-now';
document.getElementById('cf-overshow').className -= 'cf-show-now';
document.getElementById('cf-overshow').innerHTML = ''
}
// 点击开往
var noClick = 'ok';
function openToShow(){
var fetchUrl = ''
if(fdata.apiurl){
fetchUrl = fdata.apiurl + "post"
}else{
fetchUrl = fdata.apipublicurl + "post"
}
//console.log(fetchUrl)
if(noClick == 'ok'){
noClick = 'no'
fetchShow(fetchUrl)
}
}
// 展示个人文章列表
function fetchShow(url){
var closeHtml = `
<div class="cf-overshow-close" onclick="closeShow()"></div>
`
document.getElementById('cf-overlay').className = 'cf-show-now';
document.getElementById('cf-overshow').insertAdjacentHTML('afterbegin', closeHtml);
console.log("开往"+url)
fetch(url)
.then(res => res.json())
.then(json =>{
//console.log(json)
noClick = 'ok'
var statisticalData = json.statistical_data;
var articleData = eval(json.article_data);
loadFcircleShow(statisticalData,articleData)
})
}
// 初始化方法,如有本地数据首先调用
function initFriendCircle(sortNow,changeUrl){
var articleSortData = sortNow+"ArticleData";
var localStatisticalData = JSON.parse(localStorage.getItem("statisticalData"));
var localArticleData = JSON.parse(localStorage.getItem(articleSortData));
container.innerHTML = "";
// if(localStatisticalData && localArticleData){
// loadStatistical(localStatisticalData);
// loadArticleItem(localArticleData ,0,fdata.initnumber)
// console.log("本地数据加载成功")
// var fetchUrl = UrlNow + "rule="+sortNow+"&start=0&end="+fdata.initnumber
// fetch(fetchUrl)
// .then(res => res.json())
// .then(json =>{
// var statisticalData = json.statistical_data;
// var articleData = eval(json.article_data);
// //获取文章总数与第一篇文章标题
// var localSnum = localStatisticalData.article_num
// var newSnum = statisticalData.article_num
// var localAtile = localArticleData[0].title
// var newAtile = articleData[0].title
// //判断文章总数或文章标题是否一致,否则热更新
// if(localSnum !== newSnum || localAtile !== newAtile){
// document.getElementById('cf-state').remove()
// document.getElementById('cf-more').remove()
// document.getElementById('cf-footer').remove()
// container.innerHTML = "";
// var articleSortData = sortNow+"ArticleData";
// loadStatistical(statisticalData);
// loadArticleItem(articleData ,0,fdata.initnumber)
// localStorage.setItem("statisticalData",JSON.stringify(statisticalData))
// localStorage.setItem(articleSortData,JSON.stringify(articleData))
// console.log("热更新完成")
// }else{
// console.log("API数据未更新")
// }
// })
// }else{
FetchFriendCircle(sortNow,changeUrl)
// console.log("第一次加载完成")
// }
}
// 执行初始化
initFriendCircle(sortNow)

大功告成

总体结果而言比较令人满意,以后就是我自己经常访问的渠道了。

立刻前往鱼塘

常用命令

傻瓜化部署、停止服务

1
python3 deploy.py

查看日志

1
cat /tmp/crawler.log