読者です 読者をやめる 読者になる 読者になる

COBOL技術者の憂鬱

COBOLプログラマは不在にしています

2chまとめhotentryが完成しました

2chまとめホットエントリー




先週からデータストアの様子を観察していたのですが、特に問題なく運用できそうなので、このままリリースということにしたいと思います。
1位から15位くらいまでは、はてブのホットエントリーと大体かぶっているのですが、それ以降のエントリーについて、なかなか面白いものが上がってきているので、そのあたりを拾い上げることがこのサイトの意義なのではないかなと思っています。


ソースコードはこちらです。順を追って簡単に解説していきましょう。


まず初めに、以下のように二つのデータストアを定義します。



【datastores.py】

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from google.appengine.ext import db

class Sites(db.Model):
  url = db.StringProperty()
  title = db.StringProperty()
  check_time = db.DateTimeProperty()

class Entries(db.Model):
  url = db.StringProperty()
  title = db.StringProperty(multiline=True)
  date = db.StringProperty()
  count = db.IntegerProperty()
  increase1 = db.IntegerProperty()
  increase2 = db.IntegerProperty()
  increase3 = db.IntegerProperty()  
  score = db.IntegerProperty()
  parent_site = db.ReferenceProperty(Sites)

Sitesには、2ちゃんねるのまとめサイトのURLとタイトルをあらかじめ登録しておきます。
このURLから、はてブの新着エントリーの情報を取得するのですが、その際に取得時間を記録できるようにcheck_timeという項目も用意しておきます。


Entriesには、はてブの新着エントリーとして取得した個別のエントリーをどんどん格納していきます。
取得したエントリーのURLとタイトル、日時(date)とブクマ数(count)を格納し、さらに前回取得時のブクマ数との差分をincrease1〜3に格納します。scoreにはincrease1〜3に対して、重み付けを変更しながら加算した値が入り、これが最終的に画面に表示する際の表示順となります。
parent_siteにはそのエントリーの元サイトへのポインタが入ります。SitesとEntriesは1:Nの関係になるので、こうやって紐付けておくことができるわけですね。



そして、次のプログラムは、Sitesに対して初期登録する為のものです。
これを最初に一回だけ実行することで、90件の2ちゃんねるまとめサイトが登録されます。



【add_site.py】

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from google.appengine.ext import db
import datetime

import datastores

matomesites = (('http://blog.livedoor.jp/dqnplus/',u'痛いニュース'),
                         ('http://news4vip.livedoor.biz/',u'ニュー速クオリティ'),
                         ('http://hamusoku.com/',u'ハムスター速報'),
                         ('http://guideline.livedoor.biz/',u'日刊スレッドガイド'),
#全部で90件ある為、中略
                         ('http://vipper2ch.blog94.fc2.com/',u'ゲー速@2ch'),
                         ('http://miruyo.blog38.fc2.com/',u'ゲーム板見るよ!'),
                         ('http://vipvipblogblog.blog119.fc2.com/',u'ベア速'))
for matomesite in matomesites:
  query = datastores.Sites.gql('WHERE url = :url',url=matomesite[0])
  site = query.get()
  if not site:
    site = datastores.Sites()
    site.url = matomesite[0]
    site.title = matomesite[1]
    site.check_time = datetime.datetime.today()
    site.put()

そして、次のget_entry.pyをcronで定期実行してやることで、Sitesに登録されている各サイトに対して1件づつ順番に、はてブ新着エントリーを取得していきます。
取得されたエントリーは順次Entriesに登録されていき、同じタイミングで前回とのブクマ数との差分からスコアを算出し、結果を更新するようになっています。



【get_entry.py】

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from google.appengine.api import urlfetch
import re
import datetime

import datastores

query1 = datastores.Sites.gql('ORDER BY check_time ASC')
site = query1.get()

if site:
  entrylist_url = 'http://b.hatena.ne.jp/entrylist?url=' + site.url
  result = urlfetch.fetch(entrylist_url)
  if result.status_code == 200:
##score clear
    query2 = datastores.Entries.gql('WHERE parent_site = :parent_site AND score > :score',parent_site=site,score=0)
    for scorefilled_entry in query2:
      scorefilled_entry.score = 0
      scorefilled_entry.put()
