Exemple #1
0
    def __init__(self):
        '''実行時点での最新のmessage.idをセットする'''
        with Messages(role='slave') as m:
            item_latest = m.get_latest()

        r = Connect().open()
        r.set(app.config['MSG_LAST_PULLED'], item_latest['id'])
Exemple #2
0
    def run(self):
        '''
        @return None
        '''

        if not self.items['x'] or not self.items['y']:
            raise Exception('Must be called set_datasets before Train.train')

        app.config.from_object('config.Words')

        vect = TfidfVectorizer(analyzer='word',
                               max_df=0.5,
                               min_df=5,
                               max_features=1280,
                               stop_words=app.config['STOP_WORDS'],
                               ngram_range=(1, 1))

        tfidf_fit = vect.fit(self.items['x'])

        # 分類器が理解できる形式に変換
        tfidf_transform = vect.transform(self.items['x'])

        # TruncatedSVDはKernelPCAと違って標準化する必要がない
        # n_componentsが次元数を表している
        # LSAは100次元でも1000次元でも大して精度は変わらない。
        # 1000次元にすると処理時間がいっきに長くなる
        lsa = TruncatedSVD(n_components=128, random_state=0)
        lsa_fit = lsa.fit(tfidf_transform)
        lsa_transform = lsa.transform(tfidf_transform)

        # Grid searchで導き出したパラメータたち
        clf = PassiveAggressiveClassifier(C=0.1,
                                          class_weight=None,
                                          fit_intercept=True,
                                          loss='hinge',
                                          n_iter=5,
                                          n_jobs=-1,
                                          random_state=0,
                                          shuffle=True,
                                          verbose=0,
                                          warm_start=False)

        clf_fit = clf.fit(lsa_transform, self.items['y'])

        # Convert to binary
        tfidf_pickle = gzip.compress(
            pickle.dumps(tfidf_fit, pickle.HIGHEST_PROTOCOL))

        lsa_pickle = gzip.compress(
            pickle.dumps(lsa_fit, pickle.HIGHEST_PROTOCOL))

        clf_pickle = gzip.compress(
            pickle.dumps(clf_fit, pickle.HIGHEST_PROTOCOL))

        r = Connect().open()
        with r.pipeline(transaction=False) as pipe:
            pipe.set(self.tfidf_key, tfidf_pickle)
            pipe.set(self.lsa_key, lsa_pickle)
            pipe.set(self.clf_key, clf_pickle)
            pipe.execute()
Exemple #3
0
    def _set(self):
        # MSG_LAST_PULLEDに最新のmessage.idをセットする
        with Mysql_messages(role='slave') as m:
            msg = m.get_latest()

        r = Connect(host='api', role='master').open()
        r.set(app.config['MSG_LAST_PULLED'], msg['id'])
Exemple #4
0
 def launch(self, url):
     '''
     app.config['URLS']が空の場合は当メソッドを呼び出すこと
     :param str url
     '''
     self._start(url)
     r = Connect().open()
     urls = r.lrange(app.config['URLS'], 0, -1)
     print('{0} urls are pushed.'.format(len(urls)))
