Open API

很多同学希望能够通过程序/脚本控制花漾客户端,如:通过程序/脚本访问某个浏览器分身以打开/关闭花漾指纹浏览器等,从而方便与其它系统的整合。 基于此,花漾提供了 Open API 的特性,其实现原理是基于 Access Key 认证体系的 Http 请求/响应机制,理论上允许用户使用任意语言如 Java、Python、C# 等与花漾客户端进行交互。

1、开启 Open API 并获得 Access Key

在使用 Open API 之前,首先需要开启API,请切换至“团队”主页签,并在“团队资源”处,开启 Open API:  

   

《在团队资源中开启Open API》

当 Open API 开启后,可以点击“新增”按钮,创建一个新的 Access Key,请注意,每一个Access Key都需要指定一个用户身份, 换言之,当您使用某个Access Key并通过 Open API 来操纵花漾客户端时,本质上相当于使用与此Access Key绑定的用户身份进行的操作:  

   

《创建 Access Key》

需要提醒您的是,生成的 Access Key 请务必妥善保存,以避免造成信息安全风险。

2、如何调用花漾 Open API

3.1 调用端点(Endpoint)与时间格式

花漾Open API的调用端点(Endpoint)是:https://api.szdamai.com/openapi/; 输入/输出的时间格式是:"yyyy-MM-dd'T'HH:mm:ss'Z'",采用UTC+0时区。举例,当输出"2022-09-05T11:12:28Z"时, 代表东8区(北京时间)的2022-09-05 19:12:28。

3.2 认证方式

目前仅支持OAuth Token的方式进行校验,即客户使用 AccessKeyId + AccessKeySecret,跟 Endpoint 换取一个 Token, 在后续请求中,把 Token 放入http头中:'Authorization: YOUR_Token',后续所有请求将以 Access Key 代表的用户身份进行操作。

以下举例说明:

curl -X 'GET' 'https://api.szdamai.com/openapi/token?accessKeyId=YOUR_AccessKeyId&accessKeySecret=YOUR_AccessKeySecret&expireSeconds=7200' -H 'accept: */*

其中 expireSeconds 是 Token 超时时间,单位:秒,取值范围为[120,7200];当Token超时以后,请求会失败, 建议您在Token超时之前重新获取Token。

上述http请求的返回结果如下所示:

{  
  "success": true,  "code": 0,  "message": null,  
    "data": {    
      "token": "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0b2MiLCJqdGkiOiI2MTY1ODY4MjEyMWI0NzQ1OTNmZDBjYzVjYzAzOTk1NyIsInN1YiI6IkFLLUFLUVpQY3Z4YVRsMU85bkwwUUhjSHp0IiwiZXhwIjoxNjYyMzc2MzQ4fQ.TjG0VLwNrJrn8-XyxHz-OfhNjlLkalan3EBr92cdKc8GGaKwFNEqUpBInKiUJ6ixybIpKnYNpsjxktliw2nIbjYgTnCRbT1G7jHogx2Y_7eDigJjyrpyZwdlZ5ZEGHPn01fNWFS0RkgHqeraQoNwbgVDU3T2rbJfZtG3W5vGuhRrmYaC-_pXjkvWhXgc4ZkIDV7j-364hyyZXM9W326iIoW9aaj1BhAWuttdXt-sl8aEDsbESEZxCziBE6_Q_MdJ4BvBi8yGp9M2yhjlkh2pNjF_nkl68QwVpsR8HW--2jTwjZLdHhsOl45Fp55ELV_y8czLUuo0-X5Sp6443UBXGA", 
      "expireTime": "2022-09-05T11:12:28Z"  
    }
}

其中,那一长串基于Base64编码的字符串,就是认证Token,而 expireTime 则是此Token的超时时间。

当认证成功获得 Token 后,我们就可以将 Token 放入到 Http头中,以调用花漾 Open API 了。

我们以查看当前用户信息这个 API 接口为例, 请求内容如下:

