1. 程式人生 > >Python爬蟲 抓取大資料崗位招聘資訊(51job為例)

Python爬蟲 抓取大資料崗位招聘資訊(51job為例)

簡單介紹一下爬蟲原理。並給出 51job網站完整的爬蟲方案。

爬蟲基礎知識

資料來源

網路爬蟲的資料一般都來自伺服器的響應結果,通常有html和json資料等,這兩種資料也是網路爬蟲的主要資料來源。

其中html資料是網頁的原始碼,通過瀏覽器-檢視原始碼可以直接檢視,例如:

簡書主頁部分原始碼示例

json是一種資料儲存格式,往往包含了最原始的資料內容,一般不直接顯示在網頁中,這裡可以通過Chrome瀏覽器>開發者工具中的Network選項捕獲到伺服器返回的json資料,例如:

簡書首頁json資料示例

資料請求

資料請求的方式一般有兩種:GET方法和POST方法。也可以通過Chrome瀏覽器來捕獲訪問一個瀏覽器時的所有請求。這裡以簡書主頁為例,開啟Chrome瀏覽器-開發者工具(F12),切換到Network選項,在位址列輸入http://www.jianshu.com/, 選擇XHR型別,可以看到一條請求的內容,開啟Headers,在General中可以看到請求方式為GET方式,
其中的Request Headers便是訪問這個網頁時的請求資料,如下圖。

Request Headers

這個Headers可以用Python中的字典來表示,包含了使用者請求的一些資訊,例如編碼、語言、使用者登陸資訊、瀏覽器資訊等。

下面還有一個Query String Parameters,這裡麵包含了使用者請求的一些引數,也是請求資料的一部分。

利用requests庫請求資料

利用Python構建資料請求的方式有很多,在python3中,主要有urllib和requests兩個類庫可以實現該功能。urllib是官方標準庫,其官方文件傳送門。這裡主要介紹第三方庫requests,它是基於urllib編寫的,比urllib用起來更加便捷,可以節約時間。

requests安裝方法:

$  pip install
requests

利用requests構建資料請求主要方式:

import requests
req = request.get(url)

或者

import requests
req = requests.post(url)

其中,get()與post()中都可以新增headers、params等引數,以字典的形式傳遞即可。一般來說,簡單的網頁通過傳入url資料即可成功請求資料。不過一些網站採用了反爬蟲機制,需要傳入headers及params等引數,以模擬瀏覽器訪問、使用者登陸等行為,才可以正常請求資料。

利用webdriver請求資料

webdriver是一個用來進行復雜重複的web自動化測試的工具,能夠使用chrome、firefox、IE瀏覽器進行web測試,可以模擬使用者點選連結,填寫表單,點選按鈕等。因此,相對於requests庫來說,webdriver在模擬瀏覽器滑鼠點選滑動等事件上有著天然的優勢,並且真實模擬了瀏覽器的操作,不易被反爬蟲機制發現,因此是一個很好用的爬蟲工具。當然,其缺點在於速度較慢,效率不高。

webdriver安裝:

$ pip install selnium

除了安裝selnium庫,webdriver的執行還需要進行瀏覽器驅動的配置。Chrome、火狐和IE瀏覽器都有其配置方式,具體方法檢視 連結

這裡以IE瀏覽器為例,做一個簡單的示範:

from selenium import webdriver
import os
iedriver = "IEDriverServer.exe"
os.environ["webdriver.ie.driver"] = iedriver
driver = webdriver.Ie(iedriver)

如此,IE瀏覽器配置完畢,其中"IEDriverServer.exe"是IE瀏覽器驅動的儲存路徑。
於是,訪問簡書網主頁資料只需要一步:

