Profile picture

[Python] 엔카 API 분석 및 차량 보험이력 스크래핑

JaehyoJJAng2023년 05월 06일

개요

면허는 있지만 차가 없어 고민인 요즘
괜찮은 중고차가 있나 한 번 확인이라도 해보려고 여러 중고차 사이트를 돌아다녀보다 문득 encar API를 분석해서 내가 원하는 차량 정보/보험 이력 등을 조회해 나만의 데이터로 가공하면 어떨까 해서 만들어보게 되었다

API 분석

Fetch/XHR 로 필터링
image


아무 페이지 클릭
image

그러면 아래처럼 API로 부터 받은 요청이 여러개 보일텐데 Preview 탭에서 포스팅 Id를 리턴하고 있는 것을 찾아서 클릭
image


자, 이제 Headers 탭에 Requests URL을 복사하고 Payload 탭에 쿼리 파라메터들이 어떠한 형식으로 들어오는지 확인하면 분석은 끝이난다
image

데이터 추출

Query Parameter와 Request URL을 활용하여 API 서버로부터 데이터를 받아올거다

쿼리 파라메터 생성

위에서 본 Query Parameter를 페이징 처리하여 리스트로 리턴할거다
그렇게 함으로써 여러 페이지(끝 페이지까지)에 대한 데이터를 가져올 수 있게 되므로 국산 자동차 전체에 대한 데이터를 뽑아올 수도 있다.
하지만 난 1페이지만 테스트 해보도록 할거다

def create_query_format(self) -> Dict[str,str]:
    params = [
        {
        "count": "true",
        "q": "(And.Hidden.N._.CarType.Y.)",
        "sr": f"|PriceAsc|{(page * 10) * 2}|20"
        } for page in range(self.page_count)]
    return params

get 요청 보내기

send_request 라는 메소드를 하나 작성하여 get 요청으로 위에서 작성한 쿼리 파라메터를 보낼거다.

def send_request(self,param:Dict[str,str]) -> Dict[str,Union[int,list]]:
    response = rq.get(url=self._KCAR,headers={"user-agent":"Mozilla/5.0"},params=param)
    return response.json()

response = (params=data)

data 매개변수나 json 매겨변수로 데이터를 넘기면 빈 딕셔너리값이 반환된다

하지만 params 매개변수로 데이터를 넘기면 정상적으로 데이터가 들어옴


요청을 한 번 보내보자

