各大企業招聘數據抓取思路以及細節處理

去年應公司業務需求,分析師們給了我幾張關於各種公司的數據表、包括中國企業500強、創投公司、上市公司等幾十萬家公司數據。要求我抓取這些公司的招聘信息。此項目是我大三實習時獨立開發的一個較爲滿意的項目,今日在硬盤裏發現了其中部分代碼,自認爲此項目挺有意思,所以把整個項目思路和過程整理下來。php

項目要求與抓取數據源肯定

要求

要求從boss直聘、前程無憂、智聯招聘、中華英才網四大招聘網站裏選取其中一個招聘網站抓取數據。首先實現全量數據抓取,接着增量數據抓取,之後每日自動更新數據庫數據。html

抓取網站選型

爲了避免給後續抓取過程當中留坑,也爲了可以儘快完美的完成任務,選擇一個易於抓取和穩定的網站相當重要。花了2-3天研究了四大招聘網站的特色。node

boss直聘:數據量較爲豐富,崗位搜索結果較爲準確,IP地址容易被封,並且解封須要24小時(這裏真得吐槽下,我人爲用瀏覽器手動搜索,翻了幾十個頁面就把我IP封了,這樣作很容易中傷用戶,不過今年遇到一個boss直聘開發人員,說是網站已經改了這些反爬措施)python

智聯招聘:數據量較少 ,海量快速抓取過程當中可能存在觸發驗證碼登陸mysql

中華英才網:數據量較少,崗位搜索結果特別不許確git

前程無憂:數據量較爲豐富,崗位搜索結果較爲準確,反爬措施較少。github

對各大招聘網站使用單ip發送請求,結果發現,在ip被封以前前程無憂的訪問量最多。 因此,從數據量、數據準確性、抓取難度等幾個方面綜合考慮,前程無憂是最佳選擇。sql

列表頁信息抓取

在這以前,我本想着去網上找個抓取源碼借鑑一下,雖然發現網上是有一些關於前程無憂的抓取代碼。可是發現這些代碼基本都是'隨便寫寫',幾乎沒有借鑑之處。主要問題在於,網上的抓取源碼都是以招聘崗位進行抓取,只抓取某一個崗位的招聘信息,數據量不多,一次入庫,並且只有一套頁面解析規則,不少細節之處沒有處理,數據存在嚴重錯誤以及缺漏。我本身分析了一下個人這個項目的不一樣之處以及網站的頁面,是按照公司名稱進行抓取,數據量較大,每日定時自動抓取,並且每一個崗位信息詳情頁並不都是千篇一概的構造,須要針對實際狀況多寫幾套解析規則,才能抓取到特殊頁面的信息。數據庫

模塊導入

# -*- coding: utf-8 -*-
#這裏的agent_proxy模塊是本身另外寫的請求頭和IP代理隨機獲取模塊
from agent_proxy import get_random_agent,get_useful_proxy
import requests
from bs4 import BeautifulSoup
import threading
import HTMLParser
import datetime
import pymysql
import re
import ssl
import time
import random
import sys
from Queue import Queue
reload (sys)
sys.setdefaultencoding ("utf-8")
ssl._create_default_https_context = ssl._create_unverified_context

複製代碼

URL解碼

由於是以公司名稱爲關鍵詞來查找招聘信息,因此要找出關鍵詞公司名稱和url的對應變化關係。瀏覽器

好比小米招聘url以下:

search.51job.com/list/000000…

看着不少參數,無規律可言,可是把這個連接解碼兩次:

按照一樣的方式解碼‘字節跳動’url:

仔細一對比,發現變化公司名稱,對應的url裏只是換了公司名一個參數,其餘都同樣。

使用解碼後的url,去掉多餘參數進行搜索: search.51job.com/list/000000…?

發現和原來的搜索結果是一致的。

因此後續的公司招聘信息頁面可經過這個URL更改company字段獲得:

search.51job.com/list/000000…',1,1.html?’

列表頁信息獲取

從數據庫獲取到公司名,經過公司名稱獲取對應公司招聘信息列表頁

