github链接 https://github.com/yt-xy/Django-blog
统计的几种方式
·基于当次访问后后端实时处理
·基于当次访问后端延迟处理–Celery(分布式任务队列)
·前端通过JavaScript埋点或者img标签来统计
·基于Nginx日志分析来统计
文章访问统计
第一种方式除了性能问题以外,还有被刷的问题。如果有人连续刷页面,不应该累计PV,因为这种情况是无效访问。另外,对于UV来说,我们需要根据日期来处理,一个用户每天访问每天访问某一篇文章,始终应该只能增加一个UV,不然UV的统计就没意义了
区分用户:
·根据用户的IP和浏览器类型等一些信息生成MD5来标记这个用户:用户可能会重合,同一个IP下可能有非常多用户
·系统生成唯一的用户id,并将其放置到cookie中:基于浏览器,可以生成唯一的id来标识用户,但如果更换浏览器,就会产生新的用户
·让用户登录:实施难度较大,对于内容型网站,没人会登录之后才来看文章
考虑:需要在用户访问时记录用户的访问数据,这些数据应该放到缓存中,因为都是临时数据,并且特定时间就会过期
·如何生成唯一的用户id:使用Python内置的uuid库来生成
·在哪一步给用户配置id:在一个web系统中,显示是在请求的越早阶段鉴定/标记用户越好。因此,对于Django我们放到middleware中来做
·使用什么缓存:可以直接使用Django提供的缓存接口。Django缓存在后端支持多种配置,比如memcache、MySQL、文件系统、内存,以及很多第三方插件来对Redis做支持
Django的middleware在项目启动时会被初始化,等接受请求之后,会根据setting中的MIDDLEWARE配置顺序挨个调用,传递request作为参数
在接受请求之后,先生成uid,然后把uid赋值给request对象。因为request是一个类的实例,可以动态赋值,因此我们动态给其添加uid属性,这样在后面的View中就可以拿到uid并使用,最后返回response时,我们设置cookie,并且设置为httponly(即只在服务端能访问),这样用户再次请求时,就会带上同样的uid信息
blogs/middleware/user_id.py
import uuid
USER_KEY = 'uid'
TEN_YEARS = 60 * 60 * 24 * 365 * 10
class UserIDMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
uid = self.generate_uid(request)
request.uid = uid
response = self.get_response(request)
response.set_cookie(USER_KEY, uid, max_age=TEN_YEARS, httponly=True) # 只在服务端能访问
return response
def generate_uid(self, request):
try:
uid = request.COOKIES(USER_KEY)
except KeyError:
uid = uuid.uuid4().hex
return uid
blog/settings/base.py
MIDDLEWARE = [
'blogs.middleware.user_id.UserIDMiddleware',
...
]
blogs/views.py
from datetime import date
from django.core.cache import cache
from django.db.models import Q, F
class PostDetailView(CommonViewMixin, DetailView):
...
def handle_visited(self):
increase_pv = False
increase_uv = False
uid = self.request.uid
pv_key = 'pv:%s:%s' % (uid, self.request.path)
uv_key = 'uv:%s:%s:%s' % (uid, str(date.today()), self.request.path)
if not cache.get(pv_key):
increase_pv = True
cache.set(pv_key, 1, 1*60) # 1分钟有效
if not cache.get(uv_key):
increase_uv = True
cache.set(uv_key, 1, 24*60*60) # 24小时有效
if increase_pv and increase_uv:
Post.objects.filter(pk=self.object.id).update(pv=F('pv') + 1, uv=F('uv') + 1)
elif increase_pv:
Post.objects.filter(pk=self.object.id).update(pv=F('pv') + 1)
elif increase_uv:
Post.objects.filter(pk=self.object.id).update(uv=F('uv') + 1)
实现RSS输出
RSS(Really Simple Syndication,简易信息聚合)用来提供订阅接口,让网站用户可以通过RSS阅读器订阅我们的网站,在有更新时,RSS阅读器会自动获取最新内容,网站用户可以在RSS阅读器中看到最新的内容,从而避免每次都需要打开网站才能看到是否有更新
blogs/rss.py
from django.contrib.syndication.views import Feed
from django.urls import reverse
from django.utils.feedgenerator import Rss201rev2Feed
from blogs.models import Post
class ExtendedRSSFeed(Rss201rev2Feed):
def add_item_elements(self, handler, item):
super(ExtendedRSSFeed, self).add_item_elements(handler, item)
handler.addQuickElement('content:html', item['content_html'])
class LatestPostFeed(Feed):
feed_type = ExtendedRSSFeed
title = 'Blog'
link = '/rss'
description = 'Blog by django'
def items(self):
return Post.objects.filter(status=Post.STATUS_NORMAL)[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return item.desc
def item_link(self, item):
return reverse('post-detail', args=[item.pk])
def item_extra_kwargs(self, item):
return {'content_html': self.item_content_html(item)}
def item_content_html(self, item):
return item.content_html
实现sitemap
sitemap的实现跟Feed类似,都是输出文章列表,但是格式和内容均不相同
blogs/sitemap.py
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from .models import Post
class PostSitemap(Sitemap):
changefreq = 'always'
priority = 1.0
protocol = 'https'
def items(self): # 返回所有正常状态的文章
return Post.objects.filter(status=Post.STATUS_NORMAL)
def lastmod(self, obj): # 返回每篇文章的创建时间(或者最近更新时间)
return obj.created_time
def location(self, obj): # 返回每篇文章的URL
return reverse('post-detail', args=[obj.pk])
编写好sitemap数据处理的代码后,再来编写对应的模板
{% spaceless %}
的作用是去除多余的空行,因为在Django模板中使用for循环会产生很多空行
blog/themes/bootstrap/templates/sitemap.xml
<?xml version="1.0" encoding="UTF-8" ?>
<urlset
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
{% spaceless %}
{% for url in urlset %}
<url>
<loc>{{ url.location }}</loc>
{% if url.lastmod %}<lastmod>{{ url.lastmod|date:'Y-m-d' }}</lastmod>{% endif %}
{% if url.changefreq %}<changefreq>{{ url.changefreq }}</changefreq>{% endif %}
{% if url.priority %}<priority>{{ url.priority }}</priority>{% endif %}
<news:news>
{% if url.item.created_time %}<news:publication_date>{{ url.item.create_time|date:"Y-m-d" }}</news:publication_date>{% endif %}
{% if url.item.tags %}<news:keywords>{{ url.item.tags }}</news:keywords>{% endif %}
</news:news>
</url>
{% endfor %}
{% endspaceless %}
</urlset>
上面用到的url.item.tags需做下支持,因为我们的Post模型有tag这样一个多对多的关联,所以可以在模型中增加一个属性来输出配置好的tags
blogs/models.py
from django.utils.functional import cached_property
class Post(models.Model):
...
@cached_property
def tags(self):
return ','.join(self.tag.values_list('name', flat=True))
配置RSS和sitemap的urls.py
blog/urls.py
from django.contrib.sitemaps import views as sitemap_views
from blogs.rss import LatestPostFeed
from blogs.sitemap import PostSitemap
urlpatterns = [
...
url(r'^rss|feed/', LatestPostFeed(), name='rss'),
url(r'^sitemap\.xml$', sitemap_views.sitemap, {'sitemaps': {'posts': PostSitemap}}),
]