前言

参考友链文章聚合

这个项目是在今年春季时候做的。当时看到凡尘纪有个友链朋友圈的东西,是小冰大佬做的,部署在Leancloud。

此前我也曾想过尝试部署使用友链朋友圈,但因为过程繁琐,就放弃了,说到底还是懒,懒到自己重新造轮子(笑)。当然,也是因为懒,追求开发效率,我无脑地选择了Python3。

因为这个项目的作者想要标新立异,所以命名其为“友链文章聚合”而非“友链朋友圈”。也同样是因为参考了别人的东西所以就没面子放Gayhub上。

项目完成后因为升学也没有及时公开代码,今天把代码及部署过程发一下吧。

前端示例截图

后端控制台输出示例

须知

  • 本项目遵循MIT协议
  • 部署本项目至少需要一个能够运行Python3解释器包括pip)和诸如Apache2之类的Web服务土豆服务器(使用Debian及其分支GNU/Linux发行版最佳,并需要提前搭建好一个外网能够访问的网站。如果服务器在内网,可以试试内网穿透服务),以及一个会面向API编程的活人
  • 本项目会用到feedparser库;
  • 因运行环境异同,不保证在任何机器上都能运行
  • 因部署本项目造成的任何损失作者不会承担任何责任
  • 本文只涉及后端,如果要使用本项目,还麻烦自行编写前端
  • 提问前请先尝试自行解决,避免浪费时间;

本项目优点:

  • 部署起来相对而言不是那么麻烦,轻量;
  • 因为使用的是烂大街的编程语言(Python3)所以可以放心修改;
  • 因为要自行来写前端,所以你可以按你天马行空的想象力去写,反正都是同一种功能。

本项目缺陷:

  • 就算将部署过程尽可能简化了但还是对小白而言有一定门槛的,否则你连部署的过程都无法理解
  • 需要root权限,且需要设置Web服务的Access-Control-Allow-Origin跨域请求头;
  • 要自行来写前端(当然要F12抄本站的页面也是可以接受的,但我写的真的很烂)

正文

原理

定时获取友链列表,得到各友链站点的Feed URL,再逐个解析,得到友链的文章列表,排序输出一个json文件到后端站点。

前端通过ajax载入该文件,解析,格式化写入到document。

环境

  • OS: Debian 10 GNU/Linux(i686);
  • Web Server: Apache2;
  • Python3 version: Python 3.7.3.

其他的操作系统下的环境……自己研究吧。同是Linux的话应该大同小异。

部署

0. 整理友链

将友链列表整理为json友链列表,格式如下:

文件名:friends.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"class": {
"link_list": [
{
"rss": "https://www.kawashiros.club/atom.xml"
},
{
"rss": "https://zfe.space/atom.xml"
}
]
}
}

如上,class中的link_list为友链列表。列表中,每个字典都必须有rss字符串对象。rss为站点的feed,即订阅RSS的URL。以上是最简单的模板,其他的对象可以根据自己需要加入(参考),你可以使其单独作为订阅列表,使之与你的博客的友链列表分开存放,也可以像我一样通过某种手段,使之成为你博客的友链列表,每次访问友链页都载入该文件,然后输出html。

麻烦在于,需要挨个儿拜访邻居以获得feed,在这里还是推荐使用RSSHub Radar插件。

编辑完后,将该文件保存至博客所在域名能找得到的位置。

1. 安装feedparser

1
sudo python3 -m pip install feedparser -i https://pypi.tuna.tsinghua.edu.cn/simple 

2. 将以下脚本修改并保存

文件名:rsssubs.py

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
#!/usr/bin/python3
__version__ = 202104111956
##
## RSSsubs - 订阅收集(友链朋友圈 硬核Backend)
##
## (C) 2020 非科学のカッパ,License under MIT
##

##
## The MIT License (MIT)
## Copyright 2021 非科学のカッパ
##
## Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
##
## The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
##


import feedparser as Feed
import time
import traceback
import urllib
import json
import signal
import sys
import os


# (编辑)配置 ------------------------------------------------------
# 友链列表(URL)
subclistf:str = 'https://www.kawashiros.club/resources/friends.json'
# 输出
SaveJSON:str = "/var/www/html/RSSsubsfetch.json"
# 日志目录
SaveLog:str = sys.path[0]
# 间隔更新(s)
IntervalUpd:int = 3600
#--------------------------------------------------------------------