page_url='https://search.51job.com/list/000000,000000,0000,00,9,99,'+each_company+',1,1.html?'
    try:
        res=requests.get(page_url,headers =headers,proxies=page_proxie)
        soup = BeautifulSoup (res.content, 'html.parser', from_encoding='utf8')
        url_tag = soup.find_all ('p', class_="t1")
    except Exception as e:
        pass
    # 若是沒有符合條件的職位信息,調用delete_com_tail繼續獲取信息
    if not url_tag:
        delete_com_tail (each_company,each_id,page_proxie)
    #若是有符合條件的職位信息,調用parse_index_page進一步處理包含信息的url_tag標籤
    else:
        parse_index_page (soup,url_tag,each_company,each_id,page_proxie)
複製代碼

前程無憂裏的搜索機制是有缺陷的,不少公司的名稱全稱後綴通常都帶有'控股有限公司'、'股份有限公司'、'有限公司'這些詞,好比阿里巴巴集團,通常咱們說他的全稱是'阿里巴巴集團控股有限公司',包括我數據庫裏的名稱也是'阿里巴巴集團控股有限公司',可是當我用這個名稱查詢對應的招聘信息的時候,返回結果爲空,以下所示:

當去掉後綴'控股有限公司',用'阿里巴巴集團'來搜索是能匹配到正確的職位信息的

因此當用公司全稱查詢不到相關信息的時候,能夠去掉公司名後綴:

if '控股有限公司' in each_company:
        each_company = each_company.replace, "")
    elif '股份有限公司' in each_company:
        each_company = each_company.replace ("股份有限公司", "")
    elif '有限公司' in each_company:
        each_company = each_company.replace ("有限公司", "")
    if each_company == '北京':
        return
複製代碼

而後用新的公司名each_company獲取新的頁面並從新解析。

列表頁翻頁處理

有些公司的招聘信息較少,總數據量一頁不到,有些比較多,須要判斷是一頁數據仍是多頁數據,而後分別解析。

def parse_index_page(soup,url_tag,each_company,each_id,page_proxie):
    """ 解析列表頁,判斷是否須要翻頁,並進行進一步處理 :param soup: :param url_tag: :param each_company: :param each_id: :param page_proxie: :return: """
    page_num = soup.find ('span', class_="td").text
    page_num = re.findall ("\d+", page_num)[ 0 ]

    if int(page_num)!=1:#頁數>1
        try:
            for page in range(2,int(page_num)+1):
                if 'strong class="title' not in str(url_tag):
                    next_page_url = 'https://search.51job.com/list/000000,000000,0000,00,9,99,'+str(each_company)+',1,' + str(page) + '.html?'
                    res = requests.get (next_page_url, headers=headers, proxies=proxies)
                    soup = BeautifulSoup (res.content, 'html.parser', from_encoding='utf8')
                    url_tag = soup.find_all ('p', class_="t1")
                else:
                    next_page_url='https://mq.51job.com/joblist.php?keyword='+str(each_company)+'&jobarea=000000&lang=c&page='+str(page)
                    res = requests.get (next_page_url, headers=headers, proxies=proxies)
                    soup = BeautifulSoup (res.content, 'html.parser', from_encoding='utf8')
                    url_tag = soup.find_all ('strong', class_="title")
                print next_page_url
                page_proxie=get_useful_proxy()
                parse_tag_url (url_tag, each_company, each_id, page_proxie)
        except Exception as e:
            pass
    else:#頁數=1
        parse_tag_url (url_tag, each_company, each_id, page_proxie)
複製代碼

查重(判斷數據是否已經被抓取)

有些數據可能被抓取過,每條頁面都對應一個惟一的URL,因此用URL在數據庫中進行索引查詢,若是不存在,則繼續抓取

def parse_tag_url(url_tag,each_company,each_id,page_proxie):
    """ #處理列表頁的每一個標籤,取出詳情頁url,根據url在mysql進行去重 :param url_tag: :param each_company: :param each_id: :param page_proxie: :return: """
    for each in url_tag:
        each_url = each.a[ 'href' ]
        # 根據job_url去重
        sql = "select job_url from ntf_tyc_com_job where job_url = ('%s')" % (each_url)
        try:
            db2.ping (reconnect=True)
            res = cursor2.execute (sql)
            if res:
                print "數據存在:" + each_url
                continue
        except:
            print "Error"
        detail_page (each_url, each_company, each_id, page_proxie)
