整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

实习僧招聘网爬虫数据可视化

实习僧招聘网爬虫数据可视化


本来对实习僧网站是没什么好感的,因为之前自己在实习僧上投的实习简历几乎全部都石沉大海了(一个文科生偏要去投数据分析岗不碰壁才怪~_~)!

然鹅看到最近知乎爬虫圈儿里的两大趋势:爬美图;爬招聘网站。

后来大致了解下了,几乎各类大型的招聘文章都被别人爬过了,自己再去写免不了模仿之嫌,而且大神们都是用Python去爬的(Python我刚学会装包和导数据),自己也学不来。

现在只能选一个还没怎么被盯上的招聘网站,没错就它了——实习僧。

说老实话,实习僧的网站做的还是不错的,看着结构挺简单,可是我用比较主流的Rvest和RCurl都失败了(主要自己技术太渣了,抓包又抓不好)。最后只能勉强用RSelenium爬完了全部所需内容。(用代码驱动浏览器的好处就是不用怎么考虑时延和伪装包头了,但是要遍历成百上千页网址真的很耗时,爬完这个数据用了大约40多分钟)。

以下是爬虫部分:

library(rvest)

library(stringr)

library(plyr)

library(dplyr)

library(Rwebdriver)

library(dplyr)

library(Rwordseg)

library(wordcloud2)

library(treemap)

library(showtext)

library(Cairo)

library(ggplot2)

library(scales)

library(grid)

library(RColorBrewer)

library(ggimage)

library(geojsonio)

本文主要用的Rwebdriver包来驱动Chrome浏览器,使用该包需要提前配置好桌面环境:

  • 下载selenium启动服务器;

  • 下载Chrome的chromedriver插件并放入Chrome主目录(添加系统环境变量)

  • 下载Rwebdriver包

在cmd或者powershell中启动Selenium服务器:

options(stringsAsFactors=FALSE,warn=FALSE)

cd "D:/Rwebdriver-master"

java -jar selenium-server-standalone-3.3.1.jar

在Rsudio中新建进程:

start_session(root="http://localhost:4444/wd/hub/",browser="chrome")

遍历实习僧的招聘信息网页

baseurl<-"http://www.shixiseng.com/interns?k=&t=zh&c=%E5%85%A8%E5%9B%BD&p="

pageurl<-paste0(baseurl,1:500)

本文所用到的爬虫代码部分:

homepage=internship=companyweb=company=Position=address=salary=period=duration=NULL

fun<-function(url){

post.url(url=url)

baseinfo<-read_html(url,encoding="utf-8")

homepage <-baseinfo%>%html_nodes("div.po-name>div.names>a")%>% html_attr("href")

internship<-baseinfo%>%html_nodes("div.po-name>div.names>a")%>%html_text()

companyweb<-baseinfo%>%html_nodes("div.po-name>div.part>a")%>%html_attr("href")

company <-baseinfo%>%html_nodes("div.po-name>div.part>a")%>%

html_text()

Position <-baseinfo%>%html_nodes("div.po-name>div.part")%>%

html_text()

address <-baseinfo%>%html_nodes("div.po-detail>div.addr>span")%>%

html_text()

salary <-baseinfo%>%html_nodes("div.po-detail>div.xz>span:nth-child(2)")%>%

html_text()

period <-baseinfo%>%html_nodes("div.po-detail>div.xz>span:nth-child(5)")%>%

html_text()

duration <-baseinfo%>%html_nodes("div.po-detail>div.xz>span:nth-child(8)")%>%

html_text()

interninfo<-data.frame(homepage,internship,companyweb,company,Position,address,salary,period,duration)

}

用一个循环执行上述程序:

final<-data.frame()

for (i in pageurl){

final<-rbind(final,fun(i))

}

quit_session()

DT::datatable(final)

保存本地:

write.table (final,"D:/R/File/shixiseng.csv",sep=",",row.names=FALSE)

##############################################

接下来做数据清洗:

mydata<-read.csv("D:/R/File/shixiseng.csv",stringsAsFactors=FALSE,check.names=FALSE)

mydata<-mydata[-5001,]

#补全实习发布单位的招聘信息主页:

mydata$homepage<-str_c("http://www.shixiseng.com",mydata$homepage,sep="")

#补全实习发布单位的公司信息主页:

mydata$companyweb<-str_c("http://www.shixiseng.com",mydata$companyweb,sep="")

mydata$work<-str_split(mydata$Position[1:10], " - ", simplify=TRUE)[,2]

#清除salary中的空格和斜杠

mydata$salary<-str_trim(mydata$salary,side="both")

mydata$salary<-str_extract(mydata$salary,"\d+\-\d+")

#拆分实习工资的高低区间

mydata$salary_low<-str_split(mydata$salary, "-", simplify=TRUE)[,1]

mydata$salary_high<-str_split(mydata$salary, "-", simplify=TRUE)[,2]

#清除period中的汉字和特殊字符

mydata$period<-str_extract(mydata$period,"\d+")

#清除duration中的汉字和特殊字符

mydata$duration<-str_extract(mydata$duration,"\d+")

mydata <- tbl_df(mydata)

mydata<-select(mydata,-Position)

#因为address中所含的地址可能有存在多个,影响我们后续的可视化分析,这里为了方便起见,一律使用第一个地址。

mydata$address_unique<-str_split(mydata$address, ",", simplify=TRUE)[,1]

至此,数据清洗工作告一段落,接下来我们要进入分析与可视化阶段

names(mydata)

"homepage"-------公司实习职位简介

"internship"-----公司招聘性质

"companyweb"-----公司主页

"company"--------公司名称

"address"--------所在地

"address_unique"-所在地(唯一值,只取默认第一个地址)

"salary"---------实习工资区间

"salary_low"-----实习工资(最低值)

"salary_high"----实习工资(最高值)

"period"---------到岗天数(每周)

"duration"-------实习周期(按月算)

"work"-----------具体职位

我们最终获取的清洗后数据如上所示。

假如本次项目需求(虚拟)要求我们获取以下几个问题:

1、实习僧的实习招聘主页岗位主要是什么性质的?

2、哪些公司最缺实习僧?

3、实习岗位具体分布的地域和城市?

