| 1 | #!/usr/local/Python25/bin/python |
|---|
| 2 | # _*_ coding: utf-8 _*_ |
|---|
| 3 | # Copyright (C) 2007 Ayukawa Hiroshi |
|---|
| 4 | from collections import defaultdict |
|---|
| 5 | from operator import add |
|---|
| 6 | from itertools import groupby, chain |
|---|
| 7 | import math |
|---|
| 8 | import types |
|---|
| 9 | |
|---|
| 10 | import feedparser |
|---|
| 11 | import MeCab |
|---|
| 12 | |
|---|
| 13 | from wardcluster import Matrix, DoubleVec, Ward |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | MECAB_ENCODING = "utf-8" |
|---|
| 17 | |
|---|
| 18 | MEISHI = u"名詞".encode(MECAB_ENCODING) |
|---|
| 19 | MICHI = u"未知語".encode(MECAB_ENCODING) |
|---|
| 20 | GARBAGECLUSTER = 1 |
|---|
| 21 | BIGCLUSTER = 8 |
|---|
| 22 | |
|---|
| 23 | URL = "http://b.hatena.ne.jp/hotentry?mode=rss" |
|---|
| 24 | |
|---|
| 25 | def findcutplane(history, histmap): |
|---|
| 26 | """ |
|---|
| 27 | 平均高さで斬った切断面を求めます。 |
|---|
| 28 | """ |
|---|
| 29 | avgdistance = sum(map(lambda h: h.distance, history)) / float(len(history)) |
|---|
| 30 | for i, h in histmap.iteritems(): |
|---|
| 31 | if h.distance > avgdistance: |
|---|
| 32 | if not h.index1 in histmap or histmap[h.index1].distance <= avgdistance: |
|---|
| 33 | yield h.index1 |
|---|
| 34 | if not h.index2 in histmap or histmap[h.index2].distance <= avgdistance: |
|---|
| 35 | yield h.index2 |
|---|
| 36 | |
|---|
| 37 | def findleaf(t, leaftype=types.IntType): |
|---|
| 38 | """ |
|---|
| 39 | ツリーの葉全体を返します。 |
|---|
| 40 | """ |
|---|
| 41 | if type(t) == leaftype: |
|---|
| 42 | yield t |
|---|
| 43 | else: |
|---|
| 44 | for n in chain(findleaf(t[0]), findleaf(t[1])): |
|---|
| 45 | yield n |
|---|
| 46 | |
|---|
| 47 | def allparent(top, desc, n): |
|---|
| 48 | if n == top: |
|---|
| 49 | yield n |
|---|
| 50 | return |
|---|
| 51 | yield n |
|---|
| 52 | for m in allparent(top, desc, desc[n]): |
|---|
| 53 | yield m |
|---|
| 54 | |
|---|
| 55 | def repairbags(bags, findplane): |
|---|
| 56 | #切断面から除外されて落ちた葉を拾い集めてどこかに押し込む |
|---|
| 57 | for i in list(bags.keys()): |
|---|
| 58 | if len(bags[i]) <= GARBAGECLUSTER: |
|---|
| 59 | n = findplane(i) |
|---|
| 60 | if n: |
|---|
| 61 | bags[n]+=bags[i] |
|---|
| 62 | bags.pop(i) |
|---|
| 63 | else: |
|---|
| 64 | bags["other"]+=bags[i] |
|---|
| 65 | bags.pop(i) |
|---|
| 66 | return bags |
|---|
| 67 | |
|---|
| 68 | def cutdendrogram(history, size=None, subset=None, dual=False): |
|---|
| 69 | """ |
|---|
| 70 | 樹形図をカットする関数 |
|---|
| 71 | """ |
|---|
| 72 | #クラスタリング結果データの整理 |
|---|
| 73 | if subset: |
|---|
| 74 | tree = dict(zip(sorted(subset), sorted(subset))) |
|---|
| 75 | else: |
|---|
| 76 | tree = dict(zip(range(size), range(size))) # i=>i(葉)という対応を作る。 |
|---|
| 77 | histmap = {} |
|---|
| 78 | desc = {} |
|---|
| 79 | for h in history: |
|---|
| 80 | #print h.newindex, "<-", h.index1, "+", h.index2, "(", h.distance,")" |
|---|
| 81 | tree[h.newindex] = (tree[h.index1] if h.index1 in tree else h.index1, |
|---|
| 82 | tree[h.index2] if h.index2 in tree else h.index2) |
|---|
| 83 | histmap[h.newindex] = h |
|---|
| 84 | desc[h.index1] = h.newindex |
|---|
| 85 | desc[h.index2] = h.newindex |
|---|
| 86 | |
|---|
| 87 | #切断面決定 |
|---|
| 88 | cutplane = set(findcutplane(history, histmap)) |
|---|
| 89 | |
|---|
| 90 | #切断した葉ごとに分ける、同時に、少ない枝を切断面から除外 |
|---|
| 91 | bags = defaultdict(list) |
|---|
| 92 | for i in list(sorted(cutplane)): |
|---|
| 93 | bag = list(findleaf(tree[i])) |
|---|
| 94 | if len(bag) <= GARBAGECLUSTER: # 少ない枝を除外 |
|---|
| 95 | cutplane.remove(i) |
|---|
| 96 | bags[i] = bag |
|---|
| 97 | elif dual and len(bag) > BIGCLUSTER: #大きい枝をさらに分割(dualが設定されている場合) |
|---|
| 98 | parents = set() |
|---|
| 99 | for b in bag: |
|---|
| 100 | parents.update(allparent(i, desc, b)) |
|---|
| 101 | subhistory = filter(lambda h: h.newindex in parents, history) |
|---|
| 102 | subbags = repairbags(*(cutdendrogram(subhistory, subset=bag, dual=dual))) |
|---|
| 103 | #print "subbags", subbags |
|---|
| 104 | bags.update(subbags) |
|---|
| 105 | else: |
|---|
| 106 | bags[i] = bag |
|---|
| 107 | def findplane(n): |
|---|
| 108 | """ |
|---|
| 109 | 最も近い切断面を求める。 |
|---|
| 110 | """ |
|---|
| 111 | if n in histmap: |
|---|
| 112 | if histmap[n].index1 in cutplane: |
|---|
| 113 | return histmap[n].index1 |
|---|
| 114 | if histmap[n].index2 in cutplane: |
|---|
| 115 | return histmap[n].index2 |
|---|
| 116 | if n == history[-1].newindex: |
|---|
| 117 | return None |
|---|
| 118 | return findplane(desc[n]) |
|---|
| 119 | return bags, findplane |
|---|
| 120 | |
|---|
| 121 | def create(html=False): |
|---|
| 122 | """ |
|---|
| 123 | 分割します。 |
|---|
| 124 | """ |
|---|
| 125 | tagger = MeCab.Tagger() |
|---|
| 126 | feed = feedparser.parse(URL) #rss取得&解析 |
|---|
| 127 | tagnum = 0 |
|---|
| 128 | kwmaster = {} |
|---|
| 129 | kwnum = 0 |
|---|
| 130 | |
|---|
| 131 | vecs = [] |
|---|
| 132 | docs = [] |
|---|
| 133 | |
|---|
| 134 | #分かち書きしてベクトル作成 |
|---|
| 135 | for e in feed.entries: |
|---|
| 136 | docs.append(dict(title=e["title"], link=e["link"])) |
|---|
| 137 | tags = map(lambda x: x["term"], e.get("tags", [])) |
|---|
| 138 | body = u"。".join([e["title"]]+tags) |
|---|
| 139 | node = tagger.parseToNode(body.encode(MECAB_ENCODING)) |
|---|
| 140 | vec = defaultdict(int) |
|---|
| 141 | while node: |
|---|
| 142 | if node.feature.startswith(MEISHI) or node.feature.startswith(MICHI): |
|---|
| 143 | if not node.surface in kwmaster: |
|---|
| 144 | kwmaster[node.surface] = kwnum |
|---|
| 145 | vec[kwnum] += 1 |
|---|
| 146 | kwnum += 1 |
|---|
| 147 | else: |
|---|
| 148 | vec[kwmaster[node.surface]] += 1 |
|---|
| 149 | node = node.next |
|---|
| 150 | vecs.append(vec) |
|---|
| 151 | size = len(vecs) #ベクトル本数 |
|---|
| 152 | |
|---|
| 153 | #行列作成 |
|---|
| 154 | dimension = max(kwmaster.values()) |
|---|
| 155 | matrix = Matrix() |
|---|
| 156 | for vec in vecs: |
|---|
| 157 | dencev = [0.0]*(dimension+1) |
|---|
| 158 | for k,v in vec.iteritems(): |
|---|
| 159 | dencev[k] = v #math.log(v+1) |
|---|
| 160 | matrix.append(DoubleVec(dencev)) |
|---|
| 161 | |
|---|
| 162 | #クラスタリング実行 |
|---|
| 163 | ward = Ward() |
|---|
| 164 | history = ward.cluster(matrix, size) |
|---|
| 165 | |
|---|
| 166 | #樹形図をカットする |
|---|
| 167 | bags = repairbags(*(cutdendrogram(history, size, dual=True))) |
|---|
| 168 | |
|---|
| 169 | #結果表示 |
|---|
| 170 | if html: |
|---|
| 171 | print "<html>" |
|---|
| 172 | print "<body>" |
|---|
| 173 | for i in bags.keys(): |
|---|
| 174 | print "Group [", i, "]", "-"*20 #, "<br/>" |
|---|
| 175 | for j in bags[i]: |
|---|
| 176 | print (u"<a href='"+docs[j]["link"]+"'>"+docs[j]["title"]+u"</a>").encode("utf-8") |
|---|
| 177 | print "</body>" |
|---|
| 178 | print "</html>" |
|---|
| 179 | else: |
|---|
| 180 | for i in bags.keys(): |
|---|
| 181 | print "Group [", i, "]", "-"*20 |
|---|
| 182 | for j in bags[i]: |
|---|
| 183 | print j, docs[j]["title"].encode("utf-8") |
|---|
| 184 | |
|---|
| 185 | if __name__ == "__main__": |
|---|
| 186 | import optparse |
|---|
| 187 | |
|---|
| 188 | parser = optparse.OptionParser(u""" |
|---|
| 189 | はてなブックマークのホットエントリーの記事をなんとなーく分類します。 |
|---|
| 190 | """) |
|---|
| 191 | parser.add_option("-H", "--html", dest="html", default=False, action="store_true", |
|---|
| 192 | help=u"結果をHTMLとして出力します。", default=False) |
|---|
| 193 | (options, args) = parser.parse_args() |
|---|
| 194 | |
|---|
| 195 | create(options.html) |
|---|
| 196 | |
|---|