Skip to content

Commit 157fda8

Browse files
authored
Merge pull request #137 from Pseudo-Lab/feat/crawling
한국어 공고 크롤링 deadline 추가
2 parents d7861ce + 01418f2 commit 157fda8

File tree

1 file changed

+148
-67
lines changed

1 file changed

+148
-67
lines changed

preprocess/korean_jd_crawling.py

Lines changed: 148 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import json
1+
import csv
22
from re import T
33
import time
44
import os
@@ -18,55 +18,54 @@ class TestCrawlingWanted:
1818
def __init__(self):
1919
self.endpoint = "https://www.wanted.co.kr"
2020
self.job_parent_category = 518
21-
self.job_category_id = 10110 # 소프트웨어 엔지니어
22-
self.job_category_name = "소프트웨어 엔지니어"
23-
# self.job_category_id2name = {
24-
# 10110: "소프트웨어 엔지니어",
25-
# 873: "웹 개발자",
26-
# 872: "서버 개발자",
27-
# 669: "프론트엔드 개발자",
21+
self.job_category_id2name = {
22+
10110: "소프트웨어 엔지니어",
23+
873: "웹 개발자",
24+
872: "서버 개발자",
25+
669: "프론트엔드 개발자",
2826
# 660: "자바 개발자",
2927
# 900: "C,C++ 개발자",
30-
# 899: "파이썬 개발자",
31-
# 1634: "머신러닝 엔지니어",
32-
# 674: "DevOps / 시스템 관리자",
28+
899: "파이썬 개발자",
29+
1634: "머신러닝 엔지니어",
30+
674: "DevOps / 시스템 관리자",
3331
# 665: "시스템,네트워크 관리자",
34-
# 655: "데이터 엔지니어",
32+
655: "데이터 엔지니어",
3533
# 895: "Node.js 개발자",
36-
# 677: "안드로이드 개발자",
37-
# 678: "iOS 개발자",
34+
677: "안드로이드 개발자",
35+
678: "iOS 개발자",
3836
# 658: "임베디드 개발자",
3937
# 877: "개발 매니저",
40-
# 1024: "데이터 사이언티스트",
38+
1024: "데이터 사이언티스트",
4139
# 1026: "기술지원",
42-
# 676: "QA,테스트 엔지니어",
40+
676: "QA,테스트 엔지니어",
4341
# 672: "하드웨어 엔지니어",
4442
# 1025: "빅데이터 엔지니어",
45-
# 671: "보안 엔지니어",
43+
671: "보안 엔지니어",
4644
# 876: "프로덕트 매니저",
4745
# 10111: "크로스플랫폼 앱 개발자",
4846
# 1027: "블록체인 플랫폼 엔지니어",
4947
# 10231: "DBA",
5048
# 893: "PHP 개발자",
5149
# 661: ".NET 개발자",
52-
# 896: "영상,음성 엔지니어",
50+
896: "영상,음성 엔지니어",
5351
# 10230: "ERP전문가",
5452
# 939: "웹 퍼블리셔",
5553
# 898: "그래픽스 엔지니어",
56-
# 795: "CTO,Chief Technology Officer",
54+
795: "CTO,Chief Technology Officer",
5755
# 10112: "VR 엔지니어",
5856
# 1022: "BI 엔지니어",
5957
# 894: "루비온레일즈 개발자",
6058
# 793: "CIO,Chief Information Officer"
61-
# }
59+
}
6260

6361
self.tag2field_map = {
6462
"포지션 상세": "description",
6563
"주요업무": "main_work",
6664
"자격요건": "qualification",
6765
"우대사항": "preferences",
6866
"혜택 및 복지": "welfare",
69-
"기술스택 ・ 툴": "tech_list"
67+
"기술스택 ・ 툴": "tech_list",
68+
"마감일": "deadline"
7069
}
7170

7271
# Chrome 드라이버 설정
@@ -163,21 +162,19 @@ def crawl_job_detail(self, position_url):
163162
# 회사 정보
164163
company_info = job_header.find("div", class_="JobHeader_JobHeader__Tools__lyxqQ")
165164
if company_info:
166-
result['company_name'] = company_info.text.strip()
165+
raw_company_info = company_info.text.strip()
166+
result['company_name_raw'] = raw_company_info # 원본 정보 보존
167+
168+
# 회사 정보 파싱
169+
parsed_info = self.parse_company_info(raw_company_info)
170+
result['company_name'] = parsed_info['company_name']
171+
result['location'] = parsed_info['location']
172+
result['experience_requirement'] = parsed_info['experience']
173+
167174
company_link = company_info.find("a")
168175
if company_link:
169176
result['company_id'] = company_link.get("href", "")
170177

