news4 - RSS aggrigation system
Revision | dfb2a0929cc61bf4d186cfed707e85d67e04e92a (tree) |
---|---|
Zeit | 2012-09-24 22:43:51 |
Autor | hylom <hylom@hylo...> |
Commiter | hylom |
add filter, fix css
@@ -4,4 +4,7 @@ requires: | ||
4 | 4 | mako (Makotenplate library for Python) |
5 | 5 | |
6 | 6 | feedparser (feed library for Python) |
7 | - http://code.google.com/p/feedparser | |
\ No newline at end of file | ||
7 | + http://code.google.com/p/feedparser | |
8 | + | |
9 | + python-dateutil | |
10 | + | |
\ No newline at end of file |
@@ -3,6 +3,28 @@ | ||
3 | 3 | .entry-header{} |
4 | 4 | .entry-body{} |
5 | 5 | .entry-continue { |
6 | - margin-bottom: 1em; | |
6 | + margin-bottom: 1em; | |
7 | 7 | } |
8 | -.entry-footer{} | |
\ No newline at end of file | ||
8 | +.entry-footer{ | |
9 | + color: gray; | |
10 | +} | |
11 | + | |
12 | +header { | |
13 | + border-bottom: 1px solid gray; | |
14 | +} | |
15 | + | |
16 | +.main-contents h3 { | |
17 | + font-size: 130%; | |
18 | +} | |
19 | + | |
20 | +.sidebar-left h3 { | |
21 | + font-size: 100%; | |
22 | + font-weight: normal; | |
23 | +} | |
24 | + | |
25 | +.sidebar-right h3 { | |
26 | + font-size: 100%; | |
27 | + font-weight: normal; | |
28 | +} | |
29 | + | |
30 | + | |
\ No newline at end of file |
@@ -1,6 +1,10 @@ | ||
1 | +#-*- coding: utf-8 -*- | |
1 | 2 | 'fetcher.py - RSS fetcher' |
2 | 3 | |
4 | +import re | |
5 | + | |
3 | 6 | import feedparser |
7 | +from config import config as config, target_rss as target_rss | |
4 | 8 | |
5 | 9 | class FeedFetcher(object): |
6 | 10 | 'Feed fetching and parsing' |
@@ -13,6 +17,7 @@ class FeedFetcher(object): | ||
13 | 17 | entries = [] |
14 | 18 | for e in f['entries']: |
15 | 19 | entry = { |
20 | +# 'title': e.title.decode('utf8') if isinstance(e.title, str) else e.title, | |
16 | 21 | 'title': e.title, |
17 | 22 | 'link': e.link, |
18 | 23 | 'body': e.description, |
@@ -22,7 +27,41 @@ class FeedFetcher(object): | ||
22 | 27 | entries.append(entry) |
23 | 28 | return entries |
24 | 29 | |
30 | + def _embeded_filter(self, entry): | |
31 | + # remove PR entry | |
32 | + if re.search(u'^PR', entry['title']): | |
33 | + print 'delete PR entry - %s' % entry['title'] | |
34 | + return None | |
35 | + return entry | |
36 | + | |
25 | 37 | def get_entries(self): |
26 | 38 | 'get entries' |
27 | - return self._fetch() | |
39 | + entries = self._fetch() | |
40 | + entries = [self._embeded_filter(x) for x in entries] | |
41 | + entries = [x for x in entries if x] | |
42 | + | |
43 | + if 'filter' in self._feed: | |
44 | + entry_filter = self._get_filter() | |
45 | + entries = [entry_filter(x) for x in entries] | |
46 | + # remove entry which is None | |
47 | + entries = [x for x in entries if x] | |
48 | + return entries | |
49 | + | |
50 | + def _get_filter(self): | |
51 | + 'load filter by seed settings' | |
52 | + filter_name = self._feed.get('filter', None) | |
53 | + | |
54 | + # fallback when filter isn't defined | |
55 | + if filter_name is None: | |
56 | + return lambda x:x | |
57 | + | |
58 | + # import module | |
59 | + mods = __import__(config['filter_directory'], | |
60 | + globals(), | |
61 | + locals(), | |
62 | + [filter_name,]) | |
63 | + mod = mods.__getattribute__(filter_name) | |
28 | 64 | |
65 | + # return module's entry_filter function | |
66 | + return mod.entry_filter | |
67 | + |
@@ -0,0 +1 @@ | ||
1 | +# __init__.py stub |
@@ -0,0 +1,16 @@ | ||
1 | +# filter for slashdot.jp | |
2 | +# -*- coding: utf-8 -*- | |
3 | + | |
4 | +import re | |
5 | + | |
6 | + | |
7 | +def entry_filter(entry): | |
8 | + # すべて読む、関連ストーリーを削除 | |
9 | + body = entry['body'] | |
10 | + body = re.sub(ur'<p> <a href=.*>\s*すべて読む</a>.*?</p>', '', body) | |
11 | + body = re.sub(ur'<p>\s*関連ストーリー:.*?</p>', '', body) | |
12 | + entry['body'] = body | |
13 | + | |
14 | + return entry | |
15 | + | |
16 | + |
@@ -8,14 +8,18 @@ import os.path | ||
8 | 8 | |
9 | 9 | def main(): |
10 | 10 | "TODO: argv check" |
11 | + | |
12 | + # fetch rss feed | |
11 | 13 | entries = [] |
12 | 14 | for feed in target_rss: |
13 | 15 | f = fetcher.FeedFetcher(feed) |
14 | 16 | e = f.get_entries() |
15 | 17 | entries.extend(e) |
16 | 18 | |
19 | + # sort by date | |
17 | 20 | entries.sort(lambda x,y: -cmp(x["date"],y["date"])) |
18 | 21 | |
22 | + # render entries | |
19 | 23 | r = renderer.Renderer() |
20 | 24 | for (output_file, tmpl) in config["templates"]: |
21 | 25 | output_fullpath = os.path.join( |
@@ -1,12 +1,19 @@ | ||
1 | 1 | 'renderer.py - rendering html and some items' |
2 | 2 | |
3 | +import datetime | |
4 | + | |
3 | 5 | from mako.template import Template |
4 | 6 | from mako.lookup import TemplateLookup |
5 | 7 | from mako.exceptions import RichTraceback |
8 | +import dateutil.parser | |
6 | 9 | |
7 | 10 | from config import config, target_rss |
8 | 11 | from propertizer import propertize |
9 | 12 | |
13 | +def date_format(date): | |
14 | + dt = dateutil.parser.parse(date) | |
15 | + return dt.strftime('%Y/%m/%d %H:%M') | |
16 | + | |
10 | 17 | class Renderer(object): |
11 | 18 | def __init__(self): |
12 | 19 | self.template_dir = config['template_directory'] |
@@ -25,7 +32,10 @@ class Renderer(object): | ||
25 | 32 | t = self._get_template(template) |
26 | 33 | |
27 | 34 | |
28 | - kwargs = { 'entries': entries } | |
35 | + kwargs = { | |
36 | + 'entries': entries, | |
37 | + 'date_format': date_format, | |
38 | + } | |
29 | 39 | kwargs.update(config["site_parameter"]) |
30 | 40 | for key in kwargs: |
31 | 41 | d = propertize(kwargs[key]) |
@@ -1,44 +1,87 @@ | ||
1 | 1 | <!DOCTYPE html> |
2 | 2 | <html lang="ja"> |
3 | 3 | <head> |
4 | - <meta charset="UTF-8"> | |
4 | + <meta charset="UTF-8"> | |
5 | 5 | <title>${site_name}</title> |
6 | 6 | <!-- Bootstrap --> |
7 | 7 | <link href="${css_directory}/bootstrap.min.css" rel="stylesheet"> |
8 | 8 | <link href="${css_directory}/gnews.css" rel="stylesheet"> |
9 | 9 | </head> |
10 | + | |
10 | 11 | <body class="wrap"> |
11 | - <div class="containter"> | |
12 | - <div class="row"> | |
13 | - <div class="span12"> | |
14 | - <h1>${site_name}</h1> | |
15 | - </div> | |
16 | - </div> | |
17 | - <div class="row"> | |
18 | - <div class="span3"></div> | |
19 | - <div class="span9"> | |
20 | - <script src="http://code.jquery.com/jquery-latest.js"></script> | |
21 | - <script src="${js_directory}/bootstrap.min.js"></script> | |
22 | - | |
23 | - % for entry in entries: | |
24 | - <div class="entry"> | |
25 | - <div class="entry-header"> | |
26 | - <h3> | |
27 | - <a href='${entry.link}' target="_blank_">${entry.title}</a> | |
28 | - </h3> | |
29 | - </div> | |
30 | - <div class="entry-body">${entry.body}</div> | |
31 | - <div class="entry-footer"> | |
32 | - <div class="entry-continue"> | |
33 | - <a href='${entry.link}'>[続きを読む]</a> | |
34 | - </div> | |
35 | - <div class="entry-footer"> | |
36 | - <span>情報元:<a href='${entry.feed.source_url}'>${entry.feed.name}</a></span> | |
37 | - <span>${entry.date}</span></div> | |
38 | - </div> | |
39 | - % endfor | |
40 | - </div> | |
41 | - </div> | |
12 | + <div class="container"> | |
13 | + | |
14 | + <!-- タイトル --> | |
15 | + <div class="row"> | |
16 | + <div class="span12"> | |
17 | + <header> | |
18 | + <h1>${site_name}</h1> | |
19 | + </header> | |
42 | 20 | </div> |
21 | + </div> | |
22 | + | |
23 | + <!-- コンテンツ本体 --> | |
24 | + <div class="row"> | |
25 | + | |
26 | + <!-- 左サイドバー --> | |
27 | + <div class="span2 sidebar-left"> | |
28 | + <div class="keywords"> | |
29 | + <h3>キーワード:</h3> | |
30 | + <ul class="nav nav-pills nav-stacked"> | |
31 | + <li><a href="#">iPhone</a></li> | |
32 | + <li><a href="#">iOS</a></li> | |
33 | + <li><a href="#">Internet</a></li> | |
34 | + <li><a href="#">Firefox</a></li> | |
35 | + <li><a href="#">Facebook</a></li> | |
36 | + </ul> | |
37 | + </div> | |
38 | + </div> | |
39 | + | |
40 | + <!-- メインコンテンツ --> | |
41 | + <div class="span7 main-contents"> | |
42 | + <script src="http://code.jquery.com/jquery-latest.js"></script> | |
43 | + <script src="${js_directory}/bootstrap.min.js"></script> | |
44 | + | |
45 | + % for entry in entries: | |
46 | + <div class="entry"> | |
47 | + <!-- ヘッダ --> | |
48 | + <div class="entry-header"> | |
49 | + <h3> | |
50 | + <a href='${entry.link}' target="_blank_">${entry.title}</a> | |
51 | + </h3> | |
52 | + </div> | |
53 | + | |
54 | + | |
55 | + <!-- 本文テキスト --> | |
56 | + <div class="entry-body">${entry.body}</div> | |
57 | + | |
58 | + <!-- フッタ --> | |
59 | + <div class="entry-footer"> | |
60 | + <div class="entry-continue"> | |
61 | + <a href='${entry.link}'>[続きを読む]</a> | |
62 | + </div> | |
63 | + <div class="entry-footer"> | |
64 | + <span>情報元:<a href='${entry.feed.source_url}'>${entry.feed.name}</a></span> | |
65 | + <span>(${date_format(entry.date)})</span></div> | |
66 | + </div> | |
67 | + </div> | |
68 | + % endfor | |
69 | + | |
70 | + </div><!-- main-contents --> | |
71 | + | |
72 | + <!-- 右サイドバー --> | |
73 | + <div class="span3 sidebar-right"> | |
74 | + <div class="feed-provider"> | |
75 | + <h3>情報提供サイト:</h3> | |
76 | + <ul class="nav nav-pills nav-stacked"> | |
77 | + <li><a href="#">Slashdot Japan</a></li> | |
78 | + <li><a href="#">SourceForge.JP Magazine</a></li> | |
79 | + </ul> | |
80 | + </div> | |
81 | + </div> | |
82 | + | |
83 | + </div><!-- .row --> | |
84 | + | |
85 | + </div><!-- .container --> | |
43 | 86 | </body> |
44 | 87 | </html> |