Python爬虫之薪资分析
程序员文章站
2022-03-04 19:05:04
...
Python爬虫之薪资分析
准备环境
- python3
- BeautifulSoup
- PyCharm
- Echart
背景
想看看智联招聘上各个行业的评价薪资是多少,最后生成个图表,最好还能排除培训机构,因为培训机构并不招人但是招聘广告上的工资却很高….
最终效果
从图上看出,我们会把需要行业的招聘信息抓取下来,然后讲他们的平均薪资记录下来生成柱状图,当点击其中的柱状图的时候,可以显示这个岗位的薪资分布
整体流程
整体会由以下几个模块组成:
- url_manager: 用来管理所有的url的
- htmldownloader: 根据url将页面上的数据下载下来
- html_parser: 根据下载下来的数据,来解析出我们需要的平均薪资,并且排除掉一些常见的培训机构
- html_outter: 将最后统计的结果输出成html页面
- spider_main: 入口,并负责启动各个页面
流程
spider_main来初始化所有的模块 => 从url_manager中取出一个url => 将url交给html_downloader来下载html => 将下载好的页面交给html_parser来进行解析 => 最后将解析好的结果通过html_outter来输出成html页面
模块详情
html_manager
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urllib.parse
class UrlManager(object):
def __init__(self, keys, p):
self.urls = set()
# 基础URL没有搜索的关键字
self.base_url = 'http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%A4%A7%E8%BF%9E&&isadv=0'
for key in keys:
for i in range(1, p + 1):
self.add_url(self.base_url, key, i)
def add_url(self, url, key, p):
key = urllib.parse.quote(key) # url编码
url = '%s&kw=%s&p=%d' % (url, key, p)
print(url)
self.urls.add(url)
pass
def get_url(self):
"""
:return: 未抓取的url
"""
return self.urls.pop()
def has_new_url(self):
"""
:return: 是否还有url没有被抓取
"""
return len(self.urls) != 0
- 在初始化的时候,根据基础网址来拼接要抓取的关键字,和页数来生成指定的url
- 所有的url放在一个set中进行存储
- 当从集合中获取url的时候,会将这个url从集合中删除
html_downloader
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import urllib.request
class HtmlDownloader(object):
def download(self, url):
if url:
req = urllib.request.Request(url)
req.add_header("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36")
with urllib.request.urlopen(req) as opener:
return opener.read().decode('utf-8')
- 这里使用urllib.request来进行网络请求
- 添加User-Agent头来假装我们是一个浏览器
- 最后在读取信息的时候需要decode(‘utf-8’)
html_parser
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from bs4 import BeautifulSoup
import urllib.parse
black_words = ['达内', '睿道', '文思海辉', '中软', '鹏讯']
class HtmlParser(object):
def parser(self, page_url, html_cont):
soup = BeautifulSoup(html_cont, 'html.parser')
moneys = []
result = urllib.parse.urlparse(page_url)
params = urllib.parse.parse_qs(result.query, True)
rows = soup.find_all('tr', class_="")
for row in rows:
try:
company_node = row.find('td', class_="gsmc").find('a')
company_name = company_node.get_text()
for black in black_words:
if black in company_name:
print("黑名单:", company_name)
continue
money_node = row.find('td', class_="zwyx")
money = money_node.get_text()
money_range = money.split('-')
if len(money_range) == 2:
moneys.append(int(money_range[0]) + int(money_range[1]) / 2)
except Exception as e:
print("-------------")
print(row)
print("-------------------")
print('can not parser:', e)
return params['kw'][0], moneys
- 使用BeautifulSoup来进行解析html页面,在使用之前需要安装BeautifulSoup
- 在解析的过程中是有可能失败的,所以使用try来捕获异常防止程序崩溃
- 在解析的时候,不光要解析薪资,还需要解析出公司的名字,如果包含培训机构的名字,我们就不统计它
- 解析的时候其实就是去找网页的规律
- 由于薪资都是个范围,所以在统计的时候取上限,和下限的平均值
- 还有些薪资是面议,就不统计了
- 最后将这一页的数据收集起来并返回
html_outter
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class HtmlOuter(object):
def __init__(self):
self.datas = {}
def collect_data(self, k, m):
if k not in self.datas:
self.datas[k] = []
self.datas[k] = self.datas[k] + m
def output_html(self):
fout = open('out/output.html', 'wb')
fin = open('template/output_t.html', 'rb')
result = []
for k, v in self.datas.items():
num = len(v) if len(v) != 0 else 1
result.append([k, sum(v) / num])
self._output_detail(k, v)
print(result)
for line in fin.readlines():
line = line.decode("utf-8")
line = line.replace('{{data}}', repr(result))
fout.write(line.encode("utf-8"))
fin.close()
fout.close()
def _output_detail(self, key, values):
fout = open('out/%s.html' % key, 'wb')
fin = open('template/detail_t.html', 'rb')
result = [['5000以下', 0], ['5000-7000', 0], ['7000-10000', 0], ['10000以上', 0]]
for value in values:
if value < 5000:
result[0][1] = result[0][1] + 1
elif value < 7000:
result[1][1] = result[1][1] + 1
elif value < 10000:
result[2][1] = result[2][1] + 1
else:
result[3][1] = result[3][1] + 1
for line in fin.readlines():
line = line.decode("utf-8")
line = line.replace('{{data}}', repr(result))
line = line.replace('{{title}}', key)
fout.write(line.encode('utf-8'))
fin.close()
fout.close()
- 输出的时候需要收集每一页的信息
- 最后输出的时候实际上是先写好模板,然后将指定的字符串替换成数据
html模板
output_t.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" src="echarts.common.min.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: '薪资分布',
left: 'center',
top: 20,
textStyle: {
color: '#000000'
}
},
tooltip: {},
legend: {},
// 全局调色盘。
xAxis: {type: 'category'},
yAxis: {},
dataset: {
source: {{data}}
},
series: [
{type: 'bar', color: '#c23531', // 高亮样式。
emphasis: {
itemStyle: {
// 高亮时点的颜色。
color:'#ea926b'
},
label: {
show: true,
// 高亮时标签的文字。
color:'#314134'
}
}}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
//点击事件
myChart.on('click',function (params) {
window.location.href='/CollectSalary/out/'+params.name+'.html'
})
</script>
</body>
</html>
detail_t.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" src="echarts.common.min.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个具备大小(宽高)的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
backgroundColor: '#2c343c',
title: {
text: '{{title}}',
left: 'center',
top: 20,
textStyle: {
color: '#ccc'
}
},
// 全局调色盘。
color: ['#c23531','#2f4554', '#61a0a8', '#d48265', '#91c7ae','#749f83', '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3'],
dataset: {
source: {{data}}
},
tooltip: {
trigger: 'item',
formatter: "{a} <br/>{b} : {c} ({d}%)"
},
visualMap: {
show: false,
min: 80,
max: 600,
inRange: {
colorLightness: [0, 1]
}
},
series: [
{
name: '职位个数',
type: 'pie',
radius: '55%',
center: ['50%', '50%'],
label: {
normal: {
textStyle: {
color: 'rgba(255, 255, 255, 0.3)'
}
}
},
labelLine: {
normal: {
lineStyle: {
color: 'rgba(255, 255, 255, 0.3)'
},
smooth: 0.2,
length: 10,
length2: 20
}
},
animationType: 'scale',
animationEasing: 'elasticOut',
animationDelay: function (idx) {
return Math.random() * 200;
}
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
另外 这里是直接用pyCharm打开的网页,不是打开的静态页面
spider_main
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import html_downloader
import html_outter
import html_parser
import url_manager
class SpiderMain(object):
def __init__(self, keys, p):
self.urls = url_manager.UrlManager(keys, p)
self.downloader = html_downloader.HtmlDownloader()
self.parser = html_parser.HtmlParser()
self.outer = html_outter.HtmlOuter()
def craw(self):
while self.urls.has_new_url():
new_url = self.urls.get_url()
html_content = self.downloader.download(new_url)
k, m = self.parser.parser(new_url, html_content)
print(f'craw: {new_url}')
self.outer.collect_data(k, m)
self.outer.output_html()
if __name__ == '__main__':
key_words = ['JAVA', "Python", "运维", "Android", "大数据"]
pages = 5
SpiderMain(key_words, pages).craw()
- 在初始化的时候,输入想要抓取的关键字和要抓取的页数
- 这里没有抓取全部的,而是抓取了几页,如果一共岗位没有那么多的话,html是没有数据的,所以不会影响结果
目录结构: