init:0.4.0

This commit is contained in:
yanqs 2024-08-14 15:37:33 +08:00
parent c1365d1539
commit f1f75bc466
29 changed files with 1516 additions and 0 deletions

20
Dockerfile Normal file
View File

@ -0,0 +1,20 @@
# 基于java镜像创建新镜像
FROM adoptopenjdk/openjdk8-openj9:jdk8u412-b08_openj9-0.44.0-alpine-slim
# 作者
MAINTAINER bcrjl
EXPOSE 24803
WORKDIR /app
# 创建 bin 目录
RUN mkdir -p bin \
&& mkdir -p config \
&& mkdir -p lib \
&& mkdir -p log
COPY ./target/rss-reader.jar /app/rss-reader.jar
COPY ./target/lib/* /app/lib/
COPY ./config/* /app/config/
ENTRYPOINT ["java","-Dloader.path=/app/lib/", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/rss-reader.jar"]

32
config/config.setting Normal file
View File

@ -0,0 +1,32 @@
[system]
## 配置订阅频率
refresh=5
## 保存微博图片
saveWeiBoImages=true
## 上传图片到AList
uploadAList=false
## AList Url
aListUrl=
## AList 账号
aListUser=
## AList 密码
aListPass=
## AList 上传路径
aListUploadPath=
[mail]
## 启用邮件推送
enable=false
## 启用邮件更新推送
sendUpdate=true
## 邮件服务器的SMTP地址可选默认为smtp.<发件人邮箱后缀>
host=smtp.qq.com
## 邮件服务器的SMTP端口可选默认25
port=465
## 发件人(必须正确,否则发送失败)
from=rss-reply<no-reply@***.com>
## 用户名,默认为发件人邮箱前缀
user=xxxxxxxxxxxxxx
## 密码注意某些邮箱需要为SMTP服务单独设置授权码详情查看相关帮助
pass=
## 接收人,多人以英文逗号分割
to=

11
config/data.json Normal file
View File

@ -0,0 +1,11 @@
{
"rssList": {
"weibo": [
],
"other": [
]
}
}

1
config/rss-data.json Normal file
View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,15 @@
version: "2.24.6"
services:
rss-reader:
image: bcrjl/rss-reader:latest
container_name: rss-reader
restart: always
volumes:
- ./config:/app/config
- ./log:/app/log
- ./images:/app/images
- /etc/localtime:/etc/localtime:ro
environment:
- TZ=Asia/Shanghai
ports:
- '24803:24803'

174
pom.xml Normal file
View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bcrjl.rss</groupId>
<artifactId>rss-reader</artifactId>
<version>0.4.0</version>
<name>RSS订阅阅读器</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<maven.plugin.version>3.8.1</maven.plugin.version>
<maven.assembly.plugin.version>3.3.0</maven.assembly.plugin.version>
<maven.dependency.plugin.version>3.2.0</maven.dependency.plugin.version>
<maven.resources.plugin.version>3.2.0</maven.resources.plugin.version>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一依赖管理 -->
<spring.boot.version>2.7.18</spring.boot.version>
<sa-token.version>1.38.0</sa-token.version>
<lombok.version>1.18.34</lombok.version>
<hutool.version>5.8.29</hutool.version>
<javax.mail.version>1.6.2</javax.mail.version>
</properties>
<dependencyManagement>
<!-- SpringBoot的依赖配置-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${sa-token.version}</version>
<exclusions>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- HuTool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- mail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>${javax.mail.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<!-- 打包后的启动jar名称 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<configuration>
<executable>true</executable>
<layout>ZIP</layout>
<!--这里是填写需要包含进去的jar
必须项目中的某些模块,会经常变动,那么就应该将其坐标写进来
如果没有则nothing ,表示不打包依赖 -->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!--拷贝依赖到jar外面的lib目录-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--指定的依赖路径-->
<outputDirectory>
${project.build.directory}/lib
</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- Maven 插件配置 -->
<repositories>
<repository>
<id>nexus</id>
<name>nexus-developer</name>
<url>https://nexus.ys.bcrjl.com:41010/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>nexus</id>
<url>https://nexus.ys.bcrjl.com:41010/repository/maven-public/</url>
</pluginRepository>
</pluginRepositories>
</project>

View File

@ -0,0 +1,21 @@
package com.bcrjl.rss;
import com.bcrjl.rss.job.FileMonitor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import javax.annotation.Resource;
/**
* 项目启动类
*
* @author yanqs
*/
@EnableScheduling
@SpringBootApplication
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}