4、哪些城市对实习僧的需要最为强烈?

5、实习工资大致什么水平,与城市和地域是否有关系?

6、实习岗位一般都要求每周到岗多少天?

7、实习周期一般需要多长时间?

8、哪些职位需求最为频繁,职位需要量与城市之间的大致是如何分布的?

带着这些个问题,让我们尽情的畅游在可视化的世界里吧……

1、实习僧的实习招聘主页主要是什么性质的?

length(unique(mydata$internship))

3357

绝望了,一共爬了5000条实习职位信息,做了去重处理,显示仍有3357条,建议实习僧的产品运营团队考虑下要不要标准化一下这个职位性质,内门怎么可以创造这么多独特的职位~_~

对于这个问题,真的难倒我了,因为所爬数据中的职位性质没有统一的预设标准,所以我只能用文本分词的形式来进行提取了,先分词,然后统计高频词,最后按照词频来进行模糊分析啦(可我我对文本挖掘一窍不通啊~_~)

top100<-table(mydata$internship)%>%as.data.frame(stringsAsFactors=FALSE)%>% arrange(desc(Freq))%>%.[1:100,]

treemap(top100, index=c("Var1"), vSize="Freq",title='实习僧职位性质分布图',palette='RdBu',

fontsize.title=18,fontsize.labels=12,fontface.labels="plain",fontfamily.title="mono",fontfamily.labels="mono")

从实习职位分布图上来看,人力资源实习生职位需求最为强烈,其次是运营、财务、新媒体,这些类型的职位多为现代新兴服务业,更为符合大学生这一群体的口味和兴趣。

myrevieww<-mydata$internship

thewords <- segmentCN(myrevieww,nature=T)%>%unlist()

thewords <- gsub("[a-z]|\.", "", thewords)

thewords<-thewords[nchar(thewords)>1]

reviewdata<-table(thewords)%>%as.data.frame(stringsAsFactors=FALSE)%>% arrange(desc(Freq))%>%filter(thewords!="实习生")

wordcloud<-wordcloud2(reviewdata[1:1000,],color="random-light",minSize=.6,size=1,backgroundColor="dark",minRotation=-pi/6,maxRotation=-pi/6,fontFamily="微软雅黑");wordcloud

但是将职位性质分词整理成关键词后,似乎结果有所不同。

2、哪些公司最缺实习僧?

这里我们来统计所爬职位信息中公司发布职位的频率,发布最多的则作为评价公司对实习生需求的标准。

myjob<-table(mydata$company)%>%as.data.frame(stringsAsFactors=FALSE)%>%arrange(desc(Freq))

#看看前十名都是那些公司:

myjob15<-arrange(myjob[1:15,],desc(Freq))

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/circle-rose.png",width=1580,height=950)

showtext.begin()

ggplot(data=myjob15,aes(x=reorder(Var1,-Freq),y=Freq,fill=Freq))+

geom_linerange(aes(ymin=0,ymax=42),linetype=5,size=.2,colour="#858585")+

geom_image(aes(x=0,y=-40),image="D:/R/Image/image1.jpg", size=.2)+

geom_text(aes(y=45,label=paste0(Var1,"\n",Freq)),vjust=1,size=5)+

geom_bar(stat="identity",width=1,col="white")+

geom_hline(yintercept=54,color="black",size=.5)+

geom_point(aes(y=48,size=Freq),shape=21,fill="#ED7D31",,alpha=0.6,col="orange")+

labs(title="Top 15 of Company",caption="Data Source:shixiseng")+

coord_polar(theta="x")+

ylim(-40,60)+

scale_size_area(max_size=20)+

guides(fill=FALSE,size=guide_legend(reverse=TRUE,title=NULL))+

theme_minimal()+

theme(

axis.title=element_blank(),

axis.text=element_blank(),

panel.grid=element_blank(),

legend.text=element_text(family="myfont",size=12),

legend.title=element_text(family="myfont",size=15,hjust=1),

plot.title=element_text(family="myfont",size=35),

plot.caption=element_text(family="myfont",size=18,hjust=0,lineheight=1.2)

)

showtext.end()

dev.off()

write.table (myjob[1:100,],"D:/R/File/shixiseng_job.csv",sep=",",row.names=FALSE)

前一百个实习生需求最旺盛企业:

3、实习岗位具体分布的地域和城市?

先做一个地域分布图:

add<-table(mydata$address_unique)%>%as.data.frame(stringsAsFactors=FALSE)%>%arrange(desc(Freq))%>%filter(nchar(Var1)==2)

#城市经纬度查询:

library(rjson)

library(RCurl)

library(REmap)

library(baidumap)

address<-add$Var1

baidu_lng <- c()

baidu_lat <- c()

ak<-"X8zlxPUdSe2weshrZ1WqnWxb43cfBI2N"

for(location in address){

url<-paste("http://api.map.baidu.com/geocoder/v2/?ak=",ak,"&callback=renderOption&output=json&address=",location,sep="")

url_string <- URLencode(url)

msg.load <- tryCatch({

json <-readLines(url_string,warn=F,encoding="UTF-8")

msg.load <- "TRUE"

},error=function(e) {

"error"

}

)

if(msg.load=='error'){

Sys.sleep(runif(1,3,10))

msg.load <- tryCatch({

connect <- readLines(url_string,warn=F,encoding="UTF-8")

msg.load <- "TRUE"

}, error=function(e){

"error"

}

)

}

geo <- fromJSON(substr(json,regexpr("\(",json)+1,nchar(json)-1))

if(msg.load=='error'){

lng<-'error1'

lat<-'error1'

}else{

lng<-geo$result$location$lng

lat<-geo$result$location$lat

if(length(lng)==0){

lng <- "error2"

lat <- "error2"

}

}

lng<-geo$result$location$lng

lat<-geo$result$location$lat

baidu_lng<-c(baidu_lng,lng)

baidu_lat<-c(baidu_lat,lat)

}

result<-data.frame(address=address,long=baidu_lng,lat=baidu_lat,stringsAsFactors=FALSE)

pointdata<-left_join(add,result,by=c("Var1"="address"))

#成功获取目标城市经纬度信息:

解析来制作分布图:

