1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > 利用网络爬虫从猎聘网抓取IT类招聘信息及其数据可视化分析(python+mysql未使用爬虫框架)

利用网络爬虫从猎聘网抓取IT类招聘信息及其数据可视化分析(python+mysql未使用爬虫框架)

时间:2018-09-05 19:42:54

相关推荐

利用网络爬虫从猎聘网抓取IT类招聘信息及其数据可视化分析(python+mysql未使用爬虫框架)

目录

一、前言

1.1 关于我的水平以及该爬虫实现难度和代码中的小bug

1.2 关于用到的参考文档和参考书籍

1.3 关于数据采集量过小以及可能存在的数据冗余和数据丢失问题的说明

二、python爬虫抓取IT类招聘信息的实现

2.1 代码

2.2 代码的部分补充说明

2.2.1 getEntry(html)

2.2.2 getCountryEntry(entry)

2.2.3getCountryEmployeeInfo(CountryEntry)

2.2.4detailedInformation(detailedDescriptionHtml)

三、数据库部分

四、数据可视化的实现

4.1 直方图

4.1.1 各分类招聘信息数量直方图

4.2 圆饼图

4.2.1 按照学历要求分类绘制圆饼图

4.3 气泡图

4.3.1 各地区招聘信息数量与年薪最大值平均值和最小值平均值的关系(剔除年薪面议情况)

4.4 直观的地区分布图-Geo

4.4.1 抓取的招聘信息数量在全国各地的分布(剔除了国外的招聘信息)

4.5 词云图-WordCloud

4.5.1 所抓取的IT类招聘信息最喜欢用来吸引人的福利关键词

一、前言

1.1 关于我的水平以及该爬虫实现难度和代码中的小bug

这个关于网络爬虫还有数据可视化分析的小project是.1.19号左右开始写的,用了不到10天的时间完成,而我只是个大三学生,原来是一直用C++的,Python是.1.10号左右接触(这个时候我们正是考试周),所以从接触python到用python写这个project实际上没花什么时间,所以水平有限,我也很希望大家能帮我指正和提出建议,希望能借此提高我的编码水平。

这个project的实现难度比较小,不需要分析后台Ajex接口,网页也不是javascript动态渲染得到的,网站基本没什么反爬虫机制,也不需要处理验证码的识别问题。

代码中存在几个小bug:

在将抓取的数据插入到数据库中的时候,有时会捕获到异常导致数据插入失败,从而存在一定的数据丢失的问题,我觉得可能是数据插入量太大导致这个问题,但是我暂时不知道怎么解决,希望能有人给我提出改进建议!很不常见的情况下,会无法从服务器获取响应导致数据抓取终止;

1.2 关于用到的参考文档和参考书籍

首先声明,代码都是自己独立完成,并不是从书上抄下来的!

参考书籍:

《Python3网络爬虫开发实战》-崔庆才《Python数据可视化》-Kirthi Raman《深入浅出MySQL》

参考文档:

python 3.6官方文档;Requests:让HTTP服务人类;Beautiful Soup documentation;pyecharts;

1.3 关于数据采集量过小以及可能存在的数据冗余和数据丢失问题的说明

本身代码是能够把IT分类的所有子分类的所有招聘信息抓取完的,但是确实要花很多时间,即便我采用了进程池来减少抓取时间,但是依然会花很多时间,因此这里只是抓取了IT分类中技术类中所有子分类的招聘信息,并且我第一次抓取的时候,代码有一个bug没有解决,就是在猎聘网发布的招聘信息中有一部分是代理发布的,在爬虫尝试去获取该种招聘信息的详细信息的时候在getPageHtml函数中也就是requests会抛出missing schema的异常,从而导致该爬虫进程终止进入下一个子分类信息的抓取(也就是说该子分类的信息抓取到这里就终止了,后面可能存在所有招聘信息都没有抓取到数据库),后来等到第一遍抓取完成后,我解决了这个bug,在代码中可以看得到我将该类信息略过了,本来本着严谨的态度应当重新抓取,但是确实花的时间比较多,而且我只是把这个project作为一个小的练手project来写的,并且实现难度也不高,所以没有完全重新抓取,只是后面补上了一些数据丢失非常严重的子分类的抓取。