View File

@ -0,0 +1,25 @@
package com.bcrjl.rss.cache;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 缓存配置
*
* @author yanqs
*/
@Slf4j
@Component
public class ConfigCache {
public static Cache<String, Object> fifoCache = CacheUtil.newFIFOCache(10);
public static Object getConfig(String key) {
return fifoCache.get(key);
}
public static void setConfig(String key, Object value) {
fifoCache.put(key, value);
}
}

View File

@ -0,0 +1,65 @@
package com.bcrjl.rss.common.constant;
/**
* 应用常量
*
* @author yanqs
*/
public interface AppConstant {
int INIT_MAP = 16;
/**
* 系统配置路径
*/
String CONFIG_PATH = System.getProperty("user.dir") + "/config/config.setting";
/**
* RSS数据路径
*/
String RSS_DATA_PATH = System.getProperty("user.dir") + "/config/rss-data.json";
/**
* RSS订阅源路径
*/
String RSS_CONFIG_PATH = System.getProperty("user.dir") + "/config/data.json";
String IMAGES_PATH = System.getProperty("user.dir") + "/images/";
String SET_SYSTEM = "system";
String SET_MAIL = "mail";
/**
* 配置RSS订阅频率
*/
String REFRESH = "refresh";
/**
* 保存微博图片
*/
String SAVE_WEIBO_IMAGES = "saveWeiBoImages";
/**
* 上传图片到AList
*/
String UPLOAD_ALIST = "uploadAList";
/**
* AList Url
*/
String ALIST_URL = "aListUrl";
/**
* AList 账号
*/
String ALIST_USER = "aListUser";
/**
* AList 密码
*/
String ALIST_PASS = "aListPass";
String ALIST_UPLOAD_PATH = "aListUploadPath";
Integer MAIL_CONFIG_SIZE = 7;
String MAIL_CONFIG_ENABLE = "enable";
String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36";
}

View File

@ -0,0 +1,95 @@
package com.bcrjl.rss.common.util;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import cn.hutool.setting.Setting;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static com.bcrjl.rss.common.constant.AppConstant.*;
/**
* 功能描述:
*
* @author yanqs
* @since 2024-08-10
*/
@Slf4j
public class AListUtils {
private static final String TOKEN_URL = "/api/auth/login";
private static final String UPLOAD_URL = "/api/fs/put";
private static TimedCache<String, String> timedCache = CacheUtil.newTimedCache(86400000);
/**
* 获取AList Token
*
* @return token
*/
private static String getToken() {
try {
String alistToken = timedCache.get("alist_token");
if (StrUtil.isNotEmpty(alistToken)) {
return alistToken;
} else {
Setting setting = new Setting(CONFIG_PATH, CharsetUtil.CHARSET_UTF_8, true);
Setting systemSetting = setting.getSetting(SET_SYSTEM);
Map<String, String> params = new HashMap<>(INIT_MAP);
params.put("username", systemSetting.get(ALIST_USER));
params.put("password", systemSetting.get(ALIST_PASS));
String aListUrl = systemSetting.get(ALIST_URL);
String body = HttpRequest.post(aListUrl + TOKEN_URL)
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.body(JSONUtil.toJsonStr(params))
.execute().body();
String token = JSONUtil.parseObj(body).getJSONObject("data").getStr("token");
timedCache.put("alist_token", token);
return token;
}
} catch (Exception e) {
log.error("获取AList Token异常", e);
return "";
}
}
public static void uploadFile(byte[] fileByte, String fileName) {
try {
String time = DateUtil.format(new Date(), "yyyyMMddHH");
Setting setting = new Setting(CONFIG_PATH, CharsetUtil.CHARSET_UTF_8, true);
Setting systemSetting = setting.getSetting(SET_SYSTEM);
String aListUrl = systemSetting.get(ALIST_URL);
String uploadPath = systemSetting.get(ALIST_UPLOAD_PATH);
HttpResponse httpResponse = HttpRequest.put(aListUrl + UPLOAD_URL)
.header(Header.AUTHORIZATION, getToken())
.header(Header.CONTENT_TYPE, ContentType.MULTIPART.getValue())
.header("File-Path", uploadPath + "/" + time + "/" + fileName)
.body(fileByte)
.execute();
if (httpResponse.isOk()) {
String message = JSONUtil.parseObj(httpResponse.body()).getStr("message");
if ("success".equals(message)) {
//log.info("AList上传成功");
} else {
log.info("AList上传失败{}", message);
}
} else {
log.info("AList上传失败,响应状态码:{}", httpResponse.getStatus());
}
} catch (Exception e) {
log.error("AList上传文件异常", e);
}
}
}

