用数据告诉你为什么Python这么火
学了这么久的python了,这次我们实际操作一番,从职业推荐网站——拉勾网,用数据来说明python的热门程度
主要目标
1. 爬取拉勾网有关python职位数据
2. 将获取到的数据分别存到数据库、csv文件、json文件
3. 对文件进行可视化分析
爬虫流程
一般爬虫流程:分析网页网络请求,分析出真实url,通过头信息以及表单验证获取到网页响应,对网页进行解析,根据需求获取到相关数据进行存储
-
网页分析
image.png
我们访问主页,打开检查对元素进行分析,发现很多数据比如说翻页板块并不能直接获取到后面页面的链接,而且用requests直接进行网页内容获取时也不能获取到我们想要的结果,这是基于拉勾网的反爬虫机制,拉钩网也是通过Ajax请求加载的,所以我们需要更细致的对网络请求进行分析,获取到真实有效的数据内容
我们打开检查,切换到network,对网络请求进行分析,AJAX 一般是通过 XMLHttpRequest 对象接口发送请求的,XMLHttpRequest 一般被缩写为 XHR。点击网络面板上漏斗形的过滤按钮,过滤出 XHR 请求。挨个查看每个请求,通过访问路径和预览,找到包含信息的请求 -
ajax请求获取
network.png
这里可以看到多个请求,可以通过请求中的preview对具体信息进行判断,这里就不做赘述了,这里我们就直接能够看到页面中的数据表,每页15条数据
image.png
从上图可以发现,这正是我们需要的数据,于是我们根据这个ajax请求的方式、url、参数等信息,可以通过resquests获取到此接口返回的json数据,细心的同学们应该可以发现这里真正的头信息里面多出了不少数据,比如这里的X-Anit-Forge-Code,X-Anit-Forge-Token,Referer和form-data等,特别注意referer和后面的dorm-data,很多网站会根据referer判断访问者的浏览路径判断是否为爬虫,后面form-data的表单内容则涉及到requests获取网页时的一些验证,这里还有一个拉钩做的很细致的小陷阱就是请求方式,可能很多同学都会不注意直接用get获取,但这里需要用post提交表单才能够获取到真实的数据,否则你把你拿到的那份数据仔细分析会发现全是重复的而且和页面毫无联系
拿到真实数据之后,接着我们对刚才的表单参数进行一些分析:
'first':'true'
'pn':1
'kd':'python'
- true只有第一页是true,后面全是false,不难想出是判断是否为首页
- pn,也是比较简单就能判断是页数
- kd, 搜索关键字
有了这些数据也方便我们进行页面循环获取整个网站的所有数据了,下面我们直接上代码
代码比较长,简单描述一下,主要分为三个方法:
- get_page()——获取网页数据,并存为json文件,每个网站都有自己的独特反爬虫机制,所以一般可以通过切换代理、IP或者设置延迟等机制来避免被系统发现导致封锁IP等等,这里为了方便我们就先存为json文件,之后用json文件来进行数据的存取展示,和直接存mysql其实差不多,同学们要尝试的话只用将后面的数据库操作改一下,去掉读取json操作,在前面执行即可,这里为了速度我们就简单点。
- create_table()——创建数据表,一定记得设置字符编码,否则后面会引起很多不必要的麻烦
- insert()——数据插入,主要是读取json文件进行插入数据库,这里也可以将前面get_page()里面字典内的数据直接插入,不过响应时间会比较慢。
-
依赖库文件,可以直接用pip freeze > ./requirements.txt生成
matplotlib==3.0.2
numpy==1.15.4
PyMySQL==0.9.2
requests==2.21.0
wordcloud==1.5.0
scipy==1.1.0
Pillow==5.3.0
PyInstaller==3.4
-
爬虫文件
positions_spider.py
#!/usr/bin/python
#-*-coding:utf-8 -*-
import requests
# from bs4 import BeautifulSoup
import time
import pymysql
import json
import codecs
import io
def get_page():
#构造头信息
headers = {
'Accept-Language': "zh-CN,zh;q=0.9",
'Host': 'www.lagou.com',
'User-Agent': "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3493.3 Safari/537.36",
'Referer': "https://www.lagou.com/jobs/list_python?city=%E5%85%A8%E5%9B%BD&cl=false&fromSearch=true&labelWords=&suginput=",
'Cookie': "user_trace_token=20181205153237-2fb5c5de-ddba-45ae-a4e5-1d3f994363da; _ga=GA1.2.1973907981.1543995170; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%221677d489b4c51b-01ad0f1ff9cf7e-7d113749-1049088-1677d489b4e157%22%2C%22%24device_id%22%3A%221677d489b4c51b-01ad0f1ff9cf7e-7d113749-1049088-1677d489b4e157%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_referrer_host%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%7D%7D; LGUID=20181205153247-f6746e6e-f85f-11e8-8ce2-5254005c3644; index_location_city=%E5%85%A8%E5%9B%BD; _gid=GA1.2.498204391.1544690905; JSESSIONID=ABAAABAAAGGABCB41C4F7888779A017173A237D896F7D51; TG-TRACK-CODE=index_search; _putrc=D253F09634921DCE123F89F2B170EADC; login=true; unick=%E6%8B%89%E5%8B%BE%E7%94%A8%E6%88%B73051; hasDeliver=0; PRE_UTM=; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1544699176,1544759671,1544762963,1544767071; LGSID=20181214135750-304386ec-ff65-11e8-918d-525400f775ce; PRE_HOST=www.google.com; PRE_SITE=https%3A%2F%2Fwww.google.com%2F; PRE_LAND=https%3A%2F%2Fwww.lagou.com%2F; showExpriedIndex=1; showExpriedCompanyHome=1; showExpriedMyPublish=1; gate_login_token=be2fd084aa6ff0923a8d24de7aace358683b2c405e52960c9220540a0c9f3693; LGRID=20181214141147-23a73cc0-ff67-11e8-918d-525400f775ce; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1544767909; SEARCH_ID=57101ec6359e40efad24074934907963",
'Accept': "application/json, text/javascript, */*; q=0.01",
'X-Anit-Forge-Code': "0",
'X-Anit-Forge-Token': None,
'X-Requested-With': 'XMLHttpRequest'
}
#翻页构造
for x in range(26, 31):
form_data = {
'first': 'false',
'pn': x,
'kd': 'python'
}
print("正在解析第%s页" %form_data['pn'])
#post请求
response = requests.post("https://www.lagou.com/jobs/positionAjax.json?city=%E5%8C%97%E4%BA%AC&needAddtionalResult=false", headers=headers, data=form_data)
position = []
#直接获取json格式的数据
json_result = response.json()
print(json_result)
#一层一层的获取到需要的数据
page_positions = json_result['content']['positionResult']['result']
time.sleep(10)
result = []
for position in page_positions:
#返回数据字典
position_dict = {
'position_name': position['positionName'],
'work_year': position['workYear'],
'salary': position['salary'],
'district': position['district'],
'company_name': position['companyFullName'],
'companySize': position['companySize'],
'positionAdvantage': position['positionAdvantage']
}
# print(position_dict)
#写入json文件
f = codecs.open('positions.json', 'a', 'utf-8')
f.write(json.dumps(position_dict, ensure_ascii=False)+"\n")
f.close()
# return json.dumps(position_dict, ensure_ascii=False)
def create_table():
conn = pymysql.connect(db='test', user='root', passwd='299521', host='localhost')
cursor = conn.cursor()
# create a table
cursor.execute("drop table if exists position")
sql = """create table position (
id int not null auto_increment primary key,
position_name varchar(20) not null character set utf8mb4 COLLATE utf8mb4_general_ci,
work_year varchar(20) not null character set utf8mb4 COLLATE utf8mb4_general_ci,
salary varchar(20) not null character set utf8mb4 COLLATE utf8mb4_general_ci,
district varchar(20) not null character set utf8mb4 COLLATE utf8mb4_general_ci,
company_name varchar(20) not null character set utf8mb4 COLLATE utf8mb4_general_ci,
companySize varchar(20) not null character set utf8mb4 COLLATE utf8mb4_general_ci,
positionAdvantage varchar(40) not null character set utf8mb4 COLLATE utf8mb4_general_ci)character set utf8mb4 COLLATE utf8mb4_general_ci"""
cursor.execute(sql)
def insert():
conn = pymysql.connect(db='test', user='root', passwd='299521', host='localhost')
cursor = conn.cursor()
with open('positions.json', 'r', encoding='utf-8') as f:
i = 0
for lines in f.readlines():
i += 1
print('正在载入第%s行......' % i)
try:
# lines = f.readline() # 使用逐行读取的方法
review_text = json.loads(lines, encoding='utf-8') # 解析每一行数据
result = []
result.append((review_text['salary'], review_text['companySize'], review_text['district'],
review_text['work_year'], review_text['company_name'], review_text['position_name'],
review_text['positionAdvantage']))
print(result)
inesrt_re = "insert into position (salary, companySize, district, work_year, company_name, position_name,positionAdvantage) values (%s, %s, %s, %s,%s, %s,%s)"
cursor.executemany(inesrt_re, result)
conn.commit()
except Exception as e:
conn.rollback()
print(str(e))
break
def main():
get_page()
create_table()
insert()
if __name__ == '__main__':
main()
csv数据转化
我们将之前生成的json文件转化成csv文件方便之后的展示,主要用到csv和coedcs两个库做文件处理
json2csv.py
#!/usr/bin/python
#-*-coding:utf-8 -*-
import csv
import json
import codecs
jsonData = codecs.open('positions.json', 'r', 'utf-8')
fieldnames = ["salary", "companySize", "district", "work_year", "company_name", "position_name", "positionAdvantage"]
with open('positions.csv', mode='w', newline='') as csv_file:
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
writer.writeheader()
for data in jsonData.readlines():
r = json.loads(data)
writer.writerow(r)
-
csv文件
可视化分析
我们从csv中读取数据对基层的python岗位的工资进行展示,主要用到matplotlib进行简单图表绘制
cav2chart.py
#!/usr/bin/python
#-*-coding:utf-8 -*-
import csv
import re
import numpy as np
import matplotlib.pyplot as plt
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
import codecs
import json
def show():
f = codecs.open('positions.json', 'r', encoding='utf-8')
plt.figure(figsize=(10, 6))
x = [] # 存放x轴数据
y = []
tmp_x = []
# result = ''.join(re.findall(r'[A-Za-z]', st))
for line in f.readlines():
data = json.loads(line, encoding='utf-8')
position_name = data['position_name']
if position_name.count(''.join(re.findall(r'[A-Za-z]', position_name)).lower()) ==1:
if len(position_name)<=9:
x.append(data["position_name"])
y.append(data["salary"])
plt.bar(x, y, label="salary")
plt.title("各岗位工资梯度")
plt.legend()
plt.xlabel('x轴-position')
plt.ylabel('y轴-sarly')
plt.show()
if __name__ == '__main__':
show()
-
可视化图表
词云
我们提取职位福利里面的字段组成词云图片,不难发现凡事离不开“五险一金,弹性工作,带薪休假”
word_cloud.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
#coding=utf-8
#导入wordcloud模块和matplotlib模块
from wordcloud import WordCloud, ImageColorGenerator
import matplotlib.pyplot as plt
from scipy.misc import imread
import codecs
import json
#读取一个文件
f = codecs.open('positions.json', 'r', encoding='utf-8')
lines = f.readlines()
result = []
for line in lines:
line = json.loads(line)['positionAdvantage']
result.append(line)
text = "".join(result)
#读入背景图片
bg_pic = imread('cloud.jpg')
#生成词云
wordcloud = WordCloud(mask=bg_pic,background_color='white',scale=1.5, font_path = 'msyh.ttf').generate(text)
image_colors = ImageColorGenerator(bg_pic)
#显示词云图片
plt.imshow(wordcloud)
plt.axis('off')
plt.show()
#保存图片
wordcloud.to_file('test.jpg')
-
词云图片
根据用户输入返回相应数据
run.png差不多是从抓到的数据中做简单处理,接受用户输入,查询之后返回相应信息,相关代码我也打包成了可执行程序,感兴趣的同学可以下载下来参考一下
这里筛选出来的都是一些python行业比较基层的行业,但是相比于其他行业确实是待遇不错了,只要熬过了实习期,基本上薪资都是在15k以上,越往后面深入到算法和运维的层次工资更是攀升了好几个层次,而且从词云里面我们也不难看出目前就业情景中的关键词——“五险一金”,所以,大伙儿,在了解了这些小套路之后,开动你的小脑经,现在动手开始学Python吧,白洞,白色的明天在等着你
总结
注意点:
- 爬虫过程中注意网页请求分析,特别是头信息构造以及请求方法
- 在和数据库交互式如果没有对编码特殊处理往往会产生以\ xF0等字符开头的无法编码错误,这是因为MySQL utf8只允许使用UTF-8中的3个字节表示的Unicode字符。这里有一个需要4个字节的字符:\ xF0 \ x90 \ x8D \ x83(U + 10343 GOTHIC LETTER SAUIL)。如果你有MySQL 5.5或更高版本,你可以将列编码从更改utf8为utf8mb4。此编码允许以UTF-8存储占用4个字节的字符。
- 文件操作过程中也要格外注意编码问题,建议使用codecs来进行文件操作,可以指定唯一编码方式
- 完整代码以及文件详情参考Github