Python三百行代码实现一简约个人博客网站(全网最小巧)

这是全互联网最小巧的博客,没有比这更小的了。虽然小巧,但功能一点儿也不弱,支持文章的分页展示,文章表格,图片和代码语法高亮。文章无限制分类,访问量统计,按时间和按点击量排序,展示最新文章,最热文章,文章留言评论等功能。

前言

如果你也想拥有一个属于自己的个性博客,这种尝试将大幅降低准入门槛,让你看到实现一个个人博客网站是多么的简单。其它的又是建库建表的,或是需要登录管理后台管理,我还是觉得不够简单好用。

用这种写好的markdown文档的形式,你可以直接把markdown笔记变成动态的博客展示出来。后续可基于微信小程序做个入口,如果想要发布文章,则直接把编写好的markdown文件提交发送过去就可以啦,操作使用上更加简单和快捷。这才应该是现在博客该有的特色和创新,原有的旧的登录网站后台编辑和使用的方式,太落后了。

况且使用这种方式还有个好处,可以方便多人同时使用。比如可以在公司内部搭建一个公共博客平台,做个简单入口界面,让每个人都可以往上面提交文档,这也是个不错的内部方案交流平台和技术分享平台。相比传统word文档的方式优点很多,方便文档的查看、归档和线上展示及分享,连留言评论都有啦。

项目开源地址:

tiny-blog: 极小的个人博客,虽然很小,但是很全。使用python+flask+html实现。

GitHub – yangyongzhen/tiny-blog 

体验地址:

我的个人博客

实现原理

其实就是一个基于markdown的轻量级博客,需要按一定格式编写好markdown文件,提交到后台服务。当用户访问网页时由后台读取文件内容并经由流行的markdown-it组件渲染。

后台使用python的flask框架,整个代码仅三百多行。相当的小巧,很容易根据需要修改源码去增加一些其他的功能。当然一个css或js文件都不可能这么短,这里仅指的是后台代码。网页使用了流行的markdown-it(MarkDown渲染),highlight.js(语法高亮)和mermaid.js(画流程图、时序图等的js库)组件。留言评论功能,使用比较火的Valine博客评论系统。

网站模板我是从素材火网站上找了一个好看的博客网站模板。如果你有中意的其他博客网站模板,也可以很容易的改造过来用。文末介绍和分享几个好看的博客模板。

效果展示:

Flask介绍

Flask是一个轻量级的Python Web框架,用于构建Web应用程序和API。它基于Werkzeug工具箱和Jinja2模板引擎,并且具有灵活性和可扩展性。

Flask提供了一些基本的功能,例如路由、请求和响应处理、会话管理等,同时支持各种扩展,可以轻松添加身份验证、数据库集成、邮件发送等功能。由于其简单的学习曲线和活跃的社区支持,Flask已成为最受欢迎的Python Web框架之一。

源码实现

源码实现很简单,仅有三个文件,总代码量三百多行。为了提高访问速度,设计了一种基于字典map的简单数据结构。只有应用启动时加载一次文件,后续文章访问直接从字典数据中取数据,避免每次访问都读文件,提高访问效率。为了方便的查看数据结构,加载完文件后保存了几个json格式的文件。通过json文件很容易看出来。

发表说说和发表文章只需要进入admin界面,输入授权码,任何人都能发表说说和提交文章。

默认授权码111111,你也可以提交个文章进去看看效果。

main.py

# encoding: utf-8
# author:yangyongzhen
from flask import  Flask,render_template,request,redirect
import articles
import atexit
import json
import sys

app=Flask(__name__,template_folder="templates", static_folder="static", static_url_path="/static")

# 首页
@app.route('/')
def index():
    articles.Stat.totalVisit += 1
    page = 1
    page = request.args.get('page',1,int)
    print(page)
    nums = len(articles.AllArts)
    print(nums)
    allpage = nums / 5
    if nums %5 != 0:
        allpage = int(nums/5) + 1
    print(allpage)

    curArts = articles.AllArts
    if (page * 5) < nums :
        curArts = articles.AllArts[(page-1)*5 : page*5]
    else:
        curArts = articles.AllArts[(page-1)*5 : nums]
    #分页表
    tabs = []
    for i in range(0,allpage):
        print(i)
        tabs.append(i)
    print(tabs)
    return render_template("index.html",arts=curArts,news=articles.NewArts[:9],hots=articles.HotArts[:9],items=articles.ItemList,curPage=page,tab=tabs)  #加入变量传递

