紫鳥超級瀏覽器-SeleniumAPI通信


超級瀏覽器Webdriver自動化開發

一、概述

通過Webdriver實現對超級瀏覽器內的店鋪進行,自動化控制以及數據抓取,主要流程分為以下兩個部分

(一)與超級瀏覽器主進程通信。

這個部分是通過Socket實現與超級瀏覽器主進實現通訊的,主要工作是獲取店鋪列表以及准備店鋪環境,一個店鋪相當於一個獨立瀏覽器。

import json
import subprocess
from socket import *
from selenium import webdriver
from db.db_redis import DBRedis
from common.utility import Utility
from common.mapping import Mapping
from common.global_logger import logger
from selenium.webdriver import ActionChains
from selenium.common.exceptions import NoSuchElementException


class SuperBrowser(object):

    # 基礎配置
    utils = Utility()
    config = utils.confg

    # 初始化Redis服務
    obj_redis = DBRedis()

    # 獲取業務類型
    business_type = config.get('business_type')
    logger.info("business_type: %s" % business_type)

    # 指定使用英語
    __LANGUAGE = config.get('language')

    # ----------------------------------------->> Socket通信地址端口
    host = config.get('socket_host')
    port = int(config.get('socket_port'))
    logger.info('socket > host: %s, port: %s' % (host, port))
    # ----------------------------------------->> 請求紫鳥超級瀏覽器API方法
    __GET_BROWSER_LIST = "getBrowserList"         # 獲取店鋪列表
    __START_BROWSER = "startBrowser"              # 啟動店鋪(主程序)
    __STOP_BROWSER = "stopBrowser"                # 關閉店鋪窗口
    __GET_BROWSER_ENV_INFO = "getBrowserEnvInfo"  # 啟動店鋪(webdriver)
    __HEARTBEAT = "heartbeat"                     # 非必要接口,只是用於保活Socket連接
    __EXIT = "exit"                               # 正常退出超級瀏覽器主進程,會自動關閉已啟動店鋪並保持店鋪cookie等信息。

    def __init__(self):
        logger.info("初始化Socket連接...")
        logger.info("啟動紫鳥瀏覽器......")

        self.buf_size = int(self.config.get('socket_buf_size'))
        self.IS_HEADLESS = self.config.get('browser_is_headless')     # 瀏覽器是否啟用無頭模式 false 否、true 是

        # 獲取紫鳥·超級瀏覽器安裝路徑
        path_super_browser = self.config.get('path_super_browser')
        cmd = "{} --run_type=web_driver --socket_port={}".format(path_super_browser, self.port)
        subprocess.Popen(cmd)
        try:
            # ------------------------------創建套接字通道
            self.address = (self.host, self.port)
            self.tcpCliSock = socket(AF_INET, SOCK_STREAM)  # 創建套接字
            self.tcpCliSock.connect(self.address)           # 主動初始化TCP服務器連接
        except ConnectionRefusedError as e:
            logger.error(e)
            subprocess.Popen('taskkill /f /im superbrowser.exe')
        except Exception as e:
            logger.error(e)

    def browser_api(self, action, args=None):
        """
        紫鳥·超級瀏覽器API
        :param action: 方法
        :param args: 可選參數
        :return:
        """
        REQUEST_ID = "0123456789"  # 全局唯一標識
        user_info = json.dumps({   # 用戶信息
            "company": self.config.get('browser_company_name'),
            "username": self.config.get('browser_username'),
            "password": self.config.get('browser_password')
        })
        # 默認為獲取店鋪列表
        common = {"userInfo": user_info, "action": self.__GET_BROWSER_LIST, "requestId": REQUEST_ID}
        if action == self.__START_BROWSER or action == self.__GET_BROWSER_ENV_INFO or action == self.__STOP_BROWSER:
            common['browserOauth'] = args['browserOauth']
            common['isHeadless'] = args['isHeadless']
        common['action'] = action
        return common  

    def socket_communication(self, params):
        """
        Socket通信
        :param params: 參數對象
        :return:
        """
        try:
            args = (str(params) + '\r\n').encode('utf-8')
            # 將 string 中的數據發送到連接的套接字
            self.tcpCliSock.send(args)
            # 接收的最大數據量
            res = self.tcpCliSock.recv(self.buf_size)
            return json.loads(res)
        except ConnectionResetError as e:
            logger.warning("ConnectionResetError: %s" % e)
            logger.info("socket 連接已關閉")
        except Exception as e:
            logger.error("socket_communication error: %s" % e)
        pass

    # 舉個栗子🌰
    def browser_list(self):
        """
        獲取店鋪列表
        這里采用Redis管理店鋪,為了后期分布式部署准備。
        :return:
        """
        logger.info("")
        logger.info("獲取店鋪列表.")
        shop_list_params = self.browser_api(self.__GET_BROWSER_LIST)
        shop_info = self.socket_communication(shop_list_params)
        if shop_info['statusCode'] == 0:
            browser_size = len(shop_info['browserList'])
            logger.info("目前店鋪總數: %s, 正在記錄店鋪信息...,請稍等." % browser_size)
            current_time = Utility.curr_time()
            for index, browser in enumerate(shop_info['browserList']):
                index += 1
                # site_id 對應的值
                browser['site_name'] = Mapping.SiteIdExplain(browser['siteId'])
                browserOauth = browser['browserOauth']
                if browser['isExpired'] is False:

                    # 記錄店鋪的數據
                    key_completed = self.config.get('r_amz_shops_completed')
                    key_inProgress = self.config.get('r_amz_shops_inProgress')
                    params = json.dumps({
                        "type": self.business_type,
                        "browserOauth": browserOauth,
                        "browserName": browser['browserName'],
                        "browserIp": browser['browserIp'],
                        "siteId": browser['siteId'],
                        "site_name": browser['site_name'],
                        "isExpired": browser['isExpired']
                    })

                    # 檢索該店鋪數據是否已采集完成?
                    is_sismember = self.obj_redis.sismember(key_completed, params)
                    if is_sismember:
                        logger.info('%s, 已采集完成.' % browserOauth)
                    else:
                        self.obj_redis.sadd(key_inProgress, params)
                    pass
                else:
                    # 代理IP過期告警...
                    title = "Amazon·貨件狀態"   # 懸浮標題
                    iphone = self.config.get('ding_talk_iphone')  # @的指定人
                    spider_name = self.config.get('sn_v_shipment_status')     # 應用名稱
                    browserName = browser['browserName']    # 店鋪
                    site_name = browser['site_name']    # 所屬平台
                    browserIp = browser['browserIp']    # 代理IP
                    cloud_server = self.config.get('cloud_server_name')  # 雲服務器名稱
                    # 通知內容
                    inform_content = "##### @{} Amazon·貨件狀態*>應用名稱: {}*>店鋪: {}*>所屬平台: {}*>代理IP: {}*>" \
                                     "服務器: {}*>當前時間: {}*>店鋪ID: {}*>是否過期:" \
                                     " <font color=#FFOOOO size=3 face='隸書'>代理IP已過期</font>*>" \
                        .format(iphone, spider_name, browserName, site_name, browserIp,
                                cloud_server, current_time, browserOauth).replace('*', '\n\n')
                    self.utils.ding_talk_robot(1, title, inform_content, [iphone], False)
                    self.utils.sleep_message(5, "間歇....")
                pass
            pass
        else:
            logger.warning("statusCode:%s, err: %s" % (shop_info['statusCode'], shop_info['err']))
        pass


