月度归档:2017年10月

Paypal API

Paypal提供了两套API,REST API和NVP/SOAP API。REST API没有完全覆盖NVP API的功能,NVP API历史久远,未来应该会被REST API替换。

REST API使用OAuth2标准,首先需要有一个开发者账户,然后在开发者账户中创建APP(产生client_id, client_secret),Paypal账户授权给这个APP。

开发者网站:http://developer.paypal.com,然后需要注册一个真实的Paypal账户(可以个人,也可以商用),拿这个账户作为开发者账户去登录,然后就可以创建APP,分两个环境:正式和测试。如何创建一个测试APP,那么会自动给你创建一个测试商家账户,还有一个买家账户。当然也可以自己建多个测试账户。

——————————————————————-
在REST API之前,只有NVP API。为了可以使用NVP API,需要到自己的Paypal中的API设置中取到相关API签名(API用户名和密码,以及签名),只要暴露了这三个信息,就相当于是开放了自己的账户。

每个Paypal可以作为一个第三方,其它的Paypal账户如果把某些权限赋给了它,它就可以扮演第一方的身份去获取信息。关于权限赋予的流程,官方文档有详细描述。而Paypal后台的第三方许可,实际对应这个操作,最终都是赋予第三方权限。

这里需要输入的就是第三方Paypal账户的API的用户名(每个账户中的API签名部分),点击查找后会让你选择哪些权限赋予这个第三方:

结论:Paypal可以通过API签名开放账户,也可以把权限授予其它账户,由其它账户(第三方)代理访问。

<?php

namespace Paypal;

class Api
{
    protected $username = '';

    protected $password = '';

    protected $signature = '';

    protected $version = '95.0';

    protected $endPoint = 'https://api-3t.paypal.com/nvp';

    protected $subject = '';

    public function __construct($username, $password, $signature, $subject = '', $sandbox = false)
    {
        $this->username = $username;
        $this->password = $password;
        $this->signature = $signature;
        if (!empty($subject)) {
            $this->subject = trim($subject);
        }
        if ($sandbox) {
            $this->endPoint = 'https://api-3t.sandbox.paypal.com/nvp';
        }
    }

    // 作为第三方访问Paypal
    public function setSubject($email)
    {
        $this->subject = trim($email);
    }

    // 取回交易列表
    //$params = [
    //    'STARTDATE' => $startTime,
    //    'ENDDATE' => $endTime,
    //    'RECEIVER' => '',
    //    'TRANSACTIONCLASS' => 'All'
    //];
    public function getTransactions(array $params)
    {
        $return = ['success' => 0, 'message' => '', 'data' => []];
        if (empty($params['STARTDATE']) || empty($params['ENDDATE'])) {
            $return['message'] = '参数不合法';
            return $return;
        }

        $result = $this->post('TransactionSearch', $params);
        if (false === $result) {
            $return['message'] = 'CURL请求异常';
            return $return;
        }

        $tarr = explode('&', $result);
        $data = [];
        foreach ($tarr as $item) {
            $tmp = explode('=', rawurldecode($item));

            preg_match('/^L_([a-zA-Z\_]+)([0-9]+)/', $tmp[0], $m);
            if (isset($m[0]) && isset($m[1]) && isset($m[2])) {
                $data[$m[1]][$m[2]] = trim($tmp[1]);
            } else {
                $data[$tmp[0]] = trim($tmp[1]);
            }
        }

        // ACK 等于 Warming时,数据返回不齐全(Paypal每次查询最多返回100条)
        if (empty($data['ACK']) || ($data['ACK'] == 'Failure')) {
            $return['message'] = "API调用ACK返回Failure";
        } else {
            $return['success'] = 1;
            $return['data'] = $data;
        }

        return $return;
    }
 
    // 通用封装
    protected function post($api, array $params)
    {
        $global = [
            'METHOD' => $api,
            'VERSION' => $this->version,
            'USER' => $this->username,
            'PWD' => $this->password,
            'SIGNATURE' => $this->signature
        ];
        if (!empty($this->subject)) {
            $global['SUBJECT'] = $this->subject;
        }

        return $this->doRequest($this->endPoint, array_merge($global, $params));
    }