@app.route('/about')
def about():
    msg="my name is caojianhua, China up!"
    return render_template("about.html",data=msg)  #加入变量传递

#分类页
@app.route('/items')
def items():
    try:
        id = request.args.get('id')
        print(id)
        allArts = []
        allMaps = articles.ItemMap[id]
        #遍历 hash
        for v in allMaps.values():
           allArts.append(v) 
        #按日期排序
        allArts.sort(key=lambda x: x['date'],reverse=True)
        #print(allArts)
        page = 1
        page = request.args.get('page',1,int)
        print(page)
        nums = len(allArts)
        print(nums)
        allpage = int(nums / 5)
        if nums %5 != 0:
            allpage = int(nums/5) + 1
        print(allpage)
        curArts = allArts
        if (page * 5) < nums :
            curArts = allArts[(page-1)*5 : page*5]
        else:
            curArts = allArts[(page-1)*5 : nums]
        #分页表
        tabs = []
        for i in range(0,allpage):
            #print(i)
            tabs.append(i)
        #print(tabs)
        return render_template("items.html",arts=curArts,item=id,news=articles.NewArts[:9],hots=articles.HotArts[:9],curPage=page,tab=tabs) 
    except Exception as e:
        print("Exception occured")
        print(e)
        return render_template('404.html')

# 文章详情页
@app.route('/article_detail')
def article_detail():
    art = articles.Article("","","","","","","","",0,0)
    item_index = 0
    try:
        id = request.args.get('id')
        #print(id)
        #取出分类
        item = articles.ArtRouteMap[id]['item']
        print(item)
        art = articles.ItemMap[item][id]  
        #art_index  = NewPosts.index(id)
        articles.Stat.artStat[id]['visitCnt'] += 1
        articles.Stat.totalVisit += 1
        articles.ItemMap[item][id]['visitCnt'] = articles.Stat.artStat[id]['visitCnt']
        #print(art)
        #print(item_index)
        #print(art_index)
        return render_template("article_detail.html",data=art,news=articles.NewArts[:9],hots=articles.HotArts[:9] ) 
    except Exception as e:
        print("Exception occured")
        return render_template('404.html')
   
# 说说页面  
@app.route('/moodList')
def moodList():
    msg="my name is caojianhua, China up!"
    return render_template("moodList.html",data=msg) 

# 留言页面 
@app.route('/comment')
def comment():
    msg="my name is caojianhua, China up!"
    return render_template("comment.html",data=msg) 

@app.route('/404')
def error_404():
    print("error_404")
    return render_template("404.html") 

@app.errorhandler(404)
def page_not_found(e):
    print("page_not_found")
    print(e)
    return render_template('404.html'), 404

@app.errorhandler(Exception)
def handle_exception(e):
    # 捕获其他异常
    print("handle_exception")
    print(e)
    return render_template('404.html'), 500
 
def saveData():
    #保存访问量数据       
    with open('statistic.json1', 'w',encoding='utf-8') as f:
        json.dump(articles.Stat.__dict__, f,ensure_ascii=False)
           
# 监控退出时保存统计信息
@atexit.register
def exit_handler():
    print("应用程序退出")
    saveData()

if __name__=="__main__":
    #启动时加载访问量数据
    articles.getPosts()
    
    app.run(port=8000,host="127.0.0.1",debug=False)
    print("over")

articles.py

# encoding: utf-8
# author:yangyongzhen
import glob
import os
import pprint
import hashlib
import json
import statistic

#访问量阅读量统计信息
Stat = statistic.AllStat()

# AllArts 总的文章,(按时间排过序的)
AllArts = []
# NewArts 最新文章
NewArts = []
# HotArts 热门文章
HotArts = []
# 文章分类信息
ItemList = []

ItemMap = {}
ArtRouteMap = {}