curl -X 'GET'  'https://api.szdamai.com/openapi/user/current'   -H 'accept: */*' 
  -H 'Authorization: eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0b2MiLCJqdGkiOiI2MTY1ODY4MjEyMWI0NzQ1OTNmZDBjYzVjYzAzOTk1NyIsInN1YiI6IkFLLUFLUVpQY3Z4YVRsMU85bkwwUUhjSHp0IiwiZXhwIjoxNjYyMzc2MzQ4fQ.TjG0VLwNrJrn8-XyxHz-OfhNjlLkalan3EBr92cdKc8GGaKwFNEqUpBInKiUJ6ixybIpKnYNpsjxktliw2nIbjYgTnCRbT1G7jHogx2Y_7eDigJjyrpyZwdlZ5ZEGHPn01fNWFS0RkgHqeraQoNwbgVDU3T2rbJfZtG3W5vGuhRrmYaC-_pXjkvWhXgc4ZkIDV7j-364hyyZXM9W326iIoW9aaj1BhAWuttdXt-sl8aEDsbESEZxCziBE6_Q_MdJ4BvBi8yGp9M2yhjlkh2pNjF_nkl68QwVpsR8HW--2jTwjZLdHhsOl45Fp55ELV_y8czLUuo0-X5Sp6443UBXGA'

返回结果如下所示:

{
  "success": true,
  "code": 0,
  "message": null,
  "data": {
    "id": 52394196996096,
    "tenant": null,
    "status": "ACTIVE",
    "nickname": "子非鱼",
    "createTime": "2021-08-27T07:02:51Z",
    "avatar": null,
    "gender": "UNSPECIFIC",
    "signature": "",
    "residentCity": "",
    "userType": "NORMAL",
    "partnerId": null,
    "phone": "186****2179",
    "email": "c****u@1*6.com",
    "weixin": null
  }
}

我们可以看到,通过 “https://api.szdamai.com/openapi/user/current” 接口能够成功查询当前用户的基本信息。

综上,我们已经完整的了解了如何通过Access Key获得认证Token,以及将Token放入到http头中以调用 Open API, 再通过参考花漾目前公开的API列表,理论上您可以通过任何语言/脚本来和花漾客户端进行交互了。

3、花漾 Open API 参考指引

目前花漾 Open API 仅提供了一些有限的接口列表,主要包括查询 RPA 任务详情、打开/关闭某个账号对应的浏览器等等。 目前花漾已经开放的API列表请参考:https://app.szdamai.com/open-api/ 。 需要提醒您的是,上述API参考既是一个在线文档,同时也是花漾 Open API 的调试工具,关于其更详细的介绍,请参考 API参考及Swagger在线调试工具

4、示例:通过 Java 调用花漾 Open API

我们为您准备了一个简单的基于Java语言的示例程序,如下所示:

package com.donkey.openapi.example;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class BasicHttpClientDemo {
    static String accessKeyId = "REPLACE_WITH_YOUR_AKID";
    static String accessKeySecret = "REPLACE_WITH_YOUR_AKSECRET";
    static String endpoint = "https://api.szdamai.com/openapi";

    public static void main(String[] args) throws IOException {
        //获取认证token
        Map<String, Object> resp = request("GET", 
            String.format("/token?accessKeyId=%s&accessKeySecret=%s&expireSeconds=%d", accessKeyId, accessKeySecret, 7200), null);
        String token = (String) resp.get("token");
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", token);

        //请求当前用户信息
        Map<String, Object> userObj = request("GET", "/user/current", headers);
        System.out.println(userObj);
    }

    public static Map<String, Object> request(String method, String path, Map<String, String> headers) throws IOException {
        URL url = new URL(endpoint + path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        if (headers != null) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }
        try {
            conn.connect();
            if (200 <= conn.getResponseCode() && conn.getResponseCode() < 300) {
                String responseJson = copy2String(conn.getInputStream());
                Map<String, Object> resp = JsonHelper.jsonDecode(responseJson);
                if (Boolean.TRUE.equals(resp.get("success"))) {
                    return (Map<String, Object>) resp.get("data");
                } else {
                    String msg = (String) resp.get("message");
                    if (msg == null) {
                        msg = conn.getResponseMessage();
                    }
                    throw new IOException("access openapi error :" + msg);
                }
            } else {
                throw new IOException("access openapi error :" + conn.getResponseCode() + " - " + conn.getResponseMessage());
            }
        } finally {
            conn.disconnect();
        }
    }

    public static String copy2String(InputStream inStream) throws IOException {
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inStream.read(buffer)) != -1) {
            result.write(buffer, 0, len);
        }
        String str = result.toString(StandardCharsets.UTF_8.name());
        return str;
    }
}