    // CURL请求
    protected function doRequest($url, $data)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HEADER, '');
        curl_setopt($ch, CURLOPT_URL, trim($url));
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 90);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
        if (!empty($data)) {
            if (is_array($data)) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
            } elseif (is_string($data)) {
                curl_setopt($ch, CURLOPT_POST, true);
                curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
            }
        }
        if (\PHP_OS === 'WINNT') {
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        }
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0");
        $result = curl_exec($ch);
        $error = curl_errno($ch);
        curl_close($ch);
        if ((int)$error == 0) {
            return $result;
        }
        return false;
    }
}

事件循环:JavaScript、Node.js、PHP-Swoole、PHP-workerman

在浏览器端,JavaScript的主要是与用户互动,以及操作DOM。为了避免复杂性,JavaScript被设计为只能运行在单进程单线程中。H5提出了Web Worker标准,允许JavaScript创建多个线程,但子线程受主线程控制,且不得操作DOM。

在JavaScript中,任务分为同步任务和异步任务(异步任务不堵塞当前线程),所有同步任务都在主线程上执行,形成一个执行栈,另外存在一个“任务队列”,如果异步任务有了结果,就在“任务队列”中添加一个事件(对应回调),一旦“执行栈”中的所有同步任务执行完毕,就会读取“任务队列”,取出事件对应的回调,然后进入执行栈执行回调(注:任务队列中对应的是事件,事件可以对应多个回调,回调的执行也依赖事件对象记录的原始参数)。

事件的产生包括用户比如用户点击,异步调用后触发的事件,取出事件是一个循环过程(执行栈空),所以这个过程叫事件循环(event loop)。

Node.js中的事件循环原理上和JavaScript中的(应该是浏览器)并没有很大不同。Node.js中为了对付大量的链接,使用了epoll,对于异步任务,使用了一个线程池来模拟,回调的执行全部落在主线程中,所以它是单进程单线程的(这个也是为何面对CPU密集运算时的场景不合适,尽管还有其它的CPU是空闲的,因为执行回调的,仅一个线程)。

PHP中的一个扩展Swoole,理念上和Node.js是差不多的,但是架构上有很大不同。Swoole启动后,首先启动一个主进程,在这个进程内其它若干个React线程,这些线程专门负责监听,接收数据和响应,然后启动一个manager进程和一组Worker进程和若干task进程,manager进程主要用来监控worker进程和task进程(比如退出重启等),Worker可以把耗时的任务投递给task进程,task执行时同步堵塞的,执行完毕后通过进程间通信的方式通知Worker进程。每个Worker进程维护一个事件循环,并在Worker进程内执行回调(可以应用到多核CPU)。

PHP-workerman相对Swoole来说,架构上就比较简单。它相当于只有Swoole的Worker进程这部分。每个Worker都相互独立的监听端口,执行回调,响应数据等。

MySQL 事务与锁查看

information_schema.innodb_trx

trx_id					事务ID
trx_state				事务状态
trx_started				事务执行开始时间
trx_requested_lock_id			事务等待锁ID号(等待其它事务释放锁)
trx_wait_started			事务等待锁开始时间
trx_weight						
trx_mysql_thread_id			事务线程ID
trx_query				具体的SQL
trx_operation_state			事务当前操作状态
trx_tables_in_use			事务中有多少个表被使用
trx_tables_locked				
trx_lock_structs					
trx_lock_memory_bytes			事务锁住的内存大小(B)
trx_rows_locked				事务锁住的行数
trx_rows_modified			事务更改的行数
trx_concurrency_tickets			事务并发数
trx_isolation_level			事务隔离级别
trx_unique_checks			是否唯一性检查
trx_foreign_key_checks			是否外键检查
trx_last_foreign_key_error		最后的外键错误
trx_adaptive_hash_latched		
trx_adaptive_hash_timeout		