複製代碼

詳情頁解析

終於到了咱們須要獲取的最終數據的詳細頁面,抓取數據以下:

可是多翻翻幾個職業信息的頁面,能夠發現每一個頁面結構並非都相同,好比有的頁面就沒有‘學歷要求’,有的出現了一些無用信息的標籤,有的忽然冒出一個外鏈標籤,限於篇幅,這裏就不截圖一一列舉了,所以須要作很多的細節處理。詳細處理過程可見代碼:

公司名、數據源、數據庫關聯、url、公司名稱、崗位抓取

def detail_page (each_url,each_company,each_id,page_proxie):
    """ #解析詳情頁,取出須要信息 :param each_url: :param each_company: :param each_id: :param page_proxie: :return: """
    time.sleep (0.001)

    try:
        res = requests.get (each_url, headers = headers, proxies=page_proxie)
        soup = BeautifulSoup (res.content, 'html.parser', from_encoding='utf8')
        #p判斷是不是測試的崗位,若是是,丟棄
        test_job = soup.find ('div', class_='bmsg inbox')
        if test_job and '測試使用' in str(test_job):
            return
        job_info = {}

        job_info[ 'com_name' ] = each_company # 公司名稱

        job_info[ 'come_source' ] = u'前程無憂'#數據源

        job_info[ 'com_id' ] = each_id #公司id,和天眼查公司關聯取id

        job_info[ 'job_url' ] = each_url # 來源url

        job_info[ 'com_sname' ] = soup.find ('p', class_="cname").text # 招聘公司名稱
        job_info[ 'job_name' ] = soup.find ('h1').text # 招聘崗位


        job_info[ 'job_salary' ] = soup.find ('div', class_="cn").strong.text # 薪資
        info = soup.find_all ('span', class_="sp4")
複製代碼

詳情頁工做經驗、(有無)學歷要求、招聘人數、發佈時間、截止時間的抓取

if len (info) == 4:

            job_info[ 'work_exp' ] = info[ 0 ].text # 工做經驗

            job_info[ 'job_edu' ] = info[ 1 ].text # 學歷要求

            job_info[ 'recruit_number' ] = info[ 2 ].text.replace("招","") # 招聘人數

            pub_time = info[ 3 ].text.replace ("發佈", "")
            pub_time_v2 = '2018-' + str (pub_time)
            job_info[ 'stat_date' ] = pub_time_v2.replace ("-", "/")  # 招聘發佈時間

            d = datetime.datetime.strptime (pub_time_v2, '%Y-%m-%d')
            seven_day = datetime.timedelta (days=7)
            da_days = d + seven_day
            job_info[ 'closing_date' ] = da_days.strftime ('%Y-%m-%d').replace ("-", "/")# 招聘截止時間

        # 詳情頁只有工做經驗、招聘人數、發佈時間、沒有學歷的狀況
        else:
            job_info[ 'work_exp' ] = info[ 0 ].text# 工做經驗

            job_info[ 'recruit_number' ] = info[ 1 ].text # 招聘人數

            pub_time = info[ 2 ].text.replace ("發佈", "")
            pub_time_v2 = '2018-' + str (pub_time)
            job_info[ 'stat_date' ] = pub_time_v2.replace ("-", "/")# 招聘發佈時間

            d = datetime.datetime.strptime (pub_time_v2, '%Y-%m-%d')
            seven_day = datetime.timedelta (days=7)
            da_days = d + seven_day
            job_info[ 'closing_date' ] = da_days.strftime ('%Y-%m-%d').replace ("-", "/")   # 招聘截止時間

複製代碼

這裏值得一提的是,由於頁面裏並無招聘截止日期這一數據,可是一個崗位的招聘確定具備時效性,一個崗位不可能一直招聘,存在數據庫裏時間久了會成爲失效數據。因此在數據表中添加一個招聘截止時間。

招聘截止時間=招聘發佈時間+七天

因此在後面,噹噹前日前大於招聘截止時間時,就自動刪除數據庫中的這條招聘數據。保證了招聘數據的時效性。

城市以及區域抓取