如有需要,您可以下载 Demo程序的源码

5、示例:通过 Python 调用花漾 Open API

同时,我们也为您准备了一个简单的基于Python语言的示例程序,如下所示:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import re
import sys
import requests
import json
import os
apiEndpoint = "https://api.szdamai.com/openapi"
'''
openApi: function to access api url and post parameters
'''
def openApi(url: str, params: dict = None, headers: dict = None, method: str = "GET", contentType: str = "json", timeout: int = 10) -> dict:
    if method == "GET":
        response = requests.get(apiEndpoint + url, params=params, headers=headers, timeout=timeout)
    elif method == "POST":
        if not headers:
            headers = dict()
        if contentType == "json":
            headers["Content-Type"] = "application/json"
            params = json.dumps(params) if params else None
        response = requests.post(apiEndpoint + url, data=params, headers=headers, timeout=timeout)
    elif method == "DELETE":
        response = requests.delete(apiEndpoint + url, data=params, headers=headers, timeout=timeout)
    else:
        return None
    if not response:
        return None
    if not 200 <= response.status_code <= 299:
        return None
    result=json.loads(response.text)
    if result["success"]:
        return result["data"]
    else:
        print("openApi %s error %s"%(url,result["message"]))
        return None

def getToken(accessKeyId, accessKeySecret, expireSeconds=300):
    tokenJson = openApi(
        "/token",
        params={
            "accessKeyId": accessKeyId,
            "accessKeySecret": accessKeySecret,
            "expireSeconds": expireSeconds
        },
    )
    return tokenJson["token"]
def getCurrentUser(token):
    hostJson = openApi(
        "/user/current",
        params={},
        headers={"Authorization": token}
    )
    return hostJson

def main(args, **kwargs): 
    accessKeyId =args[0]
    accessKeySecret =args[1]
    if not accessKeyId or not accessKeySecret:
        print("AccessKeyId and AccessKeySecret are not present.\n")
        return -1
    token = getToken(accessKeyId, accessKeySecret)
    if not token:
        print("Invalid AccessKeyId and AccessKeySecret.\n")
        return -1
    user = getCurrentUser(token=token)
    if user:
        print("Current user is  %s.\n" % (user))
    else:
        print("get Current user fail\n")
if __name__ == "__main__":  
    if len(sys.argv) != 3:
        print("Usage: openapi_demo.py AccessKeyId AccessKeySecret\n")
        sys.exit(-1)
    sys.exit(main(sys.argv[1:3], **dict(arg.split('=') for arg in sys.argv[4:])))

如有需要,您可以点这里下载 Demo程序的源码

6、示例:通过 NodeJS 调用花漾 Open API

我们为您准备了一个简单的基于 NodeJS 的示例程序,如下所示:

/**
 * 要求NodeJs 版本至少为16.20.2
 * 
 * 使用方法:
 * 1、在“团队”=》“团队资源”=>“Open API”页面中查看AccessKey,填入下面的open_api_key和open_api_secret两个变量;
 * 2、选择目标分身点开“编辑分身”按钮,复制“分身ID”,填入下面的to_open_account_id变量;
 * 3、执行npm install axios 安装相关依赖;
 * 4、执行node node-client-demo.js 查看示例效果。
 */