geojson <-readOGR("D:/R/mapdata/State/china.geojson","OGRGeoJSON",stringsAsFactors=FALSE)

Encoding(geojson$name)<-"UTF-8"

china_Mapdata<-geojson@data

china_MapdataPloygon<-fortify(geojson)

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/shixiseng_jobcity.png",width=1200,height=640)

showtext.begin()

ggplot()+

geom_polygon(data=china_MapdataPloygon,aes(x=long,y=lat,group=group),col="grey60",fill="white",size=.2,alpha=.4)+

geom_point(data=pointdata,aes(x=long,y=lat,size=Freq),shape=21,fill="#C72E29",col="#014D64",alpha=0.6)+

scale_size_area(max_size=15,guide=guide_legend(reverse=TRUE,title=NULL))+

coord_map("polyconic") +

labs(title="实习僧职位需求城市分布图",caption="数据来源:实习僧官网")+

theme(

title=element_text(family="myfont",size=18),

plot.title=element_text(size=24),

plot.caption=element_text(family="myfont",size=18,hjust=0),

panel.grid=element_blank(),

panel.background=element_blank(),

axis.text=element_blank(),

axis.ticks=element_blank(),

axis.title=element_blank(),

legend.position=c(0.02,0.6),

)

showtext.end()

dev.off()

4、哪些城市对实习僧的需要最为强烈?

从第三个问题及其分析结果上我们已经看出了整体形势,北上广深依然是需求最为旺盛的地区,这也符合城市的实际经济发展情况及我们的预期。

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/jingjixue2.png",width=800,height=600)

showtext.begin()

ggplot(pointdata[1:20,],aes(reorder(Var1,Freq),Freq))+

geom_bar(fill="#0C8DC4",stat="identity")+

coord_flip()+

labs(title="实习僧职位需求城市分布",caption="数据来源:实习僧官网")+

geom_text(aes(y=Freq+25,label=Freq),hjust=2,colour="#C72E29",size=5)+

theme_bw()+

theme(

panel.border=element_blank(),

panel.grid.major=element_line(linetype="dashed"),

panel.grid.minor=element_blank(),

plot.title=element_text(size=15,colour="#003087",family="myfont"),

plot.caption=element_text(hjust=0,size=10),

axis.title=element_blank(),

axis.text=element_text(size=15)

)

showtext.end()

dev.off()

5、实习工资大致什么水平,与城市和地域是否有关系?

myjob_salary<-mydata[,c("address_unique","salary_low","salary_high")]

myjob_salary$salary_low<-as.numeric(myjob_salary$salary_low)

myjob_salary$salary_high<-as.numeric(myjob_salary$salary_high)

myjob_salary<-na.omit(myjob_salary)%>%arrange(salary_high)%>%filter(salary_high>=20&salary_high<=500)

myjob_salary$id<-seq_len(nrow(myjob_salary))

myjob_salary$meansalary<-(myjob_salary$salary_low+myjob_salary$salary_high)/2

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/shixiseng_jobsaleryarea.png",width=1200,height=800)

showtext.begin()

ggplot(myjob_salary,aes(id))+

geom_ribbon(aes(ymin=salary_low,ymax=salary_high),fill="grey70")+

labs(title="实习僧职位工资区间分布",caption="数据来源:实习僧官网")+

geom_line(aes(y=meansalary))+

theme_minimal()+

theme(

panel.border=element_blank(),

panel.grid.major=element_line(linetype="dashed"),

panel.grid.minor=element_blank(),

plot.title=element_text(size=30,colour="#003087",family="myfont"),

plot.caption=element_text(hjust=0,size=20),

axis.title=element_blank(),

axis.text.y=element_text(size=15),

axis.text.x=element_blank()

)

showtext.end()

dev.off()

myjob_salary$address_unique<-substr(myjob_salary$address_unique,1,2)

myjobcitysalary<-aggregate(meansalary~address_unique,data=myjob_salary,FUN=mean)

treemap(na.omit(myjobcitysalary), index=c("address_unique"),

vSize="meansalary",title='实习僧职位薪酬地域分布图',palette='RdBu',

fontsize.title=18,fontsize.labels=12,fontface.labels="plain",

fontfamily.title="mono",fontfamily.labels="mono")

按照全部参与计算聚合的平均工资来看,北上广深反而没有什么吸引力了排名普遍不高,我猜想是因为一线城市的低工资职位数量过多,压低了平均值。

接下来我们按照100,150的临界点进行工资高低的划分。

salary100<-myjob_salary%>%filter(meansalary<=100)%>%select(address_unique)%>%table()%>%as.data.frame(stringsAsFactors=FALSE)

salary100<-na.omit(salary100)%>%arrange(desc(Freq));names(salary100)<-c("city","num")

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/salary100.png",width=1200,height=900)

showtext.begin()

ggplot(salary100[salary100$num>=5,],aes(reorder(city,-num),num))+

geom_bar(fill="#0C8DC4",stat="identity")+

labs(title="实习僧职位薪资城市分布(日薪低于100)",caption="数据来源:实习僧官网")+

geom_text(aes(y=num+10,label=num),hjust=.5,colour="#C72E29",size=5)+

theme_bw()+

theme(

panel.border=element_blank(),

panel.grid.major=element_line(linetype="dashed"),

panel.grid.minor=element_blank(),

plot.title=element_text(size=25,colour="#003087",family="myfont"),

plot.caption=element_text(hjust=0,size=18),

axis.title=element_blank(),

axis.text=element_text(size=15)

)

showtext.end()

dev.off()

salary200<-myjob_salary%>%filter(meansalary>=200)%>%select(address_unique)%>%table()%>%as.data.frame(stringsAsFactors=FALSE)

salary200<-na.omit(salary200)%>%arrange(desc(Freq));names(salary200)<-c("city","num")

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/salary200.png",width=1200,height=900)

showtext.begin()

ggplot(salary200,aes(reorder(city,-num),num))+

geom_bar(fill="#0C8DC4",stat="identity")+

labs(title="实习僧职位薪资城市分布(日薪高于200)",caption="数据来源:实习僧官网")+

geom_text(aes(y=num+10,label=num),hjust=.5,colour="#C72E29",size=5)+