#文章信息类定义
class Article(object):
    def __init__(self, id, item, title, date, summary, body, imgFile, author, cmtCnt, visitCnt):
        self.id = id
        self.item = item
        self.title = title
        self.date = date
        self.summary = summary
        self.body = body
        self.imgFile = imgFile
        self.author = author
        self.cmtCnt = cmtCnt
        self.visitCnt = visitCnt
    
    def mPrint(self):
        print(self.id)
        print(self.item)
        print(self.title)
        print(self.date)
        print(self.summary)
        print(self.body)
        print(self.imgFile)
        print(self.author)


class ArticlesData():
    def __init__(self):
        self.item = ""
        self.articlesMap = {}


# ArticleRoute 文章的分类和路径信息
class ArticleRoute:
    def __init__(self, item, name):
        self.item = item
        self.name = name

#文章分类 是个集合类型,无重复元素
class ItemCfg:
    def __init__(self):
        self.items = set()
        
        
def strTrip(src):
    str = src.replace(" ", "")
    str = str.replace("\r", "")
    str = str.replace("\n", "")
    return str


#从文件中读取信息,并返回itemMap,artRouteMap,items
def getPosts():
    global ItemMap,ArtRouteMap,ItemList,NewArts
    # 打开访问量统计JSON文件并读取内容
    with open('statistic.json1', 'r',encoding='utf-8') as file:
        json_data = file.read()
        # 将JSON字符串反序列化为字典对象
        Stat.__dict__ = json.loads(json_data)
        print(Stat.totalVisit)
    # 获取"posts/"目录下的所有文件
    files = glob.glob("posts/*")
    # 按修改日期进行排序
    files = sorted(files, key=lambda x: os.path.getmtime(x))
    articleMap = {}
    itemMap = {}
    artRouteMap = {}
    #文章分类,是个集合类型,无重复
    items = set()
    # 遍历文件列表
    for i, f in enumerate(files):
        #print(i)
        #print(f)

        file = f
        file = os.path.basename(file) # 转换路径分隔符
        print(file)
        file = os.path.splitext(file)[0]  # 移除".md"后缀
        fileread = open(f, 'r',encoding='utf-8').read()
        lines = fileread.split('\n')
        #print(lines)
        title = lines[0].strip()
        date = strTrip(lines[1].strip())
        summary = lines[2].strip()
        imgfile = lines[3].strip()
        id = hashlib.md5(file.encode(encoding='UTF-8'))
        id = id.hexdigest()
        #print(id.hexdigest())
        item = strTrip(lines[4].strip())
        author = lines[5].strip()
        imgfile = strTrip(imgfile)
        body = '\n'.join(lines[6:])  # 获取第6行及以后的行
        ar = ArticleRoute(item, title)
        artRouteMap[id] = ar.__dict__
        visitCnt = 0
        if id in Stat.artStat:
           visitCnt = Stat.artStat[id]['visitCnt']
           #print(visitCnt)
        else:
            Stat.artStat[id] = statistic.ArtStat(title,visitCnt,0).__dict__
        post = Article(id,item,title, date, summary, body, imgfile, author, 0,visitCnt)
        articleMap[id] = post
        items.add(item)
    #json_data = json.dumps(posts,ensure_ascii=False)
    #print(json_data)
    itemList = []
    for itm in items:
        itmMp= {}
        for v in articleMap.values():
            if(v.item == itm):
                itmMp[v.id] = v.__dict__
        itemMap[itm] = itmMp
        itemList.append(itm)
            
        
    #保存为json文件
    with open('Articles.json1', 'w',encoding='utf-8') as f:
        json.dump(itemMap, f,ensure_ascii=False)
        
    with open('artRouteMap.json1', 'w',encoding='utf-8') as f:
        json.dump(artRouteMap, f,ensure_ascii=False)
    itemList.sort()
    
    #遍历字典
    for key, val in itemMap.items():
        for idx,art in val.items():
            AllArts.append(art)
            HotArts.append(art)        
    #按日期排序
    AllArts.sort(key=lambda x: x['date'],reverse=True)
    #按访问量排序
    HotArts.sort(key=lambda x: x['visitCnt'],reverse=True)
    NewArts = AllArts
    ItemList = itemList
    #print((HotArts[0]))
    ItemMap = itemMap
    ArtRouteMap = artRouteMap
    return itemMap,artRouteMap,itemList