注:trx_started记录了事务开始的时间,如果过去了很长时间,可能是异常事务。trx_wait_started记录了等待时间,如果等待了很长时间,可能是异常事务。对应等待锁的事务,trx_query记录了具体的SQL语句。trx_mysql_thread_id可以定位到具体的线程(回话ID)

information_schema.innodb_locks

lock_id							锁ID
lock_trx_id						拥有锁的事务ID
lock_mode						锁模式
lock_type						锁类型
lock_table						被锁的表
lock_index						被锁的索引(类型)
lock_space						被锁的表空间号
lock_page						被锁的页号
lock_rec						被锁的记录号
lock_data						被锁的数据(对应索引编号,一般是ID号)

事务可以持有多个锁。锁类型有S和X。根据索引类型不同,lock_rec和lock_data可以定位行号。当开启一个事务,对相关行上锁,这个时候的锁不会出现在innodb_locks表中,只有相关的锁被其它事务等待时,产生了锁等待,才会把锁与等待的锁插入此表(换个说法就是此表是用来存放有依赖关系的锁的)

information_schema.innodb_lock_waits

requesting_trx_id					请求锁的事务ID
requested_lock_id					请求锁的锁ID
blocking_trx_id						当前拥有锁的事务ID
blocking_lock_id					当前拥有锁的锁ID
requesting_thd_id					请求锁的线程ID
blocking_thd_id						当前拥有锁的线程ID

记录依赖关系。请求锁等待持有锁。

1 当要查看有哪些线程时,直接运行show full processlist即可,这个命令动态列出当前的线程状态
2 当要查看有哪些事务时(比如检查有哪些事务长时间未结束),可以直接查看innodb_trx表,这个表中的trx_started记录了开始事务的时间。
3 当要查看是否有锁等待时,可以查看innodb_locks,只要有记录,就说明产生了锁等待,具体是哪个依赖哪个,需要查看innodb_lock_waits的关系。

一般来说,产生了锁等待,如果超时,事务会自动释放,但是如果事务开启了,单长时间没有结束,就应该去innodb_trx查看确认(从线程基本无法查看到已经开启了事务)。

获得导致行锁等待和行锁等待超时的会话:

select l.* from ( select 'Blocker' role, p.id, p.user, left(p.host, locate(':', p.host) - 1) host, tx.trx_id, tx.trx_state, tx.trx_started, timestampdiff(second, tx.trx_started, now()) duration, lo.lock_mode, lo.lock_type, lo.lock_table, lo.lock_index, tx.trx_query, lw.requesting_thd_id Blockee_id, lw.requesting_trx_id Blockee_trx from information_schema.innodb_trx tx, information_schema.innodb_lock_waits lw, information_schema.innodb_locks lo, information_schema.processlist p where lw.blocking_trx_id = tx.trx_id and p.id = tx.trx_mysql_thread_id and lo.lock_id = lw.blocking_lock_id union select 'Blockee' role, p.id, p.user, left(p.host, locate(':', p.host) - 1) host, tx.trx_id, tx.trx_state, tx.trx_started, timestampdiff(second, tx.trx_started, now()) duration, lo.lock_mode, lo.lock_type, lo.lock_table, lo.lock_index, tx.trx_query, null, null from information_schema.innodb_trx tx, information_schema.innodb_lock_waits lw, information_schema.innodb_locks lo, information_schema.processlist p where lw.requesting_trx_id = tx.trx_id and p.id = tx.trx_mysql_thread_id and lo.lock_id = lw.requested_lock_id) l order by role desc, trx_state desc;

对于复杂的多个会话相互行锁等待情况,建议先终止 Role 为 Blocker 且 trx_state 为 RUNNING 的会话;终止后再次检查,如果仍旧有行锁等待,再终止新结果中的 Role 为 Blocker 且 trx_state 为 RUNNING 的会话。

