목차
개요
https://www.tradingview.com/ 라는 사이트에서 외환 거래 데이터를 수집해야 하는 일을 맡게 되었다.
처음엔 단순하게 페이지를 분석해서 데이터를 얻을 수 있는 동적 크롤링이나 polling 방식을 통해 데이터를 얻을 수 있겠지 싶었는데 이 방법들로는 데이터를 수집하기엔 무리가 있었다.
WebSocket이다보니 실시간으로 발생하는 데이터이기 때문에 코드 레벨에서 WebSocket에 연결한 뒤 데이터를 수집해야 했고 더 나아가 이렇게 수집한 데이터를 저장해야 되는 작업을 필요로 했다.
해놓고 보니 난이도가 높다고는 할 수 없지만 적어도 새로운 접근 방식으로 풀어내야 했기에 재밌는 일이었다.
그리하여 tradingview.com을 예시로 WebSocket을 통해 데이터를 수집할 때 필요로 했던 것을 풀어내고자 한다.
웹에서 데이터를 수집하는 3가지 방법
보통 웹에서 데이터를 수집이라는 키워드를 떠올리면 나름 다음과 같은 절차대로 접근한다.
- Open 된 API가 제공되는가?
- Open 된 API가 제공되지 않는다면 Polling 방식으로 데이터를 주고받는 요청이 일어나는지 확인되는가?
- Polling 방식으로 데이터를 주고받지 않는다면 Selenium을 통해서 데이터를 수집할 수 있는 구조인가?
1번이 가장 쉬운 방법이다. OpenAPI가 제공된다는 것은 데이터에 대한 규격을 어느 정도 공개해 놓은 것이고 공개된 규격에 따라 데이터를 요청하고 필요한 데이터를 수집하면 된다.
만약 2번과 같이 Open API가 제공되진 않지만 Polling 방식으로 데이터를 주고받는 요청이 일어나는 게 확인된다면 “어떤 URI에 어떤 데이터가 있는가?”를 파악해야 하기 때문에 시간이 더 걸리지만 이 방법까지는 어떤 식으로 응답/요청을 하는지 분석할 수 있는 과정이 재밌기 때문에 시간과 삽질의 트레이드오프가 적절한 수준이다.
만약 1번과 2번도 안 되는 상황이라면 3번과 같이 동적 크롤링을 통해 진행하는데 이 방법을 사용하게 되면 Selenium이라는 라이브러리와 html source를 얻어다가 parsing 하는 작업이 필요하다. 이는 곧 1,2번 방법에 비해 성능이 현저히 느리며 또한 필요한 데이터를 찾기 위해 html 구조를 분석해야 하기 때문에 소요되는 시간도 만만치 않다.
WebSocket을 통해 데이터를 수집하기
tradingview.com의 데이터 수집 방식은 1,2,3번 그 어디에도 해당하지 않는다.
‘개요’에서 언급했듯이 WebSocket을 통해 웹 페이지의 그래프에 데이터를 표출해주고 있으므로 이 데이터를 수집하려면 코드 레벨에서 WebSocket의 client 통신을 재현하여 데이터를 얻어와야 하는 상황이기 때문이다.
WebSocket Client 통신을 재현하기 위해 requests를 사용하여 이를 코딩하려 했으나 난이도가 올라갔다. WebSocket은 연결과정에서 Swiching Protocol을 사용하기 때문에 해당 Protocol이 요구하는 방식을 재현하기 위해 직접 header를 조작해야 했기 때문에 더 편리한 방법을 찾아야 했다.
이를 해결하기 위해 다음과 같은 라이브러리를 사용할 수 있었다.
websocket-client==1.4.2
라이브러리에 대한 사용 방법은 해당 라이브러리 문서에 나와있기 때문에 생략한다.
WebSocket 연결 과정 분석하기
WebSocket 연결을 재현하려면 먼저 WebSocket에서 client와 server 간 어떤 흐름을 통해 연결을 맺는지 분석하는 절차를 먼저 밟을 필요가 있다. tradingview.com의 경우 Chrome Network를 통해 WebSocket이 어떤 식으로 통신하는지 확인할 수 있다.
tradingview.com에서는 chart에 데이터를 생성하기 전 다음과 같은 요청을 보낸다는 걸 확인했다.
p 안에 있는 데이터가 어떤 종류의 데이터인지 감이 잡히지 않아 분석한 결과 chart에 데이터를 표출해 주기 위한 random의 chart_session_id라는 것을 짐작했다. 계속 관찰한 결과 tradingview.com에서 chart에 특정 통화쌍 데이터를 표출하기까지 ‘m’이라는 key 안에 value에 아래의 텍스트를 넣은 뒤 server에 요청하는 것을 알 수 있었다.
set_auth_token
chart_create_session
quote_create_session
quote_add_symbols
quote_set_fields
resolve_symbol
quote_fast_symbols
create_study
quote_hibernate_al
연결과정에서 사용된 method을 잡아낸 것이기 때문에 위 메서드들이 정확히 어떤 동작을 수행하는지 알 수 없었지만 굳이 알아낼 필요도 없었다. method명을 통해 어느 정도 유추가 가능했고 필요한 파라미터는 Chrome Network에서 관찰한 파라미터를 그때그때 넣어주면 쉽게 사용이 가능했기 때문이다.
그리하여 websocket의 on_open 시점에 이를 수행하는 코드를 다음과 같이 작성했다.
def on_open(self, ws):
websocketopendispatcher = WebSocketOpenDispatcher(
session_id=self._session_id,
chart_session_id=self._chart_session_id,
symbol_name=self.symbol_name
)
try:
# WebSocket 연결 시 전송하는 메서드
for tradingview_method in websocketopendispatcher.open_method_list():
self.send(tradingview_method)
except Exception as e:
print(e)
self.close()
WebSocket 세션은 종료될 수 있다.
위의 과정까지 조사를 마치고 나서 나머지 영역의 코드를 작성하니 WebSocket을 통해 데이터를 성공적으로 가져올 수 있었다. 그런데 데이터를 가져오다 3분 정도가 지나자 연결이 끊기는 현상을 발견했다. Browser 상으로는 데이터가 계속해서 갱신되기 때문에 미처 신경 쓰지 못한 부분이었다.
당연하게 health check 쪽을 확인해야 했다. 보통 WebSocket에서의 health check는 ping-pong이기 때문에 Chrome Network에서도 이를 주시하여 보고 있었는데 tradingview.com에서는 이가 보이지 않았다. 그런데 이와 비슷한 양상을 보이는 통신을 포착했다.
특정 payload가 보이지 않아 이 부분이 연결을 지속하는데 쓰이는 health check 부분이라고 추측했다. ping-pong을 보내는 대신 다음과 같은 함수를 구현해냄으로써 연결을 지속할 수 있었다.
def health_check(ws_app, recv_msg):
""" Connection 유지를 위한 Health Check """
pattern = re.compile("~m~\\d+~m~~h~\\d+$")
if pattern.match(recv_msg):
ws_app.send(recv_msg)
WebSocket 연결 중 다른 동작이 필요하다면?
사실 지금까지의 과정은 데이터 요청 시점을 기준으로부터 발생하는 데이터를 가져오는 작업이다. 그런데 한 가지 더 신경 써야 할 점은 chart라는 게 왼쪽으로 이동할 수 있는 성질이 있고 왼쪽으로 이동한다는 것은 과거 데이터를 얻어온다는 것을 의미한다.
처음엔 어떻게 해결해야 될지 잘 몰랐는데 조언을 구한 뒤 thread를 이용해 처리할 수 있었다. 코드는 아래와 같은 모양새이다.
threading.Thread(
target=ws_utils.chart_left_shift, args=(self,),
daemon=True
).start()
그런데 왼쪽으로 이동하는 요청을 보내야 했기 때문에 다시 Websocket 통신을 분석해야 했다. 이 과정에서 다음과 같은 메서드를 서버로 보내는 것을 확인할 수 있었다.
get_request_more_tickmarks
get_request_more_data
맺음말
짧게 짧게 썼지만 지금까지 기술한 내용들이 tradingview.com을 예시로 websocket에서 데이터를 가져오기 위한 방법 중 핵심만 담은 내용이기도 하다.
요약하자면 다음과 같다.
- Chrome Network Tab을 통해 WebSocket 연결 분석
- python websocket-client 라이브러리를 이용해 WebSocket 연결 요청 재현
- WebSocket 세션 종료를 대비한 health check 통신 관찰
평소 크롤링이나 API를 통해 데이터를 얻고 적재하는 작업을 거친다면 본 포스팅에 기재된 내용을 마주할 일은 별로 없을 것이라고 본다. 하지만 WebSocket은 간간히 쓰이고 있는 만큼 이러한 방법도 있다는 것을 알아두면 도움이 될 것이다.
'개발 노트 > Experience' 카테고리의 다른 글
[Experience] wkhtmltopdf 사용으로 인해 발생한 SSTI 해결하기 (8) | 2024.09.02 |
---|---|
[AWS] SESv2를 이용한 수신측 메일 열람여부 확인하기 (0) | 2024.06.03 |
Django에 pytest 도입하기 (0) | 2023.03.26 |
Flask Scheduling이 2번 실행되는 현상에 대해 (0) | 2023.03.04 |
Insecure XMLHttpRequest EndPoint (0) | 2022.07.11 |