if __name__=="__main__":
    p,p1,items = getPosts()
    #print(p)
    print(type(p))
    print(type(p1))
    print(p['go学习笔记'])
    print(p1['3eaf98ad2b949b3f93d16aeaa1f45ab0'])
    print(items)

statistic.py

# encoding: utf-8
# author:yangyongzhen
# 文章和总阅读量统计

#文章访问量统计
#param:名称,访问量,评论量
class ArtStat:
    def __init__(self, title,visitCnt,cmtCnt):
        self.title = title
        self.visitCnt = visitCnt
        self.commentCnt = cmtCnt
  
#总的统计        
class AllStat:
    def __init__(self):
        #总访问量
        self.totalVisit = 0
        self.artStat = {}


if __name__=="__main__":
    pass

如何使用

除了flask,无其它依赖。下载源码,pip install flask安装好依赖。然后python main.py即可运行起来。编写新文章,需要按照特定格式写前面几行。因为文章作者,文章分类和日期等信息,都在一个文件里写的。

文章内容格式如下(test.md):

This is a title!
2020-10-22
你好aaaaaaaasssssssssssssssss! 这是你第一次使用 **Markdown编辑器** 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
08.jpg
感悟
yangyongzhen


This is the main post!

# Markdown1!
## Markdown2!
### 欢迎使用Markdown编辑器

Name    | Age
--------|------
Bob     | 27
Alice   | 23

你好! 这是你第一次使用 **Markdown编辑器** 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

```golang
package main

import "fmt"

func main() {
	fmt.Println("hello ")
}
```

网页前端小技巧 

导航栏选中高亮

网站头部导航栏根据选择自动高亮显示,代码在silder.js中实现:

var obj=null;
var As=document.querySelector(".w_header_nav").getElementsByTagName('a');
obj = As[0];
for(i=1;i<As.length;i++){if(window.location.href.indexOf(As[i].href)>=0)
obj=As[i];}
console.log(obj)
// 添加class属性
obj.classList.add("active");

返回到顶部

返回到顶部,可复用js代码:

var $backToTopEle=$('<a href="javascript:void(0)" class="Hui-iconfont toTop" title="返回顶部" alt="返回顶部" style="display:none">^^</a>').appendTo($("body")).click(function(){
	$("html, body").animate({ scrollTop: 0 }, 120);
});
var backToTopFun = function() {
	var st = $(document).scrollTop(), winh = $(window).height();
	(st > 0)? $backToTopEle.show(): $backToTopEle.hide();
	/*IE6下的定位*/
	if(!window.XMLHttpRequest){
		$backToTopEle.css("top", st + winh - 166);
	}
};
	$(function(){
		$(window).on("scroll",backToTopFun);
		backToTopFun();
	});

分页显示js代码 :

