torinaブログ

DjangoとBootstrap4で作成したブログ
Python, Django, Kivy, Bootstrap, Apache等のメモです
ソースコード

Djangoでシンプルなアクセスカウンターもどき

Python Django
2016年6月6日5:28
シンプルでガバガバなアクセスカウンターを作ります。作ってみて気づきましたが、どちらかというとアクセスログですね。

先に書いておきますが今回のアクセスカウンターもどきはパフォーマンス面ではよろしくないです。
ある程度のアクセスに耐えるものを作る場合は、他の方法を試すべきでしょう。

「django-hitcount」などのサードパーティ製ライブラリを利用してもよさそうです。
https://github.com/thornomad/django-hitcount
http://django-hitcount.readthedocs.io/en/latest/


今回は勉強もかねて、こつこちじみちに作ります。


トップ画面はこのように。


詳細ページに行くと、以下のようにアクセス数やIPが表示されます。


この詳細ページにアクセスすると、ちゃんとカウントが増えます。


Python3.4 Django1.9です。
プロジェクト名は「accesscounter」
アプリケーション名は「main」
です。

settings.py
いつものように、INSTALLED_APPSへの登録など、基本的なことをしておきます。
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'main',
)


/でアプリケーションのトップ画面へ
/detail/:id で詳細ページへアクセスさせます。

accesscounter/accesscounter/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls import include, url
from django.contrib import admin


urlpatterns = [
    url(r'^admin/', include(admin.site.urls)),
    url(r'^', include('main.urls', namespace='main')),
]

accesscounter/main/urls.py
from django.conf.urls import url
from . import views


urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^detail/(?P<post_id>\d+)/$', views.detail, name='detail'),
]



accesscounter/main/admin.py
from django.contrib import admin
from main.models import Post, Counter

admin.site.register(Post)
admin.site.register(Counter)  # なくてもいい



accesscounter/main/models.py
from datetime import date
from django.db import models


class Post(models.Model):
    """ ブログの記事 """

    title = models.CharField(max_length=255)
    text = models.TextField()

    def __str__(self):
        return self.title

    def counter_today(self):
        return Counter.objects.filter(access_at=date.today(), post=self)

    def counter_today_unique(self):
        return Counter.objects.filter(
            access_at=date.today(), post=self).values('ip').distinct()


class Counter(models.Model):
    """ アクセスカウンター """

    ip = models.GenericIPAddressField()
    access_at = models.DateField(auto_now_add=True)
    post = models.ForeignKey(Post)

    def __str__(self):
        return "{0},{1},{2}".format(self.ip, self.post, self.access_at)



アクセスカウンターは、ipと、アクセス日時と、どの記事のアクセスか?を指定します。
class Counter(models.Model):
    """ アクセスカウンター """

    ip = models.GenericIPAddressField()
    access_at = models.DateField(auto_now_add=True)
    post = models.ForeignKey(Post)

    def __str__(self):
        return "{0},{1},{2}".format(self.ip, self.post, self.access_at)


ブログの記事です。titleとtextは、ブログタイトルとブログ本文ですが...
class Post(models.Model):
    """ ブログの記事 """

    title = models.CharField(max_length=255)
    text = models.TextField()

    def __str__(self):
        return self.title

    def counter_today(self):
        return Counter.objects.filter(access_at=date.today(), post=self)

    def counter_today_unique(self):
        return Counter.objects.filter(
            access_at=date.today(), post=self).values('ip').distinct()


この2つは、本日のアクセスと本日のユニークアクセスです。
今回はPostモデルのメソッドとして定義しています。
access_atに今日の日付を、postに自身(つまり、その記事へのアクセス)を指定します。
    def counter_today(self):
        return Counter.objects.filter(access_at=date.today(), post=self)

    def counter_today_unique(self):
        return Counter.objects.filter(
            access_at=date.today(), post=self).values('ip').distinct()


これは重複をなくし、同じIPを除外しています。ユニークなIPの取得です。
検索したデータ(QuerySet)から、あるfieldの値の重複をなくしたい場合にも使用できます。
.values('ip').distinct()


もう少しvaluesについて説明しましょう。
valuesはValueQuerySetという、QuerySet のサブクラ スを返します。辞書が入った、リストを返すイメージです。
以下を見るとわかりやすいでしょう(distinctをはずしています。)

