前言
通過 Python 爬取十萬博文之后,最重要的是要讓互聯網用戶訪問到,那么如何做呢?
選型
從后台框架、前端模板、數據庫連接池、緩存、代理服務、限流等組件多個維度選型。
- 后台框架 SpringBoot2+、JPA
- 前端框架 Vue
- 模塊框架 Thymeleaf
- 數據庫連接池 HikariCP
- 緩存 Redis
- 限流 Guava
- 代理服務 Nginx
- 文章編輯 Markdown
架構
博文
我們可以通過以下方式訪問:
https://blog.52itstyle.top/49.html
亦或是:
https://blog.52itstyle.top/49.shtml
當然,如果你願意你也可以顯示為:
https://blog.52itstyle.top/49.php
https://blog.52itstyle.top/49.asp
https://blog.52itstyle.top/49.jsp
只需要在后台配置對應的映射關系即可:
/**
* 博文
*/
@RequestMapping("{id}.html")
public String blog(@PathVariable("id") Long id, ModelMap model) {
Blog blog = blogService.getById(id);
model.addAttribute("blog",blog);
return "article";
}
由於數據庫存儲的是 markedown 格式的數據,前台我們通過 editormd 轉為 html 代碼顯示,這里只展示部分代碼:
<script type='text/javascript' src='js/jquery.min.js'></script>
<!--省略部分代碼-->
<script type='text/javascript' src="editor/editormd.min.js"></script>
<!--省略部分代碼-->
<div id="article">
<textarea th:text="${blog.content}" style="display:none;" placeholder="markdown語言">
</textarea>
</div>
<!--省略部分代碼-->
<script>
editormd.markdownToHTML("article", {
htmlDecode : "style,script,iframe",
emoji : true,
taskList : true,
tex : true, // 默認不解析
flowChart : true, // 默認不解析
sequenceDiagram : true // 默認不解析
});
</script>
緩存
爬取的博文一般、基本、大概不會修改,所以我們完全可以緩存起來,避免跟數據庫直接交互,順便提升一下訪問速速。正好手頭有個 256MB 的阿里雲 Redis 服務,拿來就用了。
首相引入以下組件:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 redis:
spring.redis.database=1
spring.redis.host=r-m5e4873fd882de14.redis.rds.aliyuncs.com
spring.redis.port=6379
spring.redis.password=6347888
spring.redis.pool.max-active=8
spring.redis.pool.max-wait=-1
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.timeout=3000ms
spring.cache.type = redis
接口實現,引入 Cacheable 注解:
@Override
@Cacheable(cacheNames ="blog")
public Blog getById(Long id) {
String nativeSql = "SELECT * FROM blog WHERE id=?";
return dynamicQuery.nativeQuerySingleResult(Blog.class,nativeSql,new Object[]{id});
}
配置完成之后,我們打開數據庫配置,多次訪問博文地址,如果只是初次打印 SQL 說明配置成功:
spring.jpa.show-sql = true
限流
萬一哪天流量暴漲亦或是有人惡意攻擊,爾等小服務器根本扛不住,所以有時候我們需要一定的手段進行限流,比如限制IP訪問的頻率次數。
這里我們使用開源的第三方組件庫,引入以下組件:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
自定義注解:
/**
* 自定義注解 限流
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLimit {
/**
* 描述
*/
String description() default "";
/**
* key
*/
String key() default "";
/**
* 類型
*/
LimitType limitType() default LimitType.CUSTOMER;
enum LimitType {
/**
* 自定義key
*/
CUSTOMER,
/**
* 根據請求者IP
*/
IP
}
}
限流邏輯:
/**
* 限流 AOP
*/
@Aspect
@Configuration
public class LimitAspect {
//根據IP分不同的令牌桶, 每天自動清理緩存
private static LoadingCache<String, RateLimiter> caches = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, RateLimiter>() {
@Override
public RateLimiter load(String key){
// 新的IP初始化 每秒只發出5個令牌
return RateLimiter.create(5);
}
});
//Service層切點 限流
@Pointcut("@annotation(com.itstyle.blog.common.limit.ServiceLimit)")
public void ServiceAspect() {
}
@Around("ServiceAspect()")
public Object around(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
ServiceLimit limitAnnotation = method.getAnnotation(ServiceLimit.class);
ServiceLimit.LimitType limitType = limitAnnotation.limitType();
String key = limitAnnotation.key();
Object obj;
try {
if(limitType.equals(ServiceLimit.LimitType.IP)){
key = IPUtils.getIpAddr();
}
RateLimiter rateLimiter = caches.get(key);
Boolean flag = rateLimiter.tryAcquire();
if(flag){
obj = joinPoint.proceed();
}else{
throw new RrException("小同志,你訪問的太頻繁了");
}
} catch (Throwable e) {
throw new RrException("小同志,你訪問的太頻繁了");
}
return obj;
}
}
收錄
完事具備,就差被搜索引擎收錄了,我們可以通過手動生成網站地圖,提交給百度。
/**
* 生成地圖
* 參見:https://blog.52itstyle.top/sitemap.xml
*/
@Component
public class SitemapTask {
@Autowired
private DynamicQuery dynamicQuery;
protected Logger logger = LoggerFactory.getLogger(getClass());
@Value("${blog.url}")
private String blogUrl;
//每天23點執行一次
@Scheduled(cron = "0 0 23 * * ?")
public void createSitemap() {
logger.info("定時提交百度收錄開始");
StringBuffer xml = new StringBuffer();
xml.append("<?xml version='1.0' encoding='utf-8'?>\n");
xml.append("<urlset>\n");
String nativeSql = "SELECT id,create_time FROM blog";
List<Object[]> list = dynamicQuery.query(nativeSql,new Object[]{});
list.forEach(blog -> {
String url = blogUrl+blog[0]+".html";
xml.append(" <url>\n");
xml.append(" <loc>"+url+"</loc>\n");
xml.append(" <lastmod>"+blog[1]+"</lastmod>\n");
xml.append(" </url>\n");
});
xml.append("</urlset>\n");
saveAsFileWriter(xml.toString());
logger.info("定時提交百度收錄結束");
}
private static void saveAsFileWriter(String content) {
String path = ClassUtils.getDefaultClassLoader().getResource("").getPath();
String filePath = path + "static"+ SystemConstant.SF_FILE_SEPARATOR+"sitemap.xml";
FileWriter fwriter = null;
try {
fwriter = new FileWriter(filePath, false);
fwriter.write(content);
} catch (IOException ex) {
ex.printStackTrace();
} finally {
try {
fwriter.flush();
fwriter.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
打包
盡量不要以Jar包形式部署,為了以后方便部署,最好放置到 外置Tomcat 下。
pom.xml 中移除內置 Tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
修改啟動類:
/**
* 啟動類
* 創建者 科幫網
* 創建時間 2019年7月21日
*/
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class Application extends SpringBootServletInitializer {
private static final Logger logger = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
logger.info("項目啟動");
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
代理
項目部署后,最好加一層代理服務,這里我們使用Nginx:
server {
listen 80;
server_name blog.52itstyle.top;
return 301 https://$server_name$request_uri;
}
server{
listen 443 ssl;
server_name blog.52itstyle.top;
#證書路徑
ssl_certificate /usr/local/openresty/nginx/cert/2543486_blog.52itstyle.top.pem;
#私鑰路徑
ssl_certificate_key /usr/local/openresty/nginx/cert/2543486_blog.52itstyle.top.key;
#緩存有效期
ssl_session_timeout 5m;
#可選的加密算法,順序很重要,越靠前的優先級越高.
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
#安全鏈接可選的加密協議
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
location = /500.html {
root /usr/local/openresty/nginx/html;
}
error_page 500 502 503 504 = /503/503.html;
location / {
proxy_pass http://127.0.0.1:8080;
}
location ~ /\.ht {
deny all;
}
}
動靜分離,將靜態文件交由Nginx處理,加速博客訪問:
#靜態文件交給nginx處理
location ~ .*\.(js|css|gif|jpg|jpeg|png|bmp)?$
{
root /home/tomcat8/webapps/ROOT/WEB-INF/classes/static;
expires 2h;
}
源碼:https://gitee.com/52itstyle/Python
列表:https://blog.52itstyle.top/index
詳情:https://blog.52itstyle.top/49.shtml
小結
擼完整個項目,基本能接觸的都用上了,前后端框架、連接池、限流、緩存、動靜分離,HTTPS安全認證、百度收錄等等,特別適合有一定開發基礎的小伙伴!