/* 
*page plugin 1.0 
*/
(function ($) {
  //默认参数
  var defaults = {
    totalPages: 9,//总页数
    liNums: 9,//分页的数字按钮数(建议取奇数)
    activeClass: 'active' ,//active类
    firstPage: '首页',//首页按钮名称
    lastPage: '末页',//末页按钮名称
    prv: '«',//前一页按钮名称
    next: '»',//后一页按钮名称
    hasFirstPage: true,//是否有首页按钮
    hasLastPage: true,//是否有末页按钮
    hasPrv: true,//是否有前一页按钮
    hasNext: true,//是否有后一页按钮
    callBack : function(page){
        //回掉,page选中页数
    }
  };

  //插件名称
  $.fn.Page = function (options) {
    //覆盖默认参数
    var opts = $.extend(defaults, options);
    //主函数
    return this.each(function () {
      var obj = $(this);
      var l = opts.totalPages;
      var n = opts.liNums;
      var active = opts.activeClass;
      var str = '';
      var str1 = '<li><a href="javascript:" class="'+ active +'">1</a></li>';
      if (l > 1&&l < n+1) {
        for (i = 2; i < l + 1; i++) {
          str += '<li><a href="javascript:">' + i + '</a></li>';
        }
      }else if(l > n){
        for (i = 2; i < n + 1; i++) {
          str += '<li><a href="javascript:">' + i + '</a></li>';
        }
      }
      var dataHtml = '';
      if(opts.hasNext){
          dataHtml += '<div class="next fr">' + opts.next + '</div>';
      }
      if(opts.hasLastPage){
          dataHtml += '<div class="last fr">' + opts.lastPage + '</div>';
      }
          dataHtml += '<ul class="pagingUl">' + str1 + str + '</ul>';
      if(opts.hasFirstPage){
          dataHtml += '<div class="first fr">' + opts.firstPage + '</div>';
      }
      if(opts.hasPrv){
          dataHtml += '<div class="prv fr">' + opts.prv + '</div>';
      }
      
      obj.html(dataHtml).off("click");//防止插件重复调用时,重复绑定事件
      
      obj.on('click', '.next', function () {
        var pageshow = parseInt($('.' + active).html());
        var nums,flag;
        var a = n % 2;
        if(a == 0){
          //偶数
          nums = n;
          flag = true;
        }else if(a == 1){
          //奇数
          nums = (n+1);
          flag = false;
        }
        if(pageshow >= l) {
          return;
        }else if(pageshow > 0&&pageshow <= nums/2){
          //最前几项
          $('.' + active).removeClass(active).parent().next().find('a').addClass(active);
        }else if((pageshow > l-nums/2&&pageshow < l&&flag==false)||(pageshow > l-nums/2-1&&pageshow < l&&flag==true)){
          //最后几项
          $('.' + active).removeClass(active).parent().next().find('a').addClass(active);
        }else{
          $('.' + active).removeClass(active).parent().next().find('a').addClass(active);
          fpageShow(pageshow+1);
        }
        opts.callBack(pageshow+1);
      });
      obj.on('click', '.prv', function () {
        var pageshow = parseInt($('.' + active).html());
        var nums = odevity(n);
        if (pageshow <= 1) {
            return;
        }else if((pageshow > 1&&pageshow <= nums/2)||(pageshow > l-nums/2&&pageshow < l+1)){
          //最前几项或最后几项
          $('.' + active).removeClass(active).parent().prev().find('a').addClass(active);
        }else {
          $('.' + active).removeClass(active).parent().prev().find('a').addClass(active);
          fpageShow(pageshow-1);
        }
        opts.callBack(pageshow-1);
      });

      obj.on('click', '.first', function(){
        var activepage = parseInt($('.' + active).html());
        if (activepage <= 1){
          return
        }//当前第一页
        opts.callBack(1);
        fpagePrv(0);
      });
      obj.on('click', '.last', function(){
        var activepage = parseInt($('.' + active).html());
        if (activepage >= l){
          return;
        }//当前最后一页
        opts.callBack(l);
        if(l>n){
          fpageNext(n-1);
        }else{
          fpageNext(l-1);
        }
      });

      obj.on('click', 'li', function(){
        var $this = $(this);
        var pageshow = parseInt($this.find('a').html());
        var nums = odevity(n);
        opts.callBack(pageshow);
        if(l>n){
          if(pageshow > l-nums/2&&pageshow < l+1){
            //最后几项
            fpageNext((n-1)-(l-pageshow));
          }else if(pageshow > 0&&pageshow < nums/2){
            //最前几项
            fpagePrv(pageshow-1);
          }else{
            fpageShow(pageshow);
          }
        }else{
          $('.' + active).removeClass(active);
          $this.find('a').addClass(active);
        }
      });

          //重新渲染结构
        /*activePage 当前项*/
        function fpageShow(activePage){
          var nums = odevity(n);
          var pageStart = activePage - (nums/2-1);
          var str1 = '';
          for(i=0;i<n;i++){
            str1 += '<li><a href="javascript:" class="">' + (pageStart+i) + '</a></li>'
          }
          obj.find('ul').html(str1);
          obj.find('ul li').eq(nums/2-1).find('a').addClass(active);
        }
        /*index 选中项索引*/
        function fpagePrv(index){
          var str1 = '';
          if(l>n-1){
            for(i=0;i<n;i++){
              str1 += '<li><a href="javascript:" class="">' + (i+1) + '</a></li>'
            }
          }else{
            for(i=0;i<l;i++){
              str1 += '<li><a href="javascript:" class="">' + (i+1) + '</a></li>'
            }
          }
          obj.find('ul').html(str1);
          obj.find('ul li').eq(index).find('a').addClass(active);
        }
        /*index 选中项索引*/
        function fpageNext(index){
          var str1 = '';
          if(l>n-1){
            for(i=l-(n-1);i<l+1;i++){
              str1 += '<li><a href="javascript:" class="">' + i + '</a></li>'
            }
           obj.find('ul').html(str1);
           obj.find('ul li').eq(index).find('a').addClass(active);
          }else{
            for(i=0;i<l;i++){
              str1 += '<li><a href="javascript:" class="">' + (i+1) + '</a></li>'
            }
           obj.find('ul').html(str1);
           obj.find('ul li').eq(index).find('a').addClass(active);
          }
        }
        //判断liNums的奇偶性
        function odevity(n){
          var a = n % 2;
          if(a == 0){
            //偶数
            return n;
          }else if(a == 1){
            //奇数
            return (n+1);
          }
        }
    });
  }
})(jQuery);