对于标识为 Blocker 的会话(持有锁阻塞其他会话的 DML 操作,导致行锁等待和行锁等待超时),确认业务可以接受其对应的事务回滚的情况下,可以将其终止。比如,可以通过 Kill 命令来今后会话终止。

浏览器编程:WebDriver – ChromeDriver

浏览器编程有两个主要应用:
1 自动化测试
2 通过浏览器自动抓取内容(针对防抓的网站,模拟人工点击)

Selenium Server只是作为一个代理,它的作用是当要驱动远程浏览器(驱动一般只能监听本地端口),或需要驱动不同版本的浏览器时会有很大的作用。否则,应用程序直接面对具体的驱动即可(Selenium Server仅转发Json)。
注意:PhantomJS视乎是没有提供驱动,为了驱动这个无头浏览器,Selenium Server应该是把Json数据转换成了JS脚本让其执行(未证实)

目前主流浏览器(Chrome Firefox)都提供了WebDriver的实现,比如Chrome对应的是ChromeDriver,Firefox对应的FirefoxDriver。注:WebDriver只是一个规范标准(https://w3c.github.io/webdriver/webdriver-spec.html),而实现的方式可以不同,而https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol这里描述的就是一种实现方式,任何实现了这个规范的驱动,都具有相同的API。

WebDriver is an open source tool for automated testing of webapps across many browsers. It provides capabilities for navigating to web pages, user input, JavaScript execution, and more. ChromeDriver is a standalone server which implements WebDriver’s wire protocol for Chromium. ChromeDriver is available for Chrome on Android and Chrome on Desktop (Mac, Linux, Windows and ChromeOS).
ChromeDriver是一个实现了WebDriver无线协议的独立服务器,所以需要下载这个服务器(驱动,启动后在本地监听9515端口,所有的操作发送到9515端口,这个驱动负责解析数据并操作浏览器,所以它是一个中间件)。

下载地址:https://sites.google.com/a/chromium.org/chromedriver/downloads(有三个平台,需要注意的是不同的版本对应的Chrome版本是不同的)。

关于使用,ChromeDriver提供了一个文档:
https://sites.google.com/a/chromium.org/chromedriver/getting-started(关键点:Chrome需要安装在默认位置(否则需要指定),下载正确的ChromeDriver版本)

ChromeDriver作为一个独立的服务,可以手动启动并监控,也可以在使用SDK中提供的方法启动。

#####
@RunWith(BlockJUnit4ClassRunner.class)
public class ChromeTest extends TestCase {

  private static ChromeDriverService service;
  private WebDriver driver;

  @BeforeClass
  public static void createAndStartService() {
    service = new ChromeDriverService.Builder()
        .usingDriverExecutable(new File("path/to/my/chromedriver"))
        .usingAnyFreePort()
        .build();
    service.start();
  }

  @AfterClass
  public static void createAndStopService() {
    service.stop();
  }

  @Before
  public void createDriver() {
    driver = new RemoteWebDriver(service.getUrl(),
        DesiredCapabilities.chrome());
  }

  @After
  public void quitDriver() {
    driver.quit();
  }

  @Test
  public void testGoogleSearch() {
    driver.get("http://www.google.com");
    // rest of the test...
  }
}

####独立启动
$ ./chromedriver
Started ChromeDriver
port=9515
version=14.0.836.0

WebDriver driver = new RemoteWebDriver("http://127.0.0.1:9515", DesiredCapabilities.chrome());
driver.get("http://www.google.com");

####PHP
$service = new \Facebook\WebDriver\Chrome\ChromeDriverService(‘path/to/my/chromedriver’, 9515);
$service->start();
$service->stop();

$driver = RemoteWebDriver::create( $service->getURL(),[
                ChromeOptions::CAPABILITY => $options,
                WebDriverCapabilityType::PROXY => [
                    'proxyType'=> 'manual',
                    'httpProxy' => 'SOCKS5://127.0.0.1:1086',
                    'sslProxy' => 'SOCKS5://127.0.0.1:1086',
                    'socksProxy' => 'SOCKS5://127.0.0.1:1086'
                ]]);

ChromeDriver实际释放的是一套RESTfull API,可以参考:https://chromium.googlesource.com/chromium/src/+/master/docs/chromedriver_status.md,所以只要按照规范发送数据即可,也可以使用SDK,比如PHP,Facebook提供了一套实现:

composer require facebook/webdriver

应用实例:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Laravel\Dusk\Chrome\ChromeProcess;
use Laravel\Dusk\Browser;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\WebDriverCapabilityType;
use Facebook\WebDriver\Remote\DriverCommand;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;

class Test extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'test';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        //$process = (new ChromeProcess())->toProcess();
        //$process->start();

        try {
            // 取回已经打开的SessionID
            $reuseSessionId = '51492cf202343defea198867e32a81e3';
            try {
                $driver = $this->driverBy($reuseSessionId);
            } catch (\Exception $e) {
                $driver = $this->driver();
            }

            $driver->execute(DriverCommand::CLICK);
        } catch (\Exception $e) {
            echo 'Browser can not start up: ' . $e->getMessage();
            return;
        }

        // 取回所有SESSION
        print_r($driver->getAllSessions('http://localhost:9515'));

        $sessionID = $driver->getSessionID();
        echo "\n";
        echo $sessionID;
        echo "\n";

        $browser = new Browser($driver);
        $browser->visit('https://www.baidu.com/')->type("#kw", 'ip')->press("#su");
        $browser->visit('https://www.amazon.com');

        $driver->close();
        $driver->get('http://blog.ifeeline.com');
    }

    protected function driverBy($sessionId)
    {
        $driver = RemoteWebDriver::createBySessionID($sessionId, 'http://localhost:9515');
        $driver->execute(DriverCommand::CLICK);

        return $driver;
    }

    protected function driver()
    {
        $options = (new ChromeOptions)->addArguments([
            //'--disable-gpu',
            //'--headless'
        ]);

        return RemoteWebDriver::create(
            'http://localhost:9515',
            [
                ChromeOptions::CAPABILITY => $options,
                WebDriverCapabilityType::PROXY => [
                    'proxyType'=> 'manual',
                    'httpProxy' => 'SOCKS5://127.0.0.1:1086',
                    'sslProxy' => 'SOCKS5://127.0.0.1:1086',
                    'socksProxy' => 'SOCKS5://127.0.0.1:1086'
                ]
            ]
        );

        /*
        return RemoteWebDriver::create(
            'http://localhost:9515',
            DesiredCapabilities::chrome()->setCapability(
                ChromeOptions::CAPABILITY, $options
            )->setCapability(
                WebDriverCapabilityType::PROXY,
                [
                    'proxyType'=> 'manual',
                    'httpProxy' => 'SOCKS5://127.0.0.1:1086',
                    'sslProxy' => 'SOCKS5://127.0.0.1:1086',
                    'socksProxy' => 'SOCKS5://127.0.0.1:1086'
                ]
            )
        );
        */
    }
}

在控制浏览器上,可以应用了–disable-gpu和–headless,这样就是一个无头浏览器了(不显示具体的过程)。另外,在创建的浏览器时,可以指定代理。另外,如果一个SESSION没有正确退出,那么它还是活动的,但是它却无法重用。在知道SESSIONID的情况下,SESSION可以重用。 一般来说,进行一个任务就开启一个浏览器,完毕后正常退出记录。

Laravel中的Dusk程序包,封装的更加狠一些,连ChromeDriver二进制程序包都拉取回来,自动启动监听,对个Facebook SDK进行二次封装,使API更加友好。

如果模拟人工进行大量操作,就会频繁启动关闭浏览器,实际上,浏览器启动后对应一个SESSION,接下来只要重用这个SESSION即可,基本思路:如果当前有可重用的SESSION,就重用,没有就新建;在任务执行完后,判断SESSION是否超过了最大值,超过则关闭,否则,标记该SESSION可重用(配合定时重启脚本,防止意外)。