1- import  json 
1+ import  csv 
22from  re  import  T 
33import  time 
44import  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
262348if  __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