Compare commits
10 Commits
0990cb9b66
...
c621228996
Author | SHA1 | Date | |
---|---|---|---|
c621228996 | |||
|
d6def4acd0 | ||
b5bee828ca | |||
d163726756 | |||
ba7f8c0456 | |||
69f883f0a5 | |||
c9ef0ccca3 | |||
02bbaaa788 | |||
75ee065752 | |||
47918d49a1 |
3
.github/workflows/build-and-run.yml
vendored
@ -8,7 +8,8 @@ on:
|
||||
jobs:
|
||||
build-and-run:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: 1. Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
2
.github/workflows/docker-publish.yml
vendored
@ -3,7 +3,7 @@ name: Publish Docker image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
215
README.md
@ -4,26 +4,22 @@ EndOfYear 点燃个人博客的年度辉煌!
|
||||
|
||||
![EndOfYear](static/endofyear.jpg)
|
||||
|
||||
## 用法
|
||||
## 使用方法
|
||||
|
||||
### 要求
|
||||
|
||||
- RSS 源必须输出文章全部内容,否则数据分析不准确。
|
||||
- Github 运行可能无法访问 RSS 源,请使用本地 Docker 运行。
|
||||
- 如果生成年度报告,请结合博客实际情况设置 RSS 输出文章数量。
|
||||
- **确保 RSS 源提供完整的文章内容**:为了保证数据分析的准确性,RSS 源需要输出文章的全部内容。
|
||||
- **在 GitHub 上运行**:由于 GitHub 运行环境可能无法访问某些 RSS 源,请考虑在本地 Docker 环境中运行。
|
||||
- **适当设置 RSS 文章数量**:如果您的目的是生成年度报告,请根据博客的实际情况调整 RSS 输出的文章数量。
|
||||
|
||||
### Github
|
||||
### 在 GitHub 上的使用步骤
|
||||
|
||||
1. Fork 项目到个人仓库。
|
||||
2. 手动配置仓库的 Workflow permissions 设置为 **Read and write permissions**,否则无法写入 html 分支。
|
||||
1. 导航到 **Settings**(设置)选项卡。
|
||||
2. 在左侧导航栏中,点击 **Actions**(操作)。
|
||||
3. 在 **General**(常规)页面下滑,找到 **Workflow permissions**(工作流权限)。
|
||||
4. 在 **Workflow permissions** 中,选择 **Read and write permissions**(读写权限)。
|
||||
5. 最后点击 **Save**(保存)。
|
||||
3. 在仓库首页打开目录下的 `config.ini` 配置文件,点击右上角工具栏的 **🖋️(钢笔)** 图标,在线编辑文件。
|
||||
- `web`:配置为 `false`。
|
||||
- `rss`:配置为 RSS 地址。
|
||||
1. 将项目 Fork 到您的个人仓库。
|
||||
|
||||
2. 在仓库首页,找到并打开 `config.ini` 文件。点击右上角的 🖋️ 符号进行在线编辑。
|
||||
|
||||
- `web` 字段:将其**设置为 `false` 以启用静态网站模式**(适用于 GitHub 运行)。
|
||||
- `rss` 字段:填写您的 RSS 源地址,确保源地址提供全文输出。
|
||||
|
||||
```ini
|
||||
[default]
|
||||
@ -31,46 +27,54 @@ web = false
|
||||
|
||||
[blog]
|
||||
rss = https://blog.7wate.com/rss.xml
|
||||
data =
|
||||
```
|
||||
|
||||
4. 点击右上角的 **Commit changes** 提交到 `main` 分支,会自动运行 Actions。
|
||||
5. 等待 Actions 运行成功,将会部署静态网站文件至 `html` 分支。
|
||||
6. 开启 GitHub 仓库的 Pages 功能,默认为根目录。
|
||||
7. 访问个人网址,就可以看到啦~
|
||||
3. 编辑完成后,点击页面右上角的 **Commit changes** 将更改提交到 `main` 分支。
|
||||
|
||||
### Docker
|
||||
4. 提交后,GitHub Actions 会自动运行并生成静态网站文件,最终推送至 `html` 分支。
|
||||
|
||||
1. 拉取 [endofyear](https://hub.docker.com/r/sevenwate/endofyear) 最新镜像。
|
||||
5. 在 GitHub 仓库的 Settings 中开启 Pages 功能,并将源设置为 `html` 分支的根目录。
|
||||
|
||||
6. 稍后访问 GitHub Pages 分配的网址,即可看到生成的内容。
|
||||
|
||||
### 使用 Docker
|
||||
|
||||
1. **拉取 Docker 镜像**:从 [endofyear](https://hub.docker.com/r/sevenwate/endofyear) Docker Hub 页面拉取最新镜像。
|
||||
|
||||
```shell
|
||||
docker pull sevenwate/endofyear:latest
|
||||
```
|
||||
|
||||
2. 映射容器 7777 端口至宿主机端口,指定 `rss` 环境变量,然后运行 Docker。
|
||||
2. **运行 Docker 容器**:映射容器的 7777 端口到宿主机的端口,并设置 `rss` 环境变量。
|
||||
|
||||
```shell
|
||||
# 请将 https://blog.7wate.com/rss.xml 替换为自己的 RSS 地址。
|
||||
# 将 RSS 地址替换为您自己的。
|
||||
docker run -p 7777:7777 --env rss=https://blog.7wate.com/rss.xml sevenwate/endofyear:latest
|
||||
```
|
||||
|
||||
3. 访问网址 `localhost:7777`
|
||||
3. **访问本地网站**:在浏览器中访问 `localhost:7777`,即可查看结果。
|
||||
|
||||
## Q&A
|
||||
|
||||
### Github Actions 运行失败
|
||||
|
||||
请查阅 Actions 的第六步输出的 Log 日志排错。
|
||||
请首先检查 Actions 日志的第六步输出,这里包含了导致运行失败的详细错误信息。
|
||||
|
||||
### Docker 运行无法访问 Web 服务
|
||||
|
||||
1. 请检查**容器映射端口**至宿主机。
|
||||
2. 请检查是否配置 **rss 环境变量**。
|
||||
3. 请查看 Docker **运行日志**。
|
||||
如果在使用 Docker 时无法访问 Web 服务,请按照以下步骤进行故障排除:
|
||||
|
||||
1. **检查端口映射**:确保您已正确设置容器的端口映射到宿主机。
|
||||
2. **确认 rss 环境变量**:请检查是否已正确配置 `rss` 环境变量。
|
||||
3. **查看 Docker 日志**:如果以上步骤均无法解决问题,请查看 Docker 容器的运行日志以获取更多信息。
|
||||
|
||||
### 博客数据分析不准确
|
||||
|
||||
目前会根据个人时间进一步迭代,可以点个 Watch 订阅进度。
|
||||
目前提供的博客数据分析功能已经相对完善且准确。未来,我计划结合 AI 进一步优化分析效果,以提供更精准的数据维度。
|
||||
|
||||
### 主题不够丰富
|
||||
|
||||
由于个人时间有限,目前**我仅能承诺每年末前更新一款主题。**尽管如此,我仍然致力于为您的写作之旅带来愉悦和丰富的体验,并感谢您的理解和支持!
|
||||
|
||||
## 流程
|
||||
|
||||
@ -78,50 +82,115 @@ EndOfYear 通过 RSS 获取博客文章数据,对文章数据进行统计、
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Flask
|
||||
participant Config
|
||||
participant Generator
|
||||
participant Scraper
|
||||
participant Analyzer
|
||||
participant User
|
||||
participant Flask as Flask Server
|
||||
participant Config as Configuration
|
||||
participant Generator as Data Generator
|
||||
participant Scraper as Data Scraper
|
||||
participant Analyzer as Data Analyzer
|
||||
|
||||
User->>Flask: Access Service
|
||||
Flask->>User: Redirect to painting theme
|
||||
User->>Flask: Request painting theme
|
||||
Flask->>Config: Invoke Data Generator
|
||||
Config->>Generator: Run Data Scraper
|
||||
Generator->>Scraper: Fetch RSS data
|
||||
Scraper->>Analyzer: Analyze Data
|
||||
Analyzer->>Scraper: Return Analyzed Data
|
||||
Scraper->>Generator: Send Structured Data
|
||||
Generator->>Flask: Return Data to Flask
|
||||
Flask->>Flask: Render HTML Page with Data
|
||||
Flask->>User: Return Rendered HTML Page
|
||||
|
||||
User ->> Flask: Access service
|
||||
Flask ->> Config: Check cache
|
||||
activate Config
|
||||
alt Cache exists
|
||||
Config -->> Flask: Return cached data
|
||||
else Cache does not exist
|
||||
Config ->> Generator: Run data generator
|
||||
activate Generator
|
||||
Generator ->> Scraper: Run data scraping
|
||||
activate Scraper
|
||||
Scraper -->> Generator: Return scraped data
|
||||
deactivate Scraper
|
||||
Generator ->> Analyzer: Run data analysis
|
||||
activate Analyzer
|
||||
Analyzer -->> Generator: Return analyzed data
|
||||
deactivate Analyzer
|
||||
Generator -->> Config: Return organized data
|
||||
deactivate Generator
|
||||
Config -->> Flask: Return data
|
||||
end
|
||||
Flask -->> User: Return HTML page
|
||||
deactivate Config
|
||||
```
|
||||
|
||||
1. 用户访问 Flask 服务。
|
||||
2. Flask 检查缓存是否存在。
|
||||
- 如果缓存存在,Flask直接返回缓存数据。
|
||||
- 如果缓存不存在,继续下一步。
|
||||
2. Flask 根路由跳转 painting 主题。
|
||||
3. Config 模块运行数据生成器(Generator)。
|
||||
4. Generator 模块运行数据抓取器(Scraper)来获取RSS数据。
|
||||
5. Scraper 将抓取的数据返回给 Generator。
|
||||
6. Generator 运行数据分析器(Analyzer)对数据进行分析。
|
||||
7. Analyzer 将分析后的数据返回给 Generator。
|
||||
8. Generator 整理结构化数据后将其返回给 Flask,Config 模块。
|
||||
9. Flask 使用整理后的数据渲染 HTML 页面。
|
||||
10. Flask 返回渲染后的 HTML 页面给用户。
|
||||
5. Scraper 将抓取的数据结合(Analyzer)对数据进行分析。
|
||||
6. Analyzer 将分析后的数据返回给 Scraper。
|
||||
7. Generator 整理(Scraper)结构化数据后将其返回给 Flask。
|
||||
8. Flask 使用(Generator)的数据渲染 HTML 页面。
|
||||
9. Flask 返回渲染后的 HTML 页面给用户。
|
||||
|
||||
## 主题开发
|
||||
|
||||
EndOfYear 使用 Python 结合 Flask 利用 Jinja2 模板进行数据渲染,目前提供四个数据模型。
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
Site ||--o{ Generator : contains
|
||||
Blog ||--o{ Generator : contains
|
||||
Post ||--o{ Generator : contains
|
||||
Custom ||--o{ Generator : contains
|
||||
|
||||
Site {
|
||||
string service
|
||||
string title
|
||||
}
|
||||
|
||||
Blog {
|
||||
string name
|
||||
string link
|
||||
int life
|
||||
int article_count
|
||||
int article_word_count
|
||||
string top_post_keys
|
||||
string category
|
||||
}
|
||||
|
||||
Post {
|
||||
string title
|
||||
string content
|
||||
string[] keys
|
||||
string time
|
||||
string date
|
||||
|
||||
}
|
||||
|
||||
Custom {
|
||||
string yiyan
|
||||
}
|
||||
|
||||
Generator {
|
||||
Site site
|
||||
Blog blog
|
||||
Post special_post
|
||||
Post sentiment_post
|
||||
Post long_post
|
||||
Post short_post
|
||||
Custom custom
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果进行主题开发可以使用 Jinja2 的模板语言,结合下面的数据定制一款专属主题。
|
||||
|
||||
| 数据 | 描述 |
|
||||
|----------------|---------|
|
||||
| site | 站点数据 |
|
||||
| blog | 博客数据 |
|
||||
| special_post | 特殊日期文件 |
|
||||
| sentiment_post | 情感分最高文章 |
|
||||
| long_post | 篇幅最长文章 |
|
||||
| short_post | 篇幅最短文章 |
|
||||
| custom | 自定义数据 |
|
||||
|
||||
如果有额外数据需求,可以修改 `custom` 模型,并在 `main.py` 中传参,最后在 HTML 模板中使用。以下是一个简单的模板示例:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ site.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
亲爱的{{ blog.name }}
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 路线图
|
||||
|
||||
@ -129,15 +198,15 @@ EndOfYear 目前处于初始阶段,如果您有兴趣,可以为其做出贡
|
||||
|
||||
### V1
|
||||
|
||||
- [ ] 对博客系统的数据源进行全面、规模性的测试。
|
||||
- [ ] 进一步细化数据分析维度和数据颗粒度,精准描绘用户画像。
|
||||
- [ ] 渲染数据的规范,约束主题开发,提高主题的兼容性。
|
||||
- [ ] 剥离数据分析和主题,提供更好地适用方式。
|
||||
- [x] 结合互联网公开博客的数据源对 EndOfYear 进行全面、规模性的测试。
|
||||
- [x] 默认主题进一步细化数据分析维度和数据颗粒度,精准描绘用户画像。
|
||||
- [x] EndOfYear 渲染数据的规范,约束主题开发,提高主题的兼容性。
|
||||
- [x] 进一步丰富和完善主题。
|
||||
|
||||
### V2
|
||||
|
||||
- [ ] 进一步丰富和完善主题。
|
||||
- [ ] EndOfYear 项目展示首页,使用文档,主题开发等。
|
||||
- [ ] 剥离主题,提供更好地主题开发方式。
|
||||
- [ ] EndOfYear 项目网站首页,使用文档,主题开发等。
|
||||
- [ ] 实现轻量化的运行部署,一键运行。
|
||||
- [ ] 探索以插件的方式附加到博客系统的方法。
|
||||
|
||||
|
@ -3,4 +3,3 @@ web = true
|
||||
|
||||
[blog]
|
||||
rss =
|
||||
data =
|
3117
data/stop_words.txt
66
main.py
@ -1,8 +1,11 @@
|
||||
from flask import Flask, render_template, redirect, url_for
|
||||
from loguru import logger
|
||||
|
||||
from src import const
|
||||
from src import models
|
||||
from src import tools
|
||||
from src.config import Config
|
||||
from src.generator import build_data
|
||||
from src.generator import Generator
|
||||
|
||||
app = Flask(__name__)
|
||||
logger.add("endofyear.log")
|
||||
@ -10,28 +13,63 @@ logger.add("endofyear.log")
|
||||
|
||||
@app.route('/')
|
||||
def home():
|
||||
# 默认主题 painting
|
||||
# 重定向 painting
|
||||
return redirect(url_for('painting'))
|
||||
|
||||
|
||||
@app.route('/painting')
|
||||
def painting():
|
||||
if Config("config.ini").web_status:
|
||||
# web 服务
|
||||
# 如果数据存在,直接返回
|
||||
if blog_data := Config("config.ini").blog_data:
|
||||
return render_template('painting.html', data=blog_data, web_status=1)
|
||||
# 读取配置文件
|
||||
config = Config("config.ini")
|
||||
|
||||
# 站点数据
|
||||
site = models.Site(
|
||||
service=config.web_status,
|
||||
title=const.SITE_NAME
|
||||
).to_dict()
|
||||
|
||||
# 自定义数据
|
||||
custom = models.Custom(
|
||||
yiyan=tools.get_yiyan()
|
||||
).to_dict()
|
||||
|
||||
# 初始化数据生成器
|
||||
generator = Generator(config.rss_url)
|
||||
logger.info(f"Site: {site}")
|
||||
logger.info(f"Blog: {generator.blog()}")
|
||||
logger.info(f"Special Post: {generator.special_post()}")
|
||||
logger.info(f"Sentiment Post: {generator.sentiment_post()}")
|
||||
logger.info(f"Long Post: {generator.long_post()}")
|
||||
logger.info(f"Short Post: {generator.short_post()}")
|
||||
|
||||
# 服务模式
|
||||
if config.web_status == const.SITE_SERVICE_STATIC:
|
||||
# 静态网站模式
|
||||
html_static_file = render_template('painting.html',
|
||||
site=site,
|
||||
blog=generator.blog(),
|
||||
special_post=generator.special_post(),
|
||||
sentiment_post=generator.sentiment_post(),
|
||||
long_post=generator.long_post(),
|
||||
short_post=generator.short_post(),
|
||||
custom=custom
|
||||
)
|
||||
|
||||
# 如果数据不存在,需要生成,并写入配置
|
||||
return render_template('painting.html', data=build_data(), web_status=1)
|
||||
else:
|
||||
# Github 静态
|
||||
# 数据需要生成,并写入静态文件
|
||||
html_data = render_template('painting.html', data=build_data(), web_status=0)
|
||||
with open("static/index.html", "w") as f:
|
||||
f.write(html_data)
|
||||
f.write(html_static_file)
|
||||
|
||||
return 'OK'
|
||||
else:
|
||||
# web 模式
|
||||
return render_template('painting.html',
|
||||
site=site,
|
||||
blog=generator.blog(),
|
||||
special_post=generator.special_post(),
|
||||
sentiment_post=generator.sentiment_post(),
|
||||
long_post=generator.long_post(),
|
||||
short_post=generator.short_post(),
|
||||
custom=custom
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -1,8 +1,9 @@
|
||||
beautifulsoup4==4.12.2
|
||||
blinker==1.6.3
|
||||
blinker==1.7.0
|
||||
certifi==2023.7.22
|
||||
charset-normalizer==3.3.1
|
||||
charset-normalizer==3.3.2
|
||||
click==8.1.7
|
||||
docopt==0.6.2
|
||||
feedparser==6.0.10
|
||||
Flask==3.0.0
|
||||
idna==3.4
|
||||
|
133
src/analyzer.py
@ -1,5 +1,3 @@
|
||||
from typing import Any
|
||||
|
||||
import jieba.analyse
|
||||
import pytz
|
||||
from dateutil.parser import parse
|
||||
@ -7,31 +5,52 @@ from loguru import logger
|
||||
from lunardate import LunarDate
|
||||
from snownlp import SnowNLP
|
||||
|
||||
from . import const
|
||||
|
||||
|
||||
# 计算文本内容情感分数
|
||||
def analyze_sentiment(text):
|
||||
def analyze_sentiment(keys):
|
||||
"""
|
||||
博客文章情感分计算(有点问题,酌情使用)
|
||||
:param text:文章文本
|
||||
博客文章情感分计算
|
||||
|
||||
:param keys:文章关键字
|
||||
:return:分数
|
||||
"""
|
||||
s = SnowNLP(text)
|
||||
return round(s.sentiments * 100)
|
||||
score_lists = [SnowNLP(key).sentiments for key in keys]
|
||||
all_score = sum(score_lists)
|
||||
|
||||
if len(score_lists) > 10:
|
||||
max_score = max(score_lists)
|
||||
min_score = min(score_lists)
|
||||
average_score = (all_score - max_score - min_score) / (len(keys) - 2)
|
||||
return int(average_score * 1000)
|
||||
elif 10 > len(score_lists) > 6:
|
||||
average_score = all_score / len(keys)
|
||||
return int(average_score * 900)
|
||||
elif 6 > len(score_lists) > 3:
|
||||
average_score = all_score / len(keys)
|
||||
return int(average_score * 800)
|
||||
elif 3 > len(score_lists) > 0:
|
||||
average_score = all_score / len(keys)
|
||||
return int(average_score * 500)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
def classify_and_extract_keywords(text: str, topK: int, stopwords: str,
|
||||
tech_terms_file: str) -> tuple[None, list[Any]] | tuple[int, Any]:
|
||||
def extract_keywords(text,
|
||||
topK,
|
||||
stopwords):
|
||||
"""
|
||||
博客文章关键字提取
|
||||
文章关键字提取
|
||||
:param text:文章文本
|
||||
:param topK:关键字数量,建议20个
|
||||
:param stopwords:停词文本,去掉无意义词组
|
||||
:param tech_terms_file:专业词语,区分文章类目
|
||||
:param topK:关键字数量
|
||||
:param stopwords:停词文本(去掉无意义词组)
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
jieba.analyse.set_stop_words(stopwords)
|
||||
keywords = jieba.analyse.extract_tags(text, topK=topK)
|
||||
return keywords
|
||||
except ValueError as e:
|
||||
logger.error(f"关键词提取出错:{e}")
|
||||
return None, []
|
||||
@ -39,72 +58,52 @@ def classify_and_extract_keywords(text: str, topK: int, stopwords: str,
|
||||
logger.error(f"关键词提取出错:{e}")
|
||||
return None, []
|
||||
|
||||
|
||||
def check_category(tech_terms_file, keywords):
|
||||
"""
|
||||
文章分类判断
|
||||
:param keywords: 文章关键词
|
||||
:param tech_terms_file: 分类词典文件
|
||||
:return: 分类常量
|
||||
"""
|
||||
with open(tech_terms_file, 'r', encoding='utf-8') as f:
|
||||
tech_terms_set = {line.strip().lower() for line in f}
|
||||
tech_terms_set = {line.strip().lower() for line in f} # 读取分类词典文件,将其转化为小写并创建集合
|
||||
|
||||
for keyword in keywords:
|
||||
if keyword.lower() in tech_terms_set:
|
||||
return 1, keywords
|
||||
if keyword.lower() in tech_terms_set: # 判断关键词是否在分类词典集合中
|
||||
return const.BLOG_POST_CATEGORY_TECH # 若关键词存在,则返回技术类分类常量
|
||||
|
||||
return 2, keywords
|
||||
return const.BLOG_POST_CATEGORY_LIFE # 若关键词不存在,则返回生活类分类常量
|
||||
|
||||
|
||||
def calculate_weight(time_str: str):
|
||||
def calculate_weight(time_str: str) -> int:
|
||||
"""
|
||||
博客文章特殊日期权重分数计算。
|
||||
计算文章特殊日期的权重分数。
|
||||
- 传统节假日 +10
|
||||
- 节假日 +7
|
||||
- 凌晨 +5
|
||||
- 早上 +4
|
||||
- 下午 +3
|
||||
- 晚上 +2
|
||||
|
||||
:param time_str: 时间字符串
|
||||
:return:总分数,特殊日期
|
||||
:return: 总分数(整数)
|
||||
"""
|
||||
dt = parse(time_str)
|
||||
dt = dt.astimezone(pytz.timezone('Asia/Shanghai'))
|
||||
dt = dt.astimezone(pytz.timezone(const.TIME_ZONE))
|
||||
|
||||
weight = 0
|
||||
date_str = ""
|
||||
|
||||
# 农历节日权重计算
|
||||
LUNAR_HOLIDAYS = {
|
||||
(1, 1): '春节',
|
||||
(1, 15): '元宵节',
|
||||
(2, 2): '龙抬头',
|
||||
(5, 5): '端午节',
|
||||
(7, 7): '七夕节',
|
||||
(7, 15): '中元节',
|
||||
(8, 15): '中秋节',
|
||||
(9, 9): '重阳节',
|
||||
(12, 8): '腊八节',
|
||||
(12, 23): '小年',
|
||||
(12, 30): '除夕'
|
||||
}
|
||||
|
||||
# 计算农历节假日的权重
|
||||
lunar_date = LunarDate.fromSolarDate(dt.year, dt.month, dt.day)
|
||||
if (lunar_date.month, lunar_date.day) in LUNAR_HOLIDAYS:
|
||||
if (lunar_date.month, lunar_date.day) in const.LUNAR_HOLIDAYS:
|
||||
weight += 10
|
||||
date_str = LUNAR_HOLIDAYS[(lunar_date.month, lunar_date.day)]
|
||||
|
||||
# 公历节日权重计算
|
||||
SOLAR_HOLIDAYS = {
|
||||
(1, 1): '元旦',
|
||||
(2, 14): '情人节',
|
||||
(3, 8): '国际妇女节',
|
||||
(4, 4): '清明节',
|
||||
(5, 1): '国际劳动节',
|
||||
(10, 1): '国庆节',
|
||||
(12, 13): '南京大屠杀纪念日',
|
||||
(9, 18): '九一八事变纪念日',
|
||||
(12, 7): '南京保卫战胜利纪念日',
|
||||
(8, 15): '抗日战争胜利纪念日'
|
||||
}
|
||||
|
||||
if (dt.month, dt.day) in SOLAR_HOLIDAYS:
|
||||
# 计算公历节假日的权重
|
||||
if (dt.month, dt.day) in const.SOLAR_HOLIDAYS:
|
||||
weight += 7
|
||||
date_str = SOLAR_HOLIDAYS[(dt.month, dt.day)]
|
||||
|
||||
# 计算时间节点的权重
|
||||
if 22 <= dt.hour or dt.hour < 7:
|
||||
weight += 5
|
||||
elif 7 <= dt.hour < 12:
|
||||
@ -116,7 +115,25 @@ def calculate_weight(time_str: str):
|
||||
else:
|
||||
weight += 0
|
||||
|
||||
if not date_str:
|
||||
date_str = f"{dt.month}月{dt.day}日"
|
||||
return weight
|
||||
|
||||
return weight, date_str
|
||||
|
||||
def special_date_calculation(time_str):
|
||||
"""
|
||||
特殊日期计算。
|
||||
:param time_str: 时间字符串
|
||||
:return:总分数
|
||||
"""
|
||||
dt = parse(time_str)
|
||||
dt = dt.astimezone(pytz.timezone(const.TIME_ZONE))
|
||||
|
||||
# 农历节假日计算
|
||||
lunar_date = LunarDate.fromSolarDate(dt.year, dt.month, dt.day)
|
||||
if (lunar_date.month, lunar_date.day) in const.LUNAR_HOLIDAYS:
|
||||
return const.LUNAR_HOLIDAYS[(lunar_date.month, lunar_date.day)]
|
||||
|
||||
# 公历节假日计算
|
||||
if (dt.month, dt.day) in const.SOLAR_HOLIDAYS:
|
||||
return const.SOLAR_HOLIDAYS[(dt.month, dt.day)]
|
||||
|
||||
return f"{dt.month}月{dt.day}日"
|
||||
|
@ -1,11 +1,11 @@
|
||||
import configparser
|
||||
import json
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from src.tools import check_website_status
|
||||
from . import const
|
||||
from .tools import check_website_status
|
||||
|
||||
|
||||
class Config:
|
||||
@ -31,8 +31,6 @@ class Config:
|
||||
logger.error(f"没有权限读取配置文件 {self.path}: {str(e)}")
|
||||
raise
|
||||
|
||||
logger.info(f"配置文件 {self.path} 加载成功")
|
||||
|
||||
@property
|
||||
def rss_url(self):
|
||||
try:
|
||||
@ -71,30 +69,6 @@ class Config:
|
||||
|
||||
return '.'.join(domain_parts[-2:])
|
||||
|
||||
@property
|
||||
def blog_data(self):
|
||||
try:
|
||||
data = self.config.get('blog', 'data', fallback=None)
|
||||
except configparser.NoSectionError:
|
||||
logger.error('未找到 section 配置项,请检查拼写')
|
||||
return None
|
||||
|
||||
if not data:
|
||||
logger.error('data 配置值为空')
|
||||
return None
|
||||
|
||||
return json.loads(data)
|
||||
|
||||
@blog_data.setter
|
||||
def blog_data(self, value):
|
||||
if not self.config.has_section('blog'):
|
||||
self.config.add_section('blog')
|
||||
|
||||
self.config.set('blog', 'data', json.dumps(value))
|
||||
|
||||
with open(self.path, 'w') as configfile:
|
||||
self.config.write(configfile)
|
||||
|
||||
@property
|
||||
def web_status(self):
|
||||
try:
|
||||
@ -105,10 +79,10 @@ class Config:
|
||||
|
||||
if web_status is None:
|
||||
logger.error('web 配置值为空')
|
||||
return True
|
||||
return const.SITE_SERVICE_WEB
|
||||
|
||||
if web_status == "True" or web_status == "true":
|
||||
return True
|
||||
if web_status == "True" or web_status == "true" or web_status == "t" or web_status == "T":
|
||||
return const.SITE_SERVICE_WEB
|
||||
|
||||
if web_status == "False" or web_status == "false":
|
||||
return False
|
||||
if web_status == "False" or web_status == "false" or web_status == "f" or web_status == "F":
|
||||
return const.SITE_SERVICE_STATIC
|
||||
|
50
src/const.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 站点标题
|
||||
SITE_NAME = "EndOfYear"
|
||||
|
||||
# 站点服务模式
|
||||
SITE_SERVICE_WEB = 1
|
||||
SITE_SERVICE_STATIC = 0
|
||||
|
||||
# 时区
|
||||
TIME_ZONE = "Asia/Shanghai"
|
||||
|
||||
# 时间格式
|
||||
FORMAT_TIME = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# 博客文章分类-生活
|
||||
BLOG_POST_CATEGORY_LIFE = 1
|
||||
|
||||
# 博客文章分类-技术
|
||||
BLOG_POST_CATEGORY_TECH = 2
|
||||
|
||||
# 博客文章关键字数量
|
||||
BLOG_MAX_KEYS = 7
|
||||
|
||||
# 农历节假日
|
||||
LUNAR_HOLIDAYS = {
|
||||
(1, 1): '春节',
|
||||
(1, 15): '元宵节',
|
||||
(2, 2): '龙抬头',
|
||||
(5, 5): '端午节',
|
||||
(7, 7): '七夕节',
|
||||
(7, 15): '中元节',
|
||||
(8, 15): '中秋节',
|
||||
(9, 9): '重阳节',
|
||||
(12, 8): '腊八节',
|
||||
(12, 23): '小年',
|
||||
(12, 30): '除夕'
|
||||
}
|
||||
|
||||
# 公历节假日
|
||||
SOLAR_HOLIDAYS = {
|
||||
(1, 1): '元旦',
|
||||
(2, 14): '情人节',
|
||||
(3, 8): '妇女节',
|
||||
(4, 4): '清明节',
|
||||
(5, 1): '劳动节',
|
||||
(10, 1): '国庆节',
|
||||
(12, 13): '南京大屠杀纪念日',
|
||||
(9, 18): '九一八事变纪念日',
|
||||
(12, 7): '南京保卫战胜利纪念日',
|
||||
(8, 15): '抗日战争胜利纪念日'
|
||||
}
|
180
src/generator.py
@ -1,95 +1,119 @@
|
||||
from collections import Counter
|
||||
from functools import lru_cache
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from .analyzer import analyze_sentiment, calculate_weight, classify_and_extract_keywords
|
||||
from .config import Config
|
||||
from .scraper import Blog
|
||||
from . import models
|
||||
from . import scraper
|
||||
|
||||
|
||||
def build_data():
|
||||
@lru_cache(maxsize=None)
|
||||
class Generator:
|
||||
|
||||
def __init__(self, rss):
|
||||
"""
|
||||
目前只有一个主题,构建数据部分后期会再进行重构拆分
|
||||
:return: 网页渲染数据
|
||||
初始化Generator类
|
||||
:param rss: RSS链接
|
||||
"""
|
||||
# 读取配置
|
||||
config = Config("config.ini")
|
||||
|
||||
# 创建博客对象
|
||||
try:
|
||||
my_blog = Blog(config.rss_url)
|
||||
except Exception as e:
|
||||
logger.error(f"Feed 无法创建博客对象: {str(e)}")
|
||||
return None
|
||||
|
||||
logger.debug(my_blog)
|
||||
|
||||
# 构建博客基本数据
|
||||
data = {
|
||||
"blog_name": my_blog.title,
|
||||
"blog_link": my_blog.link,
|
||||
"blog_article_count": my_blog.article_count,
|
||||
"blog_article_word_count": my_blog.article_word_count,
|
||||
}
|
||||
|
||||
if my_blog.life is None:
|
||||
data.update({
|
||||
"blog_life": 0
|
||||
})
|
||||
else:
|
||||
data.update({
|
||||
"blog_life_year": my_blog.life // 365,
|
||||
"blog_life_day": my_blog.life % 365,
|
||||
})
|
||||
|
||||
# 博客文章处理
|
||||
for i, post in enumerate(my_blog.post_lists(), 1):
|
||||
# 情感分
|
||||
post.score = analyze_sentiment(post.content)
|
||||
# 分类, 关键字
|
||||
post.category, post.keys = classify_and_extract_keywords(text=post.content, topK=21,
|
||||
stopwords='data/stop_words.txt',
|
||||
tech_terms_file='data/tech_terms.txt')
|
||||
# 权重, 日子计算
|
||||
post.weight, post.date = calculate_weight(post.time)
|
||||
|
||||
self._my_blog = scraper.Blog(rss)
|
||||
logger.debug(self._my_blog)
|
||||
for i, post in enumerate(self._my_blog.post_lists, 1):
|
||||
logger.info(f"Post #{i}:")
|
||||
logger.info(post)
|
||||
except Exception as e:
|
||||
logger.error(f"Generator 无法创建 Blog 对象: {str(e)}")
|
||||
|
||||
# 博客文章权重计算
|
||||
weights = [post.weight for post in my_blog.post_lists()]
|
||||
max_weight = max(weights)
|
||||
max_item = [post for post in my_blog.post_lists() if post.weight == max_weight][0]
|
||||
def blog(self):
|
||||
"""
|
||||
获取博客信息
|
||||
:return: Blog字典
|
||||
"""
|
||||
return models.Blog(
|
||||
name=self._my_blog.title,
|
||||
link=self._my_blog.link,
|
||||
life=self._my_blog.life,
|
||||
article_count=self._my_blog.article_count,
|
||||
article_word_count=self._my_blog.article_word_count,
|
||||
top_post_keys=self._my_blog.keys,
|
||||
category=self._my_blog.category
|
||||
).to_dict()
|
||||
|
||||
data.update({
|
||||
"blog_title": max_item.title,
|
||||
"blog_content": max_item.content[0:50],
|
||||
"blog_content_date": max_item.date,
|
||||
})
|
||||
def special_post(self):
|
||||
"""
|
||||
获取特殊日期的文章
|
||||
:return: Post字典
|
||||
"""
|
||||
max_item_special_date = self._get_post_with_max("special_date_score")
|
||||
return models.Post(
|
||||
title=max_item_special_date.title,
|
||||
content=max_item_special_date.content,
|
||||
keys=max_item_special_date.keys,
|
||||
time=max_item_special_date.time,
|
||||
date=max_item_special_date.date
|
||||
).to_dict()
|
||||
|
||||
# 暂时只有一个主题
|
||||
# 博客关键词计算 5 个
|
||||
all_keys = []
|
||||
for post in my_blog.post_lists():
|
||||
all_keys.extend(post.keys)
|
||||
def sentiment_post(self):
|
||||
"""
|
||||
获取情感最优文章
|
||||
:return: Post字典
|
||||
"""
|
||||
max_item_sentiment = self._get_post_with_max("sentiment_score")
|
||||
return models.Post(
|
||||
title=max_item_sentiment.title,
|
||||
content=max_item_sentiment.content,
|
||||
keys=max_item_sentiment.keys,
|
||||
time=max_item_sentiment.time,
|
||||
date=max_item_sentiment.date
|
||||
).to_dict()
|
||||
|
||||
keyword_counts = Counter(all_keys)
|
||||
top_keywords = keyword_counts.most_common(5)
|
||||
data.update({
|
||||
"blog_top_keywords": top_keywords
|
||||
})
|
||||
def long_post(self):
|
||||
"""
|
||||
获取最长文章数据
|
||||
:return: Post字典
|
||||
"""
|
||||
max_item_long = self._get_post_with_max("word_count")
|
||||
return models.Post(
|
||||
title=max_item_long.title,
|
||||
content=max_item_long.content,
|
||||
keys=max_item_long.keys,
|
||||
time=max_item_long.time,
|
||||
date=max_item_long.date,
|
||||
).to_dict()
|
||||
|
||||
# 博客分类计算
|
||||
categories = [post.category for post in my_blog.post_lists()]
|
||||
cat_counts = Counter(categories)
|
||||
most_common_cat = cat_counts.most_common(1)[0][0]
|
||||
def short_post(self):
|
||||
"""
|
||||
获取最短文章数据
|
||||
:return: Post字典
|
||||
"""
|
||||
max_item_short = self._get_post_with_min("word_count")
|
||||
return models.Post(
|
||||
title=max_item_short.title,
|
||||
content=max_item_short.content,
|
||||
keys=max_item_short.keys,
|
||||
time=max_item_short.time,
|
||||
date=max_item_short.date,
|
||||
).to_dict()
|
||||
|
||||
data.update({
|
||||
"blog_category": "技术" if most_common_cat == 1 else "生活"
|
||||
})
|
||||
def _get_post_with_max(self, score_attr):
|
||||
"""
|
||||
获取具有最大属性值的文章
|
||||
:param score_attr: 属性
|
||||
:return:
|
||||
"""
|
||||
max_score = max(getattr(post, score_attr) for post in self._my_blog.post_lists)
|
||||
max_posts = [post for post in self._my_blog.post_lists if getattr(post, score_attr) == max_score]
|
||||
if max_posts:
|
||||
return max_posts[0]
|
||||
return None
|
||||
|
||||
# 输出
|
||||
logger.debug(data)
|
||||
# 写入 config.ini 避免重复计算
|
||||
config.blog_data = data
|
||||
return data
|
||||
def _get_post_with_min(self, score_attr):
|
||||
"""
|
||||
获取具有最小属性值的文章
|
||||
:param score_attr:
|
||||
:return:
|
||||
"""
|
||||
min_score = min(getattr(post, score_attr) for post in self._my_blog.post_lists)
|
||||
min_posts = [post for post in self._my_blog.post_lists if getattr(post, score_attr) == min_score]
|
||||
if min_posts:
|
||||
return min_posts[0]
|
||||
return None
|
||||
|
80
src/models.py
Normal file
@ -0,0 +1,80 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
@dataclass
|
||||
class Site:
|
||||
"""
|
||||
站点数据模型
|
||||
- service: 服务模式
|
||||
- title: 站点标题
|
||||
"""
|
||||
service: int
|
||||
title: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
将Site对象转换为字典形式
|
||||
"""
|
||||
return {k: v if not isinstance(v, Enum) else v.value for k, v in vars(self).items()}
|
||||
|
||||
@dataclass
|
||||
class Blog:
|
||||
"""
|
||||
博客数据模型
|
||||
- name:名称
|
||||
- link:链接
|
||||
- life:域名注册天数
|
||||
- article_count:博客文章总和
|
||||
- article_word_count:博客文章字数总和
|
||||
- top_post_keys:博客关键字
|
||||
- category:博客分类
|
||||
"""
|
||||
name: str
|
||||
link: str
|
||||
life: int
|
||||
article_count: int
|
||||
article_word_count: int
|
||||
top_post_keys: List[str]
|
||||
category: int
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
将Blog对象转换为字典形式
|
||||
"""
|
||||
return {k: v if not isinstance(v, Enum) else v.value for k, v in vars(self).items()}
|
||||
|
||||
@dataclass
|
||||
class Post:
|
||||
"""
|
||||
文章数据模型
|
||||
- title:标题
|
||||
- content:内容
|
||||
- keys:关键字列表
|
||||
- date:日期字符串
|
||||
"""
|
||||
title: str
|
||||
content: str
|
||||
keys: List[str]
|
||||
time: str
|
||||
date: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
将Post对象转换为字典形式
|
||||
"""
|
||||
return {k: v if not isinstance(v, Enum) else v.value for k, v in vars(self).items()}
|
||||
|
||||
@dataclass
|
||||
class Custom:
|
||||
"""
|
||||
自定义数据模型
|
||||
- yiyan:一言
|
||||
"""
|
||||
yiyan: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""
|
||||
将Custom对象转换为字典形式
|
||||
"""
|
||||
return vars(self)
|
217
src/scraper.py
@ -1,85 +1,139 @@
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
import feedparser
|
||||
from loguru import logger
|
||||
|
||||
from . import analyzer
|
||||
from . import const
|
||||
from . import tools
|
||||
|
||||
|
||||
class Blog:
|
||||
def __init__(self, url):
|
||||
def __init__(self, rss):
|
||||
try:
|
||||
self.feed = feedparser.parse(url)
|
||||
# 解析RSS feed
|
||||
self._feed = feedparser.parse(rss)
|
||||
# 解析feed中的所有文章
|
||||
self._posts = [Post(entry) for entry in self._feed.entries]
|
||||
except Exception as e:
|
||||
logger.error(f'解析 RSS feed 时发生错误: {str(e)}')
|
||||
logger.error(f'Feedparser 解析 RSS feed 时发生错误: {str(e)}')
|
||||
raise
|
||||
self.posts = [Post(entry) for entry in self.feed.entries]
|
||||
|
||||
def _get_feed_field(self, field):
|
||||
"""
|
||||
从 RSS feed 中获取特定字段
|
||||
"""
|
||||
field_value = self.feed.feed.get(field)
|
||||
if field_value is None:
|
||||
logger.warning(f'{field} 字段不存在!')
|
||||
if field_value := self._feed.feed.get(field):
|
||||
return field_value
|
||||
logger.warning(f'Feedparser {field} 字段不存在!')
|
||||
return ""
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._get_feed_field('title')
|
||||
# 获取RSS feed的标题
|
||||
return self._feed.feed.get('title')
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return self._get_feed_field('link')
|
||||
# 获取RSS feed的链接
|
||||
return self._feed.feed.get('link')
|
||||
|
||||
@property
|
||||
def life(self):
|
||||
domain = tools.get_domain(self.link)
|
||||
return tools.get_domain_life(domain)
|
||||
# 获取RSS feed链接的域名存活时间
|
||||
return tools.get_domain_life(self.link)
|
||||
|
||||
@property
|
||||
def article_count(self):
|
||||
return len(self.posts)
|
||||
# 获取文章数量
|
||||
return len(self._posts) if self._posts else 0
|
||||
|
||||
@property
|
||||
def article_word_count(self):
|
||||
return sum(post.word_count for post in self.posts)
|
||||
# 获取文章总字数
|
||||
return sum(post.word_count for post in self._posts) if self._posts else 0
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
if self._posts:
|
||||
# 提取所有关键字
|
||||
all_keys = [key for post in self._posts for key in post.keys]
|
||||
|
||||
# 过滤出中文关键字
|
||||
chinese_keys = [key for key in all_keys if re.search(r'[\u4e00-\u9fff]+', key)]
|
||||
|
||||
# 计算关键字出现的次数
|
||||
keyword_counts = Counter(chinese_keys)
|
||||
|
||||
# 提取出现次数最多的关键字
|
||||
top_keywords = keyword_counts.most_common(const.BLOG_MAX_KEYS)
|
||||
|
||||
return top_keywords
|
||||
|
||||
return []
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
# 获取博客的分类
|
||||
if self._posts:
|
||||
# 如果博客有帖子
|
||||
categories = [post.category for post in self._posts]
|
||||
# 获取所有帖子的分类
|
||||
cat_counts = Counter(categories)
|
||||
# 统计每个分类的个数
|
||||
most_common_cat = cat_counts.most_common(1)[0][0]
|
||||
# 获取出现次数最多的分类
|
||||
return most_common_cat
|
||||
# 如果博客没有帖子
|
||||
return const.BLOG_POST_CATEGORY_LIFE
|
||||
|
||||
@property
|
||||
def post_lists(self):
|
||||
return self.posts
|
||||
# 获取文章列表
|
||||
return self._posts if self._posts else []
|
||||
|
||||
def __str__(self):
|
||||
return f"Blog: {self.title}, Life:{self.life}, Count{self.article_count}. Word count:{self.article_word_count}"
|
||||
return f"""
|
||||
博客: {self.title}
|
||||
链接: {self.link}
|
||||
时间: {self.life} 天
|
||||
文章: {self.article_count} 篇
|
||||
字数: {self.article_word_count} 个
|
||||
分类: {self.category}
|
||||
关键字: {self.keys}
|
||||
"""
|
||||
|
||||
|
||||
class Post:
|
||||
def __init__(self, entry):
|
||||
# 日期权重
|
||||
self._weight = None
|
||||
# 日子
|
||||
self._date = None
|
||||
# 情感分
|
||||
self._score = None
|
||||
# 关键字
|
||||
self._keys = None
|
||||
# 分类
|
||||
self._category = None
|
||||
self.entry = entry
|
||||
# 文章内容
|
||||
self._content = self._get_content()
|
||||
# 文章时间
|
||||
self._time = tools.format_datetime(self._get_entry_field('published'))
|
||||
# 文章日期
|
||||
self._date = analyzer.special_date_calculation(self._time)
|
||||
# 特殊日期分
|
||||
self._special_date_score = analyzer.calculate_weight(self._get_entry_field('published'))
|
||||
# 关键字
|
||||
self._keys = analyzer.extract_keywords(text=self._content,
|
||||
topK=tools.get_multiple_of_100(self._content),
|
||||
stopwords='data/stop_words.txt')
|
||||
# 文章情感分
|
||||
self._sentiment_score = analyzer.analyze_sentiment(self._keys)
|
||||
# 分类
|
||||
self._category = analyzer.check_category(tech_terms_file='data/tech_terms.txt', keywords=self._keys)
|
||||
|
||||
def _get_entry_field(self, field):
|
||||
"""
|
||||
从 RSS entry 中获取特定字段
|
||||
"""
|
||||
field_value = self.entry.get(field)
|
||||
if field_value is None:
|
||||
pass
|
||||
# logger.warning(f'{field} 字段不存在!')
|
||||
return field_value
|
||||
return self.entry.get(field)
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self._get_entry_field('title')
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
def _get_content(self):
|
||||
"""
|
||||
获取文章内容。
|
||||
:return: 文章的描述或内容,根据以下规则:
|
||||
- 如果'content'字段存在,那么返回'content'字段的值。
|
||||
- 如果'description'字段的长度小于128,并且'content'字段存在,那么返回'content'字段的值。
|
||||
- 否则,返回'description'字段的值。
|
||||
- 如果'description'和'content'字段都不存在,返回空字符串。
|
||||
"""
|
||||
description = self._get_entry_field('description')
|
||||
content = self._get_entry_field('content')
|
||||
if content:
|
||||
@ -94,60 +148,61 @@ class Post:
|
||||
return description
|
||||
|
||||
@property
|
||||
def time(self):
|
||||
return self._get_entry_field('published')
|
||||
def title(self):
|
||||
# 获取文章标题
|
||||
return self._get_entry_field('title')
|
||||
|
||||
@property
|
||||
def link(self):
|
||||
return self._get_entry_field('link')
|
||||
def content(self):
|
||||
# 获取文章内容
|
||||
return self._content
|
||||
|
||||
@property
|
||||
def word_count(self):
|
||||
# 获取文章字数
|
||||
return len(self.content) if self.content else 0
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
return self._keys
|
||||
|
||||
@keys.setter
|
||||
def keys(self, value):
|
||||
self._keys = value
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
return self._score
|
||||
|
||||
@score.setter
|
||||
def score(self, value):
|
||||
self._score = value
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return self._category
|
||||
|
||||
@category.setter
|
||||
def category(self, value):
|
||||
self._category = value
|
||||
def time(self):
|
||||
# 获取文章时间
|
||||
return self._time
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
# 获取日期分
|
||||
return self._date
|
||||
|
||||
@date.setter
|
||||
def date(self, value):
|
||||
self._date = value
|
||||
@property
|
||||
def link(self):
|
||||
# 获取文章链接
|
||||
return self._get_entry_field('link')
|
||||
|
||||
@property
|
||||
def weight(self):
|
||||
return self._weight
|
||||
def keys(self):
|
||||
# 获取文章关键字
|
||||
return self._keys
|
||||
|
||||
@weight.setter
|
||||
def weight(self, value):
|
||||
self._weight = value
|
||||
@property
|
||||
def category(self):
|
||||
# 获取文章分类
|
||||
return self._category
|
||||
|
||||
@property
|
||||
def special_date_score(self):
|
||||
# 获取特殊日期分
|
||||
return self._special_date_score
|
||||
|
||||
@property
|
||||
def sentiment_score(self):
|
||||
# 获取文章情感分
|
||||
return self._sentiment_score
|
||||
|
||||
def __str__(self):
|
||||
return (f"Post title={self.title[:20]}..., "
|
||||
f" content={self.content[:20]}..., "
|
||||
f" time={self.time}, "
|
||||
f" link={self.link}, "
|
||||
f" word_count={self.word_count}")
|
||||
return (f" 标题:{self.title}, "
|
||||
f" 内容:{self.content[:20]}..., "
|
||||
f" 时间:{self.time}, "
|
||||
f" 链接:{self.link}, "
|
||||
f" 日期分:{self.special_date_score}"
|
||||
f" 情感分:{self.sentiment_score}"
|
||||
f" 类目:{self.category}"
|
||||
f" 关键字:{self.keys}")
|
||||
|
65
src/tools.py
@ -1,10 +1,14 @@
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytz
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from dateutil.parser import parse
|
||||
from loguru import logger
|
||||
|
||||
from . import const
|
||||
|
||||
|
||||
def check_website_status(url):
|
||||
"""
|
||||
@ -13,14 +17,14 @@ def check_website_status(url):
|
||||
:return:True 可以访问,False 不可以。
|
||||
"""
|
||||
try:
|
||||
response = requests.get(url, timeout=5) # Set timeout to 5 seconds
|
||||
response = requests.get(url, timeout=30) # Set timeout to 5 seconds
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"{url} 网站无法访问,状态码:{response.status_code}")
|
||||
return False
|
||||
except requests.Timeout as e:
|
||||
logger.error(f"{url} 请求超时,错误:{e}")
|
||||
logger.error(f"{url} 请求超时 30 秒,错误:{e}")
|
||||
return False
|
||||
except requests.ConnectionError as e:
|
||||
logger.error(f"{url} 连接错误,错误:{e}")
|
||||
@ -54,10 +58,10 @@ def get_domain_life(url):
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
|
||||
}
|
||||
domain_url = f"https://rdap.verisign.com/com/v1/domain/{url}"
|
||||
domain = get_domain(url)
|
||||
|
||||
try:
|
||||
response = requests.get(domain_url, headers=headers)
|
||||
response = requests.get(f"https://rdap.verisign.com/com/v1/domain/{domain}", headers=headers, timeout=30)
|
||||
response.raise_for_status() # Raises stored HTTPError, if one occurred.
|
||||
|
||||
registration_date = response.json().get('events')[0].get('eventDate')
|
||||
@ -87,7 +91,7 @@ def get_domain_life(url):
|
||||
except Exception as err:
|
||||
logger.error(f"未预期的错误: {err}")
|
||||
|
||||
return None
|
||||
return 0
|
||||
|
||||
|
||||
def remove_html_tags(text):
|
||||
@ -97,3 +101,54 @@ def remove_html_tags(text):
|
||||
:return:文本
|
||||
"""
|
||||
return BeautifulSoup(text, "html.parser").get_text()
|
||||
|
||||
|
||||
def get_yiyan():
|
||||
"""
|
||||
获取一言文学语句
|
||||
:return:一言
|
||||
"""
|
||||
try:
|
||||
response = requests.get("https://v1.hitokoto.cn/?c=d&min_length=16&max_length=20&encode=text",
|
||||
timeout=30) # Set timeout to 5 seconds
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
else:
|
||||
logger.error(f"一言网站无法访问,状态码:{response.status_code}")
|
||||
return False
|
||||
except requests.Timeout as e:
|
||||
logger.error(f"一言请求超时 30 秒,错误:{e}")
|
||||
return False
|
||||
except requests.ConnectionError as e:
|
||||
logger.error(f"一言连接错误,错误:{e}")
|
||||
return False
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"一言网站无法访问,错误:{e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"一言未知错误,错误:{e}")
|
||||
return False
|
||||
|
||||
|
||||
def get_multiple_of_100(string):
|
||||
"""
|
||||
获取文章长度 100 的整除
|
||||
:return:建议关键字数量
|
||||
"""
|
||||
length = len(string)
|
||||
multiple = length // 100
|
||||
if multiple < 1:
|
||||
multiple = 1
|
||||
return multiple
|
||||
|
||||
|
||||
def format_datetime(dt_str):
|
||||
"""
|
||||
格式化时间字符串为指定格式
|
||||
:param dt_str:时间字符串
|
||||
:return:指定格式
|
||||
"""
|
||||
dt = parse(dt_str)
|
||||
tz = pytz.timezone(const.TIME_ZONE)
|
||||
formatted_dt = dt.astimezone(tz).strftime(const.FORMAT_TIME)
|
||||
return formatted_dt
|
||||
|
@ -3,6 +3,11 @@
|
||||
src: url('../font/LXGWWenKaiMonoGB-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'dongzhu';
|
||||
src: url('../font/dongzhu-Extralight.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
html {
|
||||
font-size: 15px;
|
||||
@ -76,6 +81,48 @@ html {
|
||||
|
||||
#tab1 {
|
||||
background-image: url('../img/page1.jpg');
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
margin: 0 2px;
|
||||
bottom: 0.5rem;
|
||||
background: #fff;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid #d1d1d1;
|
||||
}
|
||||
|
||||
.popup > .notice > hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.popup > .selection {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.selection > button {
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.5rem 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.allow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deny {
|
||||
background-color: #059862;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
padding: 0.5rem 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.deny:hover,
|
||||
.deny:focus {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#tab2 {
|
||||
@ -84,8 +131,8 @@ html {
|
||||
|
||||
.tab2-box {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
padding: 1rem 1rem;
|
||||
top: 2rem;
|
||||
padding-left: 2rem;
|
||||
font-size: 1.75rem;
|
||||
color: #FFFFFFD9;
|
||||
}
|
||||
@ -101,8 +148,8 @@ html {
|
||||
|
||||
.tab3-box {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
padding: 1rem 1rem;
|
||||
top: 2rem;
|
||||
padding-left: 2rem;
|
||||
font-size: 1.75rem;
|
||||
color: #ffa940;
|
||||
}
|
||||
@ -117,20 +164,34 @@ html {
|
||||
}
|
||||
|
||||
.tab4-box {
|
||||
writing-mode: vertical-lr;
|
||||
position: fixed;
|
||||
position: relative;;
|
||||
top: 2rem;
|
||||
font-size: 1.75rem;
|
||||
color: #FFFFFFD9;
|
||||
left: 2rem;
|
||||
line-height: 2rem;
|
||||
color: #448288D9;
|
||||
}
|
||||
|
||||
#tab4 > .tab4-box > p:last-child {
|
||||
writing-mode: horizontal-tb;
|
||||
margin: 0 0;
|
||||
height: 30vh;
|
||||
width: 13rem;
|
||||
#tab4 > .tab4-box > p:nth-child(1) {
|
||||
width: 7rem;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
#tab4 > .tab4-box > p:nth-child(2) {
|
||||
font-size: 1.5rem;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
#tab4 > .tab4-box > p:nth-child(3) {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
#tab4 > .tab4-box > p:nth-child(4) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#tab4 > .tab4-box > p:nth-child(5) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
|
||||
@ -139,13 +200,59 @@ html {
|
||||
}
|
||||
|
||||
.tab5-box {
|
||||
writing-mode: vertical-lr;
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
font-size: 1.75rem;
|
||||
color: #FFFFFFD9;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:last-child {
|
||||
writing-mode: horizontal-tb;
|
||||
margin: 0 0;
|
||||
height: 30vh;
|
||||
width: 13rem;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
#tab6 {
|
||||
background-image: url('../img/page6.jpg');
|
||||
}
|
||||
|
||||
.tab6-box {
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
line-height: 2rem;
|
||||
color: #613f38c7;
|
||||
}
|
||||
|
||||
#tab6 > .tab6-box > p:nth-child(1) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#tab6 > .tab6-box > p:nth-child(2) {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
#tab6 > .tab6-box > p:nth-child(3) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
#tab7 {
|
||||
background-image: url('../img/page7.jpg');
|
||||
}
|
||||
|
||||
.tab7-box {
|
||||
position: relative;
|
||||
top: 2rem;
|
||||
left: 0.75rem;
|
||||
color: #597ef7;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(1) {
|
||||
#tab7 > .tab7-box > p:nth-child(1) {
|
||||
position: relative;
|
||||
top: -6rem;
|
||||
left: 0.8rem;
|
||||
@ -153,7 +260,7 @@ html {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(2) {
|
||||
#tab7 > .tab7-box > p:nth-child(2) {
|
||||
position: relative;
|
||||
top: 3.2rem;
|
||||
left: 7.8rem;
|
||||
@ -161,7 +268,7 @@ html {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(3) {
|
||||
#tab7 > .tab7-box > p:nth-child(3) {
|
||||
position: relative;
|
||||
top: 8rem;
|
||||
left: 2rem;
|
||||
@ -169,7 +276,7 @@ html {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(4) {
|
||||
#tab7 > .tab7-box > p:nth-child(4) {
|
||||
position: relative;
|
||||
top: -13rem;
|
||||
left: 11.7rem;
|
||||
@ -177,7 +284,7 @@ html {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(5) {
|
||||
#tab7 > .tab7-box > p:nth-child(5) {
|
||||
position: relative;
|
||||
top: -5.6rem;
|
||||
left: 9.7rem;
|
||||
@ -185,35 +292,36 @@ html {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(6) {
|
||||
#tab7 > .tab7-box > p:nth-child(6) {
|
||||
position: relative;
|
||||
top: 3rem;
|
||||
left: 2rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
#tab7 > .tab7-box > p:nth-child(6) > small {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
#tab7 > .tab7-box > p:nth-child(7) {
|
||||
position: relative;
|
||||
top: 2rem;
|
||||
left: 2rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(6) > small {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(7) {
|
||||
position: relative;
|
||||
top: 2rem;
|
||||
left: 2rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
#tab5 > .tab5-box > p:nth-child(7) > small {
|
||||
#tab7 > .tab7-box > p:nth-child(7) > small {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
|
||||
#tab6 {
|
||||
background-image: url('../img/page6.jpg');
|
||||
#tab8 {
|
||||
background-image: url('../img/page8.jpg');
|
||||
}
|
||||
|
||||
#tab6 > .tab6-box {
|
||||
#tab8 > .tab8-box {
|
||||
position: fixed;
|
||||
bottom: 7%;
|
||||
padding-left: 2rem;
|
||||
@ -221,7 +329,22 @@ html {
|
||||
color: #FFFFFFD9;
|
||||
}
|
||||
|
||||
#tab7 {
|
||||
background-image: url('../img/page7.jpg');
|
||||
#tab9 {
|
||||
background-image: url('../img/page9.jpg');
|
||||
}
|
||||
|
||||
#tab9 > .tab9-box {
|
||||
padding: 2rem 2rem;
|
||||
}
|
||||
|
||||
.tab9-box > p {
|
||||
font-family: 'dongzhu', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
font-size: 2.75rem;
|
||||
line-height: 3.5rem;
|
||||
writing-mode: vertical-rl;
|
||||
color: #FFFFFFD9;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
BIN
static/painting/font/dongzhu-Extralight.ttf
Normal file
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 258 KiB |
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 332 KiB |
Before Width: | Height: | Size: 448 KiB After Width: | Height: | Size: 507 KiB |
Before Width: | Height: | Size: 490 KiB After Width: | Height: | Size: 418 KiB |
BIN
static/painting/img/page8.jpg
Normal file
After Width: | Height: | Size: 448 KiB |
BIN
static/painting/img/page9.jpg
Normal file
After Width: | Height: | Size: 490 KiB |
BIN
static/painting/music/bgm.mp3
Normal file
@ -3,8 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>EndOfYear</title>
|
||||
{% if web_status == 1 %}
|
||||
<title>{{ site.title }}</title>
|
||||
{% if site.service == 1 %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='painting/css/normalize.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='painting/css/animate.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='painting/css/painting.css') }}">
|
||||
@ -13,20 +13,48 @@
|
||||
<link rel="stylesheet" href="painting/css/animate.min.css">
|
||||
<link rel="stylesheet" href="painting/css/painting.css">
|
||||
{% endif %}
|
||||
<script async src="https://umami.7wate.org/script.js" data-website-id="635fec50-bc6c-4ac2-909a-e2a7403438be"></script>
|
||||
<script async src="https://umami.7wate.org/script.js"
|
||||
data-website-id="635fec50-bc6c-4ac2-909a-e2a7403438be"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container active animate__animated animate__fadeIn animate__slow" id="tab1"></div>
|
||||
<div class="container active animate__animated animate__fadeIn animate__slow" id="tab1">
|
||||
<audio id="bgm" loop>
|
||||
{% if site.service == 1 %}
|
||||
<source src="{{ url_for('static', filename='painting/music/bgm.mp3') }}" type="audio/mpeg">Your browser does
|
||||
not support the audio element.
|
||||
{% else %}
|
||||
<source src="painting/music/bgm.mp3" type="audio/mpeg">Your browser does not support the audio element.
|
||||
{% endif %}
|
||||
|
||||
</audio>
|
||||
<div class="popup">
|
||||
<div class="notice">
|
||||
<h4>温馨提示</h4>
|
||||
<hr>
|
||||
<p>EndofYear 使用互联网公开的 RSS 数据源,并使用自建的 Umami 服务统计访问量,绝对不会主动获取个人隐私信息。🫣🫣🫣
|
||||
<br>
|
||||
<br>
|
||||
开启方式:小手轻轻点 ~
|
||||
<a href="https://github.com/7Wate/EndOfYear"></a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="selection">
|
||||
<button value="true" class="allow">开启音乐</button>
|
||||
<button value="false" class="deny">静音进入</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab2">
|
||||
<div class="tab2-box">
|
||||
<p class="animate__animated animate__fadeIn animate__delay-1s">亲爱的{{ data.blog_name }}</p>
|
||||
{% if data.blog_life == 0 %}
|
||||
<p class="animate__animated animate__fadeIn animate__delay-1s">亲爱的{{ blog.name }}</p>
|
||||
{% if blog.life == 0 %}
|
||||
<p class="animate__animated animate__fadeIn animate__delay-2s">旧事如梦,一年已过</p>
|
||||
<p class="animate__animated animate__fadeIn animate__delay-2s">贰三年、感谢有你!</p>
|
||||
{% else %}
|
||||
<p class="animate__animated animate__fadeIn animate__delay-2s">今天是我们相识的</p>
|
||||
<p class="animate__animated animate__fadeIn animate__delay-3s">第 <small>{{ data.blog_life_year }}</small> 年
|
||||
<small>{{ data.blog_life_day }}</small> 天</p>
|
||||
<p class="animate__animated animate__fadeIn animate__delay-3s">第 <small>{{ blog.life // 365 }}</small> 年
|
||||
<small>{{ blog.life % 365 }}</small> 天</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -34,47 +62,68 @@
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab3">
|
||||
<div class="tab3-box">
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-1s">这一年你写下了</p>
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-2s"><small>{{ data.blog_article_count }}</small>
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-2s"><small>{{ blog.article_count }}</small>
|
||||
篇博文</p>
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-3s"><small>{{ data.blog_article_word_count
|
||||
}}</small> 个文字</p>
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-3s">
|
||||
<small>{{ blog.article_word_count }}</small> 个文字</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab4">
|
||||
<div class="tab4-box">
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-1s">{{ data.blog_content_date }}那天,你写下了</p>
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-2s">{{ data.blog_title }}</p>
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-3s">{{ data.blog_content }}<small>……</small>
|
||||
</p>
|
||||
<p class="animate__animated animate__fadeInLeft animate__delay-1s"> 其中 </p>
|
||||
<p class="animate__animated animate__fadeInLeft animate__delay-2s"> {{ long_post.title }} </p>
|
||||
<p class="animate__animated animate__fadeInLeft animate__delay-3s"> {{ long_post.content[:50] }}</p>
|
||||
<p class="animate__animated animate__fadeIn animate__delay-4s"> 长似一江春水</p>
|
||||
<p class="animate__animated animate__fadeIn animate__delay-5s"> 永不止息 ~ </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab5">
|
||||
<div class="tab5-box">
|
||||
{% for keyword in data.blog_top_keywords %}
|
||||
<p>{{ keyword[0] }}</p>
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-1s">在{{ special_post.date }}那天,写下了</p>
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-2s">{{ special_post.title }}</p>
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-3s">{{ special_post.content[:50] }}<small>……</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab6">
|
||||
<div class="tab6-box">
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-1s">{{ sentiment_post.title }}</p>
|
||||
<p class="animate__animated animate__fadeInUp animate__delay-2s">{{ sentiment_post.content[:50] }}</p>
|
||||
<p class="animate__animated animate__fadeIn animate__delay-3s">如初春的暖阳,宁静而怡人</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab7">
|
||||
<div class="tab7-box">
|
||||
{% for keyword in blog.top_post_keys[0:5] %}
|
||||
<p class="animate__animated animate__fadeIn animate__delay-1s">{{ keyword[0] }}</p>
|
||||
{% endfor %}
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-1s">这些都是<small>你</small>的</p>
|
||||
<p class="animate__animated animate__fadeInDown animate__delay-2s">专属<small>关键词</small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab6">
|
||||
<div class="tab6-box">
|
||||
<p class="animate__animated animate__fadeInLeft animate__delay-1s">热爱{{ data.blog_category }}的你</p>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab8">
|
||||
<div class="tab8-box">
|
||||
<p class="animate__animated animate__fadeInLeft animate__delay-1s">
|
||||
热爱{% if blog.category == 1 %}生活{% else %}技术{% endif %}的你
|
||||
</p>
|
||||
<p class="animate__animated animate__fadeInLeft animate__delay-2s">一定要继续砥砺前行!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab7">
|
||||
<div class="tab7-box">
|
||||
|
||||
<div class="container animate__animated animate__fadeIn animate__slow" id="tab9">
|
||||
<div class="tab9-box">
|
||||
<p id="yiyan"> {{ custom.yiyan }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 屏幕尺寸
|
||||
console.log("Window inner size: ", window.innerWidth, "x", window.innerHeight);
|
||||
console.log("Window outer size: ", window.outerWidth, "x", window.outerHeight);
|
||||
console.log("Screen size: ", screen.width, "x", screen.height);
|
||||
console.log("Screen available size: ", screen.availWidth, "x", screen.availHeight);
|
||||
var carousel = {
|
||||
|
||||
// 轮播切换
|
||||
let carousel = {
|
||||
currentIndex: 0,
|
||||
tabs: [],
|
||||
|
||||
@ -98,6 +147,30 @@
|
||||
window.onload = function () {
|
||||
carousel.init();
|
||||
};
|
||||
|
||||
// 弹出提示
|
||||
let popup = document.querySelector('.popup');
|
||||
let allowButton = document.querySelector('.allow');
|
||||
let denyButton = document.querySelector('.deny');
|
||||
let audioElement = document.getElementById('bgm');
|
||||
|
||||
allowButton.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
audioElement.play(); // 播放音乐
|
||||
popup.style.display = 'none'; // 隐藏弹出窗口
|
||||
});
|
||||
|
||||
denyButton.addEventListener('click', function (event) {
|
||||
event.stopPropagation();
|
||||
popup.style.display = 'none'; // 隐藏弹出窗口
|
||||
});
|
||||
|
||||
// 博客最后的一言
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
let textElement = document.getElementById("yiyan");
|
||||
let text = textElement.innerHTML;
|
||||
textElement.innerHTML = text.replace(/[\u3000-\u303F\uFF01-\uFF0F\uFF1A-\uFF1E\uFF3B-\uFF3F\uFF5B-\uFF60\uFFE0-\uFFE6\u00D2]/g, "<br>");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|