View File

@ -0,0 +1,59 @@
package com.bcrjl.rss.common.util;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.bcrjl.rss.common.constant.AppConstant.USER_AGENT;
/**
* Html 工具类
*
* @author yanqs
*/
@Slf4j
public class HtmlParseUtils {
/**
* 获取html中的图片
*
* @param htmlContent html内容
* @return
*/
public static List<String> extractImageUrls(String htmlContent) {
List<String> imageUrls = new ArrayList<>();
String regex = "<img\\s+[^>]*?src\\s*=\\s*['\"]([^'\"]*?)['\"][^>]*?>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(htmlContent);
while (matcher.find()) {
String imageUrl = matcher.group(1);
imageUrls.add(imageUrl);
}
return imageUrls;
}
/**
* 获取微博图片流文件
*
* @param fileName 微博图片名称
* @return HttpResponse
*/
public static HttpResponse getWeiBoImagesHttpRequest(String fileName) {
try {
String url = "https://tvax3.sinaimg.cn/large/" + fileName;
HttpRequest request = HttpRequest.get(url)
.header(Header.REFERER, "https://weibo.com/")
.header(Header.USER_AGENT, USER_AGENT)
.timeout(20000);
return request.executeAsync();
} catch (Exception e) {
log.error("获取微博图片数据异常:", e);
return null;
}
}
}

View File

@ -0,0 +1,37 @@
package com.bcrjl.rss.common.util;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.setting.Setting;
import lombok.extern.slf4j.Slf4j;
import static com.bcrjl.rss.common.constant.AppConstant.*;
/**
* 邮件工具
*
* @author yanqs
* @since 2024-08-07
*/
@Slf4j
public class MailUtils {
public static MailAccount initMailAccount() {
Setting setting = new Setting(CONFIG_PATH, CharsetUtil.CHARSET_UTF_8, true);
Setting emailSetting = setting.getSetting(SET_MAIL);
if (emailSetting.size() >= MAIL_CONFIG_SIZE) {
MailAccount mailAccount = new MailAccount();
mailAccount.setHost(emailSetting.getWithLog("host"));
mailAccount.setPort(Integer.valueOf(emailSetting.getWithLog("port")));
mailAccount.setAuth(true);
mailAccount.setFrom(emailSetting.getWithLog("from"));
mailAccount.setUser(emailSetting.getWithLog("user"));
mailAccount.setPass(emailSetting.getWithLog("pass"));
mailAccount.setSslEnable(true);
mailAccount.setDebug(false);
return mailAccount;
} else {
log.error("请配置邮箱信息");
return null;
}
}
}

View File

