更新

22-10-28: 增加重试功能,兼容更多类型

前言

其实已经有较为成熟的朋友圈项目了。那么我之前为什么不添加这个功能呢?

  • 首先是因为比较懒,且感觉部署起来比较复杂,速度也有一点慢。
  • 其次就是因为洪哥的友链比较多,相对应的鱼塘(Heo的朋友圈名字)的文章也就非常的多,所以我之前都是直接看他的。

最近可能是因为项目更新的问题,洪哥的鱼塘不能实时的更新内容了。也就没法再白嫖了。
经过一番思考,觉得自己应该可以写出这个功能,于是就决定自己写一个简单的,最主要的就是练手。

首先我选用nodejs来写,但是不知道为什么本地运行一切正常而部署到服务器却总是有一些小BUG。

我就想着要不换成python吧,毕竟在爬虫方面python更专业一些,顺便也尝试尝试如何在服务器运行python(以前没弄过)。

经过大半天的折腾终于是做出来了。至于优化什么的就再说吧。

前端样式是参考的 @林木木@Heo 的,将两者小小的结合了一下。效果还是很不错的。

具体效果请转至:友链订阅

代码

首先我写了一个link.json文件来存放友链相关信息,并在其中写上了每一个文件的rss地址,这个文件对我来说是有用的,但是对一些人来说可能就没什么作用。且我之前用node写了一个api,为了图省事我直接把获取数据的api写在这个文件里了,这就导致整个功能更乱了。

这也是不适合写成教程的原因,我写的太有针对性,而每个人的情况又有所不同。

所以现在我写的这个朋友圈功能的流程是:python爬取数据并存储 -> node获取数据并响应请求发送数据 -> 前端接受并渲染数据。看似明确,实则很乱。

json存放友链信息
可以参考:link.json

friends.py

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
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
from webbrowser import get
import requests
import json
import xmltodict
import time

# json地址
jsonUrl = 'https://blog.leonus.cn/link.json'


def xmltojson(xmlstr):
xmlparse = xmltodict.parse(xmlstr)
jsonstr = json.dumps(xmlparse, indent=1)
return jsonstr


def timestamp(shijian, type):
try:
s_t = time.strptime(shijian, type)
mkt = int(time.mktime(s_t))
except:
return False
return(mkt)


def getData(item):
try:
ls = []
data = json.loads(xmltojson(requests.get(item["link"]+item["rss"], timeout=3, headers={
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.52'}).content.decode("utf-8")))
if "feed" in data:
als = data["feed"]["entry"]
for e in als:
ls.append({
"articletitle": e["title"]["#text"] if "#text" in e["title"] else e["title"],
"articleLink": e["id"],
"articletime": timestamp(e["published"][:19], "%Y-%m-%dT%H:%M:%S"),
"name": item["name"],
"link": item["link"],
"avatar": item["avatar"]
})
return ls
elif "rss" in data:
als = data["rss"]["channel"]["item"]
for e in als:
ls.append({
"articletitle": e["title"]["#text"] if "#text" in e["title"] else e["title"],
"articleLink": e["link"],
"articletime": timestamp(e["pubDate"][:25], "%a, %d %b %Y %H:%M:%S") or timestamp(e["pubDate"][:23], "%a, %d %b %y %H:%M:%S"),
"name": item["name"],
"link": item["link"],
"avatar": item["avatar"]
})
return ls
else:
return 0
except:
return 0


def start():
articleList = []
# 获取网站列表
ls = eval(requests.get(jsonUrl).text)['link_list']
print('开始爬取,当前时间:' +
str(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))

# 循环获取文章
for index, item in enumerate(ls):
if not(item["rss"]):
continue
print("正在爬取第" + str((index+1)) + "个:" +
str((item['name'])) + ",共" + str(len(ls)) + "个。", end='')

for i in range(3):
if i >= 1:
print('重试中...')
time.sleep(3)
data = getData(item)
if data:
articleList.extend(data)
print("爬取成功")
break
else:
print("爬取失败")

articleList.sort(key=lambda x: x['articletime'], reverse=True)
articleList = articleList[:120]
with open('friends.json', 'w', encoding='utf-8') as f:
articleList = json.dumps({
'lastGetTime': time.time(),
'data': articleList
})
f.write(str(articleList))


while True:
start()
time.sleep(14400)

api.js

nodejs实现api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require('express');
const fs = require('fs');

const app = express()
app.all('*', function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Methods', '*');
res.header('Content-Type', 'application/json;charset=utf-8');
next();
});

app.get('/friends', async(req, res) => {
var data = JSON.parse(fs.readFileSync('../friend/friends.json'))
let start = req.query.start ? Number(req.query.start) : 0
let end = req.query.length ? start + Number(req.query.length) : start + 30

data.data = data.data.slice(start, end)
res.send(JSON.stringify(data))
})

app.listen(3000)

friends/index.md

hexo新建friends页面,写下如下代码

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
---
title: 友链订阅
date: 2022-10-04 21:51:54
type:
aside: false
aplayer:
comments:
keywords:
description:
highlight_shrink:
---
<style>
div#page {
background: none;
border: 0;
padding: 0;
}

#page h1.page-title {
color: var(--font-color);
margin: 0 0 10px;
text-align: left;
}

[data-theme=dark] #twikoo .tk-content,
#twikoo .tk-content {
padding: 0;
background: transparent;
}