Exemple #5
0
    def dump(self):
        '''
        # Redisからデータセットを取り出し、pickle >> gzip >> ファイル書き出し
        Convert to pickle
        '''

        path = self.directory + self.file

        con = Connect(host='api', role='slave')
        r = con.open()

        ds_pjt_mlm_pos = r.hgetall(app.config['DATASETS_PJT_MLM_POS'])
        ds_pjt_mlm_neg = r.hgetall(app.config['DATASETS_PJT_MLM_NEG'])
        ds_msg_pos = r.hgetall(app.config['DATASETS_MSG_POS'])
        ds_msg_neg = r.hgetall(app.config['DATASETS_MSG_NEG'])
        url_blacklist = r.smembers(app.config['URL_BLACKLIST'])

        ds_pjt_mlm_pos_pickle = pickle.dumps(
            ds_pjt_mlm_pos, pickle.HIGHEST_PROTOCOL)

        ds_pjt_mlm_neg_pickle = pickle.dumps(
            ds_pjt_mlm_neg, pickle.HIGHEST_PROTOCOL)

        ds_msg_pos_pickle = pickle.dumps(
            ds_msg_pos, pickle.HIGHEST_PROTOCOL)

        ds_msg_neg_pickle = pickle.dumps(
            ds_msg_neg, pickle.HIGHEST_PROTOCOL)

        url_blacklist_pickle = pickle.dumps(
            url_blacklist, pickle.HIGHEST_PROTOCOL)

        try:
            with gzip.open(path.format('ds_pjt_mlm_pos'), 'wb') as f:
                f.write(ds_pjt_mlm_pos_pickle)

            with gzip.open(path.format('ds_pjt_mlm_neg'), 'wb') as f:
                f.write(ds_pjt_mlm_neg_pickle)

            with gzip.open(path.format('ds_msg_pos'), 'wb') as f:
                f.write(ds_msg_pos_pickle)

            with gzip.open(path.format('ds_msg_neg'), 'wb') as f:
                f.write(ds_msg_neg_pickle)

            with gzip.open(path.format('url_blacklist'), 'wb') as f:
                f.write(url_blacklist_pickle)
        except Exception as e:
            app.sentry.captureException(str(e))
            app.logger.error(str(e))
            return

        app.logger.debug('Export finished')
Exemple #6
0
    def _remove(self, item):
        '''
        @param tuple item
            e.g. (id, body)
        '''

        with Redis_objects() as r:
            items_c = r.get(self.key_name, reversed_flag=False)

        if os.environ.get('ENVIRONMENT') == 'development':
            items_c_len = len(items_c)
            # 全て表示させると遅くなるので、3の倍数の時だけ出力
            if (items_c_len % 3) == 0:
                print('{0}, Laps: {1}/{2}'.format(self.key_name,
                                                  str(items_c_len),
                                                  self.items_len))

        del_items = set([])

        for item_c in items_c:
            # 自分自身の場合はスキップ
            # 対処済みitemの場合はスキップ
            if item[0] == item_c[0] or int(item[0]) > int(item_c[0]):
                continue

            s = difflib.SequenceMatcher(None, item[1], item_c[1])

            if s.ratio() > 0.9:
                del_items.add(item_c[0])

        # 一周ごとにまとめて削除
        if del_items:
            Connect().open().hdel(self.key_name, *del_items)
Exemple #7
0
    def _send(self, room_id, body):
        '''
        @param int room_id
        @param string body
        @return int 正常に完了すれば200が返る。それ以外は500を返すようにしている
        '''

        url = self.url + 'rooms/{0}/messages'.format(room_id)
        payload = {'body': body}

        try:
            res = requests.post(url, headers=self.headers, data=payload)
            return res.status_code
        except Exception as e:
            app.sentry.captureException(str(e))
            # chatworkに接続できない場合はjsonで1つの文字列にしてキューに入れる
            # 接続できない場合はConnectionErrorが発生する
            Connect().open().rpush(
                app.config['QUEUE_CHATWORK_MSG'],
                json.dumps({
                    'room_id': room_id,
                    'body': body
                }))

            app.logger.error(
                'Chawtwork _send is failed. Reason: {0}'.format(e))
            return 500