@ -0,0 +1,63 @@
package com.bcrjl.rss.common.util;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.bcrjl.rss.model.entity.RssEntity;
import lombok.extern.slf4j.Slf4j;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* RSS 订阅工具类
*
* @author yanqs
*/
@Slf4j
public class RssUtils {
public static List<RssEntity> getRssList(String url) {
List<RssEntity> rssList = new ArrayList<>();
log.info("开始订阅:{}", url);
try {
//URL rssUrl = new URL(url);
HttpResponse httpResponse = HttpRequest.get(url).executeAsync();
if(httpResponse.isOk()){
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(httpResponse.bodyStream());
NodeList items = document.getElementsByTagName("item");
Node webTitle = document.getElementsByTagName("title").item(0);
for (int i = 0; i < items.getLength(); i++) {
Element item = (Element) items.item(i);
Element title = (Element) item.getElementsByTagName("title").item(0);
Element link = (Element) item.getElementsByTagName("link").item(0);
Element description = (Element) item.getElementsByTagName("description").item(0);
Element pubDate = (Element) item.getElementsByTagName("pubDate").item(0);
rssList.add(RssEntity.builder()
.webTitle(webTitle.getTextContent())
.url(url)
.title(title.getTextContent())
.link(link.getTextContent())
.description(description.getTextContent())
.createTime(DateUtil.parse(pubDate.getTextContent(), DatePattern.HTTP_DATETIME_FORMAT))
.build());
}
}
} catch (Exception e) {
log.error("地址:{}获取RSS订阅内容异常", url, e);
}
return rssList;
}
}

View File

@ -0,0 +1,59 @@
package com.bcrjl.rss.config;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.setting.Setting;
import com.bcrjl.rss.job.RssJob;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.PeriodicTrigger;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Objects;
import static com.bcrjl.rss.common.constant.AppConstant.REFRESH;
/**
* 动态配置订阅
*
* @author yanqs
* @since 2024-08-06
*/
@Data
@Slf4j
@Configuration
@EnableScheduling
public class DynamicScheduleConfig implements SchedulingConfigurer {
@Resource
private RssJob rssJob;
/**
* 默认五分钟执行一次
*/
private Long timer = 5L * 1000L * 60L;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(new Runnable() {
@Override
public void run() {
rssJob.subscribe();
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
log.info("当前执行速度:{}分钟", timer / 1000L / 60L);
PeriodicTrigger periodicTrigger = new PeriodicTrigger(timer);
Date nextExecutionTime = periodicTrigger.nextExecutionTime(triggerContext);
return nextExecutionTime;
}
});
}
}

View File

@ -0,0 +1,24 @@
package com.bcrjl.rss.config;
import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
import cn.dev33.satoken.stp.StpLogic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Sa-Token 配置
*
* @author yanqs
*/
@Configuration
public class SaTokenConfigure {
/**
* Sa-Token 整合 jwt (Simple 简单模式)
*
* @return
*/
@Bean
public StpLogic getStpLogicJwt() {
return new StpLogicJwtForSimple();
}
}

View File

