[Mini project 1] 잡코리아 채용정보 수집

임종우·2022년 10월 1일
1

ai_school_TIL

목록 보기
9/34

드디어 첫 번째 미니 프로젝트. 웹 크롤링으로 데이터 수집하기!

우리 팀은 잡코리아에서 데이터 분석 관련 채용 공고를 수집하는 것으로 주제를 결정했다.
아무래도 처음 해보는 프로젝트이고, 다른 사람들과 코드를 함께 짠다는 것이 낯설어서 어려움이 있었지만,
팀장님이 열심히 공부하시고 이끌어주셔서 큰 도움이 되었다.

우리 팀은 팀장님께서 이끌어주신대로 select 문을 이용해서 코드를 짰는데,
나는 내 나름대로 find_all을 이용해서 코드를 한 번 짜 보았다.
아직 SELECT문에 대해서 공부를 안 한 상태이기도 했고, 강의를 듣지 않은 채로 혼자 한번 짜보고 싶었다.


결론부터 말하자면, 할 만 하면서 어려웠다.
어찌어찌 동작을 하게는 하겠는데, 뭔가 체계적이고 효율적으로 코드를 짜지 못했다.
뭔가 휩쓸리면서 닥치는 과제에만 집중해 짠 것 같아 아쉬움이 있다.
특히, 크롤링을 하는 부분에서 태그를 찾고 이를 DataFrame에 저장하는 과정에서,
어떤 태그를 찾아 어떤 방법으로 저장하는 것이 효율적인지에 대한 고민이 필요했을 것 같다.

내가 지금 짜고 있는 방법이 좋은 방법인지 확신이 들지 않고, 다른 더 좋은 방법이 있을 것 같다는 생각이 자꾸 들었다.
역시 코드를 많이 짜보면서 배우는 것 밖에 방법이 없을 것 같다.
이론적인 공부를 아무리 해도 써먹지 않으면 적용하기도 어렵고, 까먹는구나를 또 한 번 느꼈다.

웹 크롤링은 군대에 있을 때 동기랑 노마드코더 강의 들으면서 한 번 해봤던 주제인데, 그 때의 기억이 조금씩 나면서 기억을 더듬어 해낸 것 같다. 뭔가 주체적으로 한 기분은 아니라 아쉽다.
다음주엔 꼭 git에 가입하여 사이드프로젝트라도 진행하며 1일 1커밋! 에 도전하자! 파이팅!


MINI PROJECT 1 - 잡코리아 채용 정보 수집하기

  • 목표
    잡코리아 사이트에서 '데이터분석'을 검색하였을 때 나오는 공고들을 크롤링한다. 구체적으로는 공고명, 회사명, 경력여부, 학력 조건, 직장 위치, 마감 시한 등의 정보들을 크롤링하여 DataFrame에 저장하고, CSV파일로 내보낸다.

  • 사용한 library
    Pandas : 크롤링 할 데이터들을 저장할 데이터프레임에 사용했다. 데이터프레임에 어떤 방법으로 해당 정보들을 저장할 수 있을지 고민이 필요했다. 각 행 별로 하나의 df를 만든 후 concat으로 이어붙이는 방법, 혹은 나는 loc[ ]을 이용해 행에 정보를 추가해주는 방법을 사용했다. 다른 방법으로는 무엇이 있었을까? 열 별로 데이터를 추가하는 방법도 있었을 것이다.
    requests : get 함수를 사용해 웹페이지 정보를 가져오는데 사용하였다. 여기서도 get을 사용하였는데, post 요청을 사용해본 적이 한번도 없어 아쉽다. 혼자 무언가 해볼때는 post를 공부해 사용하는 것으로 해보자. 그리고 xml도! api 도전해보자.
    BeautifulSoup : requests를 사용해 가져온 웹페이지 정보를 parsing하여 본격적인 데이터 수집에 사용하였다. 다양한 태그를 가져올 수 있었는데, 밖의 큰 태그를 리스트로 가져와 세부 항목들을 거기서 찾아가며 저장하는 방법과, 각각의 태그를 각각의 리스트로 만들어 정보를 저장하는 방법 중 후자를 선택했다. 더 나은 방법이 있었을까? 뭔가 첫번째 방법이 더 체계적인 것 같아 보였지만, 결국 코드나 연산의 양은 동일할거라고 생각했다. 어차피 태그에서 text만 뽑아내어 사용하려면 각각의 요소 하나하나에 접근해야 하기 때문에...
    datetime.date : 파일 이름에 넣을 오늘 날짜를 가져오기 위해 사용했다. 근데 오류가 계속 나더라. 더 찾아봐야한다.
    tqdm : trange를 사용하여 많은 페이지를 수집할 때의 진행률을 표시하고자 사용하였다.
    time : 서버의 트래픽 과부하를 막기 위해 for문 내부에서 time.sleep()을 사용한다.

  • 동작 순서

  1. import libraries
  2. set url, dataframe
  3. scrapping with bs (두 개의 for문을 활용하여 여러 페이지 탐색과 한 페이지 내 여러 정보 탐색)
  4. append to df (for문 내부에서 이루어질 과정)
  5. pd.to_csv

