[PlanTo #9] 채용공고 일정 추가 (2)

이원진·2023년 8월 2일
0

Planto

목록 보기
9/9
post-thumbnail
post-custom-banner

목차


  1. 서론

  2. API Key 숨기기

  3. location code 크롤러 제작

  4. industry code 크롤러 제작



0. 서론


저번 글에서 구현했던 채용공고 뷰(JobList)의 코드를 살펴보면, 아래와 같이 사람인 API에 요청을 보내기 위해 base URL과 API Key를 사용합니다.

class JobList(APIView):
  # 채용공고 목록 출력
  def get(self, request, format = None):
      base_url = "https://oapi.saramin.co.kr/job-search?access-key="
      api_key = "XXX"
      
      base_url += api_key

API Key를 깃허브에 그대로 커밋할 경우 악의적으로 사용되는 등의 문제가 발생할 수 있습니다. 따라서 이번 글에서는 환경 변수를 사용해 API Key 등의 민감한 정보를 처리하는 방법에 대해 알아보겠습니다.



1. API Key 숨기기


민감한 정보 혹은 코드를 외부로부터 숨기는 방법은 여러가지가 있지만, 그 중에서 config.ini 파일과 configparser 모듈을 사용해보겠습니다. 우선, 아래 사진과 같이 job_announcement app 안에 환경설정을 위한 config.ini 파일을 생성하고 API_KEY라는 섹션 아래 SARAMIN_API_KEY라는 환경변수를 작성해줬습니다.

[API_KEYS]
SARAMIN_API_KEY = my_api_key


다음으로, 해당 환경변수를 사용할 JobList 뷰에서 다음과 같이 configparser 라이브러리를 사용해 API Key를 호출해 사용했습니다.

class JobList(APIView):
    # 채용공고 목록 출력
    def get(self, request, format = None):
        base_url = "https://oapi.saramin.co.kr/job-search?access-key="
        
        config = configparser.ConfigParser()
        config_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini")
        config.read(config_file_path)
        
        api_key = config.get("API_KEYS", "SARAMIN_API_KEY")
        base_url += api_key
        
        ...

만약 config.ini 파일이 깃허브에 올라간다면 위의 과정이 아무 의미 없게 될 것입니다. 따라서 .gitignore 파일에 config.ini를 추가했습니다.

코드 수정 이후에 Postman을 사용해 /jobs로 요청을 보내면, 아래 사진과 같이 수정 이전과 동일하게 잘 동작하는 것을 확인할 수 있습니다.





2. location code 크롤러 제작


JobList 뷰의 get() 메서드에서 사용자가 지역, 직무, 키워드를 사용해 채용공고를 검색할 수 있도록 아래와 같이 query parameter들을 정의했습니다.

class JobList(APIView):
    # 채용공고 목록 출력
    def get(self, request, format = None):
        ...
        
        # 사용자가 지역, 업종, 키워드를 검색 조건으로 사용 가능
        query_parameters = [
            "location",
            "industry",
            "keywords"
        ]

사람인 API의 명세서의 요청 변수(Request Parameters)를 살펴보면, 이들 중 키워드는 문자열 형태로 바로 입력받아 사용 가능하지만, 지역과 업종의 경우, 코드이기 때문에 바로 사용이 불가능하다는 것을 확인할 수 있습니다.

따라서 사용자가 브라우저에서 원하는 지역, 업종을 선택하면, 해당 선택지에 해당하는 코드 값을 요청 변수로 변환해 사람인 API에 요청을 보내는 기능을 구현해보겠습니다. 이를 위해서 지역, 업종의 각 항목별 코드를 저장해놔야하므로 크롤러를 제작해보겠습니다.



BeautifulSoup vs Selenium

Python으로 웹 크롤링을 하는 방법은 두 가지인데, 그 중 하나는 requests 라이브러리로 요청을 보낸 뒤 응답을 BeautifuleSoup으로 파싱하는 것이고, 다른 하나는 Selenium 라이브러리로 웹 브라우저에서 정보를 가져오는 것입니다.

Selenium의 경우 클릭 등의 웹 컨트롤을 할 수 있는 장점이 있는만큼 무겁고 느리기 때문에 동적 크롤링이 필요하지 않은 경우라면 requests와 BeautifulSoup을 활용하는 것이 효과적입니다. 단순히 지역, 업종 항목과 해당하는 코드 두 가지만 필요하기 때문에, 이번 글에서는 requests와 BeautifulSoup을 사용해보겠습니다.



크롤러 제작

크롤러를 제작하기 위해 우선 필요한 라이브러리들을 아래와 같이 설치해줬습니다.

poetry add requests
poetry add beautifulsoup4

크롤링할 대상인 사람인 API 명세서 - 근무지/지역 코드표 페이지를 보면, 아래 사진과 같이 테이블에 4개의 컬럼이 있는 것을 확인할 수 있습니다. 이 중 지역코드와 지역명에 해당하는 첫 번째, 두 번째 컬럼만 추출해보겠습니다.


아래와 같이 requests와 BeautifulSoup라이브러리를 사용해 지역 코드를 가져오는 fetch_location_codes() 메서드와, 이를 csv 파일에 저장하는 save_to_csv() 메서드를 작성했습니다.

import requests
import csv
from bs4 import BeautifulSoup

def fetch_location_codes():
    url = "https://oapi.saramin.co.kr/guide/code-table2"
    response = requests.get(url)
    
    if response.status_code == 200:
        soup = BeautifulSoup(response.content, "html.parser")
        location_codes = {}
        
        # 헤더인 첫 번째 줄은 건너뛰고 두 번째 줄부터 반복
        for row in soup.select("table tr")[1:]:
            if row.select("td"):
            
            	# 앞에서 2개 항목(컬럼)만 추출
                code, name = row.select("td")[:2]
                location_codes[name.text.strip()] = code.text
            
        return location_codes
    
    else:
        print(f"지역 코드를 가져오는 데 실패했습니다.(상태 코드: {response.status_code})")
        return None
    
def save_to_csv(location_codes):
    if not location_codes:
        return
    
    with open("location_codes.csv", "w", newline = "", encoding = "utf-8") as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(["지역명", "지역코드"])
        
        for name, code in location_codes.items():
            writer.writerow([name, code])
            
if __name__ == "__main__":
    location_codes = fetch_location_codes()
    
    if location_codes:
        save_to_csv(location_codes)
        print("지역 코드가 location_codes.csv에 저장되었습니다.")
        
    else:
        print("지역 코드를 저장하는 데 실패했습니다.")

작성한 크롤러를 실행하면, 아래 사진과 같이 지역명과 지역코드만 추출한 csv 파일이 잘 생성되었음을 확인할 수 있습니다.





3. industry code 크롤러 제작


위에서 location code 크롤러를 제작한 것과 마찬가지로, industry code 크롤러도 제작해보겠습니다.

우선 크롤링할 대상 URL을 알아야하는데, 사람인 API 명세서 - 산업/업종 코드표를 보면, 페이지가 세 개의 항목으로 나눠져있는데 브라우저에는 모두 같은 URL로 표시되어 있었습니다. 따라서 개발자 도구(검사)를 사용해 아래 사진과 같이 해당하는 항목에 걸려있는 a태그를 찾아서 세부 URL을 알아냈고, 해당 URL로 접속 시 첫 번째 항목(상위 산업/업종 코드표)이 아닌 대상 항목(업종 키워드 코드표)으로 잘 접속되는 것을 확인했습니다.



URL을 알아낸 뒤에는 location code 크롤러와 마찬가지로 fetch_location_codes() 메서드, save_to_csv() 메서드를 작성했습니다. 위 사진의 테이블에서 첫 번째, 두 번째 컬럼만 추출했습니다.

import requests
import csv
from bs4 import BeautifulSoup

def fetch_industry_codes():
	# 대상 항목 URL
    url = "https://oapi.saramin.co.kr/guide/code-table3#lasKeywordCodelist"
    response = requests.get(url)
    
    if response.status_code == 200:
        soup = BeautifulSoup(response.content, "html.parser")
        industry_codes = {}
        
        for row in soup.select("table tr")[1:]:
            if row.select("td"):
                code, name = row.select("td")[:2]
                industry_codes[name.text.strip()] = code.text
            
        return industry_codes
    
    else:
        print(f"업종 코드를 가져오는 데 실패했습니다.(상태 코드: {response.status_code})")
        return None
    
def save_to_csv(industry_codes):
    if not industry_codes:
        return
    
    with open("industry_codes.csv", "w", newline = "", encoding = "utf-8") as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(["업종명", "업종코드"])
        
        for name, code in industry_codes.items():
            writer.writerow([name, code])
            
if __name__ == "__main__":
    industry_codes = fetch_industry_codes()
    
    if industry_codes:
        save_to_csv(industry_codes)
        print("업종 코드가 industry_codes.csv에 저장되었습니다.")
        
    else:
        print("업종 코드를 저장하는 데 실패했습니다.")

작성한 크롤러를 실행하면, 아래 사진과 같이 업종명과 업종코드만 추출한 csv 파일이 잘 생성되었음을 확인할 수 있습니다.

post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

좋은 글 감사합니다. 자주 올게요 :)

답글 달기