171-
# 태그 정보
172-
tags_div = job_header.find("div", class_="Tags_tagsClass__mvehZ")
173-
if tags_div:
174-
tag_list = tags_div.find_all("span")
175-
result['tag_name'] = [tag.text.lstrip("#").strip() for tag in tag_list]
176-
result['tag_id'] = [tag.get("href", "") for tag in tag_list]
177-
else:
178-
result['tag_name'] = []
179-
result['tag_id'] = []
180-
181178
# 상세 내용
182179
job_body = soup.find("section", class_="JobContent_descriptionWrapper__RMlfm")
183180
if job_body:
@@ -186,13 +183,15 @@ def crawl_job_detail(self, position_url):
186183
for elem in job_body.find_all(["h3", "h2", "p", "li", "div"]):
187184
if elem.name in ["h2", "h3"]:
188185
title = elem.text.strip()
186+
189187
if title in self.tag2field_map:
190188
current_field = self.tag2field_map[title]
191189
result[current_field] = ""
192190
else:
193191
current_field = None
194192
elif current_field:
195193
text = elem.get_text(" ", strip=True)
194+
196195
if text and len(h3_text) > 0:
197196
result[current_field] = h3_text
198197
h3_text = text
@@ -202,12 +201,73 @@ def crawl_job_detail(self, position_url):
202201
elif current_field is None:
203202
text = elem.get_text(" ", strip=True)
204203
h3_text += text
204+
205+
206+
deadline = soup.find("article", class_="JobDueTime_JobDueTime__yvhtg")
207+
if deadline:
208+
result['deadline'] = deadline.find("span").text.strip()
205209

206-
# 기술 스택 정제
207-
if "tech_list" in result and result["tech_list"]:
208-
result["tech_list"] = [t.strip() for t in result["tech_list"].split("・") if t.strip()]
210+
# 태그 정보
211+
tags_div = soup.find("ul", class_="CompanyTags_CompanyTags__list__XmzkW")
212+
if tags_div:
213+
tag_list = tags_div.find_all("li")
214+
# 각 li에서 키워드 텍스트만 추출
215+
keywords = []
216+
for li in tag_list:
217+
keyword_span = li.find("span", class_="wds-nkj4w6")
218+
if keyword_span:
219+
keywords.append(keyword_span.text.strip())
220+
221+
result['tag_name'] = keywords
209222
else:
210-
result["tech_list"] = []
223+
result['tag_name'] = []
224+
225+
return result
226+
227+
def parse_company_info(self, company_info_text):
228+
"""
229+
회사 정보 텍스트를 파싱하여 회사명, 지역, 조건을 분리
230+
예: "퓨쳐스콜레∙서울 성동구∙경력 5년 이상" ->
231+
{
232+
"company_name": "퓨쳐스콜레",
233+
"location": "서울 성동구",
234+
"experience": "경력 5년 이상"
235+
}
236+
"""
237+
if not company_info_text:
238+
return {
239+
"company_name": "",
240+
"location": "",
241+
"experience": ""
242+
}
243+
244+
# ∙ 또는 · 문자로 분리
245+
parts = company_info_text.replace('·', '∙').split('∙')
246+
parts = [part.strip() for part in parts if part.strip()]
247+
248+
result = {
249+
"company_name": "",
250+
"location": "",
251+
"experience": ""
252+
}
253+
254+
if len(parts) >= 1:
255+
result["company_name"] = parts[0]
256+
257+
if len(parts) >= 2:
258+
# 두 번째 부분이 지역인지 확인 (시/도 이름이 포함되어 있는지)
259+
location_keywords = ['서울', '부산', '대구', '인천', '광주', '대전', '울산', '세종',
260+
'경기', '강원', '충북', '충남', '전북', '전남', '경북', '경남', '제주']
261+
if any(keyword in parts[1] for keyword in location_keywords):
262+
result["location"] = parts[1]
263+
if len(parts) >= 3:
264+
result["experience"] = parts[2]
265+
else:
266+
# 지역이 아니면 경력 조건으로 간주
267+
result["experience"] = parts[1]
268+
269+
if len(parts) >= 3 and not result["experience"]:
270+
result["experience"] = parts[2]
211271