使用方法,在需要分页展示的文章下方添加以下div标签:

<div class="page">
</div>

 在网页头部引入以下js:

<script type="text/javascript" src="plugin/fenye.page.js"></script>
<script type="text/javascript">
$(function(){
	$("#page").Page({
	  totalPages: 7,//分页总数
	  liNums: 5,//分页的数字按钮数(建议取奇数)
	  activeClass: 'activP', //active 类样式定义
	  callBack : function(page){
			//console.log(page)
	  }
  });
})
</script>

markdown文档渲染

<ul class="info>
<div class="md_render">
</div>
</ul>
	<script>
		var md;
		var defaults = {
		html: false, // Enable HTML tags in source
		xhtmlOut: false, // Use '/' to close single tags (<br />)
		breaks: false, // Convert '\n' in paragraphs into <br>
		langPrefix: 'language-', // CSS language prefix for fenced blocks
		linkify: true, // autoconvert URL-like texts to links
		typographer: true, // Enable smartypants and other sweet transforms
		// options below are for demo only
		_highlight: true,
		_strict: false,
		_view: 'html' // html / src / debug
		};
		defaults.highlight = function (str, lang) {
		var esc = md.utils.escapeHtml;
		
		if (lang && hljs.getLanguage(lang)) {
			try {
			return '<pre class="hljs"><code>' +
					hljs.highlight(lang, str, true).value +
					'</code></pre>';
			} catch (__) {}
		}
		return '<pre class="hljs"><code>' + esc(str) + '</code></pre>';
		};
		md = window.markdownit(defaults).use(window.markdownitCheckbox).use(window.markdownitEmoji).use(window.markdownitFootnote);
		var dat = document.querySelector('#enrolled_id').value;
		$('.md_render').html(md.render(dat));
	</script>

 这里有个坑需要注意,jinjia2模版是在后端渲染的,所以在网页的js代码中想使用模版变量不是那么美,不能像网页中{ {data}}的方式使用。这里采用了一种取巧的方式,通过一个隐藏标签,先把需要的内容在html网页中渲染出来,再在js中通过操作dom的方式去获取。

<input type="hidden"  id="enrolled_id" value="{{ data.body }}">

其他优秀的个人博客推荐

二猫blog-一个简单的Blog站点

POETIZE

樱花庄的白猫 | ねこ・しろ・ましろ

loading…

 Fomalhaut🥝

Twikoo评论系统配置教程(基于腾讯云开发) | 张洪Heo

LinkinStar’s Blog

其他资源

(自适应手机端)响应式个人博客网站模板

我的博客模版

JSON在线 | JSON解析格式化—SO JSON在线工具

风宇个人博客

丰富的计算机技术分享,多样化的互联网资源分享,第1页 – 吱吱工具箱butterPig

POETIZE

loading…

Fomalhaut🥝

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年12月5日
下一篇 2023年12月5日

相关推荐