爬取拉钩网,简单数据分析
最近和几个兴趣相投的同学跟着老师做一个项目,在这之中意识到必须获取海量数据,而数据从哪里来呢?想到 python
可以从网页上抓取数据,便打算自己写个小项目来巩固自己的爬虫知识。
整个项目的地址:https://github.com/New-generation-hsc/LaGou
写整个项目的时候我参考过的文章:
1、 https://github.com/YikaJ/lagou_crawler/tree/master/Lagou
2、http://www.jianshu.com/p/e9a1c1d5668e
一、工欲善其事,必先利其器
- IDE 我用的是Pycharm (推荐)
- sublime Text 3
requests
+pyquery
网页抓取和网页分析django 1.10
作为数据分析成果展示的框架pygal
+bootstrap 3
图表绘画,网页显示
二、网页分析
- 1、确定我们的目标是 拉勾网
- 2、确定我们提取的信息
这是一个Python 职位的描述,其他职位类似。
三、信息提取
拉勾网职位.png获取首页的全部职位代码的如下:
lass LG(object):
"""A base class generate the position link"""
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/55.0.2883.87 Safari/537.36'
}
self.start_url = 'https://www.lagou.com/'
self._positions = None
# a dict store the url of every job
# eg. 'python' : 'www.ladou.com/zhaopin/Python/'
def get_page_code(self, url):
"""get the given url page code"""
try:
response = requests.get(url, headers=self.headers)
if response.status_code == 200:
return response.content.decode('utf-8')
else:
logger.error("Get page source code error")
return None
except ConnectionError as e:
logger.error("requests connection error")
return None
@property
def positions(self):
"""get all the position link"""
if not self._positions:
queryset = Job.objects.all()
if queryset.exists():
position_dict = {}
for query in queryset:
position_dict[query.position] = query.url
else:
position_dict = self._parse_postion_link()
self._positions = position_dict
return self._positions
def _parse_postion_link(self):
html = self.get_page_code(self.start_url)
query = PyQuery(html)
position_dict = {}
position_data = query(".menu_sub dd a").items()
for _position in position_data:
name = _position.text()
link = _position.attr("href")
position_dict[name] = link
if name and link:
instance = Job(position=name, url=link)
instance.save()
return position_dict
不知道你会不会觉得写成类有些多余,明明写几个函数就OK。最近我从图书馆里借了一本书《Python3 面向对象编程》所以正在训练一种思维,就是通过类来管理自己的代码,让自己的代码重用性变得更高。
至于@property
,我想有些人可能觉得这有必要,不用这个也能做到呀。在这里我引用书中的一个例子来说明他的好处:
假如有一个定制化行为的普遍需求,他要求对那些难以计算或者查找起来花费过大的值(假如一个网络请求或者是数据库查询)进行缓存。我们的目的是在本地存储这个值以避免重复调用那些花费过大的计算。
我们可以通过在property
属性中使用自定义的getter
来达到这个目的。当该值第一次被检索的时候,我们执行查找或计算。接着就可以将这个值以对象中私有属性进行缓存在本地。之后,当再次请求这个值的时候,我们就可以将这个值时,我们就可以返回存储的数据。
将所有的职位的url从网页中提出来以后,我以 Python
为例,来讲解抓取具体信息。
Python
的url : https://www.lagou.com/zhaopin/Python/
下面是获取每个具体职位的url的代码:
def parse_job_link(self, page=1):
"""get all job link of one page"""
try:
url = self.positions[self.keyword] + '{}/'.format(page)
response = self.get_page_code(url)
logger.info("Ready to crawl the {}th page link".format(page))
query = PyQuery(response)
item_lists = query(".item_con_list li").items()
for item in item_lists:
link = item(".position_link").attr("href")
self.link_queue.put(link)
print(link)
except KeyError:
logger.error("NO such job")
对网页进行简单分析就可以知道:
- 第一页 url = https://www.lagou.com/zhaopin/Python/1/
- 第二页 url = https://www.lagou.com/zhaopin/Python/2/
即每一页的url = https://www.lagou.com/zhaopin/Python/ + page
每一页有着15个招聘,爬下每个招聘的url 又该如何存储? 在存储这一方面我借鉴了其他人的代码,把所有的url 放到一个queue
。这样的好处就是稍后我们便可以开启多个线程同时并发,同时从这个queue
中获取一个url,并对他进行解析,而不用关心是否会有线程冲突,从而加快解析的速度。
下一步
我们想要的数据是每个岗位的年薪,工作地点,工作经验,学历要求,任职要求
数据.png 任职要求.png通过对任职要求中相应能力进行词频统计,最后显示出掌握那种能力的需求最高。
下面是抓取相应信息的具体代码:
def parse_job_info(self, link):
"""get the job information"""
response = self.get_page_code(link)
query = PyQuery(response)
infor = query("dd.job_request")
salary = infor("span.salary").text().strip()
location = infor("span:nth-child(2)").text().strip('/')
expreience = infor("span:nth-child(3)").text().strip('/')
degree = infor("span:nth-child(4)").text().strip('/')
information = Information(url=link, salary=salary, location=location, expreience=expreience, degree=degree)
job = Job.objects.filter(position__iexact=self.keyword).first()
information.job = job
information.save()
description = query("#job_detail > dd.job_bt > div")
logger.info("正在爬取第...个职位描述")
text = description.text()
self._search_skill(text)
def _search_skill(self, text):
rule = re.compile('([a-zA-Z]+)')
results = rule.findall(text)
self.skills.extend(results)
def count_skill(self):
for i in range(len(self.skills)):
self.skills[i] = self.skills[i].lower()
_skill_frequency = Counter(self.skills).most_common(160)
三、数据存储
爬下来这么多数据如何存放?不可能每次需要的时候再去爬取一遍吧,这样实在是太慢了。所以我把它存在了 mysql
中,但是编写SQL语句真的是一种很心累的活,还有如果表没有设计好的话,真的是一种抓狂的感觉。
所以 Flask + sqlalchemy
真心是一种不错的选择,基于模型来操作数据库,再也不用担心不会书写SQL语句了。可能是我自己不熟悉 flask
, 总是感觉用起来不是很方便,于是果断的使用了 django
, 我知道这是大材小用了,但是真心用的爽。
我的数据库是这样设计的:
class Job(models.Model):
position = models.CharField(max_length=100)
# eg. 'python', 'Java', 'C++', 'PHP'
url = models.URLField()
class Meta:
ordering = ['position']
def __str__(self):
return self.position
class Information(models.Model):
url = models.URLField()
salary = models.CharField(max_length=30)
location = models.CharField(max_length=30)
expreience = models.CharField(max_length=30, null=True, blank=True)
degree = models.CharField(max_length=30, null=True, blank=True)
job = models.ForeignKey(Job, related_name='job_info')
class Meta:
ordering = ['salary', 'expreience']
def __str__(self):
return "{}/{}/{}/{}".format(self.salary, self.location, self.expreience, self.degree)
class Skill(models.Model):
skill = models.CharField(max_length=30)
frequency = models.IntegerField()
# 职位要求中的能力词频统计
job = models.ForeignKey(Job, related_name='job_skill')
class Meta:
ordering = ['-frequency']
def __str__(self):
return "{}: {}".format(self.skill, self.frequency)
四、数据展示
看到网上大部分都是js + json进行绘画图表,但是本人js学的还不够到位,所以还是借助 Python
的库 pygal
来进行展示。
在数据展示的首页会有一个搜索框,用户可以搜索自己喜欢的职位,来查看相应的数据信息。
数据展示层的代码如下:
def index(request):
keyword = request.GET.get('search', None)
# 获取用户在搜索框中输入的文字,比如某个职位
if keyword == None:
keyword = 'Python'
position = Job.objects.filter(position__iexact=keyword).first()
queryset = position.job_skill.all()[:10]
bar_chart = pygal.Bar()
bar_chart.title = 'ability frequency in {} recruit'.format(keyword)
for query in queryset:
bar_chart.add(query.skill, query.frequency)
pie_chart = pygal.Pie()
pie_chart.title = "expreience in {} recruit".format(keyword)
pie_chart.add("经验不限", position.job_info.filter(expreience="经验不限").count())
pie_chart.add("经验1-3年", position.job_info.filter(expreience="经验1-3年").count())
pie_chart.add("经验3-5年", position.job_info.filter(expreience="经验3-5年").count())
pie_chart.add("经验5-10年", position.job_info.filter(expreience="经验5-10年").count())
line_chart = pygal.Line()
line_chart.title = "salary in {} recruit".format(keyword)
salaries = ["10k-15k", "10k-18k", "10k-20k", "15k-30k", "25k-30k", "20k-40k"]
line_chart.x_labels = salaries
line_chart.add('salary', [position.job_info.filter(salary=salary).count() for salary in salaries])
location_chart = pygal.Pie()
location_query = position.job_info.all()
locations = [obj.location for obj in location_query]
location_count = Counter(locations).most_common(8)
for query in location_count:
location_chart.add(query[0], query[1])
context = {
'data': bar_chart.render(),
'pie_chart': pie_chart.render(),
'line_chart': line_chart.render(),
'location_chart': location_chart.render()
}
return render(request, 'index.html', context)
最后成果如下:
能力要求.png 经验要求.png 年薪状况.png 工作地点分布.png整个项目到此结束了,重构了有两三遍吧。
一个热爱技术的学生党,在今后的生活中会慢慢分享自己学到的东西。我希望能有兴趣相投的人能和我一起战斗。