212272
return result
213273

@@ -216,54 +276,75 @@ def run_test_crawling(self, limit=5):
216276
print("=== 원티드 테스트 크롤링 시작 ===")
217277

218278
try:
219-
# 1. URL 수집
220-
position_urls = self.get_test_job_urls(limit)
221-
if not position_urls:
222-
print("수집할 URL이 없습니다.")
223-
return
224-
225-
# 2. 상세 정보 크롤링
226-
results = []
227-
for i, url in enumerate(position_urls, 1):
228-
print(f"\n[{i}/{len(position_urls)}] 처리 중...")
229-
result = self.crawl_job_detail(url)
230-
if result:
231-
results.append(result)
232-
print(f"✅ 성공: {result.get('title', 'Unknown')}")
233-
else:
234-
print("❌ 실패")
279+
final_results = []
280+
for self.job_category_id, self.job_category_name in self.job_category_id2name.items():
235281

236-
# 요청 간격 (서버 부하 방지)
237-
time.sleep(1)
282+
position_urls = self.get_test_job_urls(limit)
283+
if not position_urls:
284+
print("수집할 URL이 없습니다.")
285+
return
286+
287+
results = []
288+
for i, url in enumerate(position_urls, 1):
289+
print(f"\n[{i}/{len(position_urls)}] 처리 중...")
290+
result = self.crawl_job_detail(url)
291+
if result:
292+
results.append(result)
293+
print(f"✅ 성공: {result.get('title', 'Unknown')}")
294+
else:
295+
print("❌ 실패")
296+
# 요청 간격 (서버 부하 방지)
297+
time.sleep(1)
298+
299+
final_results.append(results)
300+
301+
# 3. 결과 저장 (CSV 형식)
302+
output_file = "crawling_results.csv"
238303

239-
# 3. 결과 저장
240-
output_file = "test_crawling_results.json"
241-
with open(output_file, 'w', encoding='utf-8') as f:
242-
json.dump(results, f, ensure_ascii=False, indent=2)
304+
# 중첩된 리스트를 평면화
305+
flattened_results = []
306+
for category_results in final_results:
307+
flattened_results.extend(category_results)
308+
309+
if flattened_results:
310+
# CSV 헤더 정의 (첫 번째 결과의 키를 기준으로)
311+
fieldnames = list(flattened_results[0].keys())
312+
313+
with open(output_file, 'w', encoding='utf-8', newline='') as f:
314+
writer = csv.DictWriter(f, fieldnames=fieldnames)
315+
writer.writeheader()
316+
317+
for result in flattened_results:
318+
# 리스트 형태의 값들을 문자열로 변환 (예: tag_name)
319+
row = {}
320+
for key, value in result.items():
321+
if isinstance(value, list):
322+
row[key] = ', '.join(map(str, value))
323+
else:
324+
row[key] = value
325+
writer.writerow(row)
326+
else:
327+
print("저장할 데이터가 없습니다.")
328+
return final_results
243329

244330
print(f"\n=== 크롤링 완료 ===")
245-
print(f"총 {len(results)}개 항목 수집")
331+
print(f"총 {len(flattened_results)}개 항목 수집")
246332
print(f"결과 파일: {output_file}")
247333

248334
# 간단한 결과 미리보기
249335
print("\n=== 결과 미리보기 ===")
250-
for i, result in enumerate(results, 1):
336+
for i, result in enumerate(flattened_results[:5], 1): # 처음 5개만 미리보기
251337
print(f"{i}. {result.get('company_name', 'Unknown')} - {result.get('title', 'Unknown')}")
252338
print(f" URL: {result.get('url', '')}")
253339
print(f" 태그: {', '.join(result.get('tag_name', []))}")
254340
print()
255341

256-
return results
342+
return final_results
257343

258344
finally:
259345
self.driver.quit()
260346
print("브라우저 종료")
261347

262348
if __name__ == "__main__":
263349
crawler = TestCrawlingWanted()
264-
results = crawler.run_test_crawling(limit=5)
265-
266-
# # view_files.py로 결과 확인하기
267-
# if results:
268-
# print("\n결과를 HTML로 보려면:")
269-
# print("python view_files.py test_crawling_results.json")
350+
results = crawler.run_test_crawling(limit=7)

0 commit comments

Comments
 (0)