theme_bw()+

theme(

panel.border=element_blank(),

panel.grid.major=element_line(linetype="dashed"),

panel.grid.minor=element_blank(),

plot.title=element_text(size=25,colour="#003087",family="myfont"),

plot.caption=element_text(hjust=0,size=18),

axis.title=element_blank(),

axis.text=element_text(size=15)

)

showtext.end()

dev.off()

salary100_200<-myjob_salary%>%filter(meansalary>100 & meansalary<200)%>%select(address_unique)%>%table()%>%as.data.frame(stringsAsFactors=FALSE)

salary100_200<-na.omit(salary100_200)%>%arrange(desc(Freq));names(salary100_200)<-c("city","num")

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/salary100-200.png",width=1600,height=900)

showtext.begin()

ggplot(salary100_200,aes(reorder(city,-num),num))+

geom_bar(fill="#0C8DC4",stat="identity")+

labs(title="实习僧职位薪资城市分布(日薪处于100-200之间)",caption="数据来源:实习僧官网")+

geom_text(aes(y=num+10,label=num),hjust=.5,colour="#C72E29",size=5)+

theme_bw()+

theme(

panel.border=element_blank(),

panel.grid.major=element_line(linetype="dashed"),

panel.grid.minor=element_blank(),

plot.title=element_text(size=25,colour="#003087",family="myfont"),

plot.caption=element_text(hjust=0,size=18),

axis.title=element_blank(),

axis.text=element_text(size=15)

)

showtext.end()

dev.off()

分析到这里,趋势已经很明显了,因为北上广深等一线城市的职位实在是太多了,无论是高新职位还是低薪实习岗位都能排在全国各城市的前列,所以出现日均工资很普通的现象。相对而言,日薪在200以上的高薪职位更能代表各大城市对日常实习生需求的强烈程度,因为在这一阶段,北上广深的优势非常明显,遥遥领先与其他二线城市,而针对日薪高于200的实习职位统计结果可以看出来,北上技压群雄(不愧是帝都和魔都),深圳和广州处于第二线,200以上的高新实习职位遇北上相比,相差比较大,仅占前两者约1/3~1/5。而杭州、南京、武汉、合肥则稳稳处于第三梯队。

6、实习岗位一般都要求每周到岗多少天?

myperiod<-mydata$period

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/internperiod.png",width=1000,height=750)

showtext.begin()

ggplot(data=NULL,aes(myperiod)) +

geom_histogram(stat="count",show.legend=FALSE,binwidth=1,fill="#F86977",col=NA) +

labs(title="实习职位要求每周工作天数",caption="数据来源:实习僧官网")+

theme(panel.background=element_rect(fill=NA),

plot.background=element_rect(fill=NA),

plot.title=element_text(size=20,family="myfont"),

plot.caption=element_text(hjust=0,family="myfont"),

axis.line=element_line(colour="grey80"),

axis.text=element_text(size=12,family="myfont"),

axis.title=element_blank())

showtext.end()

dev.off()

从分布上看,一周五天居多,这样是正常的工作日现象。3天、4天也是用人单位补缴能接受的周工作天数。

7、实习周期一般需要多长时间?

myduration<-mydata$duration

mydurationhz<-table(myduration)%>%as.data.frame(stringsAsFactors=FALSE)%>%arrange(desc(Freq))

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/internduration.png",width=1000,height=750)

showtext.begin()

ggplot(data=mydurationhz,aes(reorder(myduration,-Freq),Freq)) +

geom_bar(stat="identity",show.legend=FALSE,width=1,fill="#065573",col=NA) +

geom_text(aes(y=Freq+20,label=Freq),hjust=.5,colour="#C72E29",size=5)+

labs(title="实习职位要求工作时间周期",caption="数据来源:实习僧官网")+

theme(panel.background=element_rect(fill=NA),

plot.background=element_rect(fill=NA),

plot.title=element_text(size=20,family="myfont"),

plot.caption=element_text(hjust=0,family="myfont"),

axis.line=element_line(colour="grey80"),

axis.text=element_text(size=12,family="myfont"),

axis.title=element_blank())

showtext.end()

dev.off()

8、哪些职位需求最为旺盛?

下面开始分析最后一个问题,也是我认为最有价值,最值得探究的问题,最后一个待分析指标是实习职位,这个指标跟我们最初分析的那个岗位性质略有不同,该变量相对比较规范(职位类别有确定的预设范围)。

我们新建一个分析数据,用职位、工资区间的中间值,地区三个变量进行后续分析:

mydata$salary_high<-as.numeric(mydata$salary_high)

mydata$salary_low<-as.numeric(mydata$salary_low)

mydata$salary_mean<-(mydata$salary_low+mydata$salary_high)/2

myworkdata<-mydata[,c("address_unique","work","salary_mean")]

首先分析下岗位的集中分布情况:

myworkcount<-table(myworkdata[,2])%>%as.data.frame(stringsAsFactors=FALSE)%>%arrange(desc(Freq))

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/interndworkcount.png",width=1000,height=750)

showtext.begin()

ggplot(data=myworkcount,aes(reorder(Var1,-Freq),Freq)) +

geom_bar(stat="identity",show.legend=FALSE,width=.75,fill="#5D8DA6",col=NA) +

geom_text(aes(y=Freq+50,label=Freq),hjust=.5,colour="#C72E29",size=5)+

labs(title="实习职位类别分布",caption="数据来源:实习僧官网")+

theme(panel.background=element_rect(fill=NA),

plot.background=element_rect(fill=NA),

plot.title=element_text(size=20,family="myfont"),

plot.caption=element_text(hjust=0,family="myfont"),

axis.line=element_line(colour="grey80"),

axis.text=element_text(size=12,family="myfont"),

axis.title=element_blank())

showtext.end()

dev.off()

各个岗位工资分布:

myworkmean<-aggregate(salary_mean~work,data=myworkdata,FUN=mean)

CairoPNG(file="E:/微信公众号/公众号——数据小魔方/2017年5月/20170512/interndworkmean.png",width=1000,height=750)

showtext.begin()

ggplot(data=myworkmean,aes(reorder(work,-salary_mean),salary_mean)) +