if __name__ == "__main__':
    KCAR : str = 'http://api.encar.com/search/car/list/premium'
    
    page_count : int = 1
    
    # Create Encar Instance
    encar : Encar = Encar(KCAR=KCAR,page_count=page_count)

    encar.send_request()
{'Count': 87832, 'SearchResults': [{'Id': '34903200', 'Separation': ['B'], 'Trust': ['Warranty'], 'Condition': ['Inspection', 'Record'], 'Photo': '/carpicture10/pic3490/34903200_', 'Photos': [{'type': '001', 'location': '/carpicture10/pic3490/34903200_001.jpg', 'updatedDate': '2023-04-24T06:01:57Z', 'ordering': 1.0}, {'type': '003', 'location': '/carpicture10/pic3490/34903200_003.jpg', 'updatedDate': '2023-04-24T06:01:57Z', 'ordering': 3.0}, {'type': '004', 'location': '/carpicture10/pic3490/34903200_004.jpg', 'updatedDate': '2023-04-24T06:01:57Z', 'ordering': 4.0}, {'type': '007', 'location': '/carpicture10/pic3490/34903200_007.jpg', 'updatedDate': '2023-04-24T06:01:57Z', 'ordering': 7.0}], 'Manufacturer': '기아', 'Model': '더 뉴 기아 레이', 'Badge': '시그니처', 'Transmission': '오토', 'FuelType': '가솔린', 'Year': 202210.0, 'FormYear': '2023', 'Mileage': 16613.0, 'ServiceCopyCar': 'ORIGINAL', 'Lease': '1', 'LeaseType': '운용리스', 'MonthLeasePrice': 35.0, 'Deposit': 0.0, 'ResidualValue': 398.0, 'Price': 35.0, 'OfficeCityState': '울산', 'ModifiedDate': '2023-05-06 23:10:10.000 +09'}
... 생략 ...

정상적으로 데이터를 받아볼 수 있었다.


포스팅 Id 추출

get_car_infos 메소드 작성

GET_CAR_ID = List[Dict[str,Union[str,List[str]]]]

def get_car_infos(self,json_data:Dict[str,Union[int,list]]) -> GET_CAR_ID:
    """ Car id 데이터 리턴 메소드 """
    # 데이터 저장용 리스트 변수
    save_list : List[Dict[str,str]] = []
    
    # 사진 저장을 위한 .jpg base url
    img_base_url : str = 'http://ci.encar.com/carpicture'
    
    for data in json_data['SearchResults']:
        # 데이터 저장용 딕셔너리 변수
        save_dict : Dict[str,str] = {}
        
        # 데이터 저장
        save_dict['Id'] = data['Id']
        save_dict['Photos'] = [img_base_url + location['location'] for location in data['Photos']]
        save_dict['Manufacturer'] = data['Manufacturer']
        save_dict['Model'] = data['Model']
        save_list.append(save_dict)
        
        # 데이터 출력            
        print(save_dict,'\n')
    
    # 저장한 데이터 리턴
    return save_list

데이터를 한번 뽑아보자 ..

{'Id': '34860832', 'Photos': ['http://ci.encar.com/carpicture/carpicture06/pic3486/34860832_001.jpg', 'http://ci.encar.com/carpicture/carpicture06/pic3486/34860832_003.jpg', 'http://ci.encar.com/carpicture/carpicture06/pic3486/34860832_004.jpg', 'http://ci.encar.com/carpicture/carpicture06/pic3486/34860832_007.jpg'], 'Manufacturer': '현대', 'Model': '그랜저 IG'}

... 생략 ...

image

잘 가져와진다.


보험 이력 스크래핑

위에서 작성한 코드를 기반으로 보험이력 조회하는 메소드를 추가해볼거다.

def get_car_detail_infos(self,car_infos: List[Dict[str,str]]) -> List[Dict[str,str]]:
    save_detail_list : List[Dict[str,str]] = []
    """ 차량 보험이력 데이터 스크래핑 """
    for car_info in car_infos:            
        kcar_detail_url : str = f'http://www.encar.com/dc/dc_cardetailview.do?method=kidiFirstPop&carid={car_info["Id"]}&wtClick_carview=044'
        
        response : rq.Response = rq.get(url=kcar_detail_url,headers=self.headers)
        soup : bs = self.get_soup_object(response=response)
        
        if soup.select_one('div.section.message > big > strong'):
            continue 
        
        table_list = soup.select('div.smlist tbody > tr')
        if len(table_list) > 2:
            model = table_list[0].text.strip()
            owner_count = table_list[2].text.strip()
            accident_history = table_list[3].text.strip()
        else:
            model = "-"
            owner_count = "-"
            accident_history = "-"
        
        # 기존 car_infos 딕셔너리에 데이터 추가
        car_info['owner_count'] = owner_count
        car_info['model'] = model
        car_info['accident_history'] = accident_history
        save_detail_list.append(car_info)
        
        # 데이터 출력
        print(car_info,'\n')

    # 최종 데이터 리턴
    return save_detail_list

출력 결과

image


Streamlit

위에서 추출한 데이터를 웹으로 출력해보자

# (... 생략 ...)
html_text : str = ""
for x in results:
    for result in x:
        html_text += f"""
        \n ![car-image]({result['Photos'][0]}) \n
        - ID: {result['Id']}
        - 모델명: {result['model']}
        - 소유자 변경 횟수: {result['owner_count']}
        - 사고 기록: {result['accident_history']}
        """

st.markdown(html_text)

image

배포

도커를 사용하여 위에서 진행한 스크래핑 프로젝트를 배포해보자

  • Docker Base Image : python:3.8-slim-buster
  • docker-compose 사용

encar.Dockerfile

FROM python:3.9-slim-buster

WORKDIR /app

COPY ./ ./

RUN pip install --upgrade pip && pip install -r requirements.txt

ARG PORT=8501

EXPOSE ${PORT}

CMD [ "streamlit", "run", "main.py" ]

docker-compose.yml

version: "3"

services:
  streamlit:
    build:
      context: python-app/
      dockerfile: encar.Dockerfile
    restart: always
    ports:
      - "8501:8501"
    container_name: streamlit

Loading script...