그럼 내가 짠 코드를 한번 정리해보자!


라이브러리 import, 변수 선언

import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup as bs   
import time
from datetime import date
from tqdm import trange

먼저 사용할 라이브러리들을 모두 import 하였다.

# 스크래핑할 웹 사이트의 url을 선언, f-string을 이용해 page number를 바꾸어가며 탐색할 수 있도록 함.
page_no = 1
url = f"https://www.jobkorea.co.kr/Search/?stext=%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B6%84%EC%84%9D&tabType=recruit&Page_No={page_no}"

# DataFrame 선언
df = pd.DataFrame(columns = ['회사명','공고명','채용 형태(경력, 신입)','학력','직장 위치','키워드','마감 기한','공고 링크'])

기본적으로 사용 할 변수를 선언했다.
url 주소를 바꾸어가며 여러 페이지의 정보를 받아와가며 크롤링 해야 하기 때문에, url을 f-string을 활용해 변수로 선언하였다.
또한, 최종적으로 데이터들을 저장할 DataFrame을 columns 정보만 주어 선언하였다.


크롤링으로 원하는 정보 가져오기

# requests를 사용하여 html 정보 받아오기
response = requests.get(url, headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'})

# BeautifulSoup 이용하여 parsing
soup = bs(response.text,'html.parser')

크롤링을 하기 위해 requests.get으로 웹 페이지의 정보를 요청한 후 bs를 활용해 parsing 하였다.

그 후 원하는 정보를 얻기 위해 페이지의 html 소스를 보며 어떤 태그를 찾아야 할지 고민했다.

# 찾아야 할 채용 공고들은 모두 li 안에 있다. class 가 list-post 라는 공통점이 있다.
# 이렇게 해봤더니 너무 광범위한 코드를 가져온다. 차라리 div 를 찾아야겠다. class 가 post-list-corp 인 div를 찾자!
# 아냐 차라리 더 구체적으로 들어가는 걸 찾아보자. a 중 class 가 name dev_view 인 것들을 뽑아와보자.
# 이렇게 회사의 이름을 뽑아올 수 있다.

company_name = soup.find_all('a','name dev_view')
# company_name[0].text

처음 생각한 것은 공고들을 포함하고 있는 가장 큰 태그인 li 태그였다. li 태그 안에 모든 공고들이 위치하고 있었기에 li 태그를 find 해서 사용할까 했었지만, 결국 그러면 find한 결과물 내에서 또 find_all 하여 정보들을 수집해야 했기에 다른 방법을 생각했다.
점점 태그의 크기를 좁혀가며 어떤 태그를 find 하는 것이 가장 적절할지 고민했지만, 결국 앞에서와 같은 이유로 찾고자 하는 카테고리 하나 당 find_all을 이용해 하나의 list를 만들어 사용하기로 결정했다.

# 나머지 정보들도 뽑아온다.

exp = soup.find_all('span','exp')
edu = soup.find_all('span','edu')
loc = soup.find_all('span','loc long')
date = soup.find_all('span','date')
etc = soup.find_all('p','etc')
# ','.join(etc[0].text.split(',')[:5])
info = soup.find_all('a','title dev_view')
# info[0].text.strip()
# "https://www.jobkorea.co.kr/" + info[0]['href'] 

etc의 경우, 여러 키워드들이 있는데 그 중 처음 5개만 사용하기 위해 split으로 나눈 후 join으로 다시 붙이는 방법을 사용했다.
지금 생각해보면, 다섯 번째로 나오는 ,를 찾아 그 앞까지만 slicing 하여 사용하거나 하는 방법도 있었을 지 모르겠다.
공고 명의 경우 공고명을 포함하고 있는 a태그의 텍스트 좌우에 공백이 존재해 .strip()으로 공백을 제거했다.
공고 링크의 경우 기본적인 주소 "https://www.jobkorea.co.kr/"에 뒤에 붙여져 사용될 query string이 a 태그의 href 속성으로 존재했기에 저렇게 써줄 수 있었다.


list에 저장한 후 Df에 추가

list_name = []
list_exp = []
list_edu = []
list_loc = []
list_date = []
list_etc = []
list_info = []

for i in range(20):
    list_name.append(company_name[0].text)
    list_exp.append(exp[0].text)
    list_edu.append(edu[0].text)
    list_loc.append(loc[0].text)
    list_date.append(date[0].text)
    list_etc.append(etc[0].text)
    list_info.append(info[0].text.strip())

처음에는 이렇게 생각했다. find_all 로 태그들의 list를 얻었으니, 해당 정보들을 list에 저장한 후 df에 추가해야겠다! 뭐 이렇게 해서 df의 컬럼에 각 list를 저장할 수도 있지 않을까?
그런데 문제가 있었다. 각 list의 길이가 모두 달랐다. 우리가 원하는 검색 결과로 나온 공고가 아닌, 광고로 나와있는 공고 등에서 동일한 이름을 가진 태그가 추출되어서 그런 것 같다. html을 살펴보니, 검색 결과로 나온 공고는 한 페이지 당 20개 씩 표시되었고, 광고보다 먼저 나왔으며, etc와 loc은 검색 결과 공고에만 존재하는 태그였다.
뭐 그럼 list의 20개씩만 잘라서 column에 넣어주거나 하는 방법도 있을 수 있지만, 각각의 column에 들어갈 list를 만들어 df에 저장하고자 한다면, 첫 페이지의 정보를 저장한 후 두번째 페이지의 정보를 저장할 때 어떻게 그 밑으로 이어 붙여야할 지 생각나지 않는다. concat을 사용해야 하나? pandas 어렵다... 공부하자...
어쨌든 그래서 list로 만들어 한번에 넣어주는 건 포기하고,,, for문을 사용하기로 했다.

df.loc[0] = [company_name[0].text, info[0].text.strip(), exp[0].text, edu[0].text, loc[0].text, etc[0].text, date[0].text, "https://www.jobkorea.co.kr/" + info[0]['href'] ]

for문을 사용해 각각의 정보를 가진 행을 하나씩 추가해주기로 했다. 위와 같은 코드로!


For 문에 넣기

그럼 이제 for문을 통해 본격적인 동작을 시도해 볼 차례이다.
근데 앞에서도 말했듯이
겉의 for문으로 페이지를 바꾸어가며 정보를 추출하고,
안쪽의 for문으로 페이지 내 추출한 정보들의 list를 순회하며 df에 행을 추가해야 하는데,
페이지가 총 몇 페이지인지 알아야 첫번째 for 문을 사용 가능하다.

수요일 들었던 강의에서는 while문을 사용하고, 페이지를 바꾸었음에도 데이터가 동일하다면 해당 페이지가 마지막 페이지 인걸로 간주하는 방법을 사용했었다.

하지만 다행히도 여기서는 그럴 필요가 없이, 아래와 같은 방법을 사용했다.

# 다행히도 총 채용 건수가 나와있다. 페이지 처음에! 해당 정보를 크롤링 한 후 페이지 당 표시 개수인 20으로 나누어 주면 총 페이지 정보를 알 수 있겠다.
pages = soup.find('p','filter-text').find('strong').text

# 그런데 문자열 형태인데다가 중간에 ,까지 들어가 있어 20으로 나누어줄 수가 없다. , 를 빼주는 코드를 작성하자.
pages = pages.replace(',','')

# int로 변경한 후 20으로 나누어준다. 그리고 반올림해주면 완성!
pages = round(int(pages)/20)

# 한 줄로?
pages = round(int(soup.find('p','filter-text').find('strong').text.replace(',',''))/20)

그럼 본격적으로 for문에 넣어보자.

for i in trange(20):                         # for i in range(pages): 를 통해 전체 페이지 탐색 가능. 시간이 오래걸려 일단 20페이지만 탐색

    url = f"https://www.jobkorea.co.kr/Search/?stext=%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B6%84%EC%84%9D&tabType=recruit&Page_No={page_no}"

    response = requests.get(url, headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'})
    soup = bs(response.text,'html.parser')
    
    company_name = soup.find_all('a','name dev_view')
    exp = soup.find_all('span','exp')
    edu = soup.find_all('span','edu')
    loc = soup.find_all('span','loc long')
    date = soup.find_all('span','date')
    etc = soup.find_all('p','etc')
    info = soup.find_all('a','title dev_view')


    for j in range(20):
        df.loc[20*i + j] = [
        company_name[j].text,                                                   # 회사 이름
        info[j].text.strip(),                                                   # 공고명
        exp[j].text,                                                            # 채용 형태(경력, 신입)
        edu[j].text,                                                            # 학력
        loc[j].text,                                                            # 회사 위치
        ','.join(etc[j].text.split(',')[:5]),                                   # 키워드
        date[j].text,                                                           # 마감 기한
        "https://www.jobkorea.co.kr/" + info[j]['href']                         # 공고 링크
        ]

    page_no += 1
    
    time.sleep(0.1)

겉의 for문에서는 먼저 url 변수를 선언해준다. 처음에 이걸 for문 밖에서 했다가, 1 페이지의 정보만 계속 추출하는 오류가 났었다.
그리고 requests 와 bs를 사용해 원하는 정보들을 찾아 list를 만든다.
그 후 내부 for문에서 각각의 list에 접근해 data를 df에 추가한다.
이때, df의 행의 인덱스로 20*i + j를 사용하여, 페이지가 넘어간 후 다음 페이지의 정보들이 이전 페이지의 정보들 아래에 추가되도록 해줄 수 있었다.
마지막으로 time.sleep(0.1)을 통해 트래픽이 과부화되거나 ip가 차단되는 일을 막았다.

만약 처음에 시도했던 대로, li 태그를 find_all 하여 list를 만들었다면, 팀원들이 한 대로 안 쪽 for문에서는 굳이 range(20)을 사용하지 않고 해당 list를 for문으로 순회하며 그 안에서 각각의 정보를 find 하여 df에 추가해줄 수 있었을 것이다. 무엇이 더 나은지는 모르겠다. list의 개수는 내가 한 방식이 훨씬 많고, find 연산을 시행하는 횟수는 해당 방식이 훨씬 많을 것 같다. 어떻게 비교할 수 있을까?
이전에 배운 %timeit 기능을 사용하여 비교해 볼 수도 있겠고, 옛날에 학교에서 O notation 이런거 배웠던 것 같은데.. 그건 잘 모르겠다. 공부하자!


CSV로 내보내기

특별히 어려울 건 없었는데, 오류가 계속 난다.

from datetime import date

file_name = str(date.today()) + "_데이터분석_채용공고.csv"

file_name
df.to_csv(file_name,index=False)
pd.read_csv(file_name)

여기서 from datetime import date를 써주지 않으면, 자꾸 today라는 속성이 없다는 오류가 떴다.
가장 첫 셀에서 import 해주었는데도! 대체 왤까??
에러 메시지는 이거다.

---> 65 file_name = str(date.today()) + "_데이터분석_채용공고.csv"
     67 file_name
     68 df.to_csv(file_name,index=False)

AttributeError: ResultSet object has no attribute 'today'. You're probably treating a list of elements like a single element. Did you call find_all() when you meant to call find()?

보니까 오류는 bs4 라이브러리에서 난건데, 여기서 나는 bs와 관련된 아무것도 하지 않았는데..! 왜...!
그냥 bs4와 datetime 의 충돌이라고 생각해야겠다.... 모르겠어...

악윽악 알았다!!!!
아니 내가 date라는 list를 만들었었네... find_all 써가지고.. 허허허허ㅓ,,,
당연히 그 변수 이름이랑 겹치니까 오류가 나지!

하나 배웠다. 이래서! 예약어나 모듈명이랑 겹치는 변수 이름에 주의해야하는구나!

코드 변경 -> from datetime import date 안하고,
import datetime 한 후
datetime.date.today()로 사용해야겠다! 와..... 그랬었구나.....

어쨌든 그렇게 에러도 수정하고, 완성!


최종본

# 최종본

import numpy as np
import pandas as pd
import requests
from bs4 import BeautifulSoup as bs   
import time
import datetime
from tqdm import trange

# 스크래핑할 웹 사이트의 url을 선언, f-string을 이용해 page number를 바꾸어가며 탐색할 수 있도록 함.
page_no = 1
url = f"https://www.jobkorea.co.kr/Search/?stext=%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B6%84%EC%84%9D&tabType=recruit&Page_No={page_no}"

# DataFrame 선언
df = pd.DataFrame( columns = ['회사명','공고명','채용 형태(경력, 신입)','학력','직장 위치','키워드','마감 기한','공고 링크'])

# 스크래핑할 웹 사이트의 총 페이지 수 파악
# '총 OOOO건' 이라는 검색 결과를 스크래핑하여 한 페이지당 표시 수인 20으로 나누기

response = requests.get(url, headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'})
soup = bs(response.text,'html.parser')

pages = soup.find('p','filter-text').find('strong').text.replace(',','')
pages = round(int(pages)/20)

# For문을 이용해 웹 사이트 탐색
# 바깥의 for문을 통해 페이지를 바꾸어가며 탐색하고
# 안쪽의 for문으로 페이지 내 20개의 공고를 옮겨가며 탐색한다.

for i in trange(20):                       # for i in trange(pages): 를 통해 전체 페이지 탐색 가능. 시간이 오래걸려 일단 20페이지만 탐색

    url = f"https://www.jobkorea.co.kr/Search/?stext=%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B6%84%EC%84%9D&tabType=recruit&Page_No={page_no}"

    response = requests.get(url, headers = {'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36'})
    soup = bs(response.text,'html.parser')
    
    company_name = soup.find_all('a','name dev_view')
    exp = soup.find_all('span','exp')
    edu = soup.find_all('span','edu')
    loc = soup.find_all('span','loc long')
    date = soup.find_all('span','date')
    etc = soup.find_all('p','etc')
    info = soup.find_all('a','title dev_view')


    for j in range(20):
        df.loc[20*i + j] = [
        company_name[j].text,                                                   # 회사 이름
        info[j].text.strip(),                                                   # 공고명
        exp[j].text,                                                            # 채용 형태(경력, 신입)
        edu[j].text,                                                            # 학력
        loc[j].text,                                                            # 회사 위치
        ','.join(etc[j].text.split(',')[:5]),                                   # 키워드
        date[j].text,                                                           # 마감 기한
        "https://www.jobkorea.co.kr/" + info[j]['href']                         # 공고 링크
        ]

    page_no += 1
    
    time.sleep(0.1)

file_name = str(datetime.date.today()) + "_데이터분석_채용공고.csv"
df.to_csv(file_name,index=False)
pd.read_csv(file_name)

내가 쓴 코드를 내가 리뷰하는 것도 아주 큰 도움이 되었다! 여러 생각도 해볼 수 있었고, 오류가 왜 난건지도 잡아낼 수 있었다.

더 열심히 공부해서 더 많은 코드를 짜보자!

파이팅!

profile
ai school 기간 동안의 TIL!

0개의 댓글