]>
RSSを登録する際、そのURIがRSSのものかを判定したい。フォーム入力内容の確認を行うには、バリデータを使えばいいようだ。validators.pyにはよく使われそうないくつかのバリデータが定義されているが、ここではfeedparserが拾ってくるversionによって判別を行うものを作ってみることにした。
Django オンラインドキュメント和訳: フォームとフィールド,マニピュレータ
UNIVERSAL FEED PARSER: Feed Type and Version Detection
feedparserによってRSSのversionが判別できないと空列が返ってくるので、そのときはValidationErrorを投げてやればよい。models.pyを少し書き換える。
from django.db import models
from django.core import validators
import feedparser
# Create your models here.
class Site(models.Model):
title = models.CharField(blank=True, maxlength=100)
rss = models.URLField(blank=False, verify_exists=True, maxlength=100)
last = models.DateTimeField(auto_now_add=True)
def save(self):
f = feedparser.parse(self.rss)
if not f.version :
raise validators.ValidationError("URL is not valid RSS")
if not self.title:
self.title = f.feed.title.encode('utf-8')
super(Site, self).save()
def __str__(self):
return self.rss
class Admin:
list_display = ('title', 'rss', 'last')
しかし、これではValidationErrorが適切にキャッチされず、入力が不適切な場合django自体のエラー画面が出力されてしまう。これを防ぐにはモデルのそれぞれのフォームを定義する際にバリデータのリストを引数として渡してやるのがよいようだ。例えばアルファベット小文字のみを入力とする属性hogeは、django/core/validators.pyで定義されたバリデータisLowerCaseを使って
hoge = models.CharField(maxlength=100, validator_list=[isLowerCase])
と書くことができる。
validators.pyにおいて、isLowerCaseの実装は
def isLowerCase(field_data, all_data):
if field_data.lower() != field_data:
raise ValidationError, gettext("Uppercase letters are not allowed here.")
となっているので、同様に入力がRSSであるか判定するバリデータisRSSが書ける。
def isRSS(field_data, all_data):
f = feedparser.parse(field_data)
if not f.version :
raise validators.ValidationError("URL is not valid RSS")
rss = models.URLField(blank=False, verify_exists=True, \
maxlength=100, validator_list=[isRSS])
raiseをlambda式中に埋め込めればなあ…。
ついでに、RSSのURIはユニークになるはずなのでunique=Trueにしておく。Django1.0でDateTimeFieldのauto_now_addがなくなるようなのでそちらも修正。
from django.db import models
from django.core import validators
import datetime
import feedparser
def isRSS(field_data, all_data):
f = feedparser.parse(field_data)
if not f.version :
raise validators.ValidationError("URL is not valid RSS")
#Create your models here.
class Site(models.Model):
title = models.CharField(blank=True, maxlength=100)
rss = models.URLField(blank=False, unique=True, verify_exists=True, \
maxlength=100, validator_list=[isRSS])
last = models.DateTimeField(null=True)
def save(self):
f = feedparser.parse(self.rss)
if not self.title:
self.title = f.feed.title.encode('utf-8')
self.last = datetime.datetime.now()
super(Site, self).save()
解決しました。
JJinuxLand: Python: Django Custom Tags
登録されたRSSから各記事を取得したいとき。RSSのURIを引数として、タイトルや記事の内容のディクショナリを返すカスタムタグ、rss_getを定義した。しかし、テンプレート内で
{% load rss_get %}
<html>
<head>
<title>Admin's RSS Reader</title>
</head>
<body>
{% for site in object_list %}
<h1>{{ site.title }}</h1>
{% rss_get site.rss as entries %}
{% for entry in entries %}
<h2>{{ entry.title }}</h2>
<p>{{ entry.description }}</p>
{% endfor %}
{% endfor %}
</body>
</html>
のように書くと、rss_getの引数site.rssがURIに置き換えられず、文字列"site.rss"がそのまま渡されてしまう(rss_getタグ以外の場所、例えばforタグ内でsite.rssを参照した場合には正しくURIに置換される)という問題。
与えられた文字列についてcontextを参照して解決するメソッドtemplate.resolve_variables()を使えばよい。引数としてcontextをとるのでrenderメソッドなどcontextが参照できるところから呼び出してやる。
template.resolve_variables(variable_str, context)
この場合は
def render(self, context):
f = feedparser.parse(template.resolve_variable(self.rss, context))
context[self.variable] = f.entries
return ''
結果rss_get.pyはこうなりました。
from django import template
from rss.site.models import Site
import feedparser
register = template.Library()
def do_rss_get(parser, token):
bits = token.contents.split()
if len(bits) != 4:
raise template.TemplateSyntaxError, "'%s' requires 3 arguments" % bits[0
]
if bits[2] != "as":
raise template.TemplateSyntaxError, "'%s' 's 2nd argument must be 'as'"
% bits[0]
return RSSNode(bits[3], bits[1])
class RSSNode(template.Node):
def __init__(self, variable, rss):
self.variable = variable
self.rss = rss
def render(self, context):
f = feedparser.parse(template.resolve_variable(self.rss, context))
context[self.variable] = f.entries
return ''
register.tag('rss_get',do_rss_get)
そしてこんな感じに。
![]()
feedから記事内容が取得できない原因が判明しました。
テンプレートの
{% load rss_get %}
<html>
<head>
<title>Admin's RSS Reader</title>
</head>
<body>
{% for site in object_list %}
<h1>{{ site.title }}</h1>
<p>{{ site.rss }}</p>
{% rss_get site.rss as entries %}
{% for entry in entries %}
<h2>{{ entry.title }}</h2>
{% endfor %}
{% endfor %}
</body>
</html>
のカスタムタグrss_get部分で、僕はてっきりsite.rssが評価され、URIに変換された後にrss_getが呼びだされるものと思っていた。がどうやら違ったようで、"site.rss"という文字列がそのままタグの引数として渡されている。当然フィードは見付からず、feedparserはNot foundを返す。インタープリタとDjangoで返ってくるものが違うのにはそういう理由があったわけ。わかってみれば納得です。
しかしなぜrss_getの引数site.rssが評価されないのか?その直前にある{{ site.rss }}ではちゃんとデータベースから拾ってきたURIが表示されているのに。組み込みタグで引数をとれるもの(ifとか)の実装をみても、別に内部で変なことをやっているわけではない、普通に処理しているだけだから、この短いテンプレートのどこかになんかまずい点があるようだ。
怪しいのはここかController部分だと思うんだけど…連休中には解決したい。
(追記)寝る前に考えていたらforタグ内でパースしている部分が怪しい気がしてきた。
(追記)template冒頭の{% load ... %}タグが抜けていました。
(追記)解決しました。
はじめての Django アプリ作成,その 3を参考にさせていただき、外部公開用のViewをつくる。データベースから情報をひっぱってくるのはすぐにできた。次は各サイトごとにRSSに含まれている記事を展開したい。そのためにはDjangoのテンプレート言語を勉強する必要がありそうだ。
>>> from django.template import Context,Template
>>> import feedparser
>>> rss = "http://www.panopticon.jp/blog/index.xml"
>>> c = Context({"entries":feedparser.parse(rss).entries})
>>> t = Template("{% for entry in entries %} \
... {{ entry.title }} \
... {% endfor %}")
>>> t.render(c)
とやるとRSS中の記事タイトル一覧が表示される。要するにTemplate中で使いたい変数をContextの中にしまっておけば、あとはDjangoが勝手に展開してくれるというわけですね。
とりあえずこんな感じでTemplateを作ってみる。site.titleとsite.rssにはデータベースの値がそのまま入る。rss_getタグは指定されたRSSから記事のリストを取得しentriesによって参照できるようにする。それぞれの記事を表示するにはentriesについてループを回してやればよい。
<html>
<head>
<title>Admin's RSS Reader</title>
</head>
<body>
{% for site in object_list %}
<h1>{{ site.title }}</h1>
<p>{{ site.rss }}</p>
{% rss_get site.rss as entries %}
{% for entry in entries %}
<h2>{{ entry.title }}</h2>
{% endfor %}
{% endfor %}
</body>
</html>
Python プログラマのための Django テンプレート言語ガイドを見ながらrss_getタグを作る……がうまくいかない。何も表示されない。
from django import template
from rss.site.models import Site
import feedparser
register = template.Library()
def do_rss_get(parser, token):
bits = token.contents.split()
if len(bits) != 4:
raise template.TemplateSyntaxError, "'%s' requires 3 arguments" % bits[0]
if bits[2] != "as":
raise template.TemplateSyntaxError, "'%s' 's 2nd argument must
be 'as'" % bits[0]
return RSSNode(bits[3], bits[1])
class RSSNode(template.Node):
def __init__(self, variable, rss):
self.variable = variable
self.rss = rss
def render(self, context):
f = feedparser.parse(self.rss).entries
context[self.variable] = f
RSSNodeを
def render(self, context):
f = feedparser.parse(self.rss)
context[self.variable] = f
としてやると、Feed, Encoding,Bozo, Version, Namespaces, Entries, Bozo_ExceptionとRSSの各属性を拾って来た。いちおうタグとして動いてはいるようだ。
>>> c = Context({"entries":feedparser.parse(rss)})
>>> t.render(c)
' Feed Status Updated Version Encoding Bozo Headers Etag Href Namespaces Entries '
とインタープリタで実行した時のほうが多くの属性を拾ってくる。なぜ。続く。
現在の状況を整理すると、
タイトル欄に直で日本語を入力→登録される
フィードから日本語の含まれるタイトルを取得→エンコードエラー
エラーメッセージは
UnicodeEncodeError at /admin/sites/site/add/ 'latin-1' codec can't encode characters in position 0-2: ordinal not in range(256)
となっているため、どこかUnicodeにすべき部分がもれているのでしょう。
通常のサーバーでしたら
MySQLのUnicodeColに日本語文字列をセットする問題の自分なりの解決
といった方法でうまくいくのかもしれません。が、僕はさくらの共有サーバーを使っているため、MySQLの設定ファイルなどを直接触ることができません。ODBC接続すらできません…。コメント欄にてデータベース作成時にutf8を指定すれば、という書き込みを頂いたのですがそれも試せそうにありません……。これはMySQLdbかDjango内部でどうにかするしかなさそうです。
Django | Model reference | Executing custom SQL
あたりを参考に、
connection.cursor().execute("SET CHARACTER SET utf8")
なんて送ってみても
OperationalError at /admin/sites/site/add/ (1115, "Unknown character set: 'utf8'")
また、MySQLdb/connection.py内にて文字コードを設定していると思しき変数charsetを'utf8'と固定にし、再インストールしてもエラーがでてしまう。
上のメッセージを読むとかなり嫌な予感がする訳ですが……MySQLのバージョンを調べてみる。
>>> import MySQLdb >>> cnx = MySQLdb.connect(host='mysql**.db.sakura.ne.jp', user='******', passwd='******') >>> cnx.get_server_info() '4.0.27'
MySQL AB :: Unicode and Other Funny Characters :: What 4.0 Did
MySQL 4.0 (and earlier versions) only supported what amounted to a combined notion of the character set and collation with single-byte character encodings, which was specified at the server level. The default was latin1, which corresponds to a character set of latin1 and collation of latin1_swedish_ci in MySQL 4.1.
No information about the character set (and collation) of the data is stored in the table, and there is no support in MySQL 4.0 for converting between character sets. To convert the data to another character set, you would need to change the server's character set, and then use an external tool to convert the data.
A common strategy for applications built using MySQL 4.0 that needed to handle data in multi-byte character encodings (such as UTF-8) that were not supported natively was to simply store the data in VARCHAR fields (or TEXT, CHAR, etc). The application would either ignore the fact that MySQL would simply sort the data incorrectly for strings that weren't really in the character set that the server thought they were, or do the sorting in the application. That strategy worked fine, you just had to handle all of the encoding issues in your application, and you also lost the ability to take full advantage of MySQL's full-text searching.
MySQL4.0は文字コード間の変換をサポートしてないから、サーバー側で設定してね、と書いてあるように見える……。しかしUnicodeをUnicodeとして扱えないにしてもデータの挿入自体はできるようなので、そこでエラーが出るというのはやっぱりどこかがおかしいんでしょう。
結局、エンコードを適当に試した結果、下のようにすると正常にタイトルの取得と挿入ができました。エンコードを行う際にunicode()だとエラーが出ますが、encode('utf-8')だと大丈夫なようです。FeedParserで取得したタイトルの文字コードがutf-8じゃなかったということなんですかね。なんか動いてはいるんですが僕自身理解できてません。
![]()
from django.db import models
import feedparser
# Create your models here.
class Site(models.Model):
title = models.CharField(blank=True, maxlength=100)
rss = models.URLField(blank=False, verify_exists=True, maxlength=100)
last = models.DateTimeField(auto_now_add=True)
def save(self):
if not self.title:
# self.title = unicode(feedparser.parse(self.rss).feed.title)
self.title = feedparser.parse(self.rss).feed.title
self.title = self.title.encode('utf-8')
super(Site, self).save()
def __str__(self):
return self.rss
class Admin:
list_display = ('title', 'rss', 'last')
ありがちですが作ってみます。
流れとしては
wwwの下に作ると閲覧者に設定ファイルなどが見えてしまうので避けたほうが良い。
$ django-admin.py startproject PROJECTNAME
上で作ったフォルダに移動して
$ python manage.py startapp APPLICATIONNAME
settings.pyに追加
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
'PROJECTNAME.APPRICATIONNAME',
)
Model, View, Controllerをそれぞれ設計して、
$ python manage.py syncdb
必要最低限の機能を考えて、タイトル、RSSのURI、最終閲覧日を属性とすることにする。RSSは入力必須。タイトルは任意で、空欄であればRSSからタイトルを取得して埋める。最終閲覧日にはとりあえず登録日を入れておく。
from django.db import models
# Create your models here.
class Site(models.Model):
title = models.CharField(blank=True, maxlength=100)
rss = models.URLField(blank=False, verify_exists=True, maxlength=100)
last = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.rss
class Admin:
list_display = ('title', 'rss', 'last')
RSSからタイトルを取得するには、FeedParserを使うと、
self.title = feedparser.parse(self.rss).feed.title
一発でタイトルが取得できる。
$ wget http://downloads.sourceforge.net/feedparser/feedparser-4.1.zip $ unzip feedparser-4.1.zip $ python setup.py install $ mv feedparser.py /home/USERNAME/local/lib/python
さくらの場合共有ディレクトリにファイルを置こうとしてエラーが出るので、Djangoをインストールする際に作成したPYTHONPATHの通ったフォルダに置いておく。
入力に応じて処理を振り分けたいときには、save()をオーバーライドすればいいようだ。
from django.db import models
import feedparser
# Create your models here.
class Site(models.Model):
title = models.CharField(blank=True, maxlength=100)
rss = models.URLField(blank=False, verify_exists=True, maxlength=100)
last = models.DateTimeField(auto_now_add=True)
def save(self):
if not self.title:
self.title = feedparser.parse(self.rss).feed.title
models.Model.save(self)
def __str__(self):
return self.rss
class Admin:
list_display = ('title', 'rss', 'last')
これでいけるかと思ったがRSSから日本語の含まれるタイトルを取得したときエンコードエラーが出る。デフォルトのエンコーディングがASCIIになっているからなのか。
/home/USERNAME/local/lib/python/site-packages/sitecustomize.pyを作成し
import sys
sys.setdefaultencoding('utf-8')
これでも直らない。どうやらMySQLdbが悪さをしているようだ。解決方法としてはMySQLdbのソースを書き換えるか、sqlobject.dburiを正しく設定してやればいいらしい。ちょっと目が疲れたのでこれは明日。
あと入力されたURIがRSSのものでない場合にはエラー画面を出したい。これもやり方を調べなくては。
[Django][レンタルサーバー] さくらインターネットでDjango - oqreの日記
「さくらのレンタルサーバ」で Python 外部モジュールを使う - EmptyPage.jp
をほぼそのまま。
MySQLdbを入れようとしたところで、setuptoolsのバージョン0.6c5を使えとエラーが出たので、それだけ追加。
% wget http://cheeseshop.python.org/packages/source/s/setuptools/setuptools-0.6c5.tar.gz % tar -zxf setuptools-0.6c5.tar.gz % cd setuptools-0.6c5 % python setup.py install --prefix=$HOME/local
/www/PROJECTNAME/settings.pyを編集
DATABASE_ENGINE = 'mysql'
DATABASE_NAME = '*****'
DATABASE_USER = '*****'
DATABASE_PASSWORD = '*******'
DATABASE_HOST = 'mysql**.db.sakura.ne.jp'
DATABASE_PORT = ''
...
TIME_ZONE = 'Japan'
...
LANGUAGE_CODE = 'ja'
...
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
)
データベースなんかがきちんと動いていれば、次のコマンドでスーパーユーザが作成される。
% python manage.py syncdb
シェルの起動
% python manage.py shell