Exemple #8
0
    def _import(self):
        '''Import from pickle'''
        path = self.directory + self.file

        # データセットを取り出して、dumpして、Redisにrestore
        try:
            with gzip.open(path.format('ds_pjt_mlm_pos'), 'rb') as f:
                ds_pjt_mlm_pos_pickle = f.read()

            with gzip.open(path.format('ds_pjt_mlm_neg'), 'rb') as f:
                ds_pjt_mlm_neg_pickle = f.read()

            with gzip.open(path.format('ds_msg_pos'), 'rb') as f:
                ds_msg_pos_pickle = f.read()

            with gzip.open(path.format('ds_msg_neg'), 'rb') as f:
                ds_msg_neg_pickle = f.read()

            with gzip.open(path.format('url_blacklist'), 'rb') as f:
                url_blacklist_pickle = f.read()
        except Exception as e:
            app.logger.error(e)
            return

        ds_pjt_mlm_pos = pickle.loads(ds_pjt_mlm_pos_pickle)
        ds_pjt_mlm_neg = pickle.loads(ds_pjt_mlm_neg_pickle)
        ds_msg_pos = pickle.loads(ds_msg_pos_pickle)
        ds_msg_neg = pickle.loads(ds_msg_neg_pickle)
        url_blacklist = pickle.loads(url_blacklist_pickle)

        r = Connect(host='api', role='master').open()

        with r.pipeline(transaction=True) as pipe:
            pipe.delete(app.config['DATASETS_PJT_MLM_POS'])
            pipe.delete(app.config['DATASETS_PJT_MLM_NEG'])
            pipe.delete(app.config['DATASETS_MSG_POS'])
            pipe.delete(app.config['DATASETS_MSG_NEG'])
            pipe.delete(app.config['URL_BLACKLIST'])

            pipe.hmset(app.config['DATASETS_PJT_MLM_POS'], ds_pjt_mlm_pos)
            pipe.hmset(app.config['DATASETS_PJT_MLM_NEG'], ds_pjt_mlm_neg)
            pipe.hmset(app.config['DATASETS_MSG_POS'], ds_msg_pos)
            pipe.hmset(app.config['DATASETS_MSG_NEG'], ds_msg_neg)
            for url in url_blacklist:
                pipe.sadd(app.config['URL_BLACKLIST'], url)

            pipe.execute()