geom_bar(stat="identity",show.legend=FALSE,width=.75,fill="#5D8DA6",col=NA) +

geom_text(aes(y=salary_mean+5,label=round(salary_mean,1)),hjust=.5,colour="#C72E29",size=10)+

labs(title="实习职位类别平均工资",caption="数据来源:实习僧官网")+

theme(panel.background=element_rect(fill=NA),

plot.background=element_rect(fill=NA),

plot.title=element_text(size=25,family="myfont"),

plot.caption=element_text(hjust=0,family="myfont",size=20),

axis.line=element_line(colour="grey80"),

axis.text=element_text(size=18,family="myfont"),

axis.title=element_blank())

showtext.end()

dev.off()

从工资均数上来看,七大类职位差距不大,说明仅就实习岗位工资而言,结合爬取的总体样本,不考虑地域差异,实习工资差异不很明显。也许是因为实习岗位中真正有含金量的并不多,换句话说,企业的高价值岗位,对于工作经验、技能的要求相对较高,而实习生则在这方面都不具备优势。(如果是社招或者小校招岗位信息的话,可能工资均值的差异会大一些)。

最后,让我们用一个桑基图来完结本次针对实习僧网站的爬虫分析:

write.table (myworkdata,"D:/R/File/shixiseng_workdata.csv",sep=",",row.names=FALSE)

桑基流向图的趋势灰常明显,北上两市在所有类别职位(7大类)上均居前列,各职位类别中,市场和运营职位需求最为强烈,北京和上海在对市场和运营职位的需求容量机会平分秋色。广州和深圳仍然略于北上,但是与其他二线城市相比较,优势仍然很明显。(也许是因为实习僧做为主打校园实习岗位信息咨询平台,针对的主流群体基本以在校大学生为主,而北上广深的高等教育资源分布差异明显,在高校数量方面,北上的要沾光很多,广州与深圳的高校资源相对比较匮乏,在以上分析的各项指标中都占尽劣势)。

爬虫和代码分析,算起来整整花了将近12个小时,虽然分析的不是很精准、客观,但是过程还是很有收获的,期待下一次做的更好!

End.

来源:R语言中文社区

国统计网,是国内最早的大数据学习网站,欢迎关注!

I 鉴赏

早几年学习前端,大家都非常热衷于研究jQuery源码。

我至今还记得当初从jQuery源码中学到一星半点应用技巧的时候常会有一种发自内心的惊叹,“原来JavaScript居然可以这样用!”

但是随着前端的迅猛发展,另外几种前端框架的崛起,jQuery慢慢变得不再是必须。所有人对jQuery的热情都降低了许多。

jQuery在前端史上有它非常超然的历史地位,许多从中学到的技巧在实践开发中仍然非常好用。简单的了解它有助于我们更加深入的理解JavaScript。如果你能够从中看明白jquery是如何一步步被取代的,那么,我想你的收益远不止学会使用了一个库那么简单。

因此,我的态度是,项目中你可以不用,但是我仍然建议你学。

这篇文章的主要目的,是从面向对象的角度,跟大家分享jquery对象是如何封装的。算是对大家进一步学习jQuery源码的一个抛砖引玉。

1

使用jQuery对象时,我们这样写:

// 声明一个jQuery对象
$('.target')

// 获取元素的css属性
$('.target').css('width')

// 获取元素的位置信息
$('.target').offset()

在使用之初可能会有许多疑问,比如$是怎么回事?为什么不用new就可以直接声明一个对象?等等。了解之后,才知道原来这正是jQuery对象创建的巧妙之处。

先直接用代码展示出来,再用图跟大家解释是怎么回事。

;
(function (ROOT) {

  // 构造函数
  var jQuery=function (selector) {

    // 在jQuery中直接返回new过的实例,这里的init是jQuery的真正构造函数
    return new jQuery.fn.init(selector)
  }

  jQuery.fn=jQuery.prototype={
    constructor: jQuery,

    version: '1.0.0',

    init: function (selector) {
      // 在jquery中这里有一个复杂的判断,但是这里我做了简化
      var elem, selector;
      elem=document.querySelector(selector);
      this[0]=elem;

      // 在jquery中返回一个由所有原型属性方法组成的数组,我们这里简化,直接返回this即可
      // return jQuery.makeArray(selector, this);
      return this;
    },

    // 在原型上添加一堆方法
    toArray: function () { },
    get: function () { },
    each: function () { },
    ready: function () { },
    first: function () { },
    slice: function () { }
    // ... ...
  }

  jQuery.fn.init.prototype=jQuery.fn;

  // 实现jQuery的两种扩展方式
  jQuery.extend=jQuery.fn.extend=function (options) {

    // 在jquery源码中会根据参数不同进行很多判断,我们这里就直接走一种方式,所以就不用判断了
    var target=this;
    var copy;

    for (name in options) {
      copy=options[name];
      target[name]=copy;
    }
    return target;
  }

  // jQuery中利用上面实现的扩展机制,添加了许多方法,其中

  // 直接添加在构造函数上,被称为工具方法
  jQuery.extend({
    isFunction: function () { },
    type: function () { },
    parseHTML: function () { },
    parseJSON: function () { },
    ajax: function () { }
    // ...
  })

  // 添加到原型上
  jQuery.fn.extend({
    queue: function () { },
    promise: function () { },
    attr: function () { },
    prop: function () { },
    addClass: function () { },
    removeClass: function () { },
    val: function () { },
    css: function () { }
    // ...
  })

  // $符号的由来,实际上它就是jQuery,一个简化的写法,在这里我们还可以替换成其他可用字符
  ROOT.jQuery=ROOT.$=jQuery;
})(window);

在上面的代码中,我封装了一个简化版的jQuery对象。

它向大家简单展示了jQuery的整体骨架。

在代码中可以看到,jQuery自身对于原型的处理使用了一些巧妙的方式,比如jQuery.fn=jQuery.prototype,jQuery.fn.init.prototype=jQuery.fn;等,这几句正是jQuery对象的关键所在。看图分析。

jQuery对象核心图

2

对象封装分析

在上面的实现中,首先在jQuery构造函数里声明了一个fn属性,并将其指向了原型jQuery.prototype。然后在原型中添加了init方法。