const axios = require('axios');

// 在“团队”=》“团队资源”=>“Open API”页面中查看AccessKey
const open_api_key = '你的AKID';
const open_api_secret = '你的AK密钥';

// 要打开的分身ID,在编辑分身的对话框中查看
const to_open_account_id = 123456;

let access_token = '';
const open_api_endpoint = 'https://api.szdamai.com/api/openapi';

/**
 * 获取 accessToken,之后的所有请求都需要带上这个token。在指定的有效期内这个token可以缓存
 * @return {Promise<*>}
 */
async function getToken() {
    const url = `${open_api_endpoint}/token?accessKeyId=${open_api_key}&accessKeySecret=${open_api_secret}&expireSeconds=300`;
    const response = (await axios.request({
        method: 'GET',
        url: url,
    })).data;
    const data = response.data;
    return data.token;
}

async function waitTask(taskId) {
    for(let i = 0; i < 20; i++) {
        await new Promise((resolve => {
            setTimeout(()=>resolve(1), 1000);
        }));

        const url = `${open_api_endpoint}/task/${taskId}`;
        const response = (await axios.request({
            method: 'GET',
            url: url,
            headers: {
                Authorization: access_token
            }
        })).data;
        let data = response.data[0];
        if(data.status != 'Pending') {
            return data.resourceId;
        }
    }
    throw '打开会话超时';
}

/**
 * 通过 分身名称 打开会话
 * @return {Promise<void>}
 */
async function openSession() {
    const url = `${open_api_endpoint}/session/open`
    const response = (await axios.request({
        method: 'POST',
        url: url,
        headers: {
            Authorization: access_token
        },
        params: {
            accountId: to_open_account_id,
            // deviceId: to_open_device_id
        }
    })).data;
    let sessionId = await waitTask(response.requestId);
    return sessionId;
}

async function closeSession(sessionId) {
    const url = `${open_api_endpoint}/session/${sessionId}/close`
    const response = (await axios.request({
        method: 'PUT',
        url: url,
        headers: {
            Authorization: access_token
        }
    })).data;
    return response;
}

(async function main() {
    try {
        access_token = await getToken();
        console.log(access_token);

        const sessionId = await openSession();
        console.log('打开会话成功,sessionId=' + sessionId);
        
        await new Promise((resolve => {
            setTimeout(()=>resolve(1), 10*1000);
        }));

        console.log('开始关闭会话,sessionId=' + sessionId);
        await closeSession(sessionId);
        console.log('任务完成');
    } catch (e) {
        console.error(e);
    }
})();

7、示例:通过 Selenium 控制花漾浏览器 (Python)

示例二 Python 程序 的基础上,您可以通过调用花漾 Open API 打开浏览器分身,并使用 Selenium 进行控制。 这里有几点需要注意:

  1. 可以通过 api 指定控制端口,比如下面的程序中指定的控制端口是9222;
  2. 可以通过 api 的 browserSwitches 参数指定启动浏览器的参数;
  3. accountId 指的是分身ID,获取方式见 这里
  4. 需要在本地准备好对应版本的chromedriver,您可以这里下载 chromedriver ,或者直接使用我们提供的 chromedriver
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import sys
import requests
import json
apiEndpoint = "https://api.szdamai.com/openapi"
'''
openApi: function to access api url and post parameters
'''

def openApi(url: str, params: dict = None, headers: dict = None, method: str = "GET", contentType: str = "json", timeout: int = 10) -> dict:
    if method == "GET":
        response = requests.get(
            apiEndpoint + url, params=params, headers=headers, timeout=timeout)
    elif method == "POST":
        if not headers:
            headers = dict()
        if contentType == "json":
            headers["Content-Type"] = "application/json"
            params = json.dumps(params) if params else None
        response = requests.post(
            apiEndpoint + url, data=params, headers=headers, timeout=timeout)
    elif method == "DELETE":
        response = requests.delete(
            apiEndpoint + url, data=params, headers=headers, timeout=timeout)
    else:
        return None
    if not response:
        return None
    if not 200 <= response.status_code <= 299:
        return None
    result = json.loads(response.text)
    if result["success"]:
        return result["data"]
    else:
        print("openApi %s error %s" % (url, result["message"]))
        return None