(二)通過Selenium API 啟動和控制超級瀏覽器內核

這個部分主要是由自動化程序開發者自行開發,需要自行了解Selenium API如何使用。啟動Selenium時有些參數依賴與超級瀏覽器主進程通訊的結果

二、交互時序圖

三、必要條件以及注意事項

(一)啟動超級瀏覽器主進程必要配置啟動參數

1、--run_type=web_driver

指定以Webdriver 模式運行主進程,本質是讓超級瀏覽器以無界面狀態運行。超級瀏覽器進程會自動保證進程唯一,所以多次啟動會自動放棄后啟動進程。但是手動點擊啟動的超級瀏覽器會Kill掉Webdriver 模式運行的進程

2、--socket_port=端口號

告訴超級瀏覽器雙方socket通訊端口是什么,超級瀏覽器會以該端口在127.0.0.1啟動一個socket服務端

# 獲取紫鳥·超級瀏覽器安裝路徑
path_super_browser = self.config.get('path_super_browser')
cmd = "{} --run_type=web_driver --socket_port={}".format(path_super_browser, self.port)
subprocess.Popen(cmd)

(二)Socket通訊注意事項

1、每條Socket請求和響應數據必須以"\r\n"結尾

2、Socket請求內容和返回結果都是通過JSON結構組織,收發消息統一以UTF-8編碼

3、Socket請求的相應都是異步返回的,可以並發執行,通過在請求參數里面添加一個全局唯一的requestId字段來標識請求,該字段會在響應內返回