jQuery.fn=jQuery.prototype={
  init: {}
}

之后又将init的原型,指向了jQuery.prototype。

jQuery.fn.init.prototype=jQuery.fn;

而在构造函数jQuery中,返回了init的实例对象。

var jQuery=function (selector) {

  // 在jQuery中直接返回new过的实例,这里的init是jQuery的真正构造函数
  return new jQuery.fn.init(selector)
}

最后对外暴露入口时,将字符$与jQuery对等起来。

ROOT.jQuery=ROOT.$=jQuery;

因此当我们直接使用$('#test')创建一个对象时,实际上是创建了一个init的实例,这里的真正构造函数是原型中的init方法。

注意:许多对jQuery内部实现不太了解的盆友,常常会毫无节制使用$(),比如对于同一个元素的不同操作:

var width=parseInt($('#test').css('width'));
if(width > 20) {
  $('#test').css('backgroundColor', 'red');
}

通过我们上面的一系列分析,我们知道每当我们执行$()时,就会重新生成一个init的实例对象,因此当我们这样没有节制的使用jQuery是非常不正确的,虽然看上去方便了一些,但是对于内存的消耗非常大。正确的做法是既然是同一个对象,那么就用一个变量保存起来后续使用即可。

var $test=$('#test');
var width=parseInt($test.css('width'));
if(width > 20) {
  $test.css('backgroundColor', 'red');
}

3

扩展方法分析

在上面的代码实现中,我还简单实现了两个扩展方法。

jQuery.extend=jQuery.fn.extend=function (options) {

  // 在jquery源码中会根据参数不同进行很多判断,我们这里就直接走一种方式,所以就不用判断了
  var target=this;
  var copy;

  for (name in options) {
    copy=options[name];
    target[name]=copy;
  }
  return target;
}

要理解它的实现,首先要明确知道this的指向。如果你搞不清楚,可以回头去看看我们之前关于this指向的讲解。

传入的参数options为一个key: value模式的对象,我通过for in遍历options,将key作为jQuery的新属性,value作为该新属性所对应的新方法,分别添加到jQuery方法和jQuery.fn中。

也就是说,当我们通过jQuery.extend扩展jQuery时,方法被添加到了jQuery构造函数中,而当我们通过jQuery.fn.extend扩展jQuery时,方法被添加到了jQuery原型中。

上面的例子中,我也简单展示了在jQuery内部,许多方法的实现都是通过这两个扩展方法来完成的。

当我们通过上面的知识了解了jQuery的大体框架之后,我们对于jQuery的学习就可以具体到诸如css/val/attr等方法是如何实现这样的程度,那么源码学习起来就会轻松很多,节省更多的时间。也给一些对于源码敬而远之的朋友提供一个学习的可能。

4

有一个朋友留言给我,说她被静态方法,工具方法和实例方法这几个概念困扰了很久,到底他们有什么区别?

其实在上一篇文章中,关于封装一个对象,我跟大家分享了一个非常非常干,但是却只有少数几个读者大佬get到的知识,那就是在封装对象时,属性和方法可以具体放置的三个位置,并且对于这三个位置的不同做了一个详细的解读。

image.png

在实现jQuery扩展方法时,一部分方法需要扩展到构造函数中,一部分方法需要扩展到原型中,当我们通读jQuery源码时,还发现有一些方法放在了模块作用域中,至于为什么会有这样的区别,建议大家回过头去读读前一篇文章。

这里用一个例子简单区分一下。

// 模块内部
const a=20;

function Person(name, age) {
  this.name=name;
  this.age=age;
  // 构造函数方法,每声明一个实例,都会重新创建一次,属于实例独有
  this.getName=function() {
    return this.name;
  }
}

// 原型方法,仅在原型创建时声明一次,属于所有实例共享
Person.prototype.getAge=function() {
  return this.age;
}

// 工具方法,直接挂载在构造函数名上,仅声明一次,无法直接访问实例内部属性与方法
Person.each=function() {}

如上例中,each就是一个工具方法,或者说静态方法。

工具方法的特性也和工具一词非常贴近,他们与实例的自身属性毫无关联,仅仅只是实现一些通用的功能,我们可以通过$.each与$('div').each这2个方法来体会工具方法与实例方法的不同之处。

在实际开发中,我们运用得非常多的一个工具库就是lodash.js,大家如果时间充裕一定要去学习一下他的使用。

$.ajax()
$.isFunction()
$.each()
... ...

放在原型中的方法,在使用时必须创建了一个新的实例对象才能访问,因此这样的方法叫做实例方法。也正是因为这一点,他的使用成本会比工具方法高一些。但是相比构造函数方法,原型方法更节省内存。

$('#test').css()
$('#test').attr()
$('div').each()

这样,那位同学的疑问就被搞定啦。我们在学习的时候,一定不要过分去纠结一些概念,而要明白这些概念背后的具体场景是怎么回事儿,那么学习这件事情就不会在一些奇奇怪怪的地方卡住了。

所以通过$.extend扩展的方法,其实就是对工具方法的扩展,而通过$.fn.extend扩展的方法,就是对于实例方法的扩展。

5

jQuery插件的实现

我在初级阶段时,觉得要自己编写一个jQuery插件是超级高大上的事情,可望不可即。但是通过对于上面的理解,我就知道扩展jQuery插件其实是一件我们自己也可以完成的事情。

在前面我跟大家分享了jQuery如何实现,以及他们的方法如何扩展,并且前一篇文章分享了拖拽对象的具体实现。所以建议大家暂时不要阅读下去,自己动手尝试将拖拽扩展成为jQuery的一个实例方法,这就是一个jQuery插件了。

具体也没有什么可多说的了,大家看了代码就可以明白一切。

// Drag对象简化代码,完整源码可在上一篇文章中查看
;
(function () {

  // 构造
  function Drag(selector) { }


  // 原型
  Drag.prototype={
    constructor: Drag,

    init: function () {
      // 初始时需要做些什么事情
      this.setDrag();
    },

    // 稍作改造,仅用于获取当前元素的属性,类似于getName
    getStyle: function (property) { },

    // 用来获取当前元素的位置信息,注意与之前的不同之处
    getPosition: function () { },

    // 用来设置当前元素的位置
    setPostion: function (pos) { },

    // 该方法用来绑定事件
    setDrag: function () { }
  }

  // 一种对外暴露的方式
  window.Drag=Drag;
})();

