爬取拉钩网,简单数据分析

最近和几个兴趣相投的同学跟着老师做一个项目,在这之中意识到必须获取海量数据,而数据从哪里来呢?想到 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、确定我们提取的信息
拉钩网职位.png

这是一个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/

具体职位.png

下面是获取每个具体职位的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/ + 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 来进行展示。
在数据展示的首页会有一个搜索框,用户可以搜索自己喜欢的职位,来查看相应的数据信息。

搜索.png

数据展示层的代码如下:

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

整个项目到此结束了,重构了有两三遍吧。
一个热爱技术的学生党,在今后的生活中会慢慢分享自己学到的东西。我希望能有兴趣相投的人能和我一起战斗。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容