Module: TwitterWithAutoPagination::REST::Extension::Clusters
- Includes:
- Utils
- Included in:
- API
- Defined in:
- lib/twitter_with_auto_pagination/rest/extension/clusters.rb
Constant Summary collapse
- PROFILE_SPECIAL_WORDS =
%w(20↑ 成人済 腐女子)
- PROFILE_SPECIAL_REGEXP =
nil- PROFILE_EXCLUDE_WORDS =
%w(in at of my to no er by is RT DM the and for you inc Inc com from info next gmail 好き こと 最近 紹介 連載 発売 依頼 情報 さん ちゃん くん 発言 関係 もの 活動 見解 所属 組織 代表 連絡 大好き サイト ブログ つぶやき 株式会社 最新 こちら 届け お仕事 ツイ 返信 プロ 今年 リプ ヘッダー アイコン アカ アカウント ツイート たま ブロック 無言 時間 お願い お願いします お願いいたします イベント フォロー フォロワー フォロバ スタッフ 自動 手動 迷言 名言 非公式 リリース 問い合わせ ツイッター)
- PROFILE_EXCLUDE_REGEXP =
Regexp.union(/\w+@\w+\.(com|co\.jp)/, %r[\d{2,4}(年|/)\d{1,2}(月|/)\d{1,2}日], %r[\d{1,2}/\d{1,2}], /\d{2}th/, URI.regexp)
Constants included from Utils
Instance Method Summary collapse
- #count_freq_hashtags(tweets, with_prefix: true, use_regexp: false, debug: false) ⇒ Object
- #fetch_lists(user, debug: false) ⇒ Object
- #hashtag_clusters(hashtags, limit: 10, debug: false) ⇒ Object
- #list_clusters(lists, shrink: false, shrink_limit: 100, list_member: 300, total_member: 3000, total_list: 50, rate: 0.3, limit: 10, debug: false) ⇒ Object
- #tweet_clusters(tweets, limit: 10, debug: false) ⇒ Object
Methods included from Collector
#collect_with_cursor, #collect_with_max_id
Instance Method Details
#count_freq_hashtags(tweets, with_prefix: true, use_regexp: false, debug: false) ⇒ Object
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/twitter_with_auto_pagination/rest/extension/clusters.rb', line 51 def (tweets, with_prefix: true, use_regexp: false, debug: false) puts "tweets: #{tweets.size}" if debug return {} if tweets.blank? prefix = %w(# #) regexp = /[##]([A-Za-zA-Za-z_一-鿆0-90-9ぁ-ヶヲ-゚ー]+)/ tweets = if use_regexp tweets.select { |t| t.text && prefix.any? { |char| t.text.include?(char)} } else tweets.select { |t| (t) } end puts "tweets with hashtag: #{tweets.size}" if debug = if use_regexp tweets.map { |t| t.text.scan(regexp).flatten.map(&:strip) } else tweets.map { |t| (t) } end.flatten = .map { |h| "#{prefix[0]}#{h}" } if with_prefix .each_with_object(Hash.new(0)) { |h, memo| memo[h] += 1 }.sort_by { |k, v| [-v, -k.size] }.to_h end |
#fetch_lists(user, debug: false) ⇒ Object
104 105 106 107 108 109 |
# File 'lib/twitter_with_auto_pagination/rest/extension/clusters.rb', line 104 def fetch_lists(user, debug: false) memberships(user, count: 500, call_limit: 2).sort_by { |li| li.member_count } rescue Twitter::Error::ServiceUnavailable => e puts "#{__method__}: #{e.class} #{e.} #{user.inspect}" if debug [] end |
#hashtag_clusters(hashtags, limit: 10, debug: false) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/twitter_with_auto_pagination/rest/extension/clusters.rb', line 77 def hashtag_clusters(, limit: 10, debug: false) puts "hashtags: #{.take(10)}" if debug hashtag, count = .take(3).each_with_object(Hash.new(0)) do |tag, memo| tweets = search(tag) puts "tweets #{tag}: #{tweets.size}" if debug memo[tag] = (tweets).reject { |t, c| t == tag }.values.sum end.max_by { |_, c| c } = (search(hashtag)).reject { |t, c| t == hashtag }.keys queries = .take(3).combination(2).map { |ary| ary.join(' AND ') } puts "selected #{hashtag}: #{queries.inspect}" if debug tweets = queries.map { |q| search(q) }.flatten puts "tweets #{queries.inspect}: #{tweets.size}" if debug if tweets.empty? tweets = search(hashtag) puts "tweets #{hashtag}: #{tweets.size}" if debug end members = tweets.map { |t| t.user } puts "members count: #{members.size}" if debug count_freq_words(members.map { |m| m.description }, special_words: PROFILE_SPECIAL_WORDS, exclude_words: PROFILE_EXCLUDE_WORDS, special_regexp: PROFILE_SPECIAL_REGEXP, exclude_regexp: PROFILE_EXCLUDE_REGEXP, debug: debug).take(limit) end |
#list_clusters(lists, shrink: false, shrink_limit: 100, list_member: 300, total_member: 3000, total_list: 50, rate: 0.3, limit: 10, debug: false) ⇒ Object
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/twitter_with_auto_pagination/rest/extension/clusters.rb', line 111 def list_clusters(lists, shrink: false, shrink_limit: 100, list_member: 300, total_member: 3000, total_list: 50, rate: 0.3, limit: 10, debug: false) lists = lists.sort_by { |li| li.member_count } puts "lists: #{lists.size} (#{lists.map { |li| li.member_count }.join(', ')})" if debug return {} if lists.empty? open('lists.txt', 'w') {|f| f.write lists.map(&:full_name).join("\n") } if debug list_special_words = %w() list_exclude_regexp = %r(list[0-9]*|people-ive-faved|twizard-magic-list|my-favstar-fm-list|timeline-list|conversationlist|who-i-met) list_exclude_words = %w(it list people who met) # リスト名を - で分割 -> 1文字の単語を除去 -> 出現頻度の降順でソート words = lists.map { |li| li.full_name.split('/')[1] }. select { |n| !n.match(list_exclude_regexp) }. map { |n| n.split('-') }.flatten. delete_if { |w| w.size < 2 || list_exclude_words.include?(w) }. map { |w| SYNONYM_WORDS.has_key?(w) ? SYNONYM_WORDS[w] : w }. each_with_object(Hash.new(0)) { |w, memo| memo[w] += 1 }. sort_by { |k, v| [-v, -k.size] } puts "words: #{words.take(10)}" if debug return {} if words.empty? # 出現頻度の高い単語を名前に含むリストを抽出 _words = [] lists = filter(lists, min: 2) do |li, i| _words = words[0..i].map(&:first) name = li.full_name.split('/')[1] _words.any? { |w| name.include?(w) } end puts "lists include #{_words.inspect}: #{lists.size} (#{lists.map { |li| li.member_count }.join(', ')})" if debug return {} if lists.empty? # 中間の 25-75% のリストを抽出 while lists.size > shrink_limit percentile25 = ((lists.length * 0.25).ceil) - 1 percentile75 = ((lists.length * 0.75).ceil) - 1 lists = lists[percentile25..percentile75] puts "lists sliced by 25-75 percentile: #{lists.size} (#{lists.map { |li| li.member_count }.join(', ')})" if debug end if shrink || lists.size > shrink_limit # メンバー数がしきい値より少ないリストを抽出 _list_member = 0 _min_list_member = 10 < lists.size ? 10 : 0 _lists = filter(lists, min: 2) do |li, i| _list_member = list_member * (1.0 + 0.25 * i) _min_list_member < li.member_count && li.member_count < _list_member end lists = _lists.empty? ? [lists[0]] : _lists puts "lists limited by list member #{_min_list_member}..#{_list_member.round}: #{lists.size} (#{lists.map { |li| li.member_count }.join(', ')})" if debug return {} if lists.empty? # トータルメンバー数がしきい値より少なくなるリストを抽出 _lists = [] lists.size.times do |i| _lists = lists[0..(-1 - i)] if _lists.map { |li| li.member_count }.sum < total_member break else _lists = [] end end lists = _lists.empty? ? [lists[0]] : _lists puts "lists limited by total members #{total_member}: #{lists.size} (#{lists.map { |li| li.member_count }.join(', ')})" if debug return {} if lists.empty? # リスト数がしきい値より少なくなるリストを抽出 if lists.size > total_list lists = lists[0..(total_list - 1)] end puts "lists limited by total lists #{total_list}: #{lists.size} (#{lists.map { |li| li.member_count }.join(', ')})" if debug return {} if lists.empty? members = lists.map do |li| begin list_members(li.id) rescue Twitter::Error::NotFound => e puts "#{__method__}: #{e.class} #{e.} #{li.id} #{li.full_name} #{li.mode}" if debug nil end end.compact.flatten puts "candidate members: #{members.size}" if debug return {} if members.empty? open('members.txt', 'w') {|f| f.write members.map{ |m| m.description.gsub(/\R/, ' ') }.join("\n") } if debug 3.times do _members = members.each_with_object(Hash.new(0)) { |member, memo| memo[member] += 1 }. select { |_, v| lists.size * rate < v }.keys if _members.size > 100 members = _members break else rate -= 0.05 end end puts "members included multi lists #{rate.round(3)}: #{members.size}" if debug count_freq_words(members.map { |m| m.description }, special_words: PROFILE_SPECIAL_WORDS, exclude_words: PROFILE_EXCLUDE_WORDS, special_regexp: PROFILE_SPECIAL_REGEXP, exclude_regexp: PROFILE_EXCLUDE_REGEXP, debug: debug).take(limit) end |
#tweet_clusters(tweets, limit: 10, debug: false) ⇒ Object
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/twitter_with_auto_pagination/rest/extension/clusters.rb', line 14 def tweet_clusters(tweets, limit: 10, debug: false) return {} if tweets.blank? text = tweets.map(&:text).join(' ') if defined?(Rails) exclude_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_bad_words_path'])) special_words = JSON.parse(File.read(Rails.configuration.x.constants['cluster_good_words_path'])) else exclude_words = JSON.parse(File.read('./cluster_bad_words.json')) special_words = JSON.parse(File.read('./cluster_good_words.json')) end %w(べたら むっちゃ それとも たしかに さそう そんなに ったことある してるの しそうな おやくま ってますか これをやってるよ のせいか 面白い 可愛い).each { |w| exclude_words << w } %w(面白い 可愛い 食べ物 宇多田ヒカル ご飯 面倒 体調悪くなる 空腹 頑張ってない 眼鏡 台風 沖縄 らんま1/2 女の子 怪我 足のむくみ 彼女欲しい 彼氏欲しい 吐き気 注射 海鮮チヂミ 出勤 価格ドットコム 幹事 雑談 パズドラ ビオフェルミン 餃子 お金 まんだらけ 結婚 焼肉 タッチペン).each { |w| special_words << w } # クラスタ用の単語の出現回数を記録 frequency = special_words.map { |sw| [sw, text.scan(sw)] } .delete_if { |_, matched| matched.empty? } .each_with_object(Hash.new(0)) { |(word, matched), memo| memo[word] = matched.size } # 同一文字種の繰り返しを見付ける。漢字の繰り返し、ひらがなの繰り返し、カタカナの繰り返し、など text.scan(/[一-龠〆ヵヶ々]+|[ぁ-んー~]+|[ァ-ヴー~]+|[a-zA-ZA-Z0-9]+|[、。!!??]+/). # 複数回繰り返される文字を除去 map { |w| w.remove /[?!?!。、w]|(ー{2,})/ }. # 文字数の少なすぎる単語、除外単語を除去する delete_if { |w| w.length <= 2 || exclude_words.include?(w) }. # 出現回数を記録 each { |w| frequency[w] += 1 } # 複数個以上見付かった単語のみを残し、出現頻度順にソート frequency.select { |_, v| 2 < v }.sort_by { |k, v| [-v, -k.size] }.take(limit).to_h end |