Exemple #9
0
    def _notify_msg(self, item, debug=False):
        '''
        通知済みのユーザは2度通知しない
        @param dict item
        @return int 200 | None
        '''

        if not debug:
            r = Connect(role='slave').open()
            # 検知済みuserの取得
            # @return set型
            spam_user_ids = r.smembers(app.config['MSG_DETECTED_USER_ID'])

            # 通知済みのuserの場合は通知しない
            if str(item['user_id']) in spam_user_ids:
                return None

            # 違反判定されたmessageの作成者の場合は追加して、次から通知しないようにする
            r_m = Connect().open()
            r_m.sadd(app.config['MSG_DETECTED_USER_ID'], item['user_id'])

        chatwork = Chatwork()
        res = chatwork.post(
            app.config['ROOM_ID_MSG'],
            '''---{0}-----------------------------------\nScore: {1}\nVocabulary: {2}\nBoard Url: {3}\nUser Edit Url: {4}'''
            .format(
                datetime.now(
                    pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d %H:%M:%S"),
                item['score'], item['vocabulary'],
                app.config['URL_BOARD_ADMIN'].format(item['board_id']),
                app.config['URL_USER_ADMIN'].format(item['user_id'])))

        return res
Exemple #10
0
    def _get(self):
        '''
        最後に取得した以降に作成されたデータを取得
        @return list
        '''

        r = Connect().open()
        last_pulled_id = r.get(app.config['MSG_LAST_PULLED'])

        with Messages(role='slave') as m:
            items = m.get_for_local(last_pulled_id)

        if not items:
            return []

        r.set(app.config['MSG_LAST_PULLED'], items[-1]['id'])

        return items
Exemple #11
0
    def prepare_urls_pos(self):
        '''
        blacked判定されたユーザのメッセージからurlを全て抜き出し、Redisに保存
        本番環境では使わないメソッド
        http:// がないケースも抜き出してみる。goo.gl/xxx/xxxとか。
        @return int
        '''
        # まず消す
        self.r.delete(app.config['URL_BLACKLIST'])

        # blackedユーザのmessageを全て取得
        with Messages(role='slave') as m:
            messages = m.get_pos()

        # 各メッセージからurlを抜き出し、セット
        urls = []
        for msg in messages:
            url_extract = self.extract_url(msg['description'].decode('utf-8'))
            if url_extract:
                urls.extend(url_extract)

        urls_pos = []
        urls_google = []

        for url in urls:
            # 対象にしないurlたち
            if self._filter_url(url):
                continue

            # goo.gl短縮URLの場合は本来のURLを抽出
            if 'goo.gl' in url:
                urls_google.append(url)
            else:
                urls_pos.append(url)

        # set型に変換することで重複しているurlを消す
        urls_pos = set(urls_pos)
        urls_google = set(urls_google)

        urls_google_original = []
        for url in urls_google:
            url_original = self._extract_href_short_google(url)
            if url_original:
                if self._filter_url(url_original):
                    continue
                urls_google_original.append(url_original)
                #print('Google url: {0} 実際のurl: {1}'.format(url, url_original))

        # set型に変換することで重複しているurlを消し、urls_posと連結
        urls_last = urls_pos.union(set(urls_google_original))

        for url in urls_last:
            self.add_url_blacklist(url)

        count = Connect(role='slave').open().scard(app.config['URL_BLACKLIST'])
        print('URL_BLACKLISTの数: {0} 個のurlをsaddした'.format(str(count)))
Exemple #12
0
    def post(self, room_id, body):
        '''
        @param int room_id
        @param string body
        @return int 正常に完了すれば200が返る
        chatworkにメッセージを送る
        '''

        r = Connect().open()

        # キューに入っているのがあればまとめてpostする
        # lrangeは値がなければ空のlistを返す
        items_len = r.llen(app.config['QUEUE_CHATWORK_MSG'])
        if items_len >= 1:
            for i in range(items_len):
                item = r.lpop(app.config['QUEUE_CHATWORK_MSG'])
                item = json.loads(item)
                self._send(item['room_id'], item['body'])

        return self._send(room_id, body)
Exemple #13
0
    def emit_spam_update_message(self, args):
        '''
        spam_update_messageイベントをemitする
        @param dict args
        @return int
            Returns the number of subscribers the message was delivered to.
        '''

        r = Connect(host='pubsub', role='master').open()

        channel_name = 'socket.io#/#{0}#'.format(args['board_id'])

        # socket.io-redisのイベント名、渡すデータはこのような形である。
        data_packed = msgpack.packb([
            'emitter',
            {
                # socket.ioはv1からバイナリ形式のデータ、つまり、画像・動画も送信できる
                # ようになった。文字列型と区別するためにtypeキーがある。
                # 文字列型は2で、バイナリ型は5を指定する
                'type':
                2,
                'data': [
                    'spam_update_message', {
                        'board_id': args['board_id'],
                        'message_id': args['message_id'],
                        'feedback_from_admin': args['feedback_from_admin'],
                        'feedback_from_user': args['feedback_from_user'],
                        'predict': args['predict'],
                    }
                ],
                # namespaceのこと
                'nsp':
                '/'
            },
            {
                'rooms': [args['board_id']],
                'flags': []
            }
        ])

        return r.publish(channel_name, data_packed)
Exemple #14
0
    def run(self, obj):
        '''
        Queueからmessage_idを取得してspamかどうかを予測する
        @param str obj msg or pjt
        @return None
        '''

        r = Connect().open()
        msg_id = r.lpop(app.config['QUEUE_BASE_MSG'])

        # キューが空であれば何もしない
        if not msg_id:
            return None

        try:
            self._detect_msg(int(msg_id))
        except Exception as e:
            app.sentry.captureException(str(e))
            app.logger.error(
                'predict message is failed. Reason: {0}'.format(e))
            # 失敗した場合はキューに戻す
            r.rpush(app.config['QUEUE_BASE_MSG'], msg_id)
            raise
Exemple #15
0
    def run(self):
        '''

        '''
        app.logger.info('run_crawler start')

        r = Connect().open()
        while True:
            url = r.rpop(app.config['URLS'])
            if not url:
                app.logger.info('Url is empty. Loop has been done.')
                print('url is empty. Loop has been done.')
                break

            p = Process(target=self._start, args=(url,))
            p.start()
            # 5秒経過しても終了しない場合はTimeout
            # 例えば、4GBのファイルダウンロードなどに当たるといつまでたっても終わらないので、
            # 強制終了させる。本当はrequets側で終了させたいが、そういう設定が無いようなので
            # ここで終了させる
            p.join(5)
            # 生成された子プロセスを終了させる。
            p.terminate()
Exemple #16
0
    def _predict(self, body):
        '''
        @param str body
        @return dict
        '''
        # decode_responses=Falseにするためにメソッド内で呼び出している
        con = Connect(role='slave', decode_responses=False)
        r_s = con.open()

        with r_s.pipeline(transaction=False) as pipe:
            pipe.get(self.vect)
            pipe.get(self.lsa)
            pipe.get(self.clf)
            pickles = pipe.execute()

        # バイナリーに戻す
        vect = pickle.loads(gzip.decompress(pickles[0]))
        lsa = pickle.loads(gzip.decompress(pickles[1]))
        clf = pickle.loads(gzip.decompress(pickles[2]))

        # TFIDFはiterableな値しか受けつけないので、リストで渡す
        tfidf = vect.transform([body])
        lsa_reduced = lsa.transform(tfidf)
        predict = clf.predict(lsa_reduced)

        score = self._get_score(clf, lsa_reduced)

        vocabulary = self._get_vocabulary(vect, tfidf)

        # 1はspam、0はspamではない
        return {
            # 1度に1つの対象をpredictすることを想定しているので、0番目を返す
            # predict[0]は <class 'numpy.int64'>型になっているのでintにする
            'predict': int(predict[0]),
            'score': score,
            'vocabulary': vocabulary
        }
Exemple #17
0
    def run(self):
        '''
        @param string key_name
        @return None
        '''

        con = Connect(role='slave')
        r_s = con.open()

        if not r_s.exists(self.key_name):
            raise Exception('The key dose not exist in Redis')

        # GET DATA
        if self.obj_type == 'pjt_mlm':
            items = self._get_pjt(work_type='mlm')

        if self.obj_type == 'pjt_vl':
            items = self._get_pjt(work_type='vl')

        if self.obj_type == 'msg':
            items = self._get_msg()

        with Pool(processes=app.config['POOL_PROCESS_NUM']) as pool:
            pool.map(self._add, items)
Exemple #18
0
    def _add(self, item):
        '''
        @param tuple
            e.g. (b_id, [b_body, b_body])
        @return dict
        '''

        if self.obj_type == 'pjt_mlm' or self.obj_type == 'pjt_vl':
            # 比較対象のitemsをセット
            # 追加しようとしているデータ集合の中に重複しているものが全然あるので、追加するたびに、
            # redisから全部取得 >> 追加しようとしているデータとの重複チェック >> セット
            # redisから全部取得 >> ... というredisとのi/oが無駄に多い処理を行う
            with Redis_objects() as r:
                items_c = r.get(self.key_name, reversed_flag=False)

            wakati = Wakati()
            item_tup = (item[0], wakati.parse(' '.join(item[1])),)

        if self.obj_type == 'msg':
            with Redis_objects() as r:
                items_c = r.get(self.key_name, reversed_flag=False)

            # descriptionを連結して、分かち書きにする
            # ('board_id', 'dec1 dec2 dec3')
            wakati = Wakati()
            item_tup = (item[0], wakati.parse(' '.join(item[1])),)

        overlap = Overlap()
        is_overlap = overlap.is_overlap(item_tup, items_c)

        if os.environ.get('ENVIRONMENT') == 'development':
            # False/:board_idだと重複していないテキストだということ
            print('{0}, Overlap: {1}/{2}'.format(
                self.key_name,
                str(is_overlap),
                str(item[0])
            ))

        if not is_overlap:
            # ここでhmset実行
            Connect().open().hmset(self.key_name, {
                item_tup[0]: item_tup[1]
            })
Exemple #19
0
class Objects():
    '''
    コンテキストマネージャで呼び出すこと。
    '''
    def __init__(self, role='slave'):
        self.role = role

    def __enter__(self):
        self.con = Connect(role=self.role)
        self.r = self.con.open()
        return self

    def __exit__(self, exc_type, exc_value, exc_tb):
        '''
        redisの場合はcloseに値するメソッドが用意されていない。
        ただ、将来の拡張性を想定して、コンテキストマネージャ型にしておく
        '''
        if exc_type is None:
            pass
        else:
            app.logger.warning('Message Redis connection closing is failed')
            return False

    def get(self, key_name, reversed_flag=True):
        '''
        hgetallで取得するデータを逆順で取れるようにするためにこのメソッドを用意している
        @param string key_name
        @param bool reversed_flag
        @return list of tuple
            e.g. [(id1, 'body1',...}]
        '''
        objects = self.r.hgetall(key_name)

        # Trueが指定されたら逆順にする
        if reversed_flag:
            # 小難しいが、key=lambda x: int(x[0]) こうすることで、id部分で逆順に並べ変えてくれる
            # redisには数値もstringで入っているので、int()変換してあげる必要がある
            o_sorted = [(o_id, o_body) for o_id, o_body in sorted(
                objects.items(), key=lambda x: x[0], reverse=True)]
        else:
            o_sorted = [(o_id, o_body) for o_id, o_body in objects.items()]

        return o_sorted
Exemple #20
0
    def _save(self, now, scheme, netloc, path, pwa, urls_external):
        '''
        :return int
        '''

        if scheme == 'http':
            scheme = 0
        elif scheme == 'https':
            scheme = 1
        else:
            app.logger.info(
                'scheme is not http or https. scheme is {0}'.format(scheme))
            return 0

        host, domain = self._extract_host_domain(netloc)

        with Urls() as m:

            if not m.is_exist(netloc):
                m.add({
                    'datetime': now,
                    'scheme': scheme,
                    'netloc': netloc,
                    'host': host,
                    'domain': domain,
                    'path': path,
                    'pwa': pwa,
                    'urls_external': json.dumps(urls_external)
                })

        if urls_external:
            with Connect().open().pipeline(transaction=False) as pipe:
                pipe.lpush(app.config['URLS'], *urls_external)
                pipe.execute()

        return 1
Exemple #21
0
class Scraper():
    '''
    spam messagesには80%以上の確率でurlが含まれる。
    url関連の処理は当classにまとめる
    '''
    def __init__(self):
        ''''''
        self.r = Connect().open()
        self.urls_black = self.r.smembers(app.config['URL_BLACKLIST'])

    def _filter_url(self, url):
        '''
        対象外にするurlが含まれるかをチェックする
        @param str url
        @return bool
        '''
        if 'chatwork.com' in url:
            return True
        elif 'lancers.jp' in url:
            return True
        #elif 'youtube.com' in url:
        #    return True
        #elif 'youtu.be' in url:
        #    return True
        elif 'drive.google.com' in url:
            return True
        elif 'accounts.google.com' in url:
            return True
        elif 'retrip.jp' in url:
            return True
        elif 'mozilla.org' in url:
            return True
        elif 'bitcoinlab.jp' in url:
            return True
        elif 'www.google.co.jp/chrome/browser' in url:
            return True
        else:
            return False

    def extract_url(self, body):
        '''
        messageの本文からurlを抜き出す。
        @body str body
        @return list
            抜き出したurlをlist形式で返す
            re.findallは、検索文字列内にパターンがなければ空のlistを返す
        '''
        url_res = []
        urls_find = re.findall(app.config['URL_PATTERN'], body)
        if urls_find:
            for url_find in urls_find:
                if not self._filter_url(url_find):
                    url_res.append(url_find)

        return url_res

    def _extract_href_short_google(self, url):
        '''
        goo.glの短縮URLサービスのために、リダイレクトページを挟んでいるページのURLを取得する
        goo.gl以外にも http://bit.ly/2xX9hPB などの短縮URLは存在するが、大抵、200番が
        返ってきてrequestsが適切に対処できないので、goo.glだけ対応する
        @param str url
        @return str
        '''
        text = self._request_url(url, allow_redirects=False)
        if not text:
            return ''

        href = self._extract_href(text)

        # hrefが存在しないということはあり得ないが念のため
        if not href:
            return ''

        # goo.glのページに含まれるURLは必ず1つである
        return href[0]

    def _extract_href(self, text):
        '''
        htmlエレメントの中か<a>タグのhref属性を全て取得する
        @param <class 'bs4.element.ResultSet'> _retrieve_url()の返り値を渡すこと
        @return list
            href属性の値、つまり、urlをlistでまとめて返す。存在しなければ空のlistを返す
        '''

        soup = BeautifulSoup(text, "lxml")
        # <body>タグ内の<a>タグを全て取得する
        # 存在しない場合は空のlistを返す
        aa = soup.body.find_all('a')

        if not aa:
            return []

        # <a>タグのhref属性を全て取得する
        href = []
        try:
            for a in aa:
                # http(s)で始まるもののみを選別する
                # 選別しないと相対パスやjavascript:void(0);とかが結構混ざってくる
                url = self._parse_url(a['href'])
                if url:
                    href.append(url)
        # <a>タグがあるにも関わらず、href属性がない場合はスキップ
        except KeyError as e:
            pass

        return href

    def _parse_url(self, url):
        '''
        @param str url
        @return str
        '''
        # '//' で始まる場合にのみ netloc を認識する。それ以外の場合は、
        # 入力は相対URLであると推定され、path 部分で始まることになる
        url_parse = urlsplit(url)
        if not url_parse.netloc:
            return ''

        # 末尾に ) が混ざることが多々あるので取り除く
        if not url_parse.path:
            path = ''
        elif url_parse.path[-1] == ')':
            path = url_parse.path[0:-2]
        else:
            path = url_parse.path

        if url_parse.query:
            query = '?' + url_parse.query
        else:
            query = ''

        return url_parse.scheme + '://' + url_parse.netloc + path + query

    def _find_url(self, url):
        '''
        ブラックリストとして登録しているURLが含まれるかを探す
        @param str url List形式でurlを渡す
        @return str
            見つけたらそのurlを返す。なければ空の文字列を返す
        '''
        url_parsed = self._parse_url(url)

        # urlの形式ではなければ、空の文字列を返す
        if not url_parsed:
            return ''

        for url_black in self.urls_black:
            if url_parsed == url_black:
                return url_parsed

        # 該当するものがなければ空の文字列を返す
        return ''

    def _request_url(self, url, allow_redirects=True):
        '''
        特定のURLのテキストデータをリクエストして取得する
        3.0秒以上経っても返ってこないレスポンスは無視
        @param str url
        @param bool allow_redirects
            301が返ってきたときに自動的にそのurlへリダイレクトするか否か。
        @return str
        '''
        try:
            r = requests.get(url,
                             timeout=3.0,
                             headers=app.config['HEADERS'],
                             allow_redirects=allow_redirects)

        # 3秒以上経過してもレスポンスが返ってこない場合
        except requests.exceptions.ConnectTimeout as e:
            app.sentry.captureException(str(e))
            return ''
        # 何かしらの例外が発生した場合
        except:
            app.sentry.captureException(str(e))
            return ''

        if r.status_code == 404:
            return ''

        if r.status_code >= 400:
            app.sentry.captureMessage(
                'スクレイピングでエラーが返ってきた。なぜだ!? status: {0}, url: {1}'.format(
                    r.status_code, url))
            return ''

        return r.text

    def _find_keyword(self, text):
        '''
        htmlエレメントの中からブラックリストとして登録しているキーワードが含まれるかをチェックする
        大文字・小文字を区別しない
        @param <class 'bs4.element.ResultSet'> _request_url()の返り値を渡すこと
        @return list
            キーワードが見つかればその文字列を返す
        '''
        soup = BeautifulSoup(text, "lxml")

        # <body>タグ内に調べたい文字列があるか調べる
        # Listで返る。なければ空のListが返る。
        # e.g. [' LINE ID ', ' LINE', 'TwitterLine', 'Facebookline'] | []
        keywords = soup.body.find_all(text=re.compile(
            app.config['PATTERN_BLACKLIST_KEYWORDS_COMPILE'], re.IGNORECASE))

        if not keywords:
            return []

        # ' LINE ID 'こんな感じに前後にスペースが入っているケースが多いので
        # strip関数を挟んでもう一度検索する
        # また、onlineこういうのも抽出してしまうので、ここでもう一度検索し直す
        texts = []
        for keyword in keywords:
            m = re.search(app.config['PATTERN_BLACKLIST_KEYWORDS_SEARCH'],
                          keyword.strip(), re.IGNORECASE)
            if m:
                texts.append(m.group(0))

        return texts

    def prepare_urls_pos(self):
        '''
        blacked判定されたユーザのメッセージからurlを全て抜き出し、Redisに保存
        本番環境では使わないメソッド
        http:// がないケースも抜き出してみる。goo.gl/xxx/xxxとか。
        @return int
        '''
        # まず消す
        self.r.delete(app.config['URL_BLACKLIST'])

        # blackedユーザのmessageを全て取得
        with Messages(role='slave') as m:
            messages = m.get_pos()

        # 各メッセージからurlを抜き出し、セット
        urls = []
        for msg in messages:
            url_extract = self.extract_url(msg['description'].decode('utf-8'))
            if url_extract:
                urls.extend(url_extract)

        urls_pos = []
        urls_google = []

        for url in urls:
            # 対象にしないurlたち
            if self._filter_url(url):
                continue

            # goo.gl短縮URLの場合は本来のURLを抽出
            if 'goo.gl' in url:
                urls_google.append(url)
            else:
                urls_pos.append(url)

        # set型に変換することで重複しているurlを消す
        urls_pos = set(urls_pos)
        urls_google = set(urls_google)

        urls_google_original = []
        for url in urls_google:
            url_original = self._extract_href_short_google(url)
            if url_original:
                if self._filter_url(url_original):
                    continue
                urls_google_original.append(url_original)
                #print('Google url: {0} 実際のurl: {1}'.format(url, url_original))

        # set型に変換することで重複しているurlを消し、urls_posと連結
        urls_last = urls_pos.union(set(urls_google_original))

        for url in urls_last:
            self.add_url_blacklist(url)

        count = Connect(role='slave').open().scard(app.config['URL_BLACKLIST'])
        print('URL_BLACKLISTの数: {0} 個のurlをsaddした'.format(str(count)))

    def add_url_blacklist(self, url):
        '''
        url_blacklistにurlを追加する
        Set型を採用しているので、重複の心配はない
        @return int
        '''
        url = self._parse_url(url)
        if not url:
            return 0

        self.r.sadd(app.config['URL_BLACKLIST'], url)

    def run(self, body):
        '''
        @param str body
            messageのdescriptionを渡せば良い
            decode()でstr型に変換してから渡すこと
        @return dict
        '''

        # メッセージの本文からurlを抜き出す
        # @return list
        urls = self.extract_url(body)

        # bodyにurlが含まれなければ終了
        if not urls:
            return {'url': {'urls': [], 'url_blacklist': []}}

        # urlが存在すればblacklistのurlかどうかをチェック
        urls_blacklist = []
        for url in urls:
            if self._filter_url(url):
                continue
            res_find = self._find_url(url)
            if res_find:
                urls_blacklist.append(res_find)

        return {'url': {'urls': urls, 'url_blacklist': urls_blacklist}}
Exemple #22
0
 def __init__(self):
     ''''''
     self.r = Connect().open()
     self.urls_black = self.r.smembers(app.config['URL_BLACKLIST'])
Exemple #23
0
 def __enter__(self):
     self.con = Connect(role=self.role)
     self.r = self.con.open()
     return self