def getToken(accessKeyId, accessKeySecret, expireSeconds=300):
    tokenJson = openApi(
        "/token",
        params={
            "accessKeyId": accessKeyId,
            "accessKeySecret": accessKeySecret,
            "expireSeconds": expireSeconds
        },
    )
    return tokenJson["token"]


def getCurrentUser(token):
    hostJson = openApi(
        "/user/current",
        params={},
        headers={"Authorization": token}
    )
    return hostJson


def openSession(token, accountId):
    browserSwitches = "--window-size=700,600\n--window-position=100,50"
    hostJson = openApi(
        "/session/openV2",
        params={
            "accountId": accountId,
            "remoteDebugPort": 9222,
            "browserSwitches": browserSwitches,
        },
        method="POST",
        headers={"Authorization": token}
    )
    return hostJson


def useSelenium():
    chrome_options = Options()

    chrome_options.add_experimental_option("debuggerAddress", "127.0.0.1:9222")

    # chromedriver.exe可以在这里下载,必须是对应花漾的内核版本:
    # https://storage.googleapis.com/chrome-for-testing-public/120.0.6099.109/win64/chromedriver-win64.zip
    driver = webdriver.Chrome('d:/chromedriver.exe', options=chrome_options)

    # 打开网站
    driver.get('https://www.baidu.com')  # 指定要打开的网站 URL
    sleep(1)
    print(driver.title)


def main(args, **kwargs):
    accessKeyId = args[0]
    accessKeySecret = args[1]
    accountId = args[2]
    if not accessKeyId or not accessKeySecret:
        print("AccessKeyId and AccessKeySecret are not present.\n")
        return -1
    token = getToken(accessKeyId, accessKeySecret)
    if not token:
        print("Invalid AccessKeyId and AccessKeySecret.\n")
        return -1
    user = getCurrentUser(token=token)
    if user:
        print("Current user is  %s.\n" % (user))
        rs = openSession(token, accountId)
        print(rs)
        sleep(10)
        useSelenium()
    else:
        print("get Current user fail\n")


if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: python SeleniumTest.py AccessKeyId AccessKeySecret accountId\n")
        sys.exit(-1)
    sys.exit(main(sys.argv[1:4], **dict(arg.split('=')
             for arg in sys.argv[5:])))

如有需要,您可以点这里下载 Demo程序的源码

8、示例:通过 Selenium 控制花漾浏览器 (Java)

您可以在 Java 中通过调用花漾 Open API 打开浏览器分身,并使用 Selenium 进行控制。 这里有几点需要注意:

  1. 可以通过 api 指定控制端口,比如下面的程序中指定的控制端口是9222;
  2. 可以通过 api 的 browserSwitches 参数指定启动浏览器的参数;
  3. accountId 指的是分身ID,获取方式见 这里
  4. 需要在本地准备好对应版本的chromedriver,您可以这里下载 chromedriver ,或者直接使用我们提供的 chromedriver

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 假设花漾客户端安装在当前机器,这个程序也跑在这个机器。
 * <p>
 * 下面的Java程序演示在当前电脑上创建打开浏览器,并连接浏览器的remote debug port。
 *
 */
public class SeleniumDemo {
    
    static String accessKeyId = "你的AKID";
    static String accessKeySecret = "你的AK密钥";
    static String endpoint = "https://api.szdamai.com/openapi";