// 通过扩展方法将拖拽扩展为jQuery的一个实例方法
(function ($) {
  $.fn.extend({
    becomeDrag: function () {
      new Drag(this[0]);
      return this;   // 注意:为了保证jQuery所有的方法都能够链式访问,每一个方法的最后都需要返回this,即返回jQuery实例
    }
  })
})(jQuery);

下一篇:前端基础进阶(十四):深入核心,详解事件循环机制

建议收藏,不然刷着刷着就可能找不到了

有什么前端的问题欢迎私信我~期待你的到来。

我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年我花了一个月整理了一份最适合2020年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取

i,大家好,我是拾光。

今天给大家带来的是js实现文本框自动填充。

下面贴图:

HTML:

<!doctype html>

<html>

<head>

<meta charset="UTF-8">

<title>Document</title>

<script src="jquery-1.8.3.min.js"></script>

<style>

*{

margin:0;

padding:0;

}

ul,li{

list-style:none;

}

.inputElem {

width:198px;

height:22px;

line-height:22px;

border:1px solid #ff4455;

}

.parentCls

{

width:200px;

}

.auto-tip li

{

width:100%;

height:22px;

line-height:22px;

font-size:14px;

}

.auto-tip li.hoverBg{

background:#ddd;

cursor:pointer;

}

.red

{

color:red;

}

.hidden {

display:none;

}

</style>

<script src="emailAutoComplete.js"></script>

</head>

<body>

<div>

<input type="text">

</div>

</body>

</html>

JS:

function EmailAutoComplete(options) {

this.config={

targetCls : '.inputElem',

parentCls : '.parentCls',

hiddenCls : '.hiddenCls',

searchForm : '.jqtransformdone',

hoverBg : 'hoverBg',

inputValColor : 'red',

mailArr : ["@qq.com","@qq2.com","@gmail.com","@126.com","@163.com","@hotmail.com","@yahoo.com","@yahoo.com.cn","@live.com","@sohu.com","@sina.com"],

isSelectHide : true,

callback : null

};

this.cache={

onlyFlag : true,

currentIndex : -1,

oldIndex : -1

};

this.init(options);

}

