本项目综合了基本数据分析的流程,包括数据采集(爬虫)、数据清洗、数据存储、数据前后端可视化等
推荐阅读顺序为:数据采集——>数据清洗——>数据库存储——>基于Flask的前后端交互,有问题的话可以留言,有时间我会解疑~
感谢阅读、点赞和关注
本项目下面有四个.ipynb的文件,下面分别阐述各个文件所对应的功能:(有py版本 可后台留言)
数据采集:分别从前程无忧网站和猎聘网上以关键词数据挖掘爬取相关数据。其中,前程无忧上爬取了270页,有超过1万多条数据;而猎聘网上只爬取了400多条数据,主要为岗位要求文本数据,最后将爬取到的数据全部储存到csv文件中。
数据清洗:对爬取到的数据进行清洗,包括去重去缺失值、变量重编码、特征字段创造、文本分词等。
数据库存储:将清洗后的数据全部储存到MySQL中,其中对文本数据使用jieba.analyse下的extract_tags来获取文本中的关键词和权重大小,方便绘制词云。
基于Flask的前后端交互:使用Python一个小型轻量的Flask框架来进行Web可视化系统的搭建,在static中有css和js文件,js中大多为百度开源的ECharts,再通过自定义controller.js来使用ajax调用flask已设定好的路由,将数据异步刷新到templates下的main.html中。
具体请看数据采集
浏览器伪装和相关参数
headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.47'
}
job, salary, area, edu, exp, company, href, content = [], [], [], [], [], [], [], []
session = requests.Session()
session.get('https://www.liepin.com/zhaopin/', headers = headers)
for i in range(int(page)):
url = f'https://www.liepin.com/zhaopin/?key={job_name}&curPage={i}'
time.sleep(np.random.randint(1, 2))
response = session.get(url, headers = headers)
html = etree.HTML(response.text)
for j in range(1, 41):
job.append(html.xpath(f'//ul[@class="sojob-list"]/li[{j}]/div/div[1]/h3/@title')[0])
info = html.xpath(f'//ul[@class="sojob-list"]/li[{j}]/div/div[1]/p[1]/@title')[0]
ss = info.split('_')
salary.append(ss[0])
area.append(ss[1])
edu.append(ss[2])
exp.append(ss[-1])
company.append(html.xpath(f'//ul[@class="sojob-list"]/li[{j}]/div/div[2]/p[1]/a/text()')[0])
href.append(html.xpath(f'//ul[@class="sojob-list"]/li[{j}]/div/div[1]/h3/a/@href')[0])
遍历每一个岗位的数据
for job_href in href:
time.sleep(np.random.randint(1, 2))
# 发现有些岗位详细链接地址不全,需要对缺失部分进行补齐
if 'https' not in job_href:
job_href = 'https://www.liepin.com' + job_href
response = session.get(job_href, headers = headers)
html = etree.HTML(response.text)
content.append(html.xpath('//section[@class="job-intro-container"]/dl[1]//text()')[3])
保存数据
df.to_csv('xxx.csv', encoding = 'gb18030', index = None)
具体请看数据清洗
read_csv读取数据集
data.head()
data.isnull().mean()
考虑舍弃薪水和工作职责上的缺失数据
data.dropna(subset = ['薪水'], inplace = True)
# 去除重复值
data.drop_duplicates(inplace = True)
# 索引重置
data.index = range(data.shape[0])
data.shape
初始化列表来接收正则提取后的每个字符串
# 初始化列表来接收正则提取后的每个字符串
salary_list = []
for i in range(len(data)):
# 使用正则表达式需要将每一列都转换为字符串形式
data['薪水'][i] = str(data['薪水'][i])
# 注意join之后便为一个字符串,我们使用正则筛选掉数字和其它符号,findall在下列中只会切割每个字,[]表示或,()表示与,{}表示匹配次数
salary_list.append(''.join(re.findall(r'[^0-9\.]', data['薪水'][i])))
# 使用unique知道有多少种写法
np.unique(salary_list)
对地域这一列进行字段重编码
data['地区'] = data['地域'].apply(lambda x: x.split('-')[0])
data
对31个省市划分and遍历
生成人数这一列
num_list = []
k = 0
for i in range(len(data)):
s = str(data['其他信息'][i])
num_str = s.split(' ')[-1]
if re.findall(r'\d', num_str):
num_list.append(int(re.findall(r'\d', num_str)[0]))
else:
num_list.append(np.nan)
k += 1
data['人数'] = num_list
print('招聘人数缺失数量有%s条,占总样本比例为%s' %(k, k / data.shape[0]))
生成学历这一列
k = 0
edu_list = []
for i in range(len(data)):
s = str(data['其他信息'][i])
edu_str = s.split(' ')[-2]
if edu_str in ['博士', '硕士', '本科', '大专', '中专', '高中', '初中及以下']:
edu_list.append(edu_str)
else:
edu_list.append(np.nan)
k += 1
data['学历'] = edu_list
print('学历未标明的数量有%s条,占总样本比例为%s' %(k, k / data.shape[0]))
生成经验这一列
jingyan_list = []
for i in range(len(data)):
try:
ss = data['其他信息'][i].split(' ')[-3]
if '经验' in ss:
if re.findall(r'(.*)\-(.*)\年', ss):
ss_num = re.findall(r'(.*)\-(.*)年', ss)[0]
jingyan = np.round((float(ss_num[0]) + float(ss_num[1])) / 2, 0)
jingyan_list.append(format(jingyan, '.0f'))
elif re.findall(r'(.*)\年', ss):
ss_num = re.findall(r'(.*)\年', ss)[0]
jingyan_list.append(format(float(ss_num), '.0f'))
else:
jingyan_list.append(0)
else:
jingyan_list.append(0)
except:
jingyan_list.append(0)
data['经验'] = jingyan_list
data[:2]
将人数和学历的缺失值全部去除
data.dropna(subset = ['人数', '学历'], inplace = True)
data.index = range(data.shape[0])
去除无意义的字段
data.drop(labels = ['其他信息', '地域', '地区'], axis = 1, inplace = True)
data['公司类型'] = data['公司类型'].fillna(data['公司类型'].mode().values[0])
data.shape
data.to_csv('./51job_data_preprocessing.csv', encoding = 'gb18030', index = None)
data.isnull().mean()
read_csv数据集
df
for i in range(len(df)):
df['岗位要求'][i] = str(df['岗位要求'][i])
if len(df['岗位要求'][i]) < 30 or len(re.findall(r'\?', df['岗位要求'][i])) / len(df['岗位要求'][i]) > 0.4:
df.drop(i, axis=0, inplace=True)
# 索引重置
df.index = range(df.shape[0])
df.shape
自定义函数来实现分词和去除停用词操作
import jieba
def m_cut(tmpstr):
return [w.strip() for w in jieba.lcut(tmpstr) if w not in stoplist and len(w) > 1]
导入自定义词典
dic = './自定义词典.txt'
jieba.load_userdict(dic)
先去除部分不相关的词汇,之后调用上述函数进行分词及去除停用词的操作
df0.to_csv('./liepin_job_detail.csv', encoding = 'gb18030', index = None)
具体请看数据库存储
需要先在数据库中定义好数据库以及表
这里改成自己数据库的用户名和密码
下面是 连接数据库 和 关闭数据库
def get_con():
con = pymysql.connect(host = 'localhost', user = '用户名', password = '密码', database = '数据库名', charset = 'utf8')
cursor = con.cursor()
return con, cursor
def con_close(con, cursor):
if cursor:
cursor.close()
if con:
con.close()
读取数据
df = pd.read_csv('51job_data_preprocessing.csv', encoding = 'gb18030')
df
将每行数据都转变为tuple数据类型,然后遍历把每条数据都添加到sql中
con, cursor = get_con()
for i in range(len(df)):
s = tuple(df.iloc[i, :])
print({s})
sql = f'insert into data_mining values{s}'
cursor.execute(sql)
con.commit()
con_close(con, cursor)
把词云部分数据也存放进数据库中
df_cloud = pd.read_csv('liepin_job_detail.csv', encoding = 'gb18030')
df_cloud
将每一列英文全部转换为大写的
df_cloud = df_cloud.apply(lambda x: [i.upper() for i in x])
df_cloud.head()
对文本进行去重操作
s = np.unique(df_cloud.sum().tolist()).tolist()
由于后期使用echarts绘制词云需要知道各个关键词的权重大小,所以下面使用jieba下的extract_tags来挖掘各个关键词和权重大小,注意extract_tags输入的是一个字符串,我们挑选出前150个关键词及权重
ss = aa.extract_tags(' '.join(s), topK = 150, withWeight = True)
ss
con, cursor = get_con()
for i in range(len(ss)):
sql = "insert into data_mining_cloud(词语, 权重) value ({0}, {1})".format(repr(ss[i][0]), ss[i][1])
cursor.execute(sql)
con.commit()
con_close(con, cursor)
具体请看基于Flask的前后端交互
class JSONEncoder(_JSONEncoder):
def default(self, o):
if isinstance(o, decimal.Decimal):
return float(o)
super(_JSONEncoder, self).default(o)
class Flask(_Flask):
json_encoder = JSONEncoder
app = Flask(__name__)
# 路由解析
@app.route('/')
def index():
return render_template("main.html")
# 获取系统当前时间
@app.route('/time')
def get_time1():
return get_time()
# 对数据库中的数据进行计数、薪资取平均值、省份和学历取众数
@app.route('/c1')
def get_c1_data1():
data = get_c1_data()
return jsonify({"employ": data[0], "avg_salary": data[1], "province": data[2], "edu": data[3]})
# 对省份进行分组,之后统计其个数
@app.route('/c2')
def get_c2_data1():
res = []
for tup in get_c2_data():
res.append({"name": tup[0], "value": int(tup[1])})
return jsonify({"data": res})
# 统计每个学历下公司数量和平均薪资(上下坐标折线图)
@app.route('/l1')
def get_l1_data1():
data = get_l1_data()
edu, sum_company, avg_salary = [], [], []
for s in data:
edu.append(s[0])
sum_company.append(int(s[1]))
avg_salary.append(float(s[2]))
return jsonify({"edu": edu, "sum_company": sum_company, "avg_salary": avg_salary})
# 统计不同学历下公司所招人数和平均经验(折线混柱图)
@app.route('/l2')
def get_l2_data1():
data = get_l2_data()
edu, num, exp = [], [], []
for s in data:
edu.append(s[0])
num.append(float(s[1]))
exp.append(float(s[2]))
return jsonify({'edu': edu, 'num': num, 'exp': exp})
# 统计不同类型公司所占的数量(饼图)
@app.route('/r1')
def get_r1_data1():
res = []
for tup in get_r1_data():
res.append({"name": tup[0], "value": int(tup[1])})
return jsonify({"data": res})
# 可视化词云
@app.route('/r2')
def get_r2_data1():
d = []
text, weight = get_r2_data()
for i in range(len(text)):
d.append({'name': text[i], 'value': weight[i]})
return jsonify({"kws": d})
if __name__ == '__main__':
app.run()