由于数据采集量比较小,仅仅只有8万条数据,所以最终的数据可视化分析的结论大多不具有可信度。

由于没有进行对相同信息的筛选过滤,比如说可能有些招聘信息是C++和C两个子分类中都存在,所以存在重复抓取,导致存在一部分数据冗余的现象。

二、python爬虫抓取IT类招聘信息的实现

2.1 代码

"""coding utf-8release python3.6"""import requestsimport lxmlimport reimport pymysqlfrom bs4 import BeautifulSoupfrom multiprocessing import Pool"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def getTableName(ID):"""有些分类标识符ID中带有MySql数据库表名不支持的符号,该函数返回合法表名"""replaceDict={"Node.JS":"NodeJS",".NET":"NET","C#":"CC","C++":"CPP","COCOS2D-X":"COCOS2DX"}if ID in replaceDict:return replaceDict[ID]else:return ID"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def parseWage(wage):"""该函数实现了解析工资字符串wage,如果是'面议'或者其它则返回列表[0,0](代表工资面议),否则返回相应工资(数值类型,单位为万)"""parsedResult=re.findall('(.*?)-(.*?)万.*?',wage,re.S)if not parsedResult:return [0,0]else:return [parsedResult[0][0],parsedResult[0][1]]"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def table_exists(cursor,table_name):"""该函数实现判断某一个表是否在数据库方案里存在,存在返回True,不存在返回False"""sql = "show tables;"cursor.execute(sql)tables = [cursor.fetchall()]table_list = re.findall('(\'.*?\')',str(tables))table_list = [re.sub("'",'',each) for each in table_list]if table_name in table_list:return Trueelse:return False"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def isUrlValid(url):"""由于在爬虫运行过程中发现有些类别招聘信息中含有的详细招聘信息的入口地址在获取响应的时候会抛出Missing Schema异常,发现是url中有些是.../job/...(往往是猎聘网自己发布的招聘信息),有些是.../a/...(这类招聘信息通常是代理发布),导致无法解析,从而使爬虫运行到此处时停止抓取数据。该函数实现对代理发布的URL进行过滤,若为代理发布的信息,则跳过该条招聘信息,函数返回False,否则返回True。"""isValid=re.search('.*?www\.liepin\.com/job/.*?$',url,re.S)if isValid:return Trueelse:return False"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def getPageHtml(url,headers=None):"""返回服务器响应页面的html,不成功返回None"""if not headers:headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",}try:response=requests.get(url,headers=headers)if response.status_code==200:return response.textelse:return Noneexcept requests.RequestException as e:#debugprint('Exception occur in funciton getPageHtml()')return None"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def getEntry(html):"""解析Html,该函数为生成器类型,每一次迭代返回某一子项目的入口地址URL和description组成的字典entry"""if not html:#html为None则返回None,无法从该html中解析出子项目入口地址#debugprint('html is None in function getEntry()')return Nonesoup=BeautifulSoup(html,'lxml')for items in soup.find_all(name='li'):for item in items.find_all(name='dd'):for usefulURL in item.find_all(name='a',attrs={"target":"_blank","rel":"nofollow"}):yield{"URL":''+usefulURL.attrs['href'],"URL_Description":usefulURL.text}"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def getCountryEntry(entry):"""entry为子项目地址URL和描述URL_Description组成的字典,该函数实现了从子项目页面信息中获取响应,并且最终返回全国子项目地址CountryURL和CountryURLDescription(实际上就是URL_Description)组成的字典"""if not entry:#debugprint('ERROR in function getCountryEntry:entry is None')return Noneheaders={"Host":"","Referer":"/it/","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"}countryHtml=getPageHtml(entry['URL'],headers=headers)soup=BeautifulSoup(countryHtml,'lxml')citiesInfo=soup.find(name='dd',attrs={"data-param":"city"})if not citiesInfo:#debugprint('ERROR in function getCountryEntry():citiesInfo is None.')return Nonedb=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()if not table_exists(cursor,entry['URL_Description']):createTableSql="""CREATE TABLE IF NOT EXISTS spider.{} like spider.positiondescription;""".format(getTableName(entry['URL_Description']))try:cursor.execute(createTableSql)print('--------------create table %s--------------------' % (entry['URL_Description']))except:print('error in function getCountryEntry():create table failed.')finally:db.close()return {"CountryURL":""+citiesInfo.find(name='a',attrs={"rel":"nofollow"}).attrs['href'],"CountryURLDescription":entry['URL_Description']}"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def getCountryEmployeeInfo(CountryEntry):"""CountryEntry是getCountryEntry函数返回的由全国招聘信息CountryURL和地址分类描述CountryURLDescription构成的字典,该函数提取出想要的信息"""if not CountryEntry:#debugprint('ERROR in function getCountryEmpolyeeInfo():CountryEntry is None.')return Nonedb=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()indexOfPage=0theMaxLength=0#遍历该类招聘信息的每一页while indexOfPage<=theMaxLength:URL=CountryEntry['CountryURL']+'&curPage='+str(indexOfPage)pageHtml=getPageHtml(URL)soup=BeautifulSoup(pageHtml,'lxml')#提取出该类招聘信息总共的页数,仅需要提取一次即可if indexOfPage==0:prepareReString=soup.find(name='a',attrs={"class":"go","href":"javascript:;"}).attrs['onclick']pattern=pile('Math\.min\(Math\.max\(\$pn,\s\d\),(.*?)\)')theMaxLength=int(re.findall(pattern,prepareReString)[0])#debug,检测访问到第几页print('Accessing page {} of {}'.format(indexOfPage,CountryEntry['CountryURLDescription']))#进入下一页indexOfPage+=1"""这里代码实现对信息的有效信息的提取"""for detailedDescriptionURL in getDetailedDescriptionURL(soup):#如果详细招聘信息入口URL是代理发布(即无效,这里不爬取这类信息),则跳过该条招聘信息if not isUrlValid(detailedDescriptionURL):continuedetailedDescriptionHtml=getPageHtml(detailedDescriptionURL)#将分区标识符(例如java、php等)添加进返回的字典result=detailedInformation(detailedDescriptionHtml)result['ID']=CountryEntry['CountryURLDescription']"""if 'ID' in result:print(type(result['ID']),'>>>',result)"""if result['Available']:#获取工资最小值和最大值min_max=parseWage(result['wage'])#有些公司没有福利tagreallyTag=''if not result['tag']:reallyTag='无'else:reallyTag=result['tag']insertSql="""insert into spider.{} values(0,'{}','{}','{}',{},{},'{}','{}','{}','{}','{}','{}','{}');""".format(getTableName(result['ID']),result['position'],result['company'],result['wage'],min_max[0],min_max[1],result['education'],result['workExperience'],result['language'],result['age'],result['description'],reallyTag,result['workPlace'])try:cursor.execute(insertSql)mit()except:db.rollback()#debugprint('ERROR in function getCountryEmployeeInfo():execute sql failed.')#爬取完该类招聘信息之后关闭数据库连接db.close()"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def getDetailedDescriptionURL(soup):"""soup为全国招聘信息列表页面解析的BeautifulSoup对象,该函数为生成器,每一次迭代产生一条招聘信息详细内容的URL字符串"""if not soup:#debugprint('ERROR in function getDetailedDescroption():soup is None.')return Nonefor item in soup.find_all(name='div',attrs={"class":"job-info"}):detailedDescriptionURL=item.find(name='a',attrs={"target":"_blank"}).attrs['href']yield detailedDescriptionURL"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""def detailedInformation(detailedDescriptionHtml):"""该函数实现对具体的一条详细招聘信息的提取,detailedDescriptionHtml为一条详细招聘信息网页的HTML,该函数返回值为职位具体要求构成的字典positionDescription"""if not detailedDescriptionHtml:#debugprint('ERROR in function detailedInformation():detailedDescriptionHtml is None.')return Nonesoup=BeautifulSoup(detailedDescriptionHtml,'lxml')#提取出招聘职位和公司,类型为strpositionItem=soup.find(name='div',attrs={"class":"title-info"})#有时候招聘信息被删除了但是招聘信息的入口仍然留在招聘列表中,这里就是防止这种情况导致运行失败if not positionItem:return {'Available':False}position=positionItem.h1.textcompany=soup.find(name='div',attrs={"class":"title-info"}).a.text#提取出工资水平(类型为str,有些是面议)、工作地点、学历要求、工作经验、语言要求和年龄要求items=soup.find(name='div',attrs={"class":"job-title-left"})wage=items.find(name='p',attrs={"class":"job-item-title"}).text.split('\r')[0]workPlace=items.find(name='a')#有些工作地点在国外,该网站不提供该地区招聘信息的网页,没有标签a,这里就是处理这样的异常情况if not workPlace:workPlace=items.find(name='p',attrs={"class":"basic-infor"}).span.text.strip()else:workPlace=workPlace.text#这里返回一个大小为4的列表,分别对应学历要求、工作经验、语言要求、年龄要求allFourNec=items.find(name='div',attrs={"class":"job-qualifications"}).find_all(name='span')#有些招聘信息中带有公司包含的福利tag,这里也提取出来,所有tag组成一个由-分隔的字符串,没有则为空字符串tagItems=soup.find(name='ul',attrs={"class":"comp-tag-list clearfix","data-selector":"comp-tag-list"})tags=''if tagItems:tempTags=[]for tag in tagItems.find_all(name='span'):tempTags.append(tag.text)tags='-'.join(tempTags)#提取出详细的职位技能要求descriptionItems=soup.find(name='div',attrs={"class":"job-item main-message job-description"})description=descriptionItems.find(name='div',attrs={"class":"content content-word"}).text.strip()positionDescription={"Available":True,"position":position,"company":company,"wage":wage,"workPlace":workPlace,"education":allFourNec[0].text,"workExperience":allFourNec[1].text,"language":allFourNec[2].text,"age":allFourNec[3].text,"tag":tags,"description":description,}return positionDescription"""----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"""if __name__=="__main__":startURL='/it/'startHtml=getPageHtml(startURL)#多进程抓取数据pool=Pool(4)for entry in getEntry(startHtml):countryEntry=getCountryEntry(entry)pool.apply_async(getCountryEmployeeInfo,args=(countryEntry,))pool.close()pool.join()print('All subprocesses done.')"""for entry in getEntry(startHtml):countryEntry=getCountryEntry(entry)getCountryEmployeeInfo(countryEntry)""""""pool=Pool()countryEntry0={"CountryURL":("/zhaopin/?init=-1&headckid=8e288c3d8c61908a&flushckid=1&dqs=&fromSear""chBtn=2&imscid=R000000035&ckid=8e288c3d8c61908a&key=%E8%87%AA%E7%84%B6%E8%AF%AD%E8%A8%80%E5""%A4%84%E7%90%86&siTag=9IxD9f0Uv9ilBXoBMeeqIA~O7qm6Hv6o5wKSctHWDgu-A&d_sfrom=search_unknown&""d_ckId=c5822d371553018b209ef5a7ed4603e0&d_curPage=0&d_pageSize=40&d_headId=c5822d371553018b""209ef5a7ed4603e0"),"CountryURLDescription":"自然语言处理"}countryEntry1={"CountryURL":("/zhaopin/?init=-1&headckid=e6fcd147b81c0d37&flushckid=1&dqs=&fromSear""chBtn=2&imscid=R000000035&ckid=e6fcd147b81c0d37&key=Ruby&siTag=mRbR_Fn-IswEai_hYVvHZA~O7qm6""Hv6o5wKSctHWDgu-A&d_sfrom=search_unknown&d_ckId=e03e58329b38476c3e348553f0cc0231&d_curPage=""0&d_pageSize=40&d_headId=e03e58329b38476c3e348553f0cc0231"),"CountryURLDescription":"Ruby"}countryEntry2={"CountryURL":("/zhaopin/?init=-1&headckid=1a63ce33c5581803&flushckid=1&dqs=&fromSear""chBtn=2&imscid=R000000035&ckid=1a63ce33c5581803&key=%E5%8A%9F%E8%83%BD%E6%B5%8B%E8%AF%95&si""Tag=AQs5v9-xdx5UdW-LpKsEPA~O7qm6Hv6o5wKSctHWDgu-A&d_sfrom=search_unknown&d_ckId=7a78481b65a""183fc4af27e9c7d7a8c6e&d_curPage=0&d_pageSize=40&d_headId=7a78481b65a183fc4af27e9c7d7a8c6e"),"CountryURLDescription":"功能测试"}countryEntry3={"CountryURL":("/zhaopin/?init=-1&headckid=2e6d76848af03a10&flushckid=1&dqs=&fromSear""chBtn=2&imscid=R000000035&ckid=2e6d76848af03a10&key=%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95&si""Tag=3dIhGakkNW1f2XBXKFwGiQ~O7qm6Hv6o5wKSctHWDgu-A&d_sfrom=search_unknown&d_ckId=d4c7f7328e8""776875fae4943831fc185&d_curPage=0&d_pageSize=40&d_headId=d4c7f7328e8776875fae4943831fc185"),"CountryURLDescription":"性能测试"}entryGroups=([countryEntry0,countryEntry1,countryEntry2,countryEntry3])pool.map(getCountryEmployeeInfo,entryGroups)pool.close()pool.join()print("++++++++++++++++++++++++++++++++++++++All subprocesses done++++++++++++++++++++++++++++++++++++++++++++++++++++")""""""for entry in getEntry(startHtml):getCountryEntry(entry)"""

2.2 代码的部分补充说明

2.2.1 getEntry(html)

爬虫初始入口地址startURL对应于猎聘网下图所示的页面

在该页面对应于代码中的函数getEntry(html),下面是子项目入口地址于HTML中所在的位置:

2.2.2 getCountryEntry(entry)

对应于如下页面:

不仅仅只抓取当前地区,需要抓取全国的招聘信息,因此要进入全国招聘信息的URL,位置如下所示:

第一条href即为全国的入口URL。

除此之外在该函数中还根据每一个招聘信息子分类创建了各自的表,由于有些分类名中含有MySQL数据库表名不支持的特殊符号,所以函数getTableName(ID)功能就是根据分类名ID返回合法表名。

2.2.3getCountryEmployeeInfo(CountryEntry)

对应如下页面:

在该页面可以抓取到每一条招聘信息详细信息的入口地址URL,以及该分类(在上图中就是Java)中总页面数量:

招聘信息详细信息的入口地址URL位置如下所示:

总页面数所在位置如下所示(即div class="pager"那一个标签中):

具体位置在这一行的如下图所示的位置(即100),采用正则表达式模块提取出总页面数量:

2.2.4detailedInformation(detailedDescriptionHtml)

对应于如下所示的页面结构:

具体在该页面上抓取的信息所在位置在函数detailedInformation(detailedDescriptionHtml)中表示得很明白。

三、数据库部分

在上面给出的代码中,在函数getCountryEntry中对每一类招聘信息都在数据库spider中(spider是我事先已经建立好的一个schema)建立了相应的表,其中有一个positionDescription表是我在抓取前建立的一个模板表,目的是为了便于各分类表的创建,各分类表的结构都和positionDescription表一样,其字段类型如下所示:

然后抓取之后就会生成各分类的表,这些表的结构和positionDescription表结构完全一样,例如(未完全显示):

四、数据可视化的实现

4.1 直方图

4.1.1 各分类招聘信息数量直方图

代码如下:

import matplotlib.pyplot as pltimport matplotlibimport pymysqldef drawPic():db=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()try:cursor.execute("select table_name as 表名,table_rows from information_schema.tables where table_schema='spider' order by table_rows desc;")results=cursor.fetchall()#print(results)tags=[]amount=[]for item in results:if item[1]:tags.append(item[0])amount.append(item[1])except:print('failed')db.close()#解决中文显示乱码问题plt.rcParams['font.sans-serif']=['FangSong']plt.rcParams['axes.unicode_minus']=Falseplt.barh(range(len(tags)),amount,height=0.7,color='steelblue',alpha=0.8)plt.yticks(range(len(tags)),tags)plt.xlim(min(amount)-10,max(amount)+100)plt.xlabel("招聘信息数量")plt.title("各分类招聘信息数量")for x,y in enumerate(amount):plt.text(y+1,x-0.4,'%s' % y)plt.show()if __name__=='__main__':drawPic()

效果如下所示:

4.2 圆饼图

4.2.1 按照学历要求分类绘制圆饼图

代码如下:

import matplotlib.pyplot as pltimport matplotlibimport pymysql#根据学历要求绘制圆饼图def drawPic():plt.rcParams['font.sans-serif']=['FangSong']plt.rcParams['axes.unicode_minus']=Falselabels='本科及以上','大专及以上','统招本科','学历不限','其它'sizes=[39519/79380*100,16726/79380*100,15844/79380*100,5781/79380*100,(1102+211+197)/79380*100]explode=(0.1,0,0,0,0)fig1,ax1=plt.subplots()ax1.pie(sizes,explode=explode,labels=labels,autopct='%1.1f%%',shadow=True,startangle=90)ax1.axis('equal')ax1.set_title('招聘信息学历要求占比')plt.show()

效果如下所示:

实际上我更喜欢pyecharts生成的圆饼图,比较熟悉,不过不知道自己为什么当时用不熟悉的matplotlib来绘图,我大概比较蠢。

4.3 气泡图

4.3.1 各地区招聘信息数量与年薪最大值平均值和最小值平均值的关系(剔除年薪面议情况)

代码如下所示:

import reimport pymysqlimport numpy as npimport seaborn as snsimport matplotlib.pyplot as plt#抓取的数据workPlace有些是类似于北京-XXX这样的形式,这里将其转换为北京,以便对一个地区招聘信息#总数进行统计,turpleInfo是双元素元组,第一个元素是workPlace,第二个元素是该地区招聘信息总数def parseWorkPlace(turpleInfo):result=re.findall('(.*?)-.*?$',turpleInfo[0],re.S)if not result:return turpleInfoelse:return (result[0],turpleInfo[1])#功能基本等同于parseWorkPlace(turpleInfo)函数,result[0]为提取出的地区信息def parse_avgWage_workPlace(turpleInfo):result=re.findall('(.*?)-.*?$',turpleInfo[2],re.S)if not result:return turpleInfoelse:return (turpleInfo[0],turpleInfo[1],result[0])#返回的字典由工作地点及招聘信息数量键值对组成的字典def workPlace_amount():db=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()try:cursor.execute("select workPlace,count(*) from allpositiondescription group by workPlace order by count(*) desc;")results=cursor.fetchall()dict_result={}for turpleInfo in results:turpleInfo=parseWorkPlace(turpleInfo)#求各地区招聘信息的总和if turpleInfo[0] in dict_result:dict_result[turpleInfo[0]]+=turpleInfo[1]else:dict_result[turpleInfo[0]]=turpleInfo[1]except:print('failed')finally:db.close()return dict_result#返回的字典由工作地点和(最低年薪均值,最高年薪均值)键值队组成def avg_wage_workPlace():db=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()try:cursor.execute("select avg(minWage),avg(maxWage),workPlace from wagenotnull_workplace group by workPlace;")results=cursor.fetchall()dict_result={}for turpleInfo in results:turpleInfo=parse_avgWage_workPlace(turpleInfo)if turpleInfo[2] in dict_result:#求年薪均值,turpleInfo[n]为Demical类,将其转换为float类avg_min=(float(turpleInfo[0])+dict_result[turpleInfo[2]][0])/2avg_max=(float(turpleInfo[1])+dict_result[turpleInfo[2]][1])/2dict_result[turpleInfo[2]]=(avg_min,avg_max)else:dict_result[turpleInfo[2]]=(float(turpleInfo[0]),float(turpleInfo[1]))except:print('failed')finally:db.close()return dict_resultif __name__=='__main__':wp_amount_dict=workPlace_amount()avg_wage_wp_dict=avg_wage_workPlace()x_minWage=[]y_maxWage=[]z_amount=[]for wp,amount in wp_amount_dict.items():if wp in avg_wage_wp_dict:z_amount.append(amount)#保留两位小数x_minWage.append(round(avg_wage_wp_dict[wp][0],2))y_maxWage.append(round(avg_wage_wp_dict[wp][1],2))#处理中文会显示为乱码的情况 sns.set_style('whitegrid',{'font.sans-serif':'FangSong'})cm=plt.cm.get_cmap('RdYlBu')fig,ax=plt.subplots(figsize=(12,10))bubble=ax.scatter(x_minWage,y_maxWage,s=(z_amount-np.min(z_amount))/5,c=z_amount,cmap=cm,linewidth=0.5,alpha=0.5)#将本来图中的栅格除掉ax.grid()fig.colorbar(bubble)ax.set_xlabel('最低年薪平均值/万',fontsize=15)ax.set_ylabel('最高年薪平均值/万',fontsize=15)ax.set_title('招聘信息数量与年薪最大值平均值及最小值平均值关系')plt.show()

效果如下所示:

4.4 直观的地区分布图-Geo

4.4.1 抓取的招聘信息数量在全国各地的分布(剔除了国外的招聘信息)

为了省事,是直接用4.3.1中代码修改得到,代码如下所示:

"""coding utf-8release python3.6"""import reimport pymysqlfrom pyecharts import Geo#抓取的数据workPlace有些是类似于北京-XXX这样的形式,这里将其转换为北京,以便对一个地区招聘信息#总数进行统计,turpleInfo是双元素元组,第一个元素是workPlace,第二个元素是该地区招聘信息总数def parseWorkPlace(turpleInfo):result=re.findall('(.*?)-.*?$',turpleInfo[0],re.S)otherCountries=["日本","柬埔寨","澳大利亚","加拿大","欧洲","美国"]if not result and (turpleInfo[0] not in otherCountries):return turpleInfoelif result and (result[0] not in otherCountries):return (result[0],turpleInfo[1])else:return (None,None)#返回的字典由工作地点及招聘信息数量键值对组成的字典def workPlace_amount():db=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()try:cursor.execute("select workPlace,count(*) from allpositiondescription group by workPlace order by count(*) desc;")results=cursor.fetchall()dict_result={}for turpleInfo in results:turpleInfo=parseWorkPlace(turpleInfo)#求各地区招聘信息的总和if turpleInfo[0] in dict_result and turpleInfo[0]:dict_result[turpleInfo[0]]+=turpleInfo[1]elif turpleInfo[0] not in dict_result and turpleInfo[0]:dict_result[turpleInfo[0]]=turpleInfo[1]except:print('failed')finally:db.close()return dict_resultif __name__=='__main__':dict_result=workPlace_amount()data=[(key,value) for key,value in dict_result.items()]#print(data)#初始化图表geo=Geo(title="抓取的招聘信息数量在全国各地的分布",width=1200,height=600,title_pos="center",)attr,value=geo.cast(data)#图表配置geo.add("",attr,value,is_visualmap=True,visual_range=[0,18000],visual_text_color="#050505",visual_range_text=["最少个数","最大个数"],symbol_size=15,)geo.render()

实际上geo.render()最后生成的是一个html文件,该函数实际上还可以指定保存路径,如果不指定保存路径,那么结果会保存在代码所在目录,或者也可以用jupyter notebook来显示,详情请参考pyecharts的文档,最后效果如下所示:

实际上左下那个条是可以调整动态显示的,例如下图所示,将鼠标移动在一个点上,可以显示该处的经纬度以及我们自定义的数量,在这个图中就是招聘信息的个数:

4.5 词云图-WordCloud

4.5.1 所抓取的IT类招聘信息最喜欢用来吸引人的福利关键词

实现代码如下所示:

import pymysqlfrom pyecharts import WordCloud#返回关键词与抓取的数据中出现的次数组成的键值对组成的字典def createWordCloud():db=pymysql.connect(host='localhost',user='root',password='123456',port=3306,db='spider')cursor=db.cursor()try:#tags_world_cloud为创建的视图,仅含tags列,是所有tags不为"无"的数据集合cursor.execute("select * from tags_world_cloud;")except:print("failed")finally:db.close()tag_amount_dict={}while True:item=cursor.fetchone()if not item:break#debug#print(type(item))for tag in item[0].split('-'):#如果该tag键不在字典里,那么在字典里加入该tag并且初始化数量为1,否则数量加1if tag not in tag_amount_dict:tag_amount_dict[tag]=1else:tag_amount_dict[tag]+=1return tag_amount_dictif __name__=="__main__":tag_amount_dict=createWordCloud()name=[]value=[]for key,val in tag_amount_dict.items():name.append(key)value.append(val)#图表初始化wordCloud=WordCloud(title="IT类公司最喜欢用来吸引人的关键词",title_pos="center",width=1300,height=650)#图表配置wordCloud.add("",name,value,shape="circle",word_size_range=[12,60],)wordCloud.render(r"F:\临时存放文件\test\wordCloudHTML.html")

效果图如下所示:

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。