def socket_communication(self, params):
    """
    Socket通信
    :param params: 參數對象
    :return:
    """
    try:
        args = (str(params) + '\r\n').encode('utf-8')
        # 將 string 中的數據發送到連接的套接字
        self.tcpCliSock.send(args)
        # 接收的最大數據量
        res = self.tcpCliSock.recv(self.buf_size)
        return json.loads(res)
    except ConnectionResetError as e:
        logger.warning("ConnectionResetError: %s" % e)
        logger.info("socket 連接已關閉")
    except Exception as e:
        logger.error("socket_communication error: %s" % e)
    pass

(三)其他注意事項

1、getBrowserEnvInfo返回數據不能復用,每次都要重新調用。

2、需要根據運行設備的配置,適當控制同時啟動的店鋪瀏覽器窗口總個數,店鋪剛剛啟動時非常消耗CPU可以考慮錯開啟動。這個部分需要開發者自行調優,沒有明確的標准。

四、Socket接口說明

(一)Action : getBrowserList

1、說明:獲取店鋪列表

2、請求參數:

{
    "userInfo": "{\"company\":\"公司\",\"username\":\"用戶名\",\"password\":\"密碼\"}",
    "action": "getBrowserList",
    "requestId": "全局唯一標識"
}

3、響應結果:

{
    "statusCode": "狀態碼",
    "err": "異常信息",
    "action": "getBrowserList",
    "requestId": "全局唯一標識",
    "browserList": [{
        "browserOauth": "店鋪ID",
	"browserName": "店鋪名稱",
	"browserIp": "店鋪IP",
	"siteId": "店鋪所屬站點",
        "isExpired": false //ip是否過期
    }]
}

4、狀態碼:

(1)0 : 成功
(2)-10000 : 未知異常
(3)-10002 : Socket參數非法
(4)-10003 : 登錄失敗
(5)-10004 : 獲取店鋪列表時服務器返回異常

def browser_list(self):
    logger.info("")
    logger.info("獲取店鋪列表.")
    shop_list_params = self.browser_api(self.__GET_BROWSER_LIST)
    shop_info = self.socket_communication(shop_list_params)
    if shop_info['statusCode'] == 0:
        browser_size = len(shop_info['browserList'])
        logger.info("目前店鋪總數: %s, 正在記錄店鋪信息...,請稍等." % browser_size)
        current_time = Utility.curr_time()
        for index, browser in enumerate(shop_info['browserList']):
            index += 1
            # site_id 對應的值
            browser['site_name'] = Mapping.SiteIdExplain(browser['siteId'])
            browserOauth = browser['browserOauth']
            if browser['isExpired'] is False:

                # 記錄店鋪的數據
                key_completed = self.config.get('r_amz_shops_completed')
                key_inProgress = self.config.get('r_amz_shops_inProgress')
                params = json.dumps({
                    "type": self.business_type,
                    "browserOauth": browserOauth,
                    "browserName": browser['browserName'],
                    "browserIp": browser['browserIp'],
                    "siteId": browser['siteId'],
                    "site_name": browser['site_name'],
                    "isExpired": browser['isExpired']
                })

                # 檢索該店鋪數據是否已采集完成?
                is_sismember = self.obj_redis.sismember(key_completed, params)
                if is_sismember:
                    logger.info('%s, 已采集完成.' % browserOauth)
                else:
                    self.obj_redis.sadd(key_inProgress, params)
                pass
            else:
                # 代理IP過期告警...
                title = "Amazon·貨件狀態"   # 懸浮標題
                iphone = self.config.get('ding_talk_iphone')  # @的指定人
                spider_name = self.config.get('sn_v_shipment_status')     # 應用名稱
                browserName = browser['browserName']    # 店鋪
                site_name = browser['site_name']    # 所屬平台
                browserIp = browser['browserIp']    # 代理IP
                cloud_server = self.config.get('cloud_server_name')  # 雲服務器名稱
                # 通知內容
                inform_content = "##### @{} Amazon·貨件狀態*>應用名稱: {}*>店鋪: {}*>所屬平台: {}*>代理IP: {}*>" \
                                 "服務器: {}*>當前時間: {}*>店鋪ID: {}*>是否過期:" \
                                 " <font color=#FFOOOO size=3 face='隸書'>代理IP已過期</font>*>" \
                    .format(iphone, spider_name, browserName, site_name, browserIp,
                            cloud_server, current_time, browserOauth).replace('*', '\n\n')
                self.utils.ding_talk_robot(1, title, inform_content, [iphone], False)
                self.utils.sleep_message(5, "間歇....")
            pass
        pass
    else:
        logger.warning("statusCode:%s, err: %s" % (shop_info['statusCode'], shop_info['err']))
    pass