job_info[ 'city' ]=soup.find ('div', class_="cn").span.text#城市
        if '-' in job_info['city']:#有「-」符號,取出城市下面的區
            city_area = job_info[ 'city' ]
            job_info[ 'city' ] = job_info[ 'city' ].split ('-')[ 0 ]

            job_info[ 'area' ] = city_area.split ('-')[ 1 ]   #城市下面的區域

複製代碼

職位詳情抓取以及無用信息標籤、外鏈標籤的處理

job_des_tag = soup.find ('div', class_="bmsg job_msg inbox")
        parser = HTMLParser.HTMLParser ()
        node = parser.unescape (job_des_tag)

        job_belong_tag = node.find ('div', class_="mt10")
        if job_belong_tag:
            job_belong_tag.decompose () # 去除包含工做類別、關鍵字等無用信息標籤

        share_tag = node.find ('div', class_="share")
        share_tag.decompose ()   # 去除分享外鏈的標籤

        job_info[ 'job_des' ] = node.text
複製代碼

數據入庫

def insert_mysql(job_info):
    """ #數據插入mysql數據庫 :param job_info: :return: """
    # lock.acquire()
    keys = ','.join (job_info.keys ())
    values = ','.join ([ '%s' ] * len (job_info))
    sql = 'INSERT INTO ntf_tyc_com_job({keys}) values({values})'.format (keys=keys, values=values)
    db2.ping (reconnect=True)
    try:
        if cursor2.execute (sql, tuple (job_info.values ())):
            print 'Data insert Succesful'
            db2.commit ()
    except:
        print "Data insert Failed"
        db2.rollback ()
    db2.close ()
複製代碼

程序每日對前程無憂會有100W+的訪問,前幾日每日數據庫更新數據量爲10W+

程序加速

多線程加速

sql = 'SELECT com_name,com_id FROM abc_company_top500 WHERE id <= 500'
    db.ping (reconnect=True)
    cursor.execute (sql)
    rowQueue = Queue(500)
    rows = cursor.fetchall ()
    for row in rows:
        rowQueue.put(row)
    threadcrawl = []
    while True:
        if threading.active_count() < 10:
            try:
                t = threading.Thread (target=get_page_url, args=(rowQueue.get(False),))
                t.setDaemon (True)
                threadcrawl.append(t)
                t.start ();
            except:
                pass

    for thread in threadcrawl:
        thread.join()

複製代碼

寫到這裏我才發現這版代碼是初代代碼,不是最終的代碼,最終的掛到服務器上的代碼留上家公司了沒copy下來,這版代碼仍是有點問題,多線程對於python的計算型的指令並不能明顯提速,爬蟲程序裏的多線程應該寫在I/O密集操做的部分,好比數據庫的插入部分。記得終版代碼裏就是將多線程寫在數據庫的插入部分,開了8-10條線程、速度提升了6-8倍。其實還能夠寫多進程、協程提速的,想一想當時大三,技術真是菜的一批。

數據庫查找加速

這裏是抓取500的這張數據表的數據,數據量較小。在獲取別的公司表數據的時候,一個表幾十萬,幾百萬數據。每次從數據庫裏依次取1000條數據加入迭代器裏進行抓取,開始sql查詢語句是用的between ...and, 後來發現用分頁查詢limit start count,在數據表很大的狀況下,可以明顯提升查詢速度。

抓取時間問題

畢竟本身是隻有道德的爬蟲,把爬蟲程序放大服務器上後,爲了避免給對方服務器形成太大壓力(仔細想一想應該也沒啥壓力,對於這類大型的招聘網站,一天幾十萬幾百萬的訪問量應該也不算啥),在訪問量高的白天限速抓取,在訪問量低的晚上高速抓取。

最後總結

寫個小爬蟲程序挺容易,可是當數據量大、頁面結構複雜多變的時候就比較麻煩了,既要提升抓取速度,同時也要考慮避免觸發反爬機制,若是要將數據抓取全面時,須要考慮不少細節,而且一個個處理掉,不然數據裏會出現錯誤或者紕漏等問題。程序寫起來花了2天,可是檢查數據,修正細節花了3天。最後才能放心的鏈接數據庫、放到服務器上實現全量+增量抓取。

最後代碼奉上: github.com/jiahaoabc/z…

技術比較菜,獻醜了,各位大佬見諒

相關文章
相關標籤/搜索