.tk-comments-container>.tk-comment,
.tk-submit:nth-child(1),
.talk_item {
background: var(--card-bg);
border: 1px rgba(188, 188, 188, 0.8) solid;
box-shadow: 0 5px 10px rgb(189 189 189 / 10%);
transition: all .3s ease-in-out;
border-radius: 12px;
}

.tk-comments-container>.tk-comment:hover,
.tk-submit:nth-child(1):hover {
border-color: #6dc3fd;
}

.tk-submit {
padding: 20px 10px 0;
}

.tk-comments-container>.tk-comment {
padding: 15px;
}

.friends {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}

.item,
.friends_bottom button {
background: var(--card-bg);
transition: 0.3s;
overflow: hidden;
position: relative;
width: 49.3%;
border: 1px solid var(--search-input-color);
border-radius: 12px;
padding: .6rem 1rem;
margin: .5rem 0;
box-shadow: 0 0 15px rgb(151 151 151 / 10%);
}

.item:hover,
.friends_bottom button:hover {
border-color: var(--leonus-main);
transform: scale(1.03);
}

.item:hover .num,
.item:hover .time {
opacity: .7
}

a.num:hover {
text-decoration: none !important;
}

.info {
display: flex;
line-height: 1;
margin: 5px 0;
justify-content: space-between;
}

.author a,
a.title {
display: block;
color: var(--font-color) !important;
font-size: 18px;
}

a.title {
margin-bottom: 10px;
}

.author a:hover,
a.title:hover {
text-decoration: none !important;
color: var(--leonus-main) !important;
}

.author {
display: flex;
align-items: center;
}

.author a {
margin-left: 5px;
font-size: 16px;
}

.author img {
height: 22px;
margin: 0 !important;
border-radius: 50%;
}

span.time {
opacity: 0.5;
}

a.num {
color: var(--font-color) !important;
position: absolute;
font-size: 50px;
top: -15%;
right: .5rem;
font-style: italic;
opacity: .3;
line-height: 1;
}

.friends_bottom {
display: none;
}

.friends_bottom button {
display: block;
margin: 10px auto;
color: var(--font-color);
font-size: 1.3rem;
}

.friends_bottom span {
display: block;
opacity: .8;
text-align: right;
width: 100%;
padding: 0 10px;
}

@media screen and (max-width: 900px) {
.item,
.friends_bottom button {
width: 95%;
}
.friends {
justify-content: center;
}
#page h1.page-title {
font-size: 1.5rem;
}
}
</style>

<div class="content"></div>

<div class="friends_bottom">
<button onclick="lookMore()">查看更多</button>
<span>最后更新:2022-01-01 08:00:00</span>
</div>

<script>
var start = 0
var lookMore = function() {
start += 30
getData('add')
}
var toTime = function(time, type) {
let d = new Date(time),
ls = [d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()];
for (let i = 0; i < ls.length; i++) {
ls[i] = ls[i] <= 9 ? '0' + ls[i] : ls[i] + ''
}
if (type == 'long') return ls[0] + '-' + ls[1] + '-' + ls[2] + ' ' + ls[3] + ':' + ls[4] + ':' + ls[5]
else return ls[0].slice(2) + '/' + ls[1] + '/' + ls[2]
}
var getData = function(type) {
fetch(`https://xxx.xxxx.xx/friends?start=${start}&length=30`).then(res => res.json()).then(res => {
let div = document.createElement('div')
div.className = 'friends'

let html = ''
res.data.forEach((e, index) => {
html += `
<div class="item">
<a target="_blank" href="${e.articleLink}" class="title">${e.articletitle}</a>
<a target="_blank" href="${e.articleLink}" class="num">${start + index + 1}</a>
<div class="info">
<div class="author">
<img class="no-lightbox" src="${e.avatar}">
<a target="_blank" href="${e.link}">${e.name}</a>
</div>
<span class="time">${toTime(e.articletime * 1000)}</span>
</div>
</div>
`
div.innerHTML = html
document.querySelector('.content').appendChild(div)
document.querySelector('.friends_bottom span').innerHTML = '最后更新:' + toTime(res.lastGetTime * 1000, 'long')
document.querySelector('.friends_bottom').style.display = 'block'
if (start + 30 >= 150) document.querySelector('.friends_bottom').style.display = 'none'
});
}).then(leonus.scrollFn)
}
getData()

</script>

后记

写出来主要是提供一个参考(顺便水一篇文章)。
代码可能会有一些乱,但是能跑,后面再慢慢优化。
如果有一些基础的且有兴趣的可以自行修改尝试。