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是没有数据的,所以不会影响结果
目录结构: