analyser_hj3415 2.0.2__py2.py3-none-any.whl → 2.1.0__py2.py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,934 +0,0 @@
1
- import pymongo
2
- import sys
3
-
4
- import copy
5
- from pymongo import errors, MongoClient
6
- import math
7
- import random
8
- import datetime
9
- from typing import List, Tuple
10
- from collections import OrderedDict
11
- from abc import *
12
- from utils_hj3415 import utils
13
- import pandas as pd
14
-
15
-
16
- import logging
17
- logger = logging.getLogger(__name__)
18
- formatter = logging.Formatter('%(levelname)s: [%(name)s] %(message)s')
19
- ch = logging.StreamHandler()
20
- ch.setFormatter(formatter)
21
- logger.addHandler(ch)
22
- logger.setLevel(logging.WARNING)
23
-
24
- """
25
- 몽고db구조
26
- RDBMS : database / tables / rows / columns
27
- MongoDB : database / collections / documents / fields
28
- """
29
-
30
-
31
- class UnableConnectServerException(Exception):
32
- """
33
- 몽고 서버 연결 에러를 처리하기 위한 커스텀 익셉션
34
- """
35
- pass
36
-
37
-
38
- def connect_mongo(addr: str, timeout=5) -> MongoClient:
39
- """
40
- 몽고 클라이언트를 만들어주는 함수.
41
- 필요할 때마다 클라이언트를 생성하는 것보다 클라이언트 한개로 데이터베이스를 다루는게 효율적이라 함수를 따로 뺐음.
42
- resolve conn error - https://stackoverflow.com/questions/54484890/ssl-handshake-issue-with-pymongo-on-python3
43
- :param addr:
44
- :param timeout:
45
- :return:
46
- """
47
- import certifi
48
- ca = certifi.where()
49
- if addr.startswith('mongodb://'):
50
- # set a some-second connection timeout
51
- client = pymongo.MongoClient(addr, serverSelectionTimeoutMS=timeout * 1000)
52
- elif addr.startswith('mongodb+srv://'):
53
- client = pymongo.MongoClient(addr, serverSelectionTimeoutMS=timeout * 1000, tlsCAFile=ca)
54
- else:
55
- raise Exception(f"Invalid address: {addr}")
56
- try:
57
- srv_info = client.server_info()
58
- conn_str = f"Connect to Mongo Atlas v{srv_info['version']}..."
59
- print(conn_str, f"Server Addr : {addr}")
60
- return client
61
- except:
62
- raise UnableConnectServerException()
63
-
64
-
65
- def extract_addr_from_client(client: MongoClient) -> str:
66
- """
67
- scaraper는 클라이언트를 인자로 받지않고 주소를 받아서 사용하기 때문에 사용하는 주소 추출함수
68
- """
69
- # client에서 mongodb주소 추출
70
- ip = str(list(client.nodes)[0][0])
71
- port = str(list(client.nodes)[0][1])
72
- return 'mongodb://' + ip + ':' + port
73
-
74
-
75
- class MongoBase:
76
- def __init__(self, client: MongoClient, db_name: str, col_name: str):
77
- self._db_name = "test"
78
- self._col_name = "test"
79
- self.client = client
80
- self.db_name = db_name
81
- self.col_name = col_name
82
-
83
- @property
84
- def db_name(self):
85
- return self._db_name
86
-
87
- @db_name.setter
88
- def db_name(self, db_name):
89
- if self.client is None:
90
- raise Exception("You should set server connection first")
91
- else:
92
- self._db_name = db_name
93
- self.my_db = self.client[self.db_name]
94
- self.my_col = self.my_db[self.col_name]
95
-
96
- @property
97
- def col_name(self):
98
- return self._col_name
99
-
100
- @col_name.setter
101
- def col_name(self, col_name):
102
- if self.db_name is None:
103
- raise Exception("You should set database first.")
104
- else:
105
- self._col_name = col_name
106
- self.my_col = self.my_db[self.col_name]
107
-
108
- # ========================End Properties=======================
109
-
110
- @staticmethod
111
- def get_all_db_name(client: MongoClient) -> list:
112
- return sorted(client.list_database_names())
113
-
114
- @staticmethod
115
- def validate_db(client: MongoClient, db_name: str) -> bool:
116
- """
117
- db_name 이 실제로 몽고db 안에 있는지 확인한다.
118
- """
119
- if db_name in client.list_database_names():
120
- return True
121
- else:
122
- return False
123
-
124
- @staticmethod
125
- def del_db(client: MongoClient, db_name: str):
126
- if MongoBase.validate_db(client, db_name):
127
- client.drop_database(db_name)
128
- print(f"Drop '{db_name}' database..")
129
- else:
130
- print(f"Invalid db name : {db_name}", file=sys.stderr)
131
-
132
- def validate_col(self) -> bool:
133
- """
134
- col_name 이 실제로 db 안에 있는지 확인한다.
135
- """
136
- if self.validate_db(self.client, self.db_name):
137
- if self.col_name in self.my_db.list_collection_names():
138
- return True
139
- else:
140
- return False
141
-
142
- def get_all_docs(self, remove_id=True) -> list:
143
- """
144
- 현재 설정된 컬렉션 안의 도큐먼트 전부를 리스트로 반환한다.
145
- """
146
- items = []
147
- if remove_id:
148
- for doc in self.my_col.find({}):
149
- del doc['_id']
150
- items.append(doc)
151
- else:
152
- items = list(self.my_col.find({}))
153
- return items
154
-
155
- def count_docs_in_col(self) -> int:
156
- """
157
- 현재 설정된 컬렉션 안의 도큐먼트의 갯수를 반환한다.
158
- """
159
- return self.my_col.count_documents({})
160
-
161
- def clear_docs_in_col(self):
162
- """
163
- 현재 설정된 컬렉션 안의 도큐먼트를 전부 삭제한다.
164
- (컬렉션 자체를 삭제하지는 않는다.)
165
- """
166
- self.my_col.delete_many({})
167
- print(f"Delete all doccument in {self.col_name} collection..")
168
-
169
- def del_col(self):
170
- """
171
- 현재 설정된 컬렉션을 삭제한다.
172
- """
173
- self.my_db.drop_collection(self.col_name)
174
- print(f"Drop {self.col_name} collection..")
175
-
176
- def list_collection_names(self) -> list:
177
- return self.my_db.list_collection_names()
178
-
179
- def del_doc(self, del_query: dict):
180
- """
181
- del_query에 해당하는 도큐먼트를 삭제한다.
182
- """
183
- self.my_col.delete_one(del_query)
184
-
185
-
186
- class Corps(MongoBase):
187
- """
188
- mongodb의 데이터 중 기업 코드로 된 데이터 베이스를 다루는 클래스
189
- """
190
- COLLECTIONS = ('c101', 'c104y', 'c104q', 'c106y', 'c106q', 'c108',
191
- 'c103손익계산서q', 'c103재무상태표q', 'c103현금흐름표q',
192
- 'c103손익계산서y', 'c103재무상태표y', 'c103현금흐름표y',
193
- 'dart', 'etc')
194
-
195
- def __init__(self, client: MongoClient, code: str, page: str):
196
- if utils.is_6digit(code) and page in self.COLLECTIONS:
197
- super().__init__(client=client, db_name=code, col_name=page)
198
- else:
199
- raise Exception(f'Invalid value - code: {code} / {page}({self.COLLECTIONS})')
200
-
201
- @property
202
- def code(self):
203
- return self.db_name
204
-
205
- @code.setter
206
- def code(self, code: str):
207
- if utils.is_6digit(code):
208
- self.db_name = code
209
- else:
210
- raise Exception(f'Invalid value : {code}')
211
-
212
- @property
213
- def page(self):
214
- return self.col_name
215
-
216
- @page.setter
217
- def page(self, page: str):
218
- if page in self.COLLECTIONS:
219
- self.col_name = page
220
- else:
221
- raise Exception(f'Invalid value : {page}({self.COLLECTIONS})')
222
-
223
- # ========================End Properties=======================
224
-
225
- @staticmethod
226
- def latest_value(data: dict, pop_count=2, nan_to_zero=False, allow_empty=False) -> Tuple[str, float]:
227
- """
228
- 가장 최근 년/분기 값 - evaltools에서도 사용할수 있도록 staticmethod로 뺐음.
229
-
230
- 해당 타이틀의 가장 최근의 년/분기 값을 튜플 형식으로 반환한다.
231
-
232
- Args:
233
- data (dict): 찾고자하는 딕셔너리 데이터
234
- pop_count: 유효성 확인을 몇번할 것인가
235
- nan_to_zero: nan의 값을 0으로 바꿀것인가
236
- allow_empty: title 항목이 없을경우에도 에러를 발생시키지 않을 것인가
237
-
238
- Returns:
239
- tuple: ex - ('2020/09', 39617.5) or ('', 0)
240
-
241
- Note:
242
- 만약 최근 값이 nan 이면 찾은 값 바로 직전 것을 한번 더 찾아 본다.\n
243
- 데이터가 없는 경우 ('', nan) 반환한다.\n
244
- """
245
- def is_valid_value(value) -> bool:
246
- """
247
- 숫자가 아닌 문자열이나 nan 또는 None의 경우 유효한 형식이 아님을 알려 리턴한다.
248
- """
249
- if isinstance(value, str):
250
- # value : ('Unnamed: 1', '데이터가 없습니다.') 인 경우
251
- is_valid = False
252
- elif math.isnan(value):
253
- # value : float('nan') 인 경우
254
- is_valid = False
255
- elif value is None:
256
- # value : None 인 경우
257
- is_valid = False
258
- else:
259
- is_valid = True
260
- """
261
- elif value == 0:
262
- is_valid = False
263
- """
264
- return is_valid
265
-
266
- logger.debug(f'Corps.latest_value raw data : {data}')
267
-
268
- # 데이터를 추출해서 사용하기 때문에 원본 데이터는 보존하기 위해서 카피해서 사용
269
- data_copied = copy.deepcopy(data)
270
-
271
- for i in range(pop_count):
272
- try:
273
- d, v = data_copied.popitem()
274
- except KeyError:
275
- # when dictionary is empty
276
- return '', 0 if nan_to_zero else float('nan')
277
- if str(d).startswith('20') and is_valid_value(v):
278
- logger.debug(f'last_one : {v}')
279
- return d, v
280
-
281
- return '', 0 if nan_to_zero else float('nan')
282
-
283
- @staticmethod
284
- def refine_data(data: dict, refine_words: list) -> dict:
285
- """
286
- 주어진 딕셔너리에서 refine_words에 해당하는 키를 삭제해서 반환하는 유틸함수.
287
- c10346에서 사용
288
- refine_words : 정규표현식 가능
289
- """
290
- copy_data = data.copy()
291
- import re
292
- for regex_refine_word in refine_words:
293
- # refine_word에 해당하는지 정규표현식으로 검사하고 매치되면 삭제한다.
294
- p = re.compile(regex_refine_word)
295
- for title, _ in copy_data.items():
296
- # data 내부의 타이틀을 하나하나 조사한다.
297
- m = p.match(title)
298
- if m:
299
- del data[title]
300
- return data
301
-
302
- @staticmethod
303
- def get_all_codes(client: MongoClient) -> list:
304
- """
305
- 기업 코드를 데이터베이스명으로 가지는 모든 6자리 숫자 코드의 db 명 반환
306
- """
307
- corp_list = []
308
- for db in MongoBase.get_all_db_name(client):
309
- if utils.is_6digit(db):
310
- corp_list.append(db)
311
- return sorted(corp_list)
312
-
313
- @staticmethod
314
- def del_all_codes(client: MongoClient):
315
- corp_list = Corps.get_all_codes(client)
316
- for corp_db_name in corp_list:
317
- MongoBase.del_db(client, corp_db_name)
318
-
319
- @staticmethod
320
- def pick_rnd_x_code(client: MongoClient, count: int) -> list:
321
- """
322
- 임의의 갯수의 종목코드를 뽑아서 반환한다.
323
- """
324
- return random.sample(Corps.get_all_codes(client), count)
325
-
326
- @staticmethod
327
- def get_name(client: MongoClient, code: str):
328
- """
329
- code를 입력받아 종목명을 반환한다.
330
- """
331
- c101 = C101(client, code)
332
- try:
333
- name = c101.get_recent()['종목명']
334
- except KeyError:
335
- name = None
336
- return name
337
-
338
- def _save_df(self, df: pd.DataFrame) -> bool:
339
- # c103, c104, c106, c108에서 주로 사용하는 저장방식
340
- if df.empty:
341
- print('Dataframe is empty..So we will skip saving db..')
342
- return False
343
- result = self.my_col.insert_many(df.to_dict('records'))
344
- return result.acknowledged
345
-
346
- def _load_df(self) -> pd.DataFrame:
347
- # cdart와 c106, c103에서 주로 사용
348
- try:
349
- df = pd.DataFrame(self.get_all_docs())
350
- except KeyError:
351
- df = pd.DataFrame()
352
- return df
353
-
354
- def _save_dict(self, dict_data: dict, del_query: dict) -> bool:
355
- # c101, cdart에서 주로 사용하는 저장방식
356
- try:
357
- result = self.my_col.insert_one(dict_data)
358
- except errors.DuplicateKeyError:
359
- self.my_col.delete_many(del_query)
360
- result = self.my_col.insert_one(dict_data)
361
- return result.acknowledged
362
-
363
-
364
- class C1034(Corps, metaclass=ABCMeta):
365
- def __init__(self, client: MongoClient, db_name: str, col_name: str):
366
- super().__init__(client=client, code=db_name, page=col_name)
367
-
368
- def get_all_titles(self) -> list:
369
- titles = []
370
- for item in self.get_all_docs():
371
- titles.append(item['항목'])
372
- return list(set(titles))
373
-
374
- @staticmethod
375
- def sum_each_data(data_list: List[dict]) -> dict:
376
- """
377
- 검색된 딕셔너리를 모은 리스트를 인자로 받아서 각각의 기간에 맞춰 합을 구해 하나의 딕셔너리로 반환한다.
378
- """
379
- sum_dict = {}
380
- periods = list(data_list[0].keys())
381
- # 여러딕셔너리를 가진 리스트의 합 구하기
382
- for period in periods:
383
- sum_dict[period] = sum(utils.nan_to_zero(data[period]) for data in data_list)
384
- return sum_dict
385
-
386
- def _find(self, title: str, refine_words: list) -> Tuple[List[dict], int]:
387
- """
388
- refine_words 에 해당하는 딕셔너리 키를 삭제하고
389
- title 인자에 해당하는 항목을 검색하여 반환한다.
390
- c103의 경우는 중복되는 이름의 항목이 있기 때문에
391
- 이 함수는 반환되는 딕셔너리 리스트와 갯수로 구성되는 튜플을 반환한다.
392
- """
393
- titles = self.get_all_titles()
394
- if title in titles:
395
- count = 0
396
- data_list = []
397
- for data in self.my_col.find({'항목': {'$eq': title}}):
398
- # 도큐먼트에서 title과 일치하는 항목을 찾아낸다.
399
- count += 1
400
- # refine_data함수를 통해 삭제를 원하는 필드를 제거하고 data_list에 추가한다.
401
- data_list.append(self.refine_data(data, refine_words))
402
- return data_list, count
403
- else:
404
- raise Exception(f'{title} is not in {titles}')
405
-
406
- def latest_value(self, title: str, pop_count=2, nan_to_zero=False, allow_empty=False) -> Tuple[str, float]:
407
- od = OrderedDict(sorted(self.find(title, allow_empty=allow_empty).items(), reverse=False))
408
- logger.debug(f'{title} : {od}')
409
- return Corps.latest_value(od, pop_count, nan_to_zero, allow_empty)
410
-
411
- @abstractmethod
412
- def find(self, title: str, allow_empty=False) -> dict:
413
- pass
414
-
415
- def sum_recent_4q(self, title: str, nan_to_zero: bool = False) -> Tuple[str, float]:
416
- """최근 4분기 합
417
-
418
- 분기 페이지 한정 해당 타이틀의 최근 4분기의 합을 튜플 형식으로 반환한다.
419
-
420
- Args:
421
- title (str): 찾고자 하는 타이틀
422
- nan_to_zero: nan 값의 경우 zero로 반환한다.
423
-
424
- Returns:
425
- tuple: (계산된 4분기 중 최근분기, 총합)
426
-
427
- Raises:
428
- TypeError: 페이지가 q가 아닌 경우 발생
429
-
430
- Note:
431
- 분기 데이터가 4개 이하인 경우 그냥 최근 연도의 값을 찾아 반환한다.
432
- """
433
- if self.col_name.endswith('q'):
434
- # 딕셔너리 정렬 - https://kkamikoon.tistory.com/138
435
- # reverse = False 이면 오래된것부터 최근순으로 정렬한다.
436
- od_q = OrderedDict(sorted(self.find(title, allow_empty=True).items(), reverse=False))
437
- logger.debug(f'{title} : {od_q}')
438
-
439
- if len(od_q) < 4:
440
- # od_q의 값이 4개 이하이면 그냥 최근 연도의 값으로 반환한다.
441
- if self.page.startswith('c103'):
442
- y = C103(self.client, self.db_name, self.col_name[:-1] + 'y')
443
- elif self.page.startswith('c104'):
444
- y = C104(self.client, self.db_name, self.col_name[:-1] + 'y')
445
- else:
446
- Exception(f'Error on sum_recent_4q func...')
447
- return y.latest_value(title, nan_to_zero=nan_to_zero, allow_empty=True)
448
- else:
449
- q_sum = 0
450
- date_list = list(od_q.keys())
451
- while True:
452
- try:
453
- latest_period = date_list.pop()
454
- except IndexError:
455
- latest_period = ""
456
- break
457
- else:
458
- if str(latest_period).startswith('20'):
459
- break
460
-
461
- for i in range(4):
462
- # last = True 이면 최근의 값부터 꺼낸다.
463
- d, v = od_q.popitem(last=True)
464
- logger.debug(f'd:{d} v:{v}')
465
- q_sum += 0 if math.isnan(v) else v
466
- return str(latest_period), round(q_sum, 2)
467
- else:
468
- raise TypeError(f'Not support year data..{self.col_name}')
469
-
470
- def find_증감율(self, title: str) -> dict:
471
- """
472
-
473
- 타이틀에 해당하는 전년/분기대비 값을 반환한다.\n
474
-
475
- Args:
476
- title (str): 찾고자 하는 타이틀
477
-
478
- Returns:
479
- float: 전년/분기대비 증감율
480
-
481
- Note:
482
- 중복되는 title 은 취급하지 않기로함.\n
483
- """
484
- try:
485
- data_list, count = self._find(title, ['_id', '항목'])
486
- except:
487
- # title을 조회할 수 없는 경우
488
- if self.col_name.endswith('q'):
489
- r = {'전분기대비': math.nan}
490
- else:
491
- r = {'전년대비': math.nan, '전년대비 1': math.nan}
492
- return r
493
- logger.info(data_list)
494
- cmp_dict = {}
495
- if count > 1:
496
- # 중복된 타이틀을 가지는 페이지의 경우 경고 메시지를 보낸다.
497
- logger.warning(f'Not single data..{self.code}/{self.page}/{title}')
498
- logger.warning(data_list)
499
- # 첫번째 데이터를 사용한다.
500
- data_dict = data_list[0]
501
- for k, v in data_dict.items():
502
- if str(k).startswith('전'):
503
- cmp_dict[k] = v
504
- return cmp_dict
505
-
506
-
507
- class C104(C1034):
508
- def __init__(self, client: MongoClient, code: str, page: str):
509
- if page in ('c104y', 'c104q'):
510
- super().__init__(client=client, db_name=code, col_name=page)
511
- else:
512
- raise Exception
513
-
514
- def get_all_titles(self) -> list:
515
- """
516
- 상위 C1034클래스에서 c104는 stamp항목이 있기 때문에 삭제하고 리스트로 반환한다.
517
- """
518
- titles = super().get_all_titles()
519
- titles.remove('stamp')
520
- return titles
521
-
522
- def find(self, title: str, allow_empty=False) -> dict:
523
- """
524
- title에 해당하는 항목을 딕셔너리로 반환한다.
525
- allow_empty를 true로 하면 에러를 발생시키지 않고 빈딕셔너리를 리턴한다.
526
- """
527
- try:
528
- l, c = super(C104, self)._find(title, ['_id', '항목', '^전.+대비.*'])
529
- return_dict = l[0]
530
- except Exception as e:
531
- if allow_empty:
532
- return_dict = {}
533
- else:
534
- raise Exception(e)
535
- return return_dict
536
-
537
- def save_df(self, c104_df: pd.DataFrame) -> bool:
538
- """데이터베이스에 저장
539
-
540
- c104는 4페이지의 자료를 한 컬렉션에 모으는 것이기 때문에
541
- stamp 를 검사하여 12시간 전보다 이전에 저장된 자료가 있으면
542
- 삭제한 후 저장하고 12시간 이내의 자료는 삭제하지 않고
543
- 데이터를 추가하는 형식으로 저장한다.
544
-
545
- Example:
546
- c104_data 예시\n
547
- [{'항목': '매출액증가율',...'2020/12': 2.78, '2021/12': 14.9, '전년대비': 8.27, '전년대비1': 12.12},
548
- {'항목': '영업이익증가율',...'2020/12': 29.62, '2021/12': 43.86, '전년대비': 82.47, '전년대비1': 14.24}]
549
-
550
- Note:
551
- 항목이 중복되는 경우가 있기 때문에 c104처럼 각 항목을 키로하는 딕셔너리로 만들지 않는다.
552
- """
553
- self.my_col.create_index('항목', unique=True)
554
- time_now = datetime.datetime.now()
555
- try:
556
- stamp = self.my_col.find_one({'항목': 'stamp'})['time']
557
- if stamp < (time_now - datetime.timedelta(days=.01)):
558
- # 스템프가 약 10분 이전이라면..연속데이터가 아니라는 뜻이므로 컬렉션을 초기화한다.
559
- print("Before save data, cleaning the collection...", end='')
560
- self.clear_docs_in_col()
561
- except TypeError:
562
- # 스템프가 없다면...
563
- pass
564
- # 항목 stamp를 찾아 time을 업데이트하고 stamp가 없으면 insert한다.
565
- self.my_col.update_one({'항목': 'stamp'}, {"$set": {'time': time_now}}, upsert=True)
566
- return super(C104, self)._save_df(c104_df)
567
-
568
- def get_stamp(self) -> datetime.datetime:
569
- """
570
- c104y, c104q가 작성된 시간이 기록된 stamp 항목을 datetime 형식으로 리턴한다.
571
- """
572
- return self.my_col.find_one({"항목": "stamp"})['time']
573
-
574
- def modify_stamp(self, days_ago: int):
575
- """
576
- 인위적으로 타임스템프를 수정한다 - 주로 테스트 용도
577
- """
578
- try:
579
- before = self.my_col.find_one({'항목': 'stamp'})['time']
580
- except TypeError:
581
- # 이전에 타임스템프가 없는 경우
582
- before = None
583
- time_2da = datetime.datetime.now() - datetime.timedelta(days=days_ago)
584
- self.my_col.update_one({'항목': 'stamp'}, {"$set": {'time': time_2da}}, upsert=True)
585
- after = self.my_col.find_one({'항목': 'stamp'})['time']
586
- logger.info(f"Stamp changed: {before} -> {after}")
587
-
588
-
589
- class C103(C1034):
590
- def __init__(self, client: MongoClient, code: str, page: str):
591
- if page in ('c103손익계산서q', 'c103재무상태표q', 'c103현금흐름표q',
592
- 'c103손익계산서y', 'c103재무상태표y', 'c103현금흐름표y',):
593
- super().__init__(client=client, db_name=code, col_name=page)
594
- else:
595
- raise Exception
596
-
597
- def save_df(self, c103_df: pd.DataFrame) -> bool:
598
- """데이터베이스에 저장
599
-
600
- Example:
601
- c103_list 예시\n
602
- [{'항목': '자산총계', '2020/03': 3574575.4, ... '전분기대비': 3.9},
603
- {'항목': '유동자산', '2020/03': 1867397.5, ... '전분기대비': 5.5}]
604
-
605
- Note:
606
- 항목이 중복되는 경우가 있기 때문에 c104처럼 각 항목을 키로하는 딕셔너리로 만들지 않는다.
607
- """
608
- self.my_col.create_index('항목', unique=False)
609
- print("Before save data, cleaning the collection...", end='')
610
- self.clear_docs_in_col()
611
- return super(C103, self)._save_df(c103_df)
612
-
613
- def load_df(self) -> pd.DataFrame:
614
- """
615
- 데이터베이스에 저장된 페이지를 데이터프레임으로 반환한다.
616
- """
617
- return super(C103, self)._load_df()
618
-
619
- def find(self, title: str, allow_empty=False) -> dict:
620
- """
621
- title에 해당하는 항목을 딕셔너리로 반환한다.
622
- allow_empty를 true로 하면 에러를 발생시키지 않고 빈딕셔너리를 리턴한다.
623
- c103의 경우는 중복된 타이틀을 가지는 항목이 있어 합쳐서 한개의 딕셔너리로 반환한다.
624
- """
625
- try:
626
- l, c = super(C103, self)._find(title, ['_id', '항목', '^전.+대비.*'])
627
- sum_dict = self.sum_each_data(l)
628
- except Exception as e:
629
- if allow_empty:
630
- sum_dict = {}
631
- else:
632
- raise Exception(e)
633
- return sum_dict
634
-
635
-
636
- class C101(Corps):
637
- def __init__(self, client: MongoClient, code: str):
638
- super().__init__(client=client, code=code, page='c101')
639
- self.my_col.create_index('date', unique=True)
640
-
641
- def save_dict(self, c101_dict: dict) -> bool:
642
- """
643
- c101의 구조에 맞는 딕셔너리값을 받아서 구조가 맞는지 확인하고 맞으면 저장한다.
644
-
645
- Note:
646
- <c101_struc>\n
647
- 'date', '코드', '종목명',\n
648
- '업종', '주가', '거래량',\n
649
- 'EPS', 'BPS', 'PER',\n
650
- '업종PER', 'PBR', '배당수익률',\n
651
- '최고52주', '최저52주', '거래대금',\n
652
- '시가총액', '베타52주', '발행주식',\n
653
- '유통비율', 'intro'\n
654
- """
655
- c101_template = ['date', '코드', '종목명', '업종', '주가', '거래량', 'EPS', 'BPS', 'PER', '업종PER', 'PBR',
656
- '배당수익률', '최고52주', '최저52주', '거래대금', '시가총액', '베타52주', '발행주식', '유통비율']
657
- # 리스트 비교하기
658
- # reference from https://codetorial.net/tips_and_examples/compare_two_lists.html
659
- if c101_dict['코드'] != self.db_name:
660
- raise Exception("Code isn't equal input data and db data..")
661
- logger.debug(c101_dict.keys())
662
- # c101 데이터가 c101_template의 내용을 포함하는가 확인하는 if문
663
- # refered from https://appia.tistory.com/101
664
- if (set(c101_template) - set(c101_dict.keys())) == set():
665
- # 스크랩한 날짜 이후의 데이터는 조회해서 먼저 삭제한다.
666
- del_query = {'date': {"$gte": c101_dict['date']}}
667
- return super(C101, self)._save_dict(c101_dict, del_query)
668
- else:
669
- raise Exception('Invalid c101 dictionary structure..')
670
-
671
- def find(self, date: str) -> dict:
672
- """
673
-
674
- 해당 날짜의 데이터를 반환한다.
675
- 만약 리턴값이 없으면 {} 을 반환한다.
676
-
677
- Args:
678
- date (str): 예 - 20201011(6자리숫자)
679
- """
680
- if utils.isYmd(date):
681
- converted_date = date[:4] + '.' + date[4:6] + '.' + date[6:]
682
- else:
683
- raise Exception(f'Invalid date format : {date}(ex-20201011(8자리숫자))')
684
- d = self.my_col.find_one({'date': converted_date})
685
- if d is None:
686
- return {}
687
- else:
688
- del d['_id']
689
- return d
690
-
691
- def get_recent(self, merge_intro=False) -> dict:
692
- """
693
- 저장된 데이터에서 가장 최근 날짜의 딕셔너리를 반환한다.
694
-
695
- 리턴값
696
- {'BPS': 50817.0,
697
- 'EPS': 8057.0,
698
- 'PBR': 1.28,
699
- 'PER': 8.08,
700
- 'date': '2023.04.14',
701
- 'intro1': '한국 및 DX부문 해외 9개 지역총괄과 DS부문 해외 5개 지역총괄, SDC, Harman 등 233개의 종속기업으로 구성된 글로벌 전자기업임.',
702
- 'intro2': '세트사업(DX)에는 TV, 냉장고 등을 생산하는 CE부문과 스마트폰, 네트워크시스템, 컴퓨터 등을 생산하는 IM부문이 있음.',
703
- 'intro3': '부품사업(DS)에서는 D램, 낸드 플래쉬, 모바일AP 등의 제품을 생산하는 반도체 사업과 TFT-LCD 및 OLED 디스플레이 패널을 생산하는 DP사업으로 구성됨.',
704
- '거래대금': '1062800000000',
705
- '거래량': '16176500',
706
- '발행주식': '5969782550',
707
- '배당수익률': '2.22',
708
- '베타52주': '0.95',
709
- '시가총액': '388632800000000',
710
- '업종': '반도체와반도체장비',
711
- '업종PER': '8.36',
712
- '유통비율': '75.82',
713
- '종목명': '삼성전자',
714
- '주가': '65100',
715
- '최고52주': '68800',
716
- '최저52주': '51800',
717
- '코드': '005930'}
718
- """
719
- try:
720
- d = self.my_col.find({'date': {'$exists': True}}, {"_id": 0}).sort('date', pymongo.DESCENDING).next()
721
- # del doc['_id'] - 위의 {"_id": 0} 으로 해결됨.
722
- if merge_intro:
723
- d['intro'] = d['intro1'] + d['intro2'] + d['intro3']
724
- del d['intro1']
725
- del d['intro2']
726
- del d['intro3']
727
- except StopIteration:
728
- d = {}
729
- return d
730
-
731
- def get_all(self) -> list:
732
- """
733
-
734
- 저장된 모든 데이터를 딕셔너리로 가져와서 리스트로 포장하여 반환한다.
735
- """
736
- items = []
737
- for doc in self.my_col.find({'date': {'$exists': True}}, {"_id": 0}).sort('date', pymongo.ASCENDING):
738
- # del doc['_id'] - 위의 {"_id": 0} 으로 해결됨.
739
- items.append(doc)
740
- return items
741
-
742
- def get_trend(self, title: str) -> dict:
743
- """
744
- title에 해당하는 데이터베이스에 저장된 모든 값을 {날짜: 값} 형식의 딕셔너리로 반환한다.
745
-
746
- title should be in ['BPS', 'EPS', 'PBR', 'PER', '주가', '배당수익률', '베타52주', '거래량']
747
-
748
- 리턴값 - 주가
749
- {'2023.04.05': '63900',
750
- '2023.04.06': '62300',
751
- '2023.04.07': '65000',
752
- '2023.04.10': '65700',
753
- '2023.04.11': '65900',
754
- '2023.04.12': '66000',
755
- '2023.04.13': '66100',
756
- '2023.04.14': '65100',
757
- '2023.04.17': '65300'}
758
- """
759
- titles = ['BPS', 'EPS', 'PBR', 'PER', '주가', '배당수익률', '베타52주', '거래량']
760
- if title not in titles:
761
- raise Exception(f"title should be in {titles}")
762
- items = dict()
763
- for doc in self.my_col.find({'date': {'$exists': True}}, {"_id": 0, "date": 1, f"{title}": 1}).sort('date', pymongo.ASCENDING):
764
- items[doc['date']] = doc[f'{title}']
765
- return items
766
-
767
- class C106(Corps):
768
- def __init__(self, client: MongoClient, code: str, page: str):
769
- if page in ('c106y', 'c106q'):
770
- super().__init__(client=client, code=code, page=page)
771
- else:
772
- raise Exception
773
-
774
- def save_df(self, c106_df: pd.DataFrame) -> bool:
775
- self.my_col.create_index('항목', unique=True)
776
- self.clear_docs_in_col()
777
- return super(C106, self)._save_df(c106_df)
778
-
779
- def load_df(self) -> pd.DataFrame:
780
- return super(C106, self)._load_df()
781
-
782
- def get_all_titles(self) -> list:
783
- titles = []
784
- for item in self.get_all_docs():
785
- titles.append(item['항목'])
786
- return list(set(titles))
787
-
788
- def find(self, title: str, allow_empty=False) -> dict:
789
- """
790
- title에 해당하는 항목을 딕셔너리로 반환한다.
791
- """
792
- titles = self.get_all_titles()
793
- if title in titles:
794
- data = self.my_col.find_one({'항목': {'$eq': title}})
795
- return self.refine_data(data, ['_id', '항목'])
796
- else:
797
- if allow_empty:
798
- return {}
799
- else:
800
- raise Exception(f'{title} is not in {titles}')
801
-
802
-
803
- class DateBase(MongoBase):
804
- """
805
- 날짜를 컬렉션으로 가지는 데이터베이스를 위한 기반클래스
806
- """
807
- def __init__(self, client: MongoClient, db_name: str, date: str):
808
- if utils.isYmd(date):
809
- super().__init__(client=client, db_name=db_name, col_name=date)
810
- else:
811
- raise Exception(f"Invalid date : {date}(%Y%m%d)")
812
-
813
- @property
814
- def date(self):
815
- return self.col_name
816
-
817
- @date.setter
818
- def date(self, date: str):
819
- if utils.isYmd(date):
820
- self.col_name = date
821
- else:
822
- raise Exception(f"Invalid date : {date}(%Y%m%d)")
823
-
824
- # ========================End Properties=======================
825
-
826
- def save_df(self, df: pd.DataFrame) -> bool:
827
- if df.empty:
828
- print('Dataframe is empty..So we will skip saving db..')
829
- return False
830
-
831
- self.clear_docs_in_col()
832
- print(f"Save new data to '{self.db_name}' / '{self.col_name}'")
833
- result = self.my_col.insert_many(df.to_dict('records'))
834
- return result.acknowledged
835
-
836
- def load_df(self) -> pd.DataFrame:
837
- try:
838
- df = pd.DataFrame(list(self.my_col.find({}))).drop(columns=['_id'])
839
- except KeyError:
840
- df = pd.DataFrame()
841
- return df
842
-
843
-
844
- class EvalByDate(DateBase):
845
- """
846
- 각 날짜별로 만들어진 eval-report 데이터프레임을 관리하는 클래스
847
- DB_NAME : eval
848
- COL_NAME : Ymd형식 날짜
849
- """
850
- EVAL_DB = 'eval'
851
-
852
- def __init__(self, client: MongoClient, date: str):
853
- super().__init__(client, self.EVAL_DB, date)
854
- # 인덱스 설정
855
- self.my_col.create_index('code', unique=True)
856
-
857
- @staticmethod
858
- def get_dates(client: MongoClient) -> List[str]:
859
- # 데이터베이스에 저장된 날짜 목록을 리스트로 반환한다.
860
- dates_list = client.eval.list_collection_names()
861
- dates_list.sort()
862
- return dates_list
863
-
864
- @classmethod
865
- def get_recent(cls, client: MongoClient, type: str):
866
- """
867
- eval 데이터베이스의 가장 최근의 유요한 자료를 반환한다.
868
- type의 종류에 따라 반환값이 달라진다.[date, dataframe, dict]
869
- """
870
- dates = cls.get_dates(client)
871
-
872
- while len(dates) > 0:
873
- recent_date = dates.pop()
874
- recent_df = cls(client, recent_date).load_df()
875
- if len(recent_df) != 0:
876
- if type == 'date':
877
- return recent_date
878
- elif type == 'dataframe':
879
- return recent_df
880
- elif type == 'dict':
881
- return recent_df.to_dict('records')
882
- else:
883
- raise Exception(f"Invalid type : {type}")
884
-
885
- return None
886
-
887
- class MI(MongoBase):
888
- """mi 데이터베이스 클래스
889
-
890
- Note:
891
- db - mi\n
892
- col - 'aud', 'chf', 'gbond3y', 'gold', 'silver', 'kosdaq', 'kospi', 'sp500', 'usdkrw', 'wti', 'avgper', 'yieldgap', 'usdidx' - 총 13개\n
893
- doc - date, value\n
894
- """
895
- MI_DB = 'mi'
896
- COL_TITLE = ('aud', 'chf', 'gbond3y', 'gold', 'silver', 'kosdaq', 'kospi',
897
- 'sp500', 'usdkrw', 'wti', 'avgper', 'yieldgap', 'usdidx')
898
-
899
- def __init__(self, client: MongoClient, index: str):
900
- if index in self.COL_TITLE:
901
- super().__init__(client=client, db_name=self.MI_DB, col_name=index)
902
- else:
903
- raise Exception(f'Invalid index : {index}({self.COL_TITLE})')
904
-
905
- @property
906
- def index(self):
907
- return self.col_name
908
-
909
- @index.setter
910
- def index(self, index: str):
911
- if index in self.COL_TITLE:
912
- self.col_name = index
913
- else:
914
- raise Exception(f'Invalid index : {index}({self.COL_TITLE})')
915
-
916
- # ========================End Properties=======================
917
-
918
- def get_recent(self) -> Tuple[str, float]:
919
- """저장된 가장 최근의 값을 반환하는 함수
920
- """
921
- if self.validate_col():
922
- d = self.my_col.find({'date': {'$exists': True}}).sort('date', pymongo.DESCENDING).next()
923
- del d['_id']
924
- return d['date'], d['value']
925
-
926
- def save_dict(self, mi_dict: dict) -> bool:
927
- """MI 데이터 저장
928
-
929
- Args:
930
- mi_dict (dict): ex - {'date': '2021.07.21', 'value': '1154.50'}
931
- """
932
- self.my_col.create_index('date', unique=True)
933
- result = self.my_col.update_one({'date': mi_dict['date']}, {"$set": {'value': mi_dict['value']}}, upsert=True)
934
- return result.acknowledged