@ -0,0 +1,44 @@
package com.bcrjl.rss.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* @author yanqs
* @since 2024-08-03
*/
@Configuration
public class WebConfigurer extends WebMvcConfigurationSupport {
/**
* 跨域过滤
*
* @return FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig());
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
//*****这里设置了优先级*****
bean.setOrder(1);
return bean;
}
private CorsConfiguration corsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedOriginPattern("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}
}

View File

@ -0,0 +1,19 @@
package com.bcrjl.rss.controller;
import com.bcrjl.rss.common.util.RssUtils;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author yanqs
* @since 2024-08-03
*/
@AllArgsConstructor
@RestController
@RequestMapping("/rss")
public class RssController {
}

View File

@ -0,0 +1,27 @@
package com.bcrjl.rss.controller;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.setting.Setting;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.bcrjl.rss.common.constant.AppConstant.CONFIG_PATH;
/**
* @author yanqs
* @since 2024-08-07
*/
@AllArgsConstructor
@RestController
@RequestMapping("/system")
public class SystemController {
@GetMapping("/config")
public Object getConfig(){
String str = ResourceUtil.readUtf8Str(CONFIG_PATH);
return str;
}
}

View File

@ -0,0 +1,111 @@
package com.bcrjl.rss.job;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.json.JSONUtil;
import cn.hutool.setting.Setting;
import com.bcrjl.rss.cache.ConfigCache;
import com.bcrjl.rss.config.DynamicScheduleConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static com.bcrjl.rss.common.constant.AppConstant.*;
/**
* 监控配置文件
*
* @author yanqs
* @since 2024-08-06
*/
@Slf4j
@Component
public class FileMonitor {
@Resource
private DynamicScheduleConfig scheduledTask;
@PostConstruct
public void initConfig() {
existFile("config.setting", "[system]\n" +
"## 配置订阅频率\n" +
"refresh = 5\n" +
"## 保存微博图片\n" +
"saveWeiBoImages=false\n" +
"## 上传图片到AList\n" +
"uploadAList=false\n" +
"## AList Url\n" +
"aListUrl=\n" +
"## AList 账号\n" +
"aListUser=\n" +
"## AList 密码\n" +
"aListPass=\n" +
"## AList 上传路径\n" +
"aListUploadPath=" +
"[mail]\n" +
"## 启用邮件推送\n" +
"enable=false\n" +
"## 启用邮件更新推送\n" +
"sendUpdate=true\n" +
"## 邮件服务器的SMTP地址可选默认为smtp.<发件人邮箱后缀>\n" +
"host=smtp.qq.com\n" +
"## 邮件服务器的SMTP端口可选默认25\n" +
"port=465\n" +
"## 发件人(必须正确,否则发送失败)\n" +
"from=rss-reply<no-reply@***.com>\n" +
"## 用户名,默认为发件人邮箱前缀\n" +
"user=xxxxxxxxxxxxxx\n" +
"## 密码注意某些邮箱需要为SMTP服务单独设置授权码详情查看相关帮助\n" +
"pass=\n" +
"## 接收人,多人以英文逗号分割\n" +
"to=\n");
Map<String, Object> dataJson = new HashMap<>();
Map<String, Object> rssList = new HashMap<>();
rssList.put("weibo",new ArrayList<>());
rssList.put("other",new ArrayList<>());
dataJson.put("rssList",rssList);
existFile("data.json", JSONUtil.toJsonPrettyStr(dataJson));
existFile("rss-data.json", "[]");
}
private void existFile(String fileName, String fileContent) {
fileName = System.getProperty("user.dir") + "/config/" + fileName;
File file = FileUtil.file(fileName);
if (!FileUtil.exist(file)) {
file = FileUtil.touch(fileName);
FileWriter dataFile = new FileWriter(file);
dataFile.write(fileContent, false);
log.info("监听配置文件不存在,系统已自动创建");
}
}
/**
* 文件监听
*/
@Scheduled(cron = "0/10 * * * * ?")
public void configMonitor() {
Setting setting = new Setting(CONFIG_PATH, CharsetUtil.CHARSET_UTF_8, true);
Long refresh = Long.valueOf(setting.getByGroup(REFRESH, SET_SYSTEM));
Long config = (Long) ConfigCache.getConfig(REFRESH);
if (Objects.isNull(config)) {
ConfigCache.setConfig(REFRESH, refresh);
config = refresh;
}
if (!config.equals(refresh)) {
log.info("配置变动,更新频率:{}分钟", refresh);
scheduledTask.setTimer(refresh * 60 * 1000L);
ConfigCache.setConfig(REFRESH, refresh);
}
}
}

View File

@ -0,0 +1,141 @@
package com.bcrjl.rss.job;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileWriter;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.extra.mail.MailAccount;
import cn.hutool.extra.mail.MailUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.setting.Setting;
import com.bcrjl.rss.common.util.AListUtils;
import com.bcrjl.rss.common.util.HtmlParseUtils;
import com.bcrjl.rss.common.util.MailUtils;
import com.bcrjl.rss.common.util.RssUtils;
import com.bcrjl.rss.model.entity.RssEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.bcrjl.rss.common.constant.AppConstant.*;
/**
* RSS 任务
*
* @author yanqs
*/
@Slf4j
@Component
public class RssJob {
/**
* RSS 订阅
*/
public void subscribe() {
if (!FileUtil.exist(RSS_DATA_PATH)) {
FileUtil.touch(RSS_DATA_PATH);
FileWriter dataFile = new FileWriter(RSS_DATA_PATH);
dataFile.write("[]");
}
JSONObject rssListObj = JSONUtil.parseObj(ResourceUtil.readUtf8Str(RSS_CONFIG_PATH)).getJSONObject("rssList");
if (Objects.nonNull(rssListObj)) {
List<RssEntity> dbList = JSONUtil.toList(ResourceUtil.readUtf8Str(RSS_DATA_PATH), RssEntity.class);
List<String> weiboRssList = JSONUtil.toList(rssListObj.getStr("weibo"), String.class);
List<RssEntity> saveList = new ArrayList<>();
weiboRssList.forEach(obj -> {
List<RssEntity> rssList = RssUtils.getRssList(obj);
saveList.addAll(rssList);
});
List<String> otherRssList = JSONUtil.toList(rssListObj.getStr("other"), String.class);
otherRssList.forEach(obj -> {
List<RssEntity> rssList = RssUtils.getRssList(obj);
saveList.addAll(rssList);
});
if (CollUtil.isEmpty(dbList)) {
FileWriter dataFile = new FileWriter(RSS_DATA_PATH);
dataFile.write(JSONUtil.toJsonPrettyStr(saveList));
sendEmailReply(saveList);
} else {
List<String> dbIds = dbList.stream().map(RssEntity::getLink).collect(Collectors.toList());
// 获取库中不包含的数据 则为新增数据
List<RssEntity> insertList = saveList.stream()
.filter(t -> !dbIds.contains(t.getLink())).collect(Collectors.toList());
dbList.addAll(insertList);
FileWriter dataFile = new FileWriter(RSS_DATA_PATH);
dataFile.write(JSONUtil.toJsonPrettyStr(dbList));
sendEmailReply(insertList);
log.info("本次新增了{}条推送内容", insertList.size());
}
} else {
log.info("未配置订阅信息");
}
log.info("订阅数据结束!");
}
/**
* 发送邮件通知
*
* @param list rssList
*/
private void sendEmailReply(List<RssEntity> list) {
if (CollUtil.isNotEmpty(list)) {
Setting setting = new Setting(CONFIG_PATH, CharsetUtil.CHARSET_UTF_8, true);
Setting emailSetting = setting.getSetting(SET_MAIL);
saveWeiBoImagesOrUpdateAlist(list);
if (emailSetting.getBool(MAIL_CONFIG_ENABLE) && emailSetting.getBool("sendUpdate")) {
// 如果邮箱开启且发送更新邮件开启 则推送通知
StringBuffer stringBuffer = new StringBuffer();
list.forEach(obj -> {
stringBuffer.append("<div>");
stringBuffer.append("<a href='").append(obj.getLink()).append("'>").append(obj.getWebTitle()).append("--").append(obj.getTitle()).append("</a></br>");
stringBuffer.append("</div>");
});
MailAccount mailAccount = MailUtils.initMailAccount();
List<String> toList = Arrays.asList(emailSetting.getStr("to").split(","));
if (CollUtil.isNotEmpty(toList)) {
MailUtil.send(mailAccount, toList, "RSS订阅推送", stringBuffer.toString(), true);
}
}
}
}
/**
* 保存微博图片到本地且上传AList
*/
private void saveWeiBoImagesOrUpdateAlist(List<RssEntity> list) {
Setting setting = new Setting(CONFIG_PATH, CharsetUtil.CHARSET_UTF_8, true);
Setting systemSetting = setting.getSetting(SET_SYSTEM);
Boolean saveImages = Boolean.valueOf(systemSetting.get(SAVE_WEIBO_IMAGES));
Boolean uploadAList = Boolean.valueOf(systemSetting.get(UPLOAD_ALIST));
if (saveImages) {
// 保存图片
list.forEach(obj -> {
List<String> imgList = HtmlParseUtils.extractImageUrls(obj.getDescription());
imgList.forEach(imgObj -> {
if (imgObj.contains("sinaimg") && !imgObj.contains("timeline_card") && !imgObj.contains("qixi2018")) {
int lastSlashIndex = imgObj.lastIndexOf('/');
// 如果找到了斜杠就从斜杠后面截取字符串
String fileName = imgObj.substring(lastSlashIndex + 1);
//log.info("微博图片文件名:{}", fileName);
HttpResponse weiBoImagesHttpRequest = HtmlParseUtils.getWeiBoImagesHttpRequest(fileName);
byte[] bytes = weiBoImagesHttpRequest.bodyBytes();
FileUtil.writeBytes(bytes, new File(IMAGES_PATH + fileName));
if (uploadAList) {
AListUtils.uploadFile(bytes, fileName);
}
}
});
});
}
}
}

View File

@ -0,0 +1,27 @@
package com.bcrjl.rss.model.entity;
import lombok.Data;
import java.util.List;
/**
* @author yanqs
* @since 2024-08-03
*/
@Data
public class DataEntity {
/**
* rss订阅链接必填
*/
private RssListEntity rssList;
/**
* rss订阅更新时间间隔单位分钟必填
*/
private Long refresh;
}
class RssListEntity {
private List<String> weibo;
private List<String> other;
}

View File

@ -0,0 +1,37 @@
package com.bcrjl.rss.model.entity;
import lombok.Builder;
import lombok.Data;
import java.util.Date;
/**
* @author yanqs
* @since 2024-08-03
*/
@Data
@Builder
public class RssEntity {
/**
* 订阅地址
*/
private String url;
/**
* 源名称
*/
private String webTitle;
/**
* 标题
*/
private String title;
/**
* 链接
*/
private String link;
/**
* 描述
*/
private String description;
private Date createTime;
}

View File

View File

@ -0,0 +1,54 @@
server:
# 服务端口
port: 24803
# 是否启用响应压缩,默认为 false
compression:
enabled: true
# 启用压缩的最小响应大小(字节),默认为 2048
min-response-size: 1024
tomcat:
# 最大连接数,默认为 10000
max-connections: 5000
# 最大线程数,默认为 200
max-threads: 100
# 接受队列长度,默认为 100
accept-count: 200
# 连接超时时间(毫秒),默认为 20000
connection-timeout: 30000
# 长连接的空闲超时时间(毫秒),默认为 20000
keep-alive-timeout: 30000
# 最大允许上传的文件大小(字节),默认为 2MB
max-swallow-size: 10485760
# 最大允许的单个文件大小(字节),默认为 1MB
max-file-size: 5242880
# 最大允许的请求大小(字节),默认为 10MB
max-request-size: 10485760
spring:
application:
name: rss-backend
profiles:
active: rss
web:
resources:
static-locations: classpath:/static/
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: false
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
is-share: true
# token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik
token-style: uuid
# 是否输出操作日志
is-log: false
log:
path: log/
encoder: UTF-8

View File

@ -0,0 +1,10 @@
█ █ █
█ █ █
█ █ █
█▒██▒ ▒███▒ ▒███▒ █▓██ ░███░ ▓██▒ █ ▒█ ███ █▒██▒ ██▓█
██ █ █▒ ░█ █▒ ░█ █▓ ▓█ █▒ ▒█ ▓█ ▓ █ ▒█ ▓▓ ▒█ █▓ ▒█ █▓ ▓█
█ █▒░ █▒░ █ █ █ █░ █▒█ █ █ █ █ █ █
█ ░███▒ ░███▒ ███ █ █ ▒████ █ ██▓ █████ █ █ █ █
█ ▒█ ▒█ █ █ █▒ █ █░ █░█░ █ █ █ █ █
█ █░ ▒█ █░ ▒█ █▓ ▓█ █░ ▓█ ▓█ ▓ █ ░█ ▓▓ █ █ █ █▓ ▓█
█ ▒███▒ ▒███▒ █▓██ ▒██▒█ ▓██▒ █ ▒█ ███▒ █ █ ██▓█

View File

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_CONTEXT_NAME" value="log"/>
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="${LOG_CONTEXT_NAME}"/>
<!-- 定义日志上下文的名称 -->
<contextName>${LOG_CONTEXT_NAME}</contextName>
<!-- 控制台输出 -->
<!--<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
&lt;!&ndash;格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度%msg日志消息%n是换行符&ndash;&gt;
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}:%L) - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>-->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!--1. 输出到控制台-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用只配置最底级别控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--info日志统一输出到这里-->
<appender name="file.info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名,按小时生成-->
<FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/info/info.%d{yyyy-MM-dd-HH}.%i.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 除按日志记录之外还配置了日志文件不能超过10M(默认)若超过10M日志文件会以索引0开始 -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度 %method 方法名 %L 行数 %msg日志消息%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--错误日志统一输出到这里-->
<appender name="file.error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名,按天生成-->
<FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/error/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 除按日志记录之外还配置了日志文件不能超过10M(默认)若超过10M日志文件会以索引0开始 -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度 %method 方法名 %L 行数 %msg日志消息%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 此日志文件只记录error级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--warn日志统一输出到这里-->
<appender name="file.warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}/warn/warn.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 除按日志记录之外还配置了日志文件不能超过10M(默认)若超过10M日志文件会以索引0开始 -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期%thread表示线程名%-5level级别从左显示5个字符宽度 %method 方法名 %L 行数 %msg日志消息%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{56}.%method:%L - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 日志输出级别 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="file.error"/>
<appender-ref ref="file.info"/>
<appender-ref ref="file.warn"/>
</root>
</configuration>