##data add
    pattern = re.compile(r'''<li class="users"> <strong><a href="/entry/(.*?)" title="(.*?)">(.*?) users</a></strong></li>
        <li class="timestamp">(.*?)</li>(.*?)
        <cite title="(.*?)"><a href="(.*?)"> 続きを読む</a></cite>''',re.S)
    entry_lists = pattern.findall(result.content)
    for entry_list in entry_lists:
      query3 = datastores.Entries.gql('WHERE url = :url',url=entry_list[6])
      entry = query3.get()
      if entry:
        entry.increase3 = entry.increase2
        entry.increase2 = entry.increase1
        entry.increase1 = int(entry_list[2]) - entry.count
        entry.count = int(entry_list[2])
      else:
        entry = datastores.Entries()
        entry.url = entry_list[6]
        entry.title = unicode(entry_list[5],'utf-8')
        entry.date = entry_list[3]
        entry.count = int(entry_list[2])
        entry.increase3 = 0
        entry.increase2 = 0
        entry.increase1 = int(entry_list[2])
        entry.parent_site = site
      entry.score = entry.increase1 + int(entry.increase2 * 0.6) + int(entry.increase3 * 0.3)
      entry.put()
    site.check_time = datetime.datetime.today()
    site.put()

最後は、画面を表示するところですね。
Entriesからscoreの降順で30件取得し、結果を表示していくだけなので、特に難しいことはなにもないでしょう。



【mainpage.py】

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import datastores

query = datastores.Entries.gql('ORDER BY score DESC')
fetched_entries = query.fetch(30)

print '''<html>
<head>
<title>2chまとめホットエントリー</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="css/html.css" />
<link rel="stylesheet" type="text/css" href="css/layout.css" />
</head>
<body>
<div id="wrapper">
<div id="content">
<div id="header">
<h1>2ch Matome Hot Entry</h1>
<h2><span class="highlight">2ちゃんねるまとめサイトだけでホットエントリー</span></h2>
</div>
<div id="page">'''


for fetched_entry in fetched_entries:
  print '<span class="result"><a href="' + fetched_entry.url.encode('utf-8')  + '" target="_blank" >' + fetched_entry.title.encode('utf-8') +'</a></span>'
  print '<br>'
  print '<span class="users"><strong><a href="http://b.hatena.ne.jp/entry/' + fetched_entry.url.encode('utf-8') + '" target="_blank">' + str(fetched_entry.count) + ' users</a></strong></span>&nbsp;&nbsp;'
  print '<span class="timestamp">' + fetched_entry.date.encode('utf-8') + '</span>'
  #print '<br>'
  #print 'score:' + str(fetched_entry.score) + '  (' + str(fetched_entry.increase1) + '/' + str(fetched_entry.increase2) + '/' + str(fetched_entry.increase3) + ')'
  print '<br><br>'

print '''<div class="footer">
<img src="http://code.google.com/appengine/images/appengine-noborder-120x30.gif" alt="Powered by Google App Engine" />
<br>
This service is created by <a href="http://d.hatena.ne.jp/quill3/" target="_blank" >quill3</a>.
<br>
(Here is <a href="http://github.com/quill3/2ch-hotentry" target="_blank" >source code</a> & <a href="http://d.hatena.ne.jp/quill3/archive?word=%2A%5B2ch%A4%DE%A4%C8%A4%E1hotentry%5D" target="_blank" >development log</a>.)
</div>

</div>
</div>
</div>

<script type="text/javascript">
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
</script>
<script type="text/javascript">
try {
var pageTracker = _gat._getTracker("UA-568420-6");
pageTracker._trackPageview();
} catch(err) {}</script>

</body>
</html>'''

今回、画面を作るにあたって、こちらのテンプレを利用させていただいたのですが、出来合いのものを利用するのは簡単なようでいて意外と難しいですね。
結果表示部分の幅をもう少し横に広げたいのですが、どうやっても変わらないので結局あきらめましたww



今回の開発では、前回のMovitterのソースや考え方をかなり流用することができたので、特に大きな問題にぶつかることもなく、驚くほどあっさりと完成までこぎつけることができました。
もう、この手の巡回系のサイトは目をつぶっていても作れるようになってきたので、次回はちょっと毛色の違ったものにチャレンジしてみようと思っています。
まだ使っていないGAEの機能が他にもたくさんあるので、できるだけそれらを全て活用していくような方針で考えていきたいと思います。