人才市場的IT職位分析
最近要找長沙的工作,于是通過湖南人才市場搜尋了一下職位。結果得到的資料讓我很難比較,作為一個 IT 業滾爬了多年的程式員,對這樣的搜尋結果很不滿意。于是,我不得不自己來整理資料了。本文内容包括:網頁資料抓取、網頁資料分析、資料挖掘,python 的多線程,多程序應用等話題。
最近要找長沙的工作,于是通過湖南人才市場搜尋了一下職位。結果得到的資料讓我很難比較,作為一個 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,好歹還可以空一個核來響應我的桌面操作。