def __init__(self, topic='covid_kor', consumer_group='my-group', host="localhost", port="9092"): """ Kafka Consumer Option (not all) - bootstrap_servers(default=9092): 브로커(host:port)를 리스트로 나열, 노드 전부를 쓸 필요는 없음 - auto_offset_reset(default='latest'): 0, 서버로부터 어떠한 ack도 기다리지 않음. 처리량 증가하지만 유실율도 증가 1, 리더의 기록을 확인 후 ack 받음. 모든 팔로워에 대해서 확인하지는 않음 'all', 모든 ISR(리더의 모든 팔로워)의 기록을 확인 후 ack 받음. 무손실 보장 - enable_auto_commit(default=True): 데이터를 압축하여 보낼 포멧 (‘gzip’, ‘snappy’, ‘lz4’, or None) - value_deserializer(default=None): 압축된 msg를 받았을 때 이를 역직렬화할 함수(callable). (producer 의 serializer와 동일한 값으로 하지않으면 예외 발생) """ try: self.consumer = KafkaConsumer( f"{topic}", bootstrap_servers=[f"{host}:{port}"], auto_offset_reset="latest", enable_auto_commit=True, group_id=consumer_group, value_deserializer=lambda x: loads(x.decode("utf-8")), consumer_timeout_ms=1000 * 60 * 5, ) except KafkaError as e: Log.e(f"KafkaConsuemr fail. {e}") Log.i("KafkaConsuemr connect.")
def on_send_error(self, exception): """ send 함수의 callback 으로 데이터가 broker 에게 전달되지 못할 시 호출됨 :param exception: 전달에 실패한 오류(kafka exception)에 대한 설명 :return: - """ Log.e(f"{exception}")
def make_days_random_data(self, origin_data, days=7, num_of_tweets=20000): if 'created_at' in origin_data['data']: if self.daily_cnt >= num_of_tweets: exit() else: start = t.time() while (num_of_tweets) > 0: data = json.dumps(origin_data, ensure_ascii=False) self.producer.send_data('test', data) num_of_tweets -= 1 print("elapsed :", t.time() - start) else: Log.e("KEYERROR no 'created_at' in origin_data['data'].")
def make_diff_time_data(self, origin_data: dict, diff_day=5, diff_time='000000'): if 'created_at' in origin_data['data']: new_data = copy.deepcopy(origin_data) datetime_origin = self.toggle_time_format( origin_data['data']['created_at']) diff = timedelta(days=diff_day, hours=int(diff_time[:2]), minutes=int(diff_time[2:4]), seconds=int(diff_time[4:6])) new_created_at = self.toggle_time_format(datetime_origin + diff) new_data['data']['created_at'] = new_created_at else: Log.e("KEYERROR no 'created_at' in origin_data['data'].")
def make_diff_time_data(self, origin_data: dict, diff_day=5, diff_time='000000'): """ 원본 tweet 을 기반으로 시간의 차이를 통해 새로운 시간의 tweet 데이터를 생성 :param origin_data: [Tweet Data] 원본 tweet 데이터 :param diff_day: [int] 생성하고자 하는 데이터와 원본 데이터의 날짜 차이 :param diff_time: [str] 생성하고자 하는 데이터와 원본 데이터의 시간 차이('HHMMSS') :return: - """ if 'created_at' in origin_data['data']: new_data = copy.deepcopy(origin_data) datetime_origin = self.toggle_time_format(origin_data['data']['created_at']) diff = timedelta(days=diff_day, hours=int(diff_time[:2]), minutes=int(diff_time[2:4]), seconds=int(diff_time[4:6])) new_created_at = self.toggle_time_format(datetime_origin + diff) new_data['data']['created_at'] = new_created_at else: Log.e("KEYERROR no 'created_at' in origin_data['data'].")
def get_stream_rules(self): rule_ids = [] try: response = self.api.request("tweets/search/stream/rules", method_override="GET") Log.i(f"[{response.status_code}] RULES: {response.text}") if response.status_code != 200: raise Exception(response.text) else: for item in response: if "id" in item: rule_ids.append(item["id"]) else: Log.i(json.dumps(item, ensure_ascii=False)) return rule_ids except TwitterRequestError as e: msg_list = ["RequestError:", e] for msg in iter(e): msg_list.append(msg) err_msg = " ".join(msg_list) Log.e(err_msg) except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(2) except Exception as e: Log.e(f"BaseException: {e}") self.prompt_reconnect_msg(2)
def __init__(self): try: auth_info = TwitterOAuth.read_file() self.api = TwitterAPI( auth_info.consumer_key, auth_info.consumer_secret, auth_type="oAuth2", api_version="2", ) except TwitterRequestError as e: msg_list = ["RequestError:", e] for msg in iter(e): msg_list.append(msg) err_msg = " ".join(msg_list) Log.e(err_msg) except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(2) except Exception as e: Log.e(f"BaseException: {e}") self.prompt_reconnect_msg(2) # comma-seperated list with no space between fields self.expansions = "attachments.media_keys,author_id,entities.mentions.username,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id,geo.place_id" self.media_fields = "preview_image_url,type,url,public_metrics" self.tweet_fields = "id,text,attachments,author_id,conversation_id,created_at,entities,in_reply_to_user_id,lang,public_metrics,possibly_sensitive,referenced_tweets,source" self.user_fields = "entities,id,name,username,profile_image_url,verified" self.place_fields = "full_name,id,country,country_code,name,place_type" self.output_file_name = "../logs/stream_result.txt"
def delete_stream_rules(self, rule_ids): try: if len(rule_ids) > 0: response = self.api.request("tweets/search/stream/rules", {"delete": { "ids": rule_ids }}) Log.i( f"[{response.status_code}] RULES DELETED: {json.dumps(response.json())}" ) if response.status_code != 200: raise Exception(response.text) except TwitterRequestError as e: msg_list = ["RequestError:", e] for msg in iter(e): msg_list.append(msg) err_msg = " ".join(msg_list) Log.e(err_msg) except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(2) except Exception as e: Log.e(f"BaseException: {e}") self.prompt_reconnect_msg(2)
def add_stream_rules(self, request_query): """ 계정에 특정 rule 을 등록 :param request_query: [str] 문법에 맞는 쿼리문 :return: - """ try: response = self.api.request("tweets/search/stream/rules", {"add": [{ "value": request_query }]}) Log.i(f"[{response.status_code}] RULE ADDED: {response.text}") if response.status_code != 201: raise Exception(response.text) except TwitterRequestError as e: msg_list = [] for msg in iter(e): msg_list.append(msg) err_msg = "RequestError: " + "|".join(msg_list) Log.e(err_msg) except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(2) except Exception as e: Log.e(f"BaseException: {e}") self.prompt_reconnect_msg(2)
def make_days_random_data(self, origin_data, days=6, num_of_tweets=20000): """ 원본 tweet 을 기반으로 원하는 날짜, 랜덤한 시간의 새로운 tweet 데이터를 생성하여 broker 에게 전송 생성하고자 하는 수만큼의 데이터가 생성되었다면 프로그램 종료 :param origin_data: [Tweet Data] 원본 tweet 데이터 :param days: [int] 생성하고자 하는 데이터와 원본 데이터의 날짜 차이. 해당하는 일 수만큼의 전날에서 하루 전날까지 포함 :param num_of_tweets: [int] 범위 내의 모든 날짜마다 생성하고자 하는 tweet 데이터의 수 :return: - """ if 'created_at' in origin_data['data']: if self.daily_cnt >= num_of_tweets: exit() else: datetime_origin = self.toggle_time_format(origin_data['data']['created_at']) if datetime_origin.month == 2 and datetime_origin.day > 4: # 특정 날짜를 제거하기 위해 filtering with open("logs/random_data_list", "a", encoding="utf-8") as output_file: # 생성한 데이터 기록 for day_ago in range(-days, 0): # 중첩 dictionary의 복제를 위해 deepcopy 사요 new_data = copy.deepcopy(origin_data) datetime_post = datetime_origin + timedelta(days=day_ago) random_hms = time(hour=random.randint(0, 23), minute=random.randint(0, 59), second=random.randint(0, 59)) # 특정 날짜와 랜덤한 시간을 합친 datetime 생성 datetime_post = datetime.combine(datetime_post.date(), random_hms) new_created_at = self.toggle_time_format(datetime_post) # print("new_created_at", new_created_at) new_data['data']['created_at'] = new_created_at new_data = json.dumps(new_data, ensure_ascii=False) self.producer.send_data('test', new_data) print(new_data, file=output_file, flush=True) self.daily_cnt += 1 print(self.daily_cnt) else: Log.e("KEYERROR no 'created_at' in origin_data['data'].")
def __init__(self, host="localhost", port="9092"): """ Kafka Producer Option (not all) - bootstrap_servers(default=9092): 브로커(host:port)를 리스트로 나열, 노드 전부를 쓸 필요는 없음 - acks(default=1): 0, 서버로부터 어떠한 ack도 기다리지 않음. 처리량 증가하지만 유실율도 증가 1, 리더의 기록을 확인 후 ack 받음. 모든 팔로워에 대해서 확인하지는 않음 'all', 모든 ISR(리더의 모든 팔로워)의 기록을 확인 후 ack 받음. 무손실 보장 - compression_type(default=None): 데이터를 압축하여 보낼 포멧 (‘gzip’, ‘snappy’, ‘lz4’, or None) - value_serializer(default=None): 유저가 보내려는 msg를 byte의 형태로 변환할 함수(callable). 여기서는 변환 후 인코딩 """ try: self.producer = KafkaProducer( api_version=(2, 7, 0), bootstrap_servers=[f"{host}:{port}"], acks=-1, compression_type="gzip", value_serializer=lambda x: x.encode("utf-8"), batch_size=1024*64, linger_ms=10, ) except KafkaError as e: Log.e(f"KafkaProducer fail. {e}") Log.i("KafkaProducer connect.")
def __init__(self): """ twitter API 를 사용하기 위해 API KEY를 이용해 연결하고, 요청에 필요한 파라미터를 초기화 실시간 데이터를 응답받기 위해선 twitter 에서 정해놓은 query 형식에 따라 rule 을 등록한 후 요청해야 함 모든 rule 은 계정(API KEY)에 연동되고 각가 id로 구분되며, 추가적인 등록/삭제가 없다면 변경되지 않음 여기서 rule 이란 특정한 조건을 만족하는 streaming data 를 받기위한 filtering 방법 """ try: auth_info = TwitterOAuth.read_file() self.api = TwitterAPI( auth_info.consumer_key, auth_info.consumer_secret, auth_type="oAuth2", api_version="2", ) except TwitterRequestError as e: msg_list = [] for msg in iter(e): msg_list.append(msg) err_msg = "RequestError: " + "|".join(msg_list) Log.e(err_msg) except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(2) except Exception as e: Log.e(f"BaseException: {e}") self.prompt_reconnect_msg(2) # twitter conifg 파일 twitter_config = config.Config('configs/twitter_config.conf') self.query_string = twitter_config.load_config('API_QUERY', 'rules') self.output_file_name = twitter_config.load_config( 'OUTPUT_FILES', 'stream_result') # comma-seperated list with no space between fields self.expansions = twitter_config.load_config('CONTENTS', 'expansions') self.media_fields = twitter_config.load_config('CONTENTS', 'media_fields') self.tweet_fields = twitter_config.load_config('CONTENTS', 'tweet_fields') self.user_fields = twitter_config.load_config('CONTENTS', 'user_fields') self.place_fields = twitter_config.load_config('CONTENTS', 'place_fields')
def send_analytic(self, tweetWrapper): try: StatusAnalyticsSender.post_analytic(tweetWrapper) except Exception as e: Log.e("EXCEPTION", str(e))
def start_stream(self, producer, topic): total_cnt = [0] # 스케줄러 함수의 인자로, 값이 변경될 수 있도록 mutable 객체인 list 로 선언 while True: try: response = self.api.request( "tweets/search/stream", { "expansions": self.expansions, "media.fields": self.media_fields, "tweet.fields": self.tweet_fields, "user.fields": self.user_fields, "place.fields": self.place_fields, }, ) Log.i(f"[{response.status_code}] START...") Log.i(response.get_quota()) # API connect 회수 조회 if response.status_code != 200 and response.status_code != 429: raise Exception(response) elif response.status_code != 200: # 그 외의 경우 예외처리 및 재연결 시도 raise Exception(response) with open(self.output_file_name, "a", encoding="utf-8") as output_file, open( "../logs/data_count.txt", "a") as cnt_file: print(f"[{datetime.datetime.now()}] file re-open", file=cnt_file) scheduler = BackgroundScheduler( ) # 5초마다 실행을 위해 백그라운드 스케줄러 사용 batch_cnt = [ 0 ] # 5초 간격으로 함수 내에서 reset 하기 위해 mutable 객체인 list 로 선언 scheduler.add_job( self.print_periodically, "cron", second="*/5", args=[batch_cnt, total_cnt, cnt_file], ) scheduler.start() for item in response: self.check_error_response(item) data = json.dumps(item, ensure_ascii=False) print(data, file=output_file, flush=True) producer.send_data(topic=topic, data=data) batch_cnt[0] += 1 total_cnt[0] += 1 except TwitterRequestError as e: msg_list = ["RequestError:", e] for msg in iter(e): msg_list.append(msg) err_msg = " ".join(msg_list) Log.e(err_msg) if e.status_code >= 500: self.prompt_reconnect_msg(2) elif e.status_code == 429: self.prompt_reconnect_msg(60) else: exit() except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(2) except Exception as e: Log.e(f"Exception: {e}") self.prompt_reconnect_msg(2)
def start_stream(self, producer, topic): """ 계정에 등록된 rule 에 따라 filtered streaming data 를 받아옴 data를 정상적으로 받아오고 있다면 오류/종료 전까지 반복문에서 빠져나오지 않음 에러 원인에 따라 재연결을 시도하여 서버가 유지될 수 있도록 함 rule 과 별개로 하나의 tweet 에서 받아오고자 하는 정보를 쿼리에 포함시킬 수 있음 kafka 의 prodcer 역할을 하는 부분으로 받아오는 data 를 producer 와 연결된 broker 로 전달함 :param producer: [kafka.producer] kafka 의 produce 객체 :param topic: [str] 데이터를 전달하고자하는 broker 의 특정 topic :return: - """ total_cnt = 0 while True: try: response = self.api.request( "tweets/search/stream", { "expansions": self.expansions, "media.fields": self.media_fields, "tweet.fields": self.tweet_fields, "user.fields": self.user_fields, "place.fields": self.place_fields, }, ) Log.i(f"[{response.status_code}] START...") Log.i(response.get_quota()) # API connect 회수 조회 if (response.status_code != 200 and response.status_code != 400 and response.status_code != 429): # 에러 원인별 다른 처리를 위해 응답 코드로 구분 raise Exception(response) with open(self.output_file_name, "a", encoding="utf-8") as output_file, open( "logs/data_count.txt", "a") as cnt_file: # data_count.txt : 유실을 확인하기위해 count # [데이터를 받아온 시간] 해당 tweet 의 게시 시간 (파일이 open 된 후 받아온 데이터의 수 / 프로그램이 실행된 후 받아온 데이터의 수) print(f"[{datetime.datetime.now()}] file re-open", file=cnt_file) for no, item in enumerate(response): self.check_error_response(item) data = json.dumps(item, ensure_ascii=False) print(data, file=output_file, flush=True) producer.send_data(topic=topic, data=data) print( f"[{datetime.datetime.now()}] {item['data']['created_at']} ({no} / {total_cnt})", file=cnt_file, flush=True, ) total_cnt += 1 except TwitterRequestError as e: # ConnectionException will be caught here msg_list = [] for msg in iter(e): msg_list.append(msg) err_msg = "RequestError: " + "|".join(msg_list) Log.e(err_msg) if e.status_code >= 500: self.prompt_reconnect_msg(3) elif e.status_code == 429: self.prompt_reconnect_msg(63) else: exit() except TwitterConnectionError as e: Log.e(f"ConnectionError: {e}") self.prompt_reconnect_msg(3) except Exception as e: Log.e(f"Exception: {e}") self.prompt_reconnect_msg(3)