    /**
     * 需要打开的分身ID,必填
     */
    private static final long accountId = 1234567890L;
    /**
     * 需要打开浏览器的设备ID(可选)
     */
    private static String deviceId = null;
    /**
     * 需要浏览器监听的端口
     */
    private static Integer remoteDebugPort = 9222;

    /**
     * 浏览器启动参数。通过换行符\n分割每个参数对。可选
     */
    private static String browserSwitches = null;

    /**
     * chromedriver.exe可以在这里下载,必须是对应花漾的内核版本:
     * https://storage.googleapis.com/chrome-for-testing-public/120.0.6099.109/win64/chromedriver-win64.zip
     */
    private static String chromedriverFile = "D:\\chromedriver.exe";

    public static void main(String[] args) throws IOException {
        System.setProperty("webdriver.chrome.driver", chromedriverFile);
        //获取认证token
        String token = refrshToken();

        //打开会话,获取到异步请求的requestId
        String requestId = openSession(token);

        //等待会话创建完成
        Long sessionId = waitForSessionOpen(token, requestId);
        System.out.println("会话创建完成sessionId=" + sessionId);

        while (true) {//探测远程调试端口是否监听
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            if (isRemoteReachable("127.0.0.1", remoteDebugPort, 1000)) {
                break;
            }
        }

        //花漾浏览器起来之后会有一个稍长的初始化过程,需要等待一会儿
        try {
            Thread.sleep(10 * 1000);
        } catch (InterruptedException e) {
        }

        ChromeOptions chromeOptions = new ChromeOptions();
        chromeOptions.setExperimentalOption("debuggerAddress", "127.0.0.1:" + remoteDebugPort);
        WebDriver webDriver = new ChromeDriver(chromeOptions);

        webDriver.get("https://www.baidu.com");

        // Get and print the page title
        String pageTitle = webDriver.getTitle();
        System.out.println("Page Title: " + pageTitle);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        webDriver.quit();
    }

    private static Long waitForSessionOpen(String token, String requestId) throws IOException {
        Long sessionId = null;
        while (true) {//等待会话创建完成
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            Map<String, Object> t = findTaskByRequestId(token, requestId);
            if (t != null && Boolean.TRUE.equals(t.get("done"))) {
                if (!"Success".equalsIgnoreCase((String) t.get("status"))) {
                    throw new IOException((String) t.get("remarks"));
                }
                sessionId = (Long) t.get("resourceId");
                System.out.println("当前打开的会话ID=" + sessionId);
                break;
            }
        }
        return sessionId;
    }

    public static String refrshToken() throws IOException {
        Map<String, Object> resp = (Map<String, Object>) request("GET", String.format("/token?accessKeyId=%s&accessKeySecret=%s&expireSeconds=%d", accessKeyId, accessKeySecret, 7200), null, null);
        String token = (String) resp.get("token");
        return token;
    }

    public static String openSession(String token) throws IOException {
        Map<String, String> headers = prepareHeaders(token);
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("accountId", accountId);
        if (deviceId != null) {
            requestBody.put("deviceId", deviceId);
        }
        if (remoteDebugPort != null) {
            requestBody.put("remoteDebugPort", remoteDebugPort);
        }
        if (browserSwitches != null) {
            requestBody.put("browserSwitches", browserSwitches);
        }
        Map<String, Object> resp = (Map<String, Object>) request("POST", "/session/openV2", requestBody, headers);
        String requestId = (String) resp.get("requestId");
        return requestId;
    }

    private static Map<String, String> prepareHeaders(String token) {
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", token);
        headers.put("Content-Type", "application/json");
        return headers;
    }

    public static Map<String, Object> findTaskByRequestId(String token, String requestId) throws IOException {
        Map<String, String> headers = prepareHeaders(token);
        List resp = (List) request("GET", "/task/" + requestId, null, headers);
        if (resp.size() > 0) {
            return (Map<String, Object>) resp.get(0);
        }
        return Collections.emptyMap();
    }