EmailAutoComplete.prototype={

constructor: EmailAutoComplete,

init: function(options){

this.config=$.extend(this.config,options || {});

var self=this,

_config=self.config,

_cache=self.cache;

$(_config.targetCls).each(function(index,item){

$(item).keyup(function(e){

var target=e.target,

targetVal=$.trim($(this).val()),

keycode=e.keyCode,

elemHeight=$(this).outerHeight(),

elemWidth=$(this).outerWidth(),

parentNode=$(this).closest(_config.parentCls);

$(parentNode).css({'position':'relative'});

if(targetVal=='') {

$(item).attr({'data-html':''});

$(_config.hiddenCls,parentNode).val('');

_cache.currentIndex=-1;

_cache.oldIndex=-1;

$(".auto-tip",parentNode) && !$(".auto-tip",parentNode).hasClass('hidden') && $(".auto-tip",parentNode).addClass('hidden');

self._removeBg(parentNode);

}else {

$(item).attr({'data-html':targetVal});

$(_config.hiddenCls,parentNode).val(targetVal);

$(".auto-tip",parentNode) && $(".auto-tip",parentNode).hasClass('hidden') && $(".auto-tip",parentNode).removeClass('hidden');

self._renderHTML({keycode:keycode,e:e,target:target,targetVal:targetVal,height:elemHeight,width:elemWidth,parentNode:parentNode});

}

});

});

$(_config.searchForm).each(function(index,item) {

$(item).keydown(function(e){

var keyCode=e.keyCode;

if(keyCode==13) {

return false;

}

});

});

$(document).click(function(e){

e.stopPropagation();

var target=e.target,

tagCls=_config.targetCls.replace(/^\./,'');

if(!$(target).hasClass(tagCls)) {

$('.auto-tip') && $('.auto-tip').each(function(index,item){

!$(item).hasClass('hidden') && $(item).addClass('hidden');

});

}

});

},

_renderHTML: function(cfg) {

var self=this,

_config=self.config,

_cache=self.cache,

curVal;

var curIndex=self._keyCode(cfg.keycode);

$('.auto-tip',cfg.parentNode).hasClass('hidden') && $('.auto-tip',cfg.parentNode).removeClass('hidden');

if(curIndex > -1){

self._keyUpAndDown(cfg.targetVal,cfg.e,cfg.parentNode);

}else {

if(/@/.test(cfg.targetVal)) {

curVal=cfg.targetVal.replace(/@.*/,'');

}else {

curVal=cfg.targetVal;

}

if(_cache.onlyFlag) {

$(cfg.parentNode).append('<input type="hidden" class="hiddenCls"/>');

var wrap='<ul class="auto-tip">';

for(var i=0; i < _config.mailArr.length; i++) {

wrap +='<li class="p-index'+i+'">'+'<span class="output-num"></span><em class="em" data-html="'+_config.mailArr[i]+'">'+_config.mailArr[i]+'</em></li>';

}

wrap +='</ul>';

_cache.onlyFlag=false;

$(cfg.parentNode).append(wrap);

$('.auto-tip',cfg.parentNode).css({'position':'absolute','top':cfg.height,'width':cfg.width - 2 + 'px','left':0,

'border':'1px solid #ccc','z-index':10000});

}

$('.auto-tip li',cfg.parentNode).each(function(index,item){

$('.output-num',item).html(curVal);

!$('.output-num',item).hasClass(_config.inputValColor) &&

$('.output-num',item).addClass(_config.inputValColor);

var emVal=$.trim($('.em',item).attr('data-html'));

$(item).attr({'data-html':curVal + '' +emVal});

});

self._accurateMate({target:cfg.target,parentNode:cfg.parentNode});

self._itemHover(cfg.parentNode);

self._executeClick(cfg.parentNode);

}

},

_accurateMate: function(cfg) {

var self=this,

_config=self.config,

_cache=self.cache;

var curVal=$.trim($(cfg.target,cfg.parentNode).attr('data-html')),

newArrs=[];

if(/@/.test(curVal)) {

var prefix=curVal.replace(/@.*/, ""),

suffix=curVal.replace(/.*@/, "");

$.map(_config.mailArr,function(n){

var reg=new RegExp(suffix);

if(reg.test(n)) {

newArrs.push(n);

}

});

if(newArrs.length > 0) {

$('.auto-tip',cfg.parentNode).html('');

$(".auto-tip",cfg.parentNode) && $(".auto-tip",cfg.parentNode).hasClass('hidden') &&

$(".auto-tip",cfg.parentNode).removeClass('hidden');

var html='';

for(var j=0, jlen=newArrs.length; j < jlen; j++) {

html +='<li class="p-index'+j+'">'+'<span class="output-num"></span><em class="em" data-html="'+newArrs[j]+'">'+newArrs[j]+'</em></li>';

}

$('.auto-tip',cfg.parentNode).html(html);

$('.auto-tip li',cfg.parentNode).each(function(index,item){

$('.output-num',item).html(prefix);

!$('.output-num',item).hasClass(_config.inputValColor) &&

$('.output-num',item).addClass(_config.inputValColor);

var emVal=$.trim($('.em',item).attr('data-html'));

$(item).attr('data-html','');

$(item).attr({'data-html':prefix + '' +emVal});

});

_cache.currentIndex=-1;

_cache.oldIndex=-1;

$('.auto-tip .output-num',cfg.parentNode).html(prefix);

self._itemHover(cfg.parentNode);

self._executeClick(cfg.parentNode);

}else {

$(".auto-tip",cfg.parentNode) && !$(".auto-tip",cfg.parentNode).hasClass('hidden') &&

$(".auto-tip",cfg.parentNode).addClass('hidden');

$('.auto-tip',cfg.parentNode).html('');

}

}

},

_itemHover: function(parentNode) {

var self=this,

_config=self.config,

_cache=self.cache;

$('.auto-tip li',parentNode).hover(function(index,item) {

!$(this).hasClass(_config.hoverBg) && $(this).addClass(_config.hoverBg);

},function() {

$(this).hasClass(_config.hoverBg) && $(this).removeClass(_config.hoverBg);

});

},

_removeBg: function(parentNode){

var self=this,

_config=self.config;

$(".auto-tip li",parentNode).each(function(index,item){

$(item).hasClass(_config.hoverBg) && $(item).removeClass(_config.hoverBg);

});

},

_keyUpAndDown: function(targetVal,e,parentNode) {

var self=this,

_cache=self.cache,

_config=self.config;

if($('.auto-tip' + ' li',parentNode) && $('.auto-tip' + ' li').length > 0) {

var plen=$('.auto-tip' + ' li',parentNode).length,

keyCode=e.keyCode;

_cache.oldIndex=_cache.currentIndex;

if(keyCode==38) {

if(_cache.currentIndex==-1) {

_cache.currentIndex=plen - 1;

}else {

_cache.currentIndex=_cache.currentIndex - 1;

if(_cache.currentIndex < 0) {

_cache.currentIndex=plen - 1;

}

}

if(_cache.currentIndex !==-1) {

!$('.auto-tip .p-index'+_cache.currentIndex,parentNode).hasClass(_config.hoverBg) &&

$('.auto-tip .p-index'+_cache.currentIndex,parentNode).addClass(_config.hoverBg).siblings().removeClass(_config.hoverBg);

var curAttr=$('.auto-tip' + ' .p-index'+_cache.currentIndex,parentNode).attr('data-html');

$(_config.targetCls,parentNode).val(curAttr);

$(_config.hiddenCls,parentNode).val(curAttr);

}

}else if(keyCode==40) {

if(_cache.currentIndex==plen - 1) {

_cache.currentIndex=0;

}else {

_cache.currentIndex++;

if(_cache.currentIndex > plen - 1) {

_cache.currentIndex=0;

}

}

if(_cache.currentIndex !==-1) {

!$('.auto-tip .p-index'+_cache.currentIndex,parentNode).hasClass(_config.hoverBg) &&

$('.auto-tip .p-index'+_cache.currentIndex,parentNode).addClass(_config.hoverBg).siblings().removeClass(_config.hoverBg);

var curAttr=$('.auto-tip' + ' .p-index'+_cache.currentIndex,parentNode).attr('data-html');

$(_config.targetCls,parentNode).val(curAttr);

$(_config.hiddenCls,parentNode).val(curAttr);

}

}else if(keyCode==13) {

var curVal=$('.auto-tip' + ' .p-index'+_cache.oldIndex,parentNode).attr('data-html');

$(_config.targetCls,parentNode).val(curVal);

$(_config.hiddenCls,parentNode).val(curVal);

if(_config.isSelectHide) {

!$(".auto-tip",parentNode).hasClass('hidden') && $(".auto-tip",parentNode).addClass('hidden');

}

_config.callback && $.isFunction(_config.callback) && _config.callback();

_cache.currentIndex=-1;

_cache.oldIndex=-1;

}

}

},

_keyCode: function(code) {

var arrs=['17','18','38','40','37','39','33','34','35','46','36','13','45','44','145','19','20','9'];

for(var i=0, ilen=arrs.length; i < ilen; i++) {

if(code==arrs[i]) {

return i;

}

}

return -1;

},

_executeClick: function(parentNode) {

var _self=this,

_config=_self.config;

$('.auto-tip' + ' li',parentNode).unbind('click');

$('.auto-tip' + ' li',parentNode).bind('click',function(e){

var dataAttr=$(this).attr('data-html');

$(_config.targetCls,parentNode).val(dataAttr);

if(_config.isSelectHide) {

!$(".auto-tip",parentNode).hasClass('hidden') && $(".auto-tip",parentNode).addClass('hidden');

}

$(_config.hiddenCls,parentNode).val(dataAttr);

_config.callback && $.isFunction(_config.callback) && _config.callback();

});

}

};

$(function() {

new EmailAutoComplete({});

});

今天就到这里咯~

大家研究一下吧~

希望大家过的愉快。

Best wishes to you.

LiuDongYu.