(二)Action : startBrowser

1、說明:啟動店鋪,關閉店鋪需要調用stopBrowser,連續兩次調用startBrowser會視為重啟

2、請求參數:

{
    "userInfo": "{\"company\":\"公司\",\"username\":\"用戶名\",\"password\":\"密碼\"}",
    "action": "startBrowser",
    "browserOauth": "店鋪ID",
    "isHeadless": true, //是否啟用無頭模式
    "requestId": "全局唯一標識"
}

3、響應結果:

{
    "statusCode": "狀態碼",
    "err": "異常信息",
    "action": "startBrowser",
    "browserOauth": "店鋪ID",
    "requestId": "全局唯一標識",
    "launcherPage": "店鋪所屬平台的默認啟動頁面",
    "debuggingPort": "調試端口"
}

4、啟動Selenium 必要參數

//根據startBrowser返回結果啟動Selenium
ChromeOptions options = new ChromeOptions();
//調試端口
options.setExperimentalOption("debuggerAddress", "127.0.0.1:" + debuggingPort);
//刪除其他不需要參數

def driver_browser(self, shop_obj):
    """
    Selenium驅動瀏覽器(Chrome)
    :param shop_obj: 店鋪信息
    :return:
    """
    # 啟動Selenium
    self.utils.sleep_message(3, "啟動Selenium.")
    launcher_page, debugging_port = shop_obj['launcherPage'], shop_obj['debuggingPort']
    logger.info("啟動Selenium必要參數: debugging_port: %s, launcher_page: %s" % (debugging_port, launcher_page))

    self.utils.sleep_message(2.5, "瀏覽器配置")
    options = webdriver.ChromeOptions()
    options.add_experimental_option("debuggerAddress", "127.0.0.1:" + str(debugging_port))
    driver = webdriver.Chrome(executable_path='./files/driver/windows/80.0.3987.163/chromedriver', options=options)
    self.utils.sleep_message(3, "進入店鋪...")
    driver.get(launcher_page)
    return driver

5、狀態碼:

(1)0 : 成功
(2)-10000 : 未知異常
(3)-10001 : 內核窗口創建失敗
(4)-10002 : Socket參數非法
(5)-10003 : 登錄失敗
(6)-10004 : browserOauth缺失
(7)-10005 : 該店鋪上次請求的startBrowser還未執行結束
(8)大於零的狀態碼:
1 : 初始化數據失敗
2 : 檢測到當前IP無法正常使用,請聯系客服
4 : 初始化時區失敗
5 : 初始化代理失敗
6 : 初始化黑白名單
7 : 啟動內核失敗
8 : 初始化瀏覽器個人目錄
9 : 初始化Cookies失敗
11 : 初始化瀏覽器設置文件
13 : 初始化代理信息配置

def start_browser(self, shop_id):
    """
    啟動店鋪
    :param shop_id: 店鋪ID
    :return:
    """
    # 啟動店鋪(兩種方式) startBrowser / getBrowserEnvInfo
    start_params = self.browser_api(self.__START_BROWSER, {"browserOauth": shop_id, "isHeadless": self.IS_HEADLESS})
    shop_obj = self.socket_communication(start_params)
    logger.info("啟動店鋪信息: %s" % shop_obj)
    return shop_obj

(三)Action : stopBrowser

1、說明:關閉店鋪窗口

2、請求參數:

{
    "userInfo": "{\"company\":\"公司\",\"username\":\"用戶名\",\"password\":\"密碼\"}",
    "action": "stopBrowser",
    "requestId": "全局唯一標識"
}

3、響應結果:

{
    "statusCode": "狀態碼",
    "err": "異常信息",
    "action": "stopBrowser",
    "requestId": "全局唯一標識"
}

4、狀態碼:

(1) 0 : 成功
(2)-10000 : 未知異常
(3)-10002 : Socket參數非法
(4)-10003 : 登錄失敗

def stop_browser(self, shop_id):
    """
    關閉店鋪
    :param shop_id: 店鋪ID
    :return:
    """
    logger.info("關閉店鋪")
    stop_params = self.browser_api(
        self.__STOP_BROWSER, {
            "browserOauth": shop_id,
            "isHeadless": self.IS_HEADLESS
        }
    )
    stop_obj = self.socket_communication(stop_params)
    logger.info("關閉店鋪信息: %s" % stop_obj)

(四)Action : getBrowserEnvInfo

