Selenium&Chrome實戰:動态爬取51job招聘信息

發布時間:2018-08-09 21:03:02編輯:Run閱讀(4603)

    Selenium3.8版本以後,已經不支持PhanTomJS了,可以使用谷歌,火狐的無頭浏覽器來代替PhanTomJS


    使用chrome的無頭浏覽器,需要下載谷歌驅動chromedriver.exe

    chromedriver.exe下載  淘寶的鏡像下載地址:https://npm.taobao.org/mirrors/chromedriver/


    對應系統下載最新版,這裡我用的win,下載win32,這裡需要注意chromedriver與chrome(谷歌浏覽器的版本)的對應關系表.

    blob.png


    我的chrome版本是:68


    blob.png

    下載chromedriver2.41

    blob.png


    下載完後,解壓到桌面裡面有個chromedriver.exe文件

    blob.png



    Selenium設置使用Chrome無頭浏覽器

    #!/usr/bin/env python
    # coding: utf-8
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    
    # 創建chrome參數對象
    chrome_options = Options()
    
    # 把chrome設置成無界面模式,不論windows還是linux都可以,自動适配對應參數
    chrome_options.set_headless()
    
    # 創建chrome無界面對象
    # 方式一: # executable_path為下載的chromedriver.exe的路徑
    # browser = webdriver.Chrome(options=chrome_options, 
    #                            executable_path=r'C:\Users\dell\Desktop\chromedriver_win32\chromedriver.exe')
    
    # 方式二:複制chromedriver.exe到python.exe的目錄下,兩種方法任意其一
    browser = webdriver.Chrome(options=chrome_options)


    使用Selenium&chrome無頭浏覽器爬取 ----->  51job招聘網站的招聘信息

    Selenium自動化測試工具,可模拟用戶輸入,選擇,提交

    爬蟲實現的功能:

     1  輸入python,選擇地點:上海,北京 ---->就去爬取上海,北京2個城市python招聘信息

     2  輸入會計,選擇地址:廣州,深圳,杭州---->就去爬取廣州,深圳,杭州3個城市會計招聘信息

     3  根據輸入的不同,動态爬取結果 

                             


    目标分析:

    selenium怎麼模拟用戶輸入關鍵字,怎麼選擇城市,怎麼點擊搜索按鈕?

    blob.png


    Selenium模拟用戶輸入關鍵字,谷歌浏覽器右鍵輸入框,點檢查,查看代碼

    blob.png


    通過selenium的find_element_by_id 找到 id = 'kwdselectid',然後send_keys('關鍵字')即可模拟用戶輸入

    代碼為:

    textElement = browser.find_element_by_id('kwdselectid')
    textElement.send_keys('python')


    selenium模拟用戶選擇城市--- (這個就難了,踩了很多坑)

    點擊城市選擇,會彈出一個框

    blob.png


    然後選擇:北京,上海,  右鍵檢查,查看源代碼

    blob.png


    可以發現:value的值變成了"北京+上海"

    那麼是否可以用selenium找到這個标簽,更改它的屬性值為"北京+上海",可以實現選擇城市呢?

    答案:不行,因為經過自己的幾次嘗試,發現真正生效的是下面的"010000,020000",這個是什麼?城市編号,也就是說在輸入"北京+上海",實際上輸入的是:"010000,020000", 那這個城市編号怎麼來的,這個就需要去爬取51job彈出城市選擇框那個頁面了,頁面代碼裡面有城市對應的編号


    獲取城市編号getcity.py代碼:

    #!/usr/bin/env python
    # coding: utf-8
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    import json
    
    
    # 設置selenium使用chrome的無頭模式
    chrome_options = Options()
    chrome_options.set_headless()
    browser = webdriver.Chrome(options=chrome_options, executable_path=\
        r'C:\Users\taoru\Desktop\chromedriver_2.41\chromedriver.exe')
    cookies = browser.get_cookies()
    browser.delete_all_cookies()
    browser.get('https://www.51job.com/')
    browser.implicitly_wait(20)
    
    # 找到城市選擇框,并模拟點擊
    button = browser.find_element_by_xpath("//div[@class='ush top_wrap']//div[@class='el on']/p\
    [@class='addbut']//input[@id='work_position_input']").click()
    
    # 選中城市彈出框
    browser.current_window_handle
    
    # 定義一個空字典
    dic = {}
    
    # 找到城市,和對應的城市編号
    find_city_elements = browser.find_elements_by_xpath("//div[@id='work_position_layer']//\
    div[@id='work_position_click_center_right_list_000000']//tbody/tr/td")
    for element in find_city_elements:
        number = element.find_element_by_xpath("./em").get_attribute("data-value")  # 城市編号
        city = element.find_element_by_xpath("./em").text  # 城市
        # 添加到字典
        dic.setdefault(city, number)
    print(dic)
    # 寫入文件
    with open('city.txt', 'w', encoding='utf8') as f:
        f.write(json.dumps(dic, ensure_ascii=False))
    browser.quit()

    運行程序,結果如下:

    {'大連': '230300', '天津': '050000', '成都': '090200', '甯波': '080300', '上海': '020000', '西安': '200200', '重慶': '060000', '鄭州': '170200', '杭州': '080200', '青島': '120300', '沈陽': '230200', '東莞': '030800', '濟南': '120200', '哈爾濱': '220200', '南京': '070200', '長春': '240200', '北京': '010000', '福州': '110200', '廣州': '030200', '深圳': '040000', '昆明': '250200', '蘇州': '070300', '合肥': '150200', '武漢': '180200', '長沙': '190200'}


    通過selenium的find_element_by_xpath 找到城市編号這個input,然後讀取city.txt文件,把對應的城市替換為城市編号,在用selenium執行js代碼,就可以加載城市了---代碼有點長,完整代碼寫在後面



    selenium模拟用戶點擊搜索

    通過selenium的find_element_by_xpath 找到 這個button按鈕,然後click() 即可模拟用戶點擊搜索

    代碼為:

    browser.find_element_by_xpath("//div[@class='ush top_wrap']/button").click()


    以上都是模拟用戶搜索的行為,下面就是對數據提取規則

    先定位總頁數:552頁

    blob.png


    找到每個崗位詳細的鍊接地址:

    blob.png


    最後定位需要爬取的數據

    崗位名,薪水,公司名,招聘信息,福利待遇,崗位職責,任職要求,上班地點,工作地點 這些數據,總之需要什麼數據,就爬什麼

    blob.png


    創建getcity.py文件

    代碼如下:

    #!/usr/bin/env python
    # coding: utf-8
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    import json
    
    
    # 設置selenium使用chrome的無頭模式
    chrome_options = Options()
    chrome_options.set_headless()
    browser = webdriver.Chrome(options=chrome_options, executable_path=\
        r'C:\Users\taoru\Desktop\chromedriver_2.41\chromedriver.exe')
    cookies = browser.get_cookies()
    browser.delete_all_cookies()
    browser.get('https://www.51job.com/')
    browser.implicitly_wait(20)
    
    # 找到城市選擇框,并模拟點擊
    button = browser.find_element_by_xpath("//div[@class='ush top_wrap']//div[@class='el on']/p\
    [@class='addbut']//input[@id='work_position_input']").click()
    
    # 選中城市彈出框
    browser.current_window_handle
    
    # 定義一個空字典
    dic = {}
    
    # 找到城市,和對應的城市編号
    find_city_elements = browser.find_elements_by_xpath("//div[@id='work_position_layer']//\
    div[@id='work_position_click_center_right_list_000000']//tbody/tr/td")
    for element in find_city_elements:
        number = element.find_element_by_xpath("./em").get_attribute("data-value")  # 城市編号
        city = element.find_element_by_xpath("./em").text  # 城市
        # 添加到字典
        dic.setdefault(city, number)
    print(dic)
    # 寫入文件
    with open('city.txt', 'w', encoding='utf8') as f:
        f.write(json.dumps(dic, ensure_ascii=False))
    browser.quit()


    需要先運行getcity.py,獲取城市編号,運行結果如下

    {'大連': '230300', '天津': '050000', '成都': '090200', '甯波': '080300', '上海': '020000', '西安': '200200', '重慶': '060000', '鄭州': '170200', '杭州': '080200', '青島': '120300', '沈陽': '230200', '東莞': '030800', '濟南': '120200', '哈爾濱': '220200', '南京': '070200', '長春': '240200', '北京': '010000', '福州': '110200', '廣州': '030200', '深圳': '040000', '昆明': '250200', '蘇州': '070300', '合肥': '150200', '武漢': '180200', '長沙': '190200'}


    創建mylog.py文件,用來記錄日志信息

    代碼如下:

    #!/usr/bin/env python
    # coding: utf-8
    import logging
    import getpass
    import sys
    
    
    # 定義MyLog類
    class MyLog(object):
        def __init__(self):
            self.user = getpass.getuser()  # 獲取用戶
            self.logger = logging.getLogger(self.user)
            self.logger.setLevel(logging.DEBUG)
    
            # 日志文件名
            self.logfile = sys.argv[0][0:-3] + '.log'  # 動态獲取調用文件的名字
            self.formatter = logging.Formatter('%(asctime)-12s %(levelname)-8s %(message)-12s\r\n')
    
            # 日志顯示到屏幕上并輸出到日志文件内
            self.logHand = logging.FileHandler(self.logfile, encoding='utf-8')
            self.logHand.setFormatter(self.formatter)
            self.logHand.setLevel(logging.DEBUG)
    
            self.logHandSt = logging.StreamHandler()
            self.logHandSt.setFormatter(self.formatter)
            self.logHandSt.setLevel(logging.DEBUG)
    
            self.logger.addHandler(self.logHand)
            self.logger.addHandler(self.logHandSt)
    
        # 日志的5個級别對應以下的5個函數
        def debug(self, msg):
            self.logger.debug(msg)
    
        def info(self, msg):
            self.logger.info(msg)
    
        def warn(self, msg):
            self.logger.warn(msg)
    
        def error(self, msg):
            self.logger.error(msg)
    
        def critical(self, msg):
            self.logger.critical(msg)
    
    
    if __name__ == '__main__':
        mylog = MyLog()
        mylog.debug(u"I'm debug 中文測試")
        mylog.info(u"I'm info 中文測試")
        mylog.warn(u"I'm warn 中文測試")
        mylog.error(u"I'm error 中文測試")
        mylog.critical(u"I'm critical 中文測試")


    創建主程序get51Job.py文件,代碼如下:

    #!/usr/bin/env python
    # coding: utf-8
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    from mylog import MyLog as mylog
    import json
    import time
    import urllib.request
    from lxml import etree
    
    
    class Item(object):
        job_name = None  # 崗位名
        company_name = None  # 公司名
        work_place = None  # 工作地點
        salary = None  # 薪資
        release_time = None  # 發布時間
        job_recruitment_details = None  # 招聘崗位詳細
        job_number_details = None  # 招聘人數詳細
        company_treatment_details = None  # 福利待遇詳細
        practice_mode = None  # 聯系方式
    
    
    class GetJobInfo(object):
        """
        the all data from 51job.com
        所有數據來自前程無憂招聘網
        """
        def __init__(self):
            self.log = mylog()  # 實例化mylog類,用于記錄日志
            self.startUrl = 'https://www.51job.com/'  # 爬取的目标網站
            self.browser = self.getBrowser()  # 設置chrome
            self.browser_input = self.userInput(self.browser)  # 模拟用戶輸入搜索
            self.getPageNext(self.browser_input)   # 找到下個頁面
    
        def getBrowser(self):
            """
            設置selenium使用chrome的無頭模式
            打開目标網站 https://www.51job.com/
            :return: browser
            """
            try:
                # 創建chrome參數對象
                chrome_options = Options()
                # 把chrome設置成無界面模式,不論windows還是linux都可以,自動适配對應參數
                chrome_options.set_headless()
                # 創建chrome無界面對象,設置成無頭
                browser = webdriver.Chrome(executable_path=\
                                               r'C:\Users\taoru\Desktop\chromedriver_2.41\chromedriver.exe')
                # 利用selenium打開網站
                browser.get(self.startUrl)
                # 等待網站js代碼加載完畢
                browser.implicitly_wait(20)
            except Exception as e:
                # 記錄錯誤日志
                self.log.error('打開目标網站失敗:{},錯誤代碼:{}'.format(self.startUrl, e))
            else:
                # 記錄成功日志
                self.log.info('打開目标網站成功:{}'.format(self.startUrl))
                # 返回實例化selenium對象
                return browser
    
        def userInput(self, browser):
            """
            北京 上海 廣州 深圳 武漢 西安 杭州
            南京  成都 重慶 東莞 大連 沈陽 蘇州
            昆明 長沙 合肥 甯波 鄭州 天津 青島
            濟南 哈爾濱 長春 福州
            隻支持以上城市,輸入其它則無效
            最多可選5個城市,每個城市用 , 隔開(英文逗号)
            :return:browser
            """
            time.sleep(1)
            # 用戶輸入關鍵字搜索
            search_for_jobs = input("請輸入職位搜索關鍵字:")
            # 用戶輸入城市
            print(self.userInput.__doc__)
            select_city = input("輸入城市信息,最多可輸入5個,多個城市以逗号隔開:")
            # 找到51job首頁上關鍵字輸入框
            textElement = browser.find_element_by_id('kwdselectid')
            # 模拟用戶輸入關鍵字
            textElement.send_keys(search_for_jobs)
    
            # 找到城市選擇彈出框,模拟選擇"北京,上海,廣州,深圳,杭州"
            button = browser.find_element_by_xpath("//div[@class='ush top_wrap']\
            //div[@class='el on']/p[@class='addbut']//input[@id='jobarea']")
    
            # 打開城市對應編号文件
            with open("city.txt", 'r', encoding='utf8') as f:
                city_number = f.read()
                # 使用json解析文件
                city_number = json.loads(city_number)
    
            new_list = []
            # 判斷是否輸入多值
            if len(select_city.split(',')) > 1:
                for i in select_city.split(','):
                    if i in city_number.keys():
                        # 把城市替換成對應的城市編号
                        i = city_number.get(i)
                        new_list.append(i)
                        # 把用戶輸入的城市替換成城市編号
                select_city = ','.join(new_list)
            else:
                for i in select_city.split(','):
                    i = city_number.get(i)
                    new_list.append(i)
                select_city = ','.join(new_list)
    
            # 執行js代碼
            browser.execute_script("arguments[0].value = '{}';".format(select_city), button)
    
            # 模拟點擊搜索
            browser.find_element_by_xpath("//div[@class='ush top_wrap']/button").click()
            self.log.info("模拟搜索輸入成功,獲取目标爬取title信息:{}".format(browser.title))
            return browser
    
        def getPageNext(self, browser):
            # 找到總頁數
            str_sumPage = browser.find_element_by_xpath("//div[@class='dw_page']/div//\
            span[@class='td'][1]").text
            sumpage = ''
            for i in str_sumPage:
                if i.isdigit():
                    sumpage += i
            self.log.info("獲取總頁數:{}".format(sumpage))
            s = 1
            while s <= int(sumpage):
                urls = self.getUrl(self.browser)
                # 獲取每個崗位的詳情
                self.items = self.spider(urls)
                # 數據下載
                self.pipelines(self.items)
                # 清空urls列表,獲取後面的url(去重,防止數據重複爬取)
                urls.clear()
                s += 1
                self.log.info('開始爬取第%d頁' % s)
                # 找到下一頁的按鈕點擊
                NextTag = browser.find_element_by_partial_link_text("下一頁").click()
                # 等待加載js代碼
                browser.implicitly_wait(20)
                time.sleep(3)
            self.log.info('獲取所有崗位成功')
            browser.quit()
    
        def getUrl(self, browser):
            # 創建一個空列表,用來存放所有崗位詳情的url
            urls = []
    
            # 創建一個特殊招聘空列表
            job_urls = []
    
            # 獲取所有崗位詳情url
            Elements = browser.find_elements_by_xpath("//div[@class='dw_table']//div[@class='el']")
            for element in Elements:
                try:
                    url = element.find_element_by_xpath("./p/span/a").get_attribute("href")
                    title = element.find_element_by_xpath('./span[@class="t2"]/a').get_attribute('title')
                except Exception as e:
                    self.log.error("獲取崗位詳情失敗,錯誤代碼:{}".format(e))
                else:
                    # 排除特殊的url,可單獨處理
                    src_url = url.split('/')[3]
                    if src_url == 'sc':
                        job_urls.append(url)
                        self.log.info("獲取不符合爬取規則的詳情成功:{},添加到job_urls".format(url))
                    else:
                        urls.append(url)
                        self.log.info("獲取詳情成功:{},添加到urls".format(url))
            return urls
    
        def spider(self, urls):
            # 數據過濾,爬取需要的數據,返回items列表
            items = []
            for url in urls:
                htmlcontent = self.getreponsecontent(url)
                html_xpath = etree.HTML(htmlcontent)
                item = Item()
                # 崗位名
                item.job_name = html_xpath.xpath("normalize-space(//div[@class='cn']/h1/text())")
                # 公司名
                item.company_name = html_xpath.xpath("normalize-space(//div[@class='cn']\
                /p[@class='cname']/a/text())")
                # 工作地點
                item.work_place = html_xpath.xpath("normalize-space(//div[@class='cn']\
                //p[@class='msg ltype']/text())").split('|')[0].strip()
                # 薪資
                item.salary = html_xpath.xpath("normalize-space(//div[@class='cn']/strong/text())")
                # 發布時間
                item.release_time = html_xpath.xpath("normalize-space(//div[@class='cn']\
                //p[@class='msg ltype']/text())").split('|')[-1].strip()
                # 招聘崗位詳細
                job_recruitment_details_tmp = html_xpath.xpath("//div[@class='bmsg job_msg inbox']//text()")
                item.job_recruitment_details = ''
                ss = job_recruitment_details_tmp.index("職能類别:")
                ceshi = job_recruitment_details_tmp[:ss - 1]
                for i in ceshi:
                    item.job_recruitment_details = item.job_recruitment_details + i.strip() + '\n'
                # 招聘人數詳細
                job_number_details_tmp = html_xpath.xpath("normalize-space(//div[@class='cn']\
                //p[@class='msg ltype']/text())").split('|')
                item.job_number_details = ''
                for i in job_number_details_tmp:
                    item.job_number_details = item.job_number_details + ' ' + i.strip()
                # 福利待遇詳細
                company_treatment_details_tmp = html_xpath.xpath("//div[@class='t1']//text()")
                item.company_treatment_details = ''
                for i in company_treatment_details_tmp:
                    item.company_treatment_details = item.company_treatment_details + ' ' + i.strip()
                # 聯系方式
                practice_mode_tmp = html_xpath.xpath("//div[@class='bmsg inbox']/p//text()")
                item.practice_mode = ''
                for i in practice_mode_tmp:
                    item.practice_mode = item.practice_mode + ' ' + i.strip()
                items.append(item)
            return items
    
        def getreponsecontent(self, url):
            # 接收url,打開目标網站,返回html
            fakeHeaders = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 \
            (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36'}
            try:
                request = urllib.request.Request(url, headers=fakeHeaders)
                response = urllib.request.urlopen(request)
                html = response.read().decode('gbk')
            except Exception as e:
                self.log.error(u'Python 返回 url:{} 數據失敗\n錯誤代碼:{}\n'.format(url, e))
            else:
                self.log.info(u'Python 返回 url:{} 數據成功\n'.format(url))
                time.sleep(1)  # 1秒返回一個結果  手動設置延遲防止被封
                return html
    
        def pipelines(self, items):  # 接收一個items列表
            # 數據下載
            filename = u'51job.txt'
            with open(filename, 'a', encoding='utf-8') as fp:
                for item in items:
                    fp.write('job_name:{}\ncompany_name:{}\nwork_place:{}\nsalary:\
                    {}\nrelease_time:{}\njob_recruitment_details:{}\njob_number_details:\
                    {}\ncompany_treatment_details:\{}\n\
                    practice_mode:{}\n\n\n\n' \
                             .format(item.job_name, item.company_name, item.work_place,
                                     item.salary, item.release_time,item.job_recruitment_details,
                                     item.job_number_details, item.company_treatment_details,
                                     item.practice_mode))
                    self.log.info(u'崗位{}保存到{}成功'.format(item.job_name, filename))
    
    
    if __name__ == '__main__':
        st = GetJobInfo()


    先運行getcity.py生成城市編号city.txt文件

    再運行主程序get51Job.py文件

    關鍵字輸入: python

    城市選擇:北京,上海,廣州,深圳,杭州

    pycharm運行截圖:

    blob.png


    生成的文件51job.txt截圖

    blob.png


    要知道我們寫的是動态爬蟲,可以根據輸入的不同,爬取不同的招聘信息, 怎麼驗證呢?

    重新運行程序

    關鍵字輸入:會計

    城市選擇: 武漢

    跟第一次運行輸入的不一樣,運行主程序get51Job.py

    pycharm截圖

    blob.png


    生成的51job.txt截圖

    blob.png


    根據輸入結果的不同,爬取不同的信息,利用selenium可以做到動态爬取



    代碼分析:

    整個項目代碼分為三個文件

    getcity.py  (首先運行)獲取城市編号,會生成一個city.txt文件

    mylog.py     日志程序,記錄爬取過程中的一些信息

    get51Job.py 爬蟲主程序,裡面包含:

    Item類  定義需要獲取的數據

    GetJobInfo類 主程序類

    getBrowser方法     設置selenium使用chrome的無頭模式,打開目标網站,返回browser對象

    userInput方法        模拟用戶輸入關鍵字,選擇城市,點擊搜索,返回browser對象

    getUrl方法               找到所有符合規則的url,返回urls列表

    spider方法               提取每個崗位url的詳情,返回items

    getresponsecontent方法  接收url,打開目标網站,返回html内容

    piplines方法            處理所有的數據,保存為51job.txt

    getPageNext方法   找到總頁數,并獲取下個頁面的url,保存數據,直到所有頁面爬取完畢

關鍵字