# 用于日志的宏(?
INFO = 0
WARN = 1
ERR = 2




# 日志函数
def log(info:str, stat:int = 0):
try:
tmpTimeStruct = time.localtime(time.time())
timestat = [
str(tmpTimeStruct.tm_year) + '-' + str(tmpTimeStruct.tm_mon) + '-' + str(tmpTimeStruct.tm_mday),
str(tmpTimeStruct.tm_hour) + ':' + str(tmpTimeStruct.tm_min) + ':' + str(tmpTimeStruct.tm_sec)
]

sign = ['[i]','<!>','(x)']

consOutMark = ['\033[;32m','\033[;33m','\033[;31m']
print('['+timestat[0]+' '+timestat[1]+']'+ consOutMark[stat] +sign[stat] + info + '\033[;0m');

open(os.path.join(SaveLog, "LOG-"+timestat[0]+'.log'),"a+").write('['+timestat[0]+' '+timestat[1]+']'+sign[stat] + info + '\n')
except:
print('无法记录日志!请检查日志保存路径及其写入权限。')
sys.exit(-1)

# 信号处理
def signal_handler(signum, frame):
log("Received Signal:" + str(signum) + ", Exit", WARN)
os._exit(0)

signal.signal(signal.SIGHUP, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGQUIT, signal_handler)
signal.signal(signal.SIGALRM, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGCONT, signal_handler)

# 加载RSS种子
# 输出-1即为异常
def loadFeed(feedurl:str,failedOnce = 0):
try:
# 伪装UA
Feed.USER_AGENT = "Mozilla/5.0 (X11; Linux x86) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17"
feed = Feed.parse(feedurl, agent="Mozilla/5.0 (X11; Linux x86) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17")


# 获取站点名
site = feed['feed']['title']
log(site + ' - 成功获取RSS订阅')

except urllib.error.URLError as einfo:
log(str(einfo), ERR)
return -1
# 键值错误--源解析失败
except KeyError:
log("无法解析此RSS种子", ERR)
return -1
# 其他错误
except:
log("未知错误\n错误信息:"+ traceback.format_exc() ,ERR)
return -1

else:
# RSS解析结果--字典
# plist = {'site': site, 'posts': []}
plist = []


for a in range(len(feed['entries'])):
# 日期转换为整数形式序列(YYYYMMDDhhmmss)
if('published_parsed' in feed['entries'][a]):
t = feed['entries'][a]['published_parsed']
ti = t.tm_year * 10000000000 + t.tm_mon *100000000 + t.tm_mday * 1000000 + t.tm_hour * 10000 + t.tm_min *100 + t.tm_sec;
dict = {'post':feed['entries'][a]['title'] ,'time':ti,"url":feed['entries'][a]['links'][0]['href'],"site":feed['feed']['title']}
plist.append(dict)
del(t)

else:
log("无对应键值,返回", WARN)
return plist

#if __name__ == "__main__":
def main():
# 载入订阅地址列表
try:
headers = {'User-Agent': "Mozilla/5.0 (X11; Linux x86) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.56 Safari/537.17"}
request = urllib.request.Request(subclistf, headers=headers)
flinklist = urllib.request.urlopen(request).read()

except urllib.error.HTTPError as einfo:
log(str(einfo), ERR)
log("友链列表获取失败,结果未更新", WARN)
return (-1, None)
except urllib.error.URLError as einfo:
log(str(einfo), ERR)
log("友链列表获取失败,结果未更新", WARN)
return (-1,None)
except:
log("未知错误\n错误信息:"+ traceback.format_exc() ,ERR)
log("友链列表获取失败,结果未更新", WARN)
return (-1,None)
else:
try:
# 友链JSON
flinks = json.loads(flinklist)['class']['link_list']
except json.decoder.JSONDecodeError as einfo:
log(str(einfo),ERR)
log("友链列表解析失败,结果未更新",WARN)
return (-1,None)
except:
log("未知错误\n错误信息:"+ traceback.format_exc() ,ERR)
log("友链列表获取失败,结果未更新", WARN)
return (-1,None)
else:
log("获取友链列表")

# 所有RSS订阅
allRSS = []
# 获取失败数目
faliures:int = 0

for a in flinks:
if('rss' not in a):
log('元素' + str(a) +'无对应键值,跳过',WARN)
pass
else:
tmp = loadFeed(a['rss'])
if(tmp == -1):
log('元素'+ str(a) + ' RSS种子读取失败',WARN)
faliures += 1
pass
else:
allRSS += tmp
return (faliures,allRSS)