    public static Object request(String method, String path, Object requestBody, Map<String, String> headers) throws IOException {
        URL url = new URL(endpoint + path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        if (headers != null) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }
        }
        if (requestBody != null) {
            conn.setDoOutput(true);
        }
        try {
            conn.connect();
            if (requestBody != null) {
                String json = JsonHelper.GSON.toJson(requestBody);
                writeToStream(json, conn.getOutputStream());
            }
            if (200 <= conn.getResponseCode() && conn.getResponseCode() < 300) {
                String responseJson = copy2String(conn.getInputStream());
                Map<String, Object> resp = JsonHelper.jsonDecode(responseJson);
                if (Boolean.TRUE.equals(resp.get("success"))) {
                    return resp.get("data");
                } else {
                    String msg = (String) resp.get("message");
                    if (msg == null) {
                        msg = conn.getResponseMessage();
                    }
                    throw new IOException("access openapi error :" + msg);
                }
            } else {
                throw new IOException("access openapi error :" + conn.getResponseCode() + " - " + conn.getResponseMessage());
            }
        } finally {
            conn.disconnect();
        }
    }

    public static String copy2String(InputStream inStream) throws IOException {
        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inStream.read(buffer)) != -1) {
            result.write(buffer, 0, len);
        }
        String str = result.toString(StandardCharsets.UTF_8.name());
        return str;
    }

    public static void writeToStream(String str, OutputStream outputStream) {
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        try {
            writer.write(str);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
            }
        }
    }

    /**
     * 探测远程端口是否监听
     *
     * @param host
     * @param port
     * @param timeout
     * @return
     */
    public static boolean isRemoteReachable(String host, int port, int timeout) {
        Socket s = null;
        try {
            s = new Socket();
            s.connect(new InetSocketAddress(host, port), timeout);
            return true;
        } catch (Exception e) {
        } finally {
            try {
                s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

如有需要,您可以点这里下载 Demo程序的源码

9、示例:通过 Selenium 控制花漾浏览器 (NodeJS)

您可以在 NodeJS 中通过调用花漾 Open API 打开浏览器分身,并使用 Selenium 进行控制。 这里有几点需要注意:

  1. 可以通过 api 指定控制端口,比如下面的程序中指定的控制端口是9222;
  2. 可以通过 api 的 browserSwitches 参数指定启动浏览器的参数;
  3. accountId 指的是分身ID,获取方式见 这里
  4. 需要在本地准备好对应版本的chromedriver,您可以这里下载 chromedriver ,或者直接使用我们提供的 chromedriver
/**
 * 要求NodeJs 版本至少为16.20.2
 * 
 * 使用方法:
 * 1、在“团队”=》“团队资源”=>“Open API”页面中查看AccessKey,填入下面的open_api_key和open_api_secret两个变量;
 * 2、选择目标分身点开“编辑分身”按钮,复制“分身ID”,填入下面的to_open_account_id变量;
 * 3、下载https://storage.googleapis.com/chrome-for-testing-public/120.0.6099.109/win64/chromedriver-win64.zip解压出chromdriver.exe,并修改path_to_webdriver变量的值为chromdriver.exe的路径;
 * 4、执行npm install 安装相关依赖;
 * 5、执行node use-selenium.js 查看示例效果。
 */


const axios = require('axios');
const net = require('net');
const { Builder } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');

// 在“团队”=》“团队资源”=>“Open API”页面中查看AccessKey
const open_api_key = '你的AKID';
const open_api_secret = '你的AK密钥';

// 要打开的分身ID,在编辑分身的对话框中查看
const to_open_account_id = 123456;

// chromedriver.exe可以在这里下载,必须是对应花漾的内核版本:
// https://storage.googleapis.com/chrome-for-testing-public/120.0.6099.109/win64/chromedriver-win64.zip
const path_to_webdriver = "d:\\chromedriver.exe";

let access_token = '';
const open_api_endpoint = 'https://api.szdamai.com/api/openapi';

/**
 * 获取 accessToken,之后的所有请求都需要带上这个token。在指定的有效期内这个token可以缓存
 * @return {Promise<*>}
 */
async function getToken() {
    const url = `${open_api_endpoint}/token?accessKeyId=${open_api_key}&accessKeySecret=${open_api_secret}&expireSeconds=300`;
    const response = (await axios.request({
        method: 'GET',
        url: url,
    })).data;
    const data = response.data;
    return data.token;
}

async function waitTask(taskId) {
    for(let i = 0; i < 20; i++) {
        await new Promise((resolve => {
            setTimeout(()=>resolve(1), 1000);
        }));

        const url = `${open_api_endpoint}/task/${taskId}`;
        const response = (await axios.request({
            method: 'GET',
            url: url,
            headers: {
                Authorization: access_token
            }
        })).data;
        let data = response.data[0];
        if(data.status != 'Pending') {
            return data.resourceId;
        }
    }
    throw '打开会话超时';
}

async function waitPortOpen(remoteDebugPort) {
    for(let i = 0; i < 20; i++) {
        await new Promise((resolve => {
            setTimeout(()=>resolve(1), 1000);
        }));

        let isOpen = await new Promise(resolve => {
            const client = new net.Socket();
            client.connect(remoteDebugPort, '127.0.0.1', function() {
                client.destroy();
                resolve(true);
            });
            client.on('error', function() {
                resolve(false);
            });
        });

        if(isOpen) {
            return;
        }
    }
    throw '等待端口打开超时';
}

/**
 * 通过 分身名称 打开会话
 * @return {Promise<void>}
 */
async function openSession(remoteDebugPort) {
    const url = `${open_api_endpoint}/session/open`
    const response = (await axios.request({
        method: 'POST',
        url: url,
        headers: {
            Authorization: access_token
        },
        params: {
            accountId: to_open_account_id,
            remoteDebugPort,
            // deviceId: to_open_device_id
        }
    })).data;
    let sessionId = await waitTask(response.requestId);
    return sessionId;
}

async function closeSession(sessionId) {
    const url = `${open_api_endpoint}/session/${sessionId}/close`
    const response = (await axios.request({
        method: 'PUT',
        url: url,
        headers: {
            Authorization: access_token
        }
    })).data;
    return response;
}

async function connectHuaYoung(remoteDebugPort) {
    //等待浏览器ready
    await new Promise((resolve => {
        setTimeout(()=>resolve(1), 5000);
    }));

    const options = new chrome.Options();
    let service = new chrome.ServiceBuilder(path_to_webdriver);
    options.debuggerAddress(`127.0.0.1:${remoteDebugPort}`);
    let driver = await new Builder()
        .forBrowser('chrome')
        .setChromeOptions(options)
        .setChromeService(service)
        .build();

    //拿到 driver 之后就可以对花漾指纹浏览器进行任何操作了
    await driver.get('https://www.qq.com');

    await driver.getTitle().then((title) => {
        console.log('网页标题为:', title);
    })

    //some code ...

    await driver.quit();
}

(async function main() {
    try {
        access_token = await getToken();
        console.log(access_token);

        let remoteDebugPort = 9222; //随便什么端口都可以,后续 selenium 会通过该端口连接花漾指纹浏览器,要确保该端口未被占用
        const sessionId = await openSession(remoteDebugPort);
        console.log('打开会话成功,sessionId=' + sessionId);

        await waitPortOpen(remoteDebugPort);

        await connectHuaYoung(remoteDebugPort);

        console.log('开始关闭会话,sessionId=' + sessionId);
        await closeSession(sessionId);
        console.log('任务完成');
    } catch (e) {
        console.error(e);
    }
})();

如有需要,您可以点这里下载 Demo程序的源码

10、其它

如果您有新的 API 方面的诉求,或者还希望我们提供其它语言的的Demo,请通过在 在线客服 联络我们。

最后更新于 2024-03-27 16:52
回到顶部