value_query_set = Counter.objects.filter(access_at=date.today(), post=self).values('ip')
print(type(value_query_set))


結果
<class 'django.db.models.query.ValuesQuerySet'>


value_query_set = Counter.objects.filter(access_at=date.today(), post=self).values('ip')
print(value_query_set)


結果
[{'ip': '127.0.0.1'}, {'ip': '127.0.0.1'}, {'ip': '127.0.0.1'}]


value_query_set = Counter.objects.filter(access_at=date.today(), post=self).values('ip')
for item in value_query_set:
    print(type(item))   
    print(item)
    print()


結果
{'ip': '127.0.0.1'}
<class 'dict'>

{'ip': '127.0.0.1'}
<class 'dict'>

{'ip': '127.0.0.1'}
<class 'dict'>


これにdistinctを付けることで、以下のように重複がきえます。
value_query_set = Counter.objects.filter(access_at=date.today(), post=self).values('ip').distinct()
print(value_query_set)

# 以下のように出力
# [{'ip': '127.0.0.1'}]



distinct("ip")という書き方もできますが、その書き方ではsqliteなどデータベースによっては以下のエラーが出ます。
DISTINCT ON fields is not supported by this database backend

もし日付の範囲を取得したいならば、以下のようにかけます。
access_at__range=(start_date, end_date)


日付の指定は、他にも書き方があります。
access_at__day=5  # 5日の指定
access_at__year=2016  # 2016年での指定
access_at__month=6  # 6月での指定


○日以降、というような書き方もできます。
# 2005年1月1日以降
access_at__gte=datetime.date(2005, 1, 1)

# 2005年1月1日より上
access_at__gt=datetime.date(2005, 1, 1)

# 2005年12月31日以前
access_at__lte=datetime.date(2005, 12, 31)

# 2005年12月31日より前
access_at__lt=datetime.date(2005, 12, 31)


これらのメソッドは、テンプレートから以下のように利用できます。
  今日の総合アクセス数:{{ post.counter_today | length}}<br>
  {% for counter in post.counter_today %}
    ipアドレス:{{ counter.ip }}<br>
  {% endfor %}



accesscounter/main/views.py
from django.shortcuts import render, redirect
from django.http import HttpResponse
from main.models import Post, Counter


def add_counter(request, post):
    """ カウンターを増やす処理 """

    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    counter = Counter(ip=ip, post=post)
    counter.save()


def index(request):
   """ /へのアクセス、トップ画面 """

    contexts = {
        'posts': Post.objects.all(),
    }
    return render(request, 'main/index.html', contexts)


def detail(request, post_id):
   """ /detail/:id へのアクセス、詳細画面 """

    post = Post.objects.get(id=post_id)
    add_counter(request, post)  # カウンターの追加処理

    contexts = {
        'post': post,
    }
    return render(request, 'main/detail.html', contexts)



カウンターの処理をdetailの中に全て書いても良かったのですが、今回は別に関数として用意しました。
def add_counter(request, post):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    counter = Counter(ip=ip, post=post)
    counter.save()


HTTP_X_FORWARDED_FORは、プロキシサーバ経由時に、アクセス元のIPが入っています。(入っていないこともあります)
ここが空ならばプロキシ経由ではないということで、単純にREMOTE_ADDRでIPを取得します。
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')




テンプレートは非常にシンプルです。
accesscounter/template/main/base.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>アクセスカウンター</title>
  <body>
    {% block content %}{% endblock %}
  </body>
</html>



accesscounter/template/main/index.html
{% extends "main/base.html" %}
{% block content %}
  {% for post in posts %}
    {{ post.title }}, <a href="{% url 'main:detail' post.id %}">詳細ページへ</a><br>
  {% endfor %}
{% endblock %}


accesscounter/template/main/detail.html
{% extends "main/base.html" %}
{% block content %}
  タイトル:{{ post.title }}<br>
  本文:{{ post.text }}<br>

  <hr>
  
  今日の総合アクセス数:{{ post.counter_today | length}}<br>
  {% for counter in post.counter_today %}
    ipアドレス:{{ counter.ip }}<br>
  {% endfor %}

  <hr>
  
  今日のユニークアクセス数:{{ post.counter_today_unique | length}}<br>
  {% for counter in  post.counter_today_unique %}
    ipアドレス:{{ counter.ip }}<br>
  {% endfor %}

{% endblock %}



以上です。