# 冒泡排序 根据时间
def postSort(arr:list):
n = len(arr)

for a in range(n):
for b in range(0,n-1-a):
if(arr[b]['time'] < arr[b+1]['time']):
arr[b], arr[b+1] = arr[b+1], arr[b]
log('排序完成')

if __name__ == "__main__":
log("RSSsubs Version " + str(__version__))
while(True):
(failures,allRSS) = main()
if(failures < 0):
log("发生了致命错误!",ERR)
sys.exit(failures)
elif(failures!=0):
log("完成抓取RSS,其中"+str(failures)+"项抓取失败", WARN)
else:
log("完成抓取RSS,无错误")

postSort(allRSS)


try:
json.dump({"subscribes":{"failures":failures,"allRSS":allRSS,"updated":int(time.strftime('%Y%m%d%H%M%S', time.localtime()))}}, open(SaveJSON, "w",encoding='utf-8'), indent = 6 ,ensure_ascii=False)
except PermissionError:
log('权限错误!请确保以root权限运行!')
sys.exit(-1)
except:
log("未知错误\n错误信息:"+ traceback.format_exc() ,ERR)
log("结果未更新", WARN)
else:
log("已输出至"+ SaveJSON)

log('下一次更新将在'+str(IntervalUpd/60)+'分钟后')
time.sleep(IntervalUpd)

修改以下几个变量:

  • subclistf: 字符串类型。友链列表的URL,即储存在博客站点下、在步骤0编辑的json文件所对应的URL。必须更改
  • SaveJSON: 字符串类型。输出json结果的保存路径,应当为输出到后端服务器的网站根目录下的路径,最好为网站根目录路径。必须更改
  • SaveLog: 字符串类型。日志保存路径,默认为程序所在目录。可以更改,但请注意权限问题
  • IntervalUpd: 整型。数据自动更新时间,单位为秒。可以更改,但请注意权限问题

3. 赋权运行测试

执行:

1
2
sudo chmod +x rsssubs.py
sudo ./rsssubs.py

正常的话,会在控制台输出:已输出至*/RSSsubsfetch.json下一次更新将在*分钟后的字样,且会在后端服务器的网站下SaveJSON变量所对应的路径下)

这时就说明成功部署,可以Ctrl+C杀死这个进程了。

4. 添加到服务

/etc/systemd/system下创建rsssubs.service,写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description = RSS Subscriptions
After = network.target

[Service]
Type = simple
User = root
Restart = 1
RestartSec = 5s
ExecStart = /usr/bin/python3 /path/to/rsssubs.py

[Install]
WantedBy = multi-user.target

ExecStart中,/usr/bin/python3是Debian系Linux下python3解释器默认安装路径,/path/to/rsssubs.py步骤2创建的rsssubs.py所在路径,根据情况修改。

然后执行:

1
2
3
sudo systemctl enable rsssubs.service
sudo systemctl start rsssubs.service
sudo systemctl status rsssubs.service

输出结果会类似于:

1
2
3
4
5
6
7
8
● rsssubs.service - RSS Subscriptions
Loaded: loaded (/etc/systemd/system/rsssubs.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2021-06-28 22:51:07 CST; 2 days ago
Main PID: 31359 (python3)
Tasks: 1 (limit: 2305)
Memory: 9.7M
CGroup: /system.slice/rsssubs.service
└─31359 /usr/bin/python3 /path/to/rsssubs.py

其中Active行,正常为active (running),否则应检查方才编辑的rsssubs.service

5. 前端调用

可以试着在浏览器中访问一下rsssubs.py输出的JSON文件。

因为这个输出的文件又臭又长,所以这里就简单地讲以下其大概的结构吧。

输出的json文件中,所有代表“时间”的量都为一个十四位数整型,如20210630134909代表20210630134909秒,可以自己设计算法把以上十四位数整形转化为时间对象。

后记

在暑假正式开始前总算是把它写完了。

应该不会有人使用我这种东西吧,又需要服务器又得折腾来折腾去的。其实也可以稍微改变以下,就可以和使订阅列表与博客一起发布,就是很没有时效性,不经常更新博客的话还是不推荐这样。

虽然是从头开始造轮子,但也并非一无所获,至少,还是折腾出来了些东西的。