View File

@ -0,0 +1,51 @@
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Html测试
*
* @author yanqs
* @since 2024-08-10
*/
public class HtmlTest {
public static void main(String[] args) throws IOException{
String str="为什么温泉♨️水那么黄? <img style=\"\" src=\"https://tvax1.sinaimg.cn/large/00759jQJly1hsg8uwgmavj31401hcdy4.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax4.sinaimg.cn/large/00759jQJly1hsg8uvtqz7j31401hck91.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax2.sinaimg.cn/large/00759jQJly1hsg8ux1psqj31401z44h1.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax3.sinaimg.cn/large/00759jQJly1hsg8uxycmvj31401hc16o.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax2.sinaimg.cn/large/00759jQJly1hsg8uyn21kj31401hc4gl.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax3.sinaimg.cn/large/00759jQJly1hsg8uzeqdzj31401hck84.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax4.sinaimg.cn/large/00759jQJly1hsg8uzyy3yj3140140gz4.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax4.sinaimg.cn/large/00759jQJly1hsg8uv9nptj31401hctma.jpg\" referrerpolicy=\"no-referrer\"><br><br><img style=\"\" src=\"https://tvax3.sinaimg.cn/large/00759jQJly1hsg8v09z04j31401404by.jpg\" referrerpolicy=\"no-referrer\"><br><br>";
List<String> strings = extractImageUrls(str);
strings.forEach(obj->{
System.out.println(obj);
});
}
public static String readHtmlFile(String filePath) throws IOException {
StringBuilder content = new StringBuilder();
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String line;
while ((line = reader.readLine()) != null) {
content.append(line);
}
reader.close();
return content.toString();
}
public static List<String> extractImageUrls(String htmlContent) {
List<String> imageUrls = new ArrayList<>();
String regex = "<img\\s+[^>]*?src\\s*=\\s*['\"]([^'\"]*?)['\"][^>]*?>";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(htmlContent);
while (matcher.find()) {
String imageUrl = matcher.group(1);
imageUrls.add(imageUrl);
}
return imageUrls;
}
}

