最近要找長沙的工作,於是通過湖南人才市場搜索了一下職位。結果得到的數據讓我很難比較,作為一個 IT 業滾爬了多年的程序員,對這樣的搜索結果很不滿意。於是,我不得不自己來整理數據了。本文內容包括:網頁數據抓取、網頁數據分析、數據挖掘,python 的多線程,多進程應用等話題。
先上結論
先給出以上概略圖,由圖可以得出以下結論:
- 長沙的的IT行業大多數工資開在 2500-3500 之間
- 有 40% 左右的企業需要面談工資或者對工資有自己的規定(不使用網站上設置的工資級別)
- Java 語言的需求量遠高於其它語言的需求量
當然,還可以挖掘出更多的有效信息。如按工資排名的職位:
這樣,我就可以先快找到當前長沙的高工資職位及要求了。這就是做程序員的好處 ^_^。以下將介紹數據抓取及數據分析。
數據抓取
我寫了一個 crawler.py 程序。內容如下:
# -*- coding: utf-8 -*- import sys, os, re import http.client import threading import time ids = [] # 生成要抓取的職位列表ID def generate_list_ids(): global ids for i in range(1, 77): ids.append(i) # 生成要抓取的職位詳情ID def generate_detail_ids(): global ids for i in range(1, 77): f = open(str(i)+'.lst', 'r', encoding='gbk') s = f.read(1024000) inputs = re.findall(r"<input type=checkbox name=chk value='.*?'>", s) for inp in inputs: m = re.match(r".*value='(.*)'", inp) ids.append(m.group(1)) f.close() # 用多線程的方式抓取,單線程太慢了 class Crawler(threading.Thread): def __init__(self, islist=True): self.h = http.client.HTTPConnection('www.hnrcsc.com') self.islist = islist threading.Thread.__init__(self) # 抓取到的網頁,將它存入文件 def write_file(self, filename): o = open(filename, 'wb') o.write(self.h.getresponse().read(1024000)) o.close() # 抓取職位列表 def get_list(self, cid): self.h.request('POST', '/Search/searchResult.asp?pagenum='+str(cid), 'flag=0&wkregion=430100&keywordtype=&postypesub=&postypemain=0100&keyword=%C7%EB%CA%E4%C8%EB%B9%D8%BC%FC%D7%D6&during=90&pagenum='+str(cid), {'Content-Type': 'application/x-www-form-urlencoded'}) self.write_file(str(cid)+'.lst') def get_detail(self, cid): try: self.h.request('GET', '/jobs/posFiles/showPosDetail.asp?posid='+str(cid)) self.write_file(str(cid)+'.det') except: print(cid) self.h.close() time.sleep(3) self.h = http.client.HTTPConnection('www.hnrcsc.com') def run(self): global ids cid = ids.pop() if len(ids)>0 else None while cid: if self.islist: self.get_list(cid) else: self.get_detail(cid) cid = ids.pop() if len(ids)>0 else None print(self.name + ' Finished!') self.h.close() if len(sys.argv) != 2: print('''Usage: crawler.py command list: get list detail: get detail clean: clean all webpage ''') exit() if sys.argv[1] == 'detail': # 抓取職位詳情 generate_detail_ids() for i in range(50): Crawler(False).start() elif sys.argv[1] == 'list': # 抓取職位列表 generate_list_ids() for i in range(10): Crawler().start() elif sys.argv[1] == 'clean': # 刪除所有抓取到的文件 os.system('del *.lst') os.system('del *.det') else: print('''Usage: crawler.py command list: get list detail: get detail clean: clean all webpage ''')
以上是最終的程序,《黑客與畫家》中提到,寫程序是一個類似於繪畫的過程,這里是可用的半成品了。我大概描述一下完成這個程序的過程:
- 用 http.client 取一個網頁的數據,很快就可以了解 http.client 的用法
- 用一個 for 循環取 2 個頁面的數據,測試一下 http.client 多次取數據的情況
- 已經完成的內容放到一個方法(get_list)中去,待用
- 寫一個讀文件,並且用正則表達式取出所有職位詳情ID的代碼,測試通過后,將這部分代碼注釋掉
- 用 http.client 取一個職位詳情的數據
- 匯總三個內容,即可得到單線程抓取網頁的程序
我運行這個程序,然后就跑出去吃飯去了,回來一看,還沒有運行完,於是狠下心來看了一下 python 多線程的內容,把它改成多線程的代碼,加上一些容錯的處理,就完工了。
數據分析
數據分析實際上就是從職位列表文件與職位詳情文件當中取到有效信息,將其放入關系數據庫中,然后就可以利用關系數據庫的強大查詢語句來得到重要信息了。數據分析的關鍵,實際上是用正則表達式匹配關鍵的數據部分。以下是我寫的 passer.py 代碼
# -*- encoding: utf-8 -*- import re, sqlite3, multiprocessing class Passer(multiprocessing.Process): def __init__(self, ids, datas): self.ids = ids self.datas = datas multiprocessing.Process.__init__(self) # 獲取職位詳情數據 def get_detail(self, fid): f = open(str(fid)+'.det', 'r', encoding='gbk') s = f.read(1024000) out = re.match(r'''.*招聘人數</td>.*<td width="217" align="left" class="shangxian">.*人 </td>.*<td width="133" align="left" bgcolor="#FAFAFA" class="shangxian" ><img src="/firstpage/ima/diand.gif" width="3" height="3" /> 發布日期</td>.*<td width="213" align="left" class="shangxian" >(.*) </td>.*</tr>.*招聘部門</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*截止日期</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*發布單位</td>.*target="_blank">(.*)</a> </td>.*工作方式</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*最低學歷要求</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*工作地區</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*薪酬待遇</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*.*詳細待遇.*class="zhongxia2" colspan="3" align="left" >(.*) </td>.*聯系電話</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*電子郵件</td>.*<td align="left" class="zhongxia2" ><a href=".*">(.*)</a> </td>.*聯 系 人</td>.*class="zhongxia2" >(.*) </td>.*通訊地址</td>.*<td colspan="3" align="left" class="zhongxia2" >(.*?) .*崗位描述</td>.*<td colspan="3" align="left" class="zhongxia2" style="line-height:25px; padding-top:8px; padding-bottom:8px;">(.*)</td>.*崗位要求</td>.*<td colspan="3" align="left" class="xiaxin" style="line-height:25px; padding-top:8px; padding-bottom:8px;">(.*)</td>.*<td colspan="6" > <!-- Baidu Button BEGIN -->''', s, re.S) if not out: out = re.match(r'''.*招聘人數</td>.*<td width="217" align="left" class="shangxian">.*人 </td>.*<td width="133" align="left" bgcolor="#FAFAFA" class="shangxian" ><img src="/firstpage/ima/diand.gif" width="3" height="3" /> 發布日期</td>.*<td width="213" align="left" class="shangxian" >(.*) </td>.*</tr>.*招聘部門</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*截止日期</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*發布單位</td>.*target="_blank">(.*)</a> </td>.*工作方式</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*最低學歷要求</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*工作地區</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*詳細待遇.*class="zhongxia2" colspan="3" align="left" >(.*) </td>.*聯系電話</td>.*<td align="left" class="zhongxia2" >(.*) </td>.*電子郵件</td>.*<td align="left" class="zhongxia2" ><a href=".*">(.*)</a> </td>.*聯 系 人</td>.*class="zhongxia2" >(.*) </td>.*通訊地址</td>.*<td colspan="3" align="left" class="zhongxia2" >(.*?) .*崗位描述</td>.*<td colspan="3" align="left" class="zhongxia2" style="line-height:25px; padding-top:8px; padding-bottom:8px;">(.*)</td>.*崗位要求</td>.*<td colspan="3" align="left" class="xiaxin" style="line-height:25px; padding-top:8px; padding-bottom:8px;">(.*)</td>.*<td colspan="6" > <!-- Baidu Button BEGIN -->''', s, re.S) f.close() return out.groups() # 處理職位列表 def handle_file(self, fileid): global datas f = open(str(fileid)+'.lst', 'r', encoding='gbk') a = re.findall('<tr class=tdgray.*?</tr>', f.read(1024000), flags=re.S) for i in a: out = re.match(r'''<tr class=.*>.*<td align=center valign=middle height=20><input type=checkbox name=chk value='.*'></td>.*<td valign=middle><a target="_blank" href='/jobs/posFiles/showDwDetailnew.asp\?dwid=(.*)' class=blue_9>(.*)</a></td>.*<td valign=middle><a target="_blank" href='/jobs/posFiles/showPosDetail.asp\?posid=(.*)' class=blue_9>(.*)</a></td>.*<td align=center valign=middle>(.*)</td>.*<td align=center valign=middle>(.*)</td>.*<td align=center valign=middle>(.*)</td>.*<td align=center valign=middle>(.*)</td>.*</tr>''', i, re.S) detail = self.get_detail(out.group(3)) if (len(detail) == 15): start,dep,end,comp,ptype,degree,area,money,salary,telephone,email,contact,address,desc,require = detail else: start,dep,end,comp,ptype,degree,area,salary,telephone,email,contact,address,desc,require = detail money = None cid,cname,pid,pname,reqnum,ldegree,larea,pubtime = out.groups() start = start.replace('.', '-') end = end.replace('.', '-') if money: submoney = re.match('(.*)-(.*)元', money) if submoney: lowmoney,highmoney = submoney.groups() else: lowmoney,highmoney = 0,0 else: lowmoney,highmoney = 0,0 telephone = telephone.strip() desc = desc.replace(' ', '').replace('<br>', '') require = require.replace(' ', '').replace('<br>', '') salary = salary.replace(' ', '').replace('<br>', '') pubtime = pubtime.split('-') month = pubtime[1] if len(pubtime) == 2 else '0'+pubtime[1] day = pubtime[2] if len(pubtime[2]) == 2 else '0'+pubtime[2] pubtime = pubtime[0] + '-' + month + '-' + day reqnum = reqnum if reqnum != '若干' else 0 self.datas.append((pid, pname, reqnum, pubtime, start, end, cid, comp, dep, ptype, degree, area, lowmoney, highmoney, salary, telephone, email, contact, address, desc, require, fileid)) f.close() def run(self): cid = self.ids.pop() if len(self.ids)>0 else None while cid: print('LEN: ', len(self.ids)) self.handle_file(cid) cid = self.ids.pop() if len(self.ids)>0 else None print(self.name + ' Finished!') # 使用多進程方法,只有主進程才執行以下部分 if __name__ == '__main__': # 用 Manager 生成數據,供各個線程共享 manager = multiprocessing.Manager() ids = manager.list() datas = manager.list() passers = [] for i in range(1, 77): ids.append(i) # 開出 7 個進程來處理 for i in range(7): p = Passer(ids, datas) p.start() passers.append(p) # 等待所有的進程都完成之后,再將數據插入到 sqlite 中去 for p in passers: p.join() # 創建 sqlite 數據庫 conn = sqlite3.connect('out.db') c = conn.cursor() # 初始化職位表 c.execute('drop table if exists position;') c.execute('''CREATE TABLE position ( pid integer, pname text, reqnum integer, pubtime datetime, start datetime, end datetime, cid integer, comp text, dep text, ptype text, degree text, area text, lowmoney integer, highmoney integer, salary text, telephone text, email text, contact text, address text, desc text, require text, pagenum integer ); ''') conn.commit() for d in datas: try: c.execute('insert into position values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)', d) except: print(d) conn.commit() c.close() conn.close()
在分析抓取數據的時候,要用多進程,這是為了充分利用CPU。抓取數據用多線程,是因為延遲在網絡IO上,所以,哪怕是多進程,也不會有多高的效率提升。
在多進程數據分析運行起來之后,我在電腦再做其它的事情,就響應緩慢了。雙核CPU都是 100% 的使用率。這時才想起多核的好處^_^,要是我有八核CPU,好歹還可以空一個核來響應我的桌面操作。