driver.get(http://www.jianshu.com/)

資料解析

使用requests請求下來的資料,可以利用.text()方法或者.content()方法訪問,對於文字請求,二者並無太大差別,主要在於編碼問題。具體用法可以參考官方文件,這裡不再贅述。使用webdriver請求下來的資料可以用.page_source屬性獲取。請求下來的資料一般包含了大量的網頁原始碼,如何將其解析以提取出想要的內容?

html型別資料解析

html語言即超文字標記語言,它是由一個個html標籤構成的,是結構化的語言,因此很容易從中匹配提取資訊。這種型別的資料解析的方法有很多,比如利用正則表示式,按照html標籤的結構進行字串匹配,或則利用lxml庫中的xpath方法使用xpath路徑定位到每一個節點、也有類似jQuery的PyQuery方法。這裡主要介紹BeautifulSoup方法。

Beautiful Soup 是一個可以從HTML或XML檔案中提取資料的Python庫.它能夠通過你喜歡的轉換器實現慣用的文件導航,查詢,修改文件的方式.Beautiful Soup會幫你節省數小時甚至數天的工作時間。該介紹來源於其官方中文文件,傳送門。利用BeautifulSoup能夠將html字串轉化為樹狀結構,並非常快速地定位到每一個標籤。

目前版本是BeautifulSoup4,pip安裝方法:

$ pip install BeautifulSoup4

或者,下載bs4的原始碼,然後解壓並執行:

$ python setup.py install 

利用BeautifulSoup解析html資料的關鍵步驟為:

from bs4 import BeautifulSoup
soup = BeautifulSoup(req.contents, "html.parser")

如果採用webdriver請求資料,那麼:

from bs4 import BeautifulSoup
soup = BeautifulSoup(driver.page_source, "html.parser")

如此,便將html資料轉換成BeautifulSoup中的樹狀結構。然後利用BeautifulSoup中的find()、find_all()等方法即可定位到每一個節點。詳情請參閱 官方文件

json型別資料解析

json型別的資料已經是高度結構化的資料,跟Python中字典的表示形式一樣,因此在解析上十分方便。可以通過:

import json
data = json.loads(req.text)

直接讀取json資料,且能夠返回字典型別。

大資料職位資料爬蟲實戰

這裡以51job網站為例,構建大資料相關職位的資料爬蟲。其中搜索關鍵詞為:

資料科學家
資料分析師
資料架構師
資料工程師
統計學家
資料庫管理員
業務資料分析師
資料產品經理

網頁分析

開啟51job首頁http://www.51job.com/, 在搜尋框中輸入“資料科學家”,將搜尋框中的地區點開,去掉當前勾選的城市,即預設在全國範圍搜尋。點選“搜尋”按鈕,得到搜尋結果。這時將網址欄URL複製出來:

 http://search.51job.com/list/000000,000000,0000,00,9,99,
%25E6%2595%25B0%25E6%258D%25AE%25E7%25A7%2591%25E5%25AD%25A6%25E5%25AE%25B6,
2,1.html?lang=c&stype=&postchannel=0000&workyear=99&cotype=99&degreefrom=99
&jobterm=99&companysize=99&providesalary=99&lonlat=0%2C0&radius=-1&ord_field=0
&confirmdate=9&fromType=&dibiaoid=0&address=&line=&specialarea=00&from=&welfare=

結果不止一頁,點選第二頁,同樣將URL複製出來:

http://search.51job.com/list/000000,000000,0000,00,9,99,
%25E6%2595%25B0%25E6%258D%25AE%25E7%25A7%2591%25E5%25AD%25A6%25E5%25AE%25B6,
2,2.html?lang=c&stype=1&postchannel=0000&workyear=99&cotype=99&degreefrom=99
&jobterm=99&companysize=99&lonlat=0%2C0&radius=-1&ord_field=0
&confirmdate=9&fromType=&dibiaoid=0&address=&line=&specialarea=00&from=&welfare=

很容易發現,這兩段url唯一的不同在於".html"前面的數字1和2,因此它代表了頁碼。其中:

%25E6%2595%25B0%25E6%258D%25AE%25E7%25A7%2591%25E5%25AD%25A6%25E5%25AE%25B6

是一種URL編碼,翻譯成中文就是“資料科學家”,轉換方式可以使用urllib庫中的quote()方法:

import urllib.quote
keyword = '資料科學家'
url = quote(keyword)

可以通過第一次的搜尋結果獲取頁碼數:

def GetPages(keyword):
    keyword = quote(keyword, safe='/:?=')
    url = 'http://search.51job.com/jobsearch/search_result.php?fromJs=1&jobarea=000000%2C00&district=000000&funtype=0000&industrytype=00&issuedate=9&providesalary=99&keyword='+keyword + \
      '&keywordtype=2&curr_page=1&lang=c&stype=1&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&lonlat=0%2C0&radius=-1&ord_field=0&list_type=0&fromType=14&dibiaoid=0&confirmdate=9'
    html = requests.get(url)
    soup = BeautifulSoup(html.content, "html.parser")
    span = soup.find('div', class_='p_in').find('span', class_='td')
    page_num = span.get_text().replace('共', '').replace('頁,到第', '')
    return page_num

由此,便可實現針對特定關鍵詞的所有搜尋結果的頁面的遍歷。

URL列表構建

開啟搜尋結果頁面,會發現,點選職位名稱可以連結到每個職位的詳情頁面,也正是所需要的資料來源。因此,只需要獲取所有的搜尋結果中的職位名稱的超連結地址,便可以遍歷所有職位的詳細資料:

def GetUrls(keyword, page_num):
    keyword = quote(keyword, safe='/:?=')
    urls = []
    p = page_num+1
    for i in range(1, p):
        url = 'http://search.51job.com/jobsearch/search_result.php?fromJs=1&jobarea=000000%2C00&district=000000&funtype=0000&industrytype=00&issuedate=9&providesalary=99&keyword='+keyword + \
            '&keywordtype=2&curr_page=' + \
            str(i) + \
            '&lang=c&stype=1&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&lonlat=0%2C0&radius=-1&ord_field=0&list_type=0&dibiaoid=0&confirmdate=9'
        html = requests.get(url)
        soup = BeautifulSoup(html.content, "html.parser")
        ps = soup.find_all('p', class_='t1')
        for p in ps:
            a = p.find('a')
            urls.append(str(a['href']))
        s = random.randint(5, 30)
        print(str(i)+'page done,'+str(s)+'s later')
        time.sleep(s)
    return urls

構造資料請求

在獲取了所有的職位資料的url之後,使用requests訪問這些url發現,並不能順利獲取資料。因此,可以考慮在請求中加入headers資料,其中包含cookie和User_Agent:

User_Agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
cookie = 'guid=14842945278988500031; slife=indexguide%3D1'
headers = {'User-Agent': User_Agent, 'cookie': cookie}

這樣,可以成功請求每個職位的詳情頁面資料:

資料解析

資料解析首先是明確資料需求,這裡將資料儘可能多的抓取下來。

以職位要求一欄為例,通過訪問多個頁面對比發現,這一欄可能顯示的要求個數不一樣:

這裡包括了經驗、學歷、招聘人數和釋出時間

而這裡則沒有對於經驗的要求。

利用瀏覽器開發者選項功能,檢視這一欄的原始碼:

這裡職位的要求都放在一個class="sp4"的span中,通過查詢功能可以發現沒有其他的class="sp4"的標籤,所以利用find_all()方法可以輕鬆定位到這些職位要求資料。

通過比較可以發現這最多的要求個數為4,所以在個數不確定的情況下,可以先新建一個包含四個空字串元素的新陣列,將所有的要求個數填入該陣列,這樣可以保證不同網頁的資料都能獲取完整。

spans = soup.find_all('span', class_='sp4')
num = len(spans)
nav = ['', '', '', '']
for i in range(0, num-1):
    nav[i] = spans[i].get_text().strip()

完整程式碼如下:

# -*- coding: utf-8 -*-
from urllib.parse import quote
import requests
from bs4 import BeautifulSoup
import time
import random


def GetPages(keyword):
    keyword = quote(keyword, safe='/:?=')
    url = 'http://search.51job.com/jobsearch/search_result.php?fromJs=1&jobarea=000000%2C00&district=000000&funtype=0000&industrytype=00&issuedate=9&providesalary=99&keyword='+keyword + \
        '&keywordtype=2&curr_page=1&lang=c&stype=1&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&lonlat=0%2C0&radius=-1&ord_field=0&list_type=0&fromType=14&dibiaoid=0&confirmdate=9'
    html = requests.get(url)
    soup = BeautifulSoup(html.content, "html.parser")
    span = soup.find('div', class_='p_in').find('span', class_='td')
    page_num = span.get_text().replace('共', '').replace('頁,到第', '')
    return page_num


def GetUrls(keyword, page_num):
    keyword = quote(keyword, safe='/:?=')
    urls = []
    p = page_num+1
    for i in range(1, p):
        url = 'http://search.51job.com/jobsearch/search_result.php?fromJs=1&jobarea=000000%2C00&district=000000&funtype=0000&industrytype=00&issuedate=9&providesalary=99&keyword='+keyword + \
            '&keywordtype=2&curr_page=' + \
            str(i) + \
            '&lang=c&stype=1&postchannel=0000&workyear=99&cotype=99&degreefrom=99&jobterm=99&companysize=99&lonlat=0%2C0&radius=-1&ord_field=0&list_type=0&dibiaoid=0&confirmdate=9'
        html = requests.get(url)
        soup = BeautifulSoup(html.content, "html.parser")
        ps = soup.find_all('p', class_='t1')
        for p in ps:
            a = p.find('a')
            urls.append(str(a['href']))
        s = random.randint(5, 30)
        print(str(i)+'page done,'+str(s)+'s later')
        time.sleep(s)
    return urls


def GetContent(url, headers):
    html = requests.get(url, headers=headers)
    soup = BeautifulSoup(html.content, "html.parser")
    PositionTitle = str(soup.find('h1')['title'])
    Location = soup.find('span', class_='lname').string
    Salary = soup.find('strong').string
    CompanyName = soup.find('p', class_='cname').get_text().strip()
    CompanyType = soup.find(
        'p', class_='msg ltype').get_text().strip().replace(' ', '').replace('  ', '').replace('  ', '').replace('  ', '')
    spans = soup.find_all('span', class_='sp4')
    num = len(spans)
    nav = ['', '', '', '']
    for i in range(0, num-1):
        nav[i] = spans[i].get_text().strip()
    Exp = nav[0]
    Degree = nav[1]
    RecruitNum = nav[2]
    PostTime = nav[3]
    Welfare = soup.find('p', class_='t2')
    if str(type(Welfare)) == "<class 'NoneType'>":
        Welfare = ''
    else:
        Welfare = Welfare.get_text().strip().replace('\n', '|')
    PositionInfo = soup.find(
        'div', class_='bmsg job_msg inbox').get_text().strip().replace('\n', '').replace('分享', '').replace('舉報', '').replace('  ', '').replace(' ', '').replace('   ', '').replace('    ', '').replace('\r', '')
    PositionType = soup.find('span', class_='el')
    if str(type(PositionType)) == "<class 'NoneType'>":
        PositionType = ''
    else:
        PositionType = PositionType.get_text().strip().replace('\n', '')
    Contact = soup.find('div', class_='bmsg inbox')
    if str(type(Contact)) == "<class 'NoneType'>":
        Contact = ''
    else:
        Contact = Contact.get_text().strip().replace(
            '   ', '').replace('    ', '').replace('地圖', '').replace('\n', '')
    ConpanyInfo = soup.find('div', class_='tmsg inbox')
    if str(type(ConpanyInfo)) == "<class 'NoneType'>":
        ConpanyInfo = ''
    else:
        ConpanyInfo = ConpanyInfo.get_text().strip().replace(
            '\n', '').replace('  ', '').replace(' ', '')
    try:
        record = PositionTitle+'\t'+Location+'\t'+Salary+'\t'+CompanyName+'\t'+CompanyType+'\t'+Exp+'\t'+Degree+'\t' + \
            RecruitNum+'\t'+PostTime+'\t'+Welfare+'\t'+PositionInfo + \
            '\t'+str(PositionType)+'\t'+str(Contact)+'\t'+str(ConpanyInfo)
    except Exception as e:
        record = ''
    else:
        pass
    finally:
        pass
    return record


def main():
    with open('keywords.txt', 'r', encoding='utf-8') as f:
        keywords = f.readlines()
    for keyword in keywords[1:]:
        keyword = keyword.strip()
        page_num = int(GetPages(keyword))
        urls = GetUrls(keyword, page_num)
        with open(keyword+'urls.txt', 'w', encoding='utf-8') as f:
            for url in urls:
                f.write(url+'\n')
        User_Agent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
        cookie = 'guid=14842945278988500031; slife=indexguide%3D1'
        headers = {'User-Agent': User_Agent, 'cookie': cookie}
        with open(keyword+'urls.txt', 'r', encoding='utf-8') as f:
            urls = f.readlines()
        records = []
        i = 0
        for url in urls:
            url = url.strip()
            if url != '':
                records.append(
                    GetContent(url, headers))
                i += 1
                s = random.randint(5, 30)
                print(str(i)+'page done,'+str(s)+'s later')
                time.sleep(s)
        with open(keyword+'.txt', 'w', encoding='utf-8') as f:
            for re in records:
                f.write(re+'\n')
        print(keyword+' Done---------------------------')


if __name__ == '__main__':
    main()