View File

@ -0,0 +1,14 @@
import cn.hutool.core.lang.Console;
import cn.hutool.extra.mail.MailAccount;
import com.bcrjl.rss.common.util.MailUtils;
/**
* @author yanqs
* @since 2024-08-09
*/
public class MailTest {
public static void main(String[] args) {
MailAccount mailAccount = MailUtils.initMailAccount();
Console.log(mailAccount);
}
}

View File

@ -0,0 +1,40 @@
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.net.URL;
/**
* RSS获取 测试类
*
* @author yanqs
*/
public class RSSReader {
public static void main(String[] args) throws Exception {
// RSS feed URL
// URL rssUrl = new URL("https://rsshub.ys.bcrjl.com/weibo/user/6489032761");
URL rssUrl = new URL("https://blog.yanqingshan.com/feed/");
// Create a DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// Parse the RSS file
Document document = builder.parse(rssUrl.openStream());
// Get all items
NodeList items = document.getElementsByTagName("item");
for (int i = 0; i < items.getLength(); i++) {
Element item = (Element) items.item(i);
Element title = (Element) item.getElementsByTagName("title").item(0);
Element link = (Element) item.getElementsByTagName("link").item(0);
// Print the title
System.out.println(title.getTextContent());
System.out.println(link.getTextContent());
}
}
}