1、說明:和startBrowser類似,區別就是內核窗口startBrowser由主程序啟動,getBrowserEnvInfo由Webdriver啟動

2、請求參數

{
    "userInfo": "{\"company\":\"公司\",\"username\":\"用戶名\",\"password\":\"密碼\"}",
    "action": "getBrowserEnvInfo",
    "browserOauth": "店鋪ID",
    "isHeadless": true, //是否啟用無頭模式
    "requestId": "全局唯一標識"
}

3、響應結果:

{
    "statusCode": "狀態碼",
    "err": "異常信息",
    "action": "getBrowserEnvInfo",
    "browserOauth": "店鋪ID",
    "requestId": "全局唯一標識",
    "browserPath": "內核exe所在位置",
    "launcherPage": "店鋪所屬平台的默認啟動頁面",
    "browserArguments": "啟動必要參數",
    "debuggingPort": "調試端口"
}

4、啟動Selenium 必要參數

//根據getBrowserEnvInfo 返回結果啟動Selenium
ChromeOptions options = new ChromeOptions();
//內核exe所在位置
options.addArguments(browserArguments);
//啟動必要參數
options.setBinary(browserPath);
//調試端口
options.addArguments("--remote-debugging-port=" + debuggingPort);

5、狀態碼:參考Action : startBrowser

(五)Action : heartbeat

1、說明:非必要接口,只是用於保活Socket連接

2、請求參數

{
    "userInfo": "{\"company\":\"公司\",\"username\":\"用戶名\",\"password\":\"密碼\"}",
    "action": "heartbeat",
    "requestId": "全局唯一標識"
}

3、響應結果:

{
    "statusCode": "狀態碼",
    "err": "異常信息",
    "action": "heartbeat",
    "requestId": "全局唯一標識"
}

4、狀態碼:

(1) 0 : 成功
(2)-10000 : 未知異常
(3)-10002 : Socket參數非法
(4)-10003 : 登錄失敗

def heartbeat(self):
    """維持心跳"""
    self.utils.sleep_message(10, "維持心跳")
    heartbeat_params = self.browser_api(self.__HEARTBEAT)
    heartbeat_obj = self.socket_communication(heartbeat_params)
    logger.info("心跳信息: %s" % heartbeat_obj)

(六)Action : exit

1、說明:正常退出超級瀏覽器主進程,會自動關閉已啟動店鋪並保持店鋪cookie等信息。

2、請求參數:

{
    "userInfo": "{\"company\":\"公司\",\"username\":\"用戶名\",\"password\":\"密碼\"}",
    "action": "exit",
    "requestId": "全局唯一標識"
}

3、響應碼:

4、狀態碼:

(1) 0 : 成功
(2)-10000 : 未知異常
(3)-10002 : Socket參數非法
(4)-10003 : 登陸失敗

def exit_browser(self):
    self.utils.sleep_message(10, "退出瀏覽器 .....")

    # 退出瀏覽器
    exit_params = self.browser_api(self.__EXIT)
    logger.info("退出瀏覽器: %s" % exit_params)
    self.socket_communication(exit_params)

    # 殺死瀏覽器進程
    # self.kill_browser()

    # ------------------------>> 釘釘通知公共參數 start
    title = "紫鳥·超級瀏覽器·退出"  # 懸浮標題
    iphone = self.config.get('ding_talk_iphone')  # @的指定人
    spider_name = self.config.get('sn_v_shipment_status')  # 應用名稱
    curr_time = Utility.curr_time()  # 當前時間
    cloud_server = self.config.get('cloud_server_name')  # 雲服務器名稱
    # ------------------------>> 釘釘通知公共參數 end
    inform_content = "##### 紫鳥·超級瀏覽器·退出*>應用名稱: {}*> 服務器: {}*>當前時間: {}*>" \
                        "<font color=#00DD00 size=3 face='隸書'>采集任務已完成,瀏覽器正常退出.</font>*>" \
        .format(spider_name, cloud_server, curr_time).replace('*', '\n\n')
    Utility.ding_talk_robot(1, title, inform_content, [iphone], False)

五、getBrowserList中的SiteId說明

index id name
1 1 🇺🇸 美國亞馬遜
2 2 🇨🇦 加拿大亞馬遜
3 3 🇯🇵 日本亞馬遜
4 4 🇬🇧 英國亞馬遜
5 5 🇫🇷 法國亞馬遜
6 7 🇮🇹 意大利亞馬遜
7 10 🇩🇪 德國亞馬遜
8 11 🇪🇸 西班牙亞馬遜

寫得有問題的地方還望各位大佬指出錯誤,謝謝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM