欢迎光临
我们一直在努力

PHP使用PDFParser解析PDF中的文字

安装

composer require smalot/pdfparser

安装完成之后,在入口文件引入自动加载文件

include 'vendor/autoload.php';  //根据自己入口文件的路径合理配置

使用方法

<?php
 
// Include Composer autoloader if not already done.
include 'vendor/autoload.php';
 
// Parse pdf file and build necessary objects.
$parser = new \Smalot\PdfParser\Parser();
$pdf    = $parser->parseFile('document.pdf');
 
$text = $pdf->getText();
echo $text;
 
?>

如何获取指定页的内容

$parser = new \Smalot\PdfParser\Parser();       
// 调用解析方法,参数为pdf文件路径,返回结果为Document类对象
$document = $parser->parseFile('238.PDF');
// 获取所有的页
$pages = $document->getPages();
//$pages[0]->getText();  //提取第一页的内容,想提取多页,可以按照下面的方法,用$key来控制要获取的页数
// 逐页提取文本
foreach($pages as $key=>$page){
    if($key === 0){
        //提取第一页的内容
        echo $pages[$key]->getText();  
    }
}   

Linux(CentOS、Ubuntu)搭建selenium+php-webdriver web自动化测试环境详细教程

因为前段时间采集的一个目标站突然做了防采集(通过js加载内容,js代码加密了),我又不懂解密js,就想着通过php模拟浏览器渲染js加载内容,查了下资料才发现有这么个玩意可以控制浏览器(chrome、firefox等,我使用的是chrome),完全满足了我的需求。不过网上查的资料都比较乱,这里就做了个整合,也当做为自己做个记录文档,方便以后查阅。

我是用的root账户安装,非root账号执行某些命令前请自行加上sudo

安装前准备

安装java环境

已有Java环境的跳过这一步

CentOS:

yum -y install java-1.8.0
-openjdk*x86_64

Ubuntu:

方法1:

apt -y install openjdk-
8
-jre-headless

方法2:

 

add-apt-repository ppa:openjdk-r/ppa
apt update
apt -y install openjdk-8-jdk

安装php

php安装方法就不说了,网上很多,也可以使用宝塔、护卫神、lnmp.org之类的一键安装包。

安装composer

composer安装教程我在另一个篇文章有写到,不会安装的可以到这里看:Linux安装composer教程

安装用到的软件包

CentOS:

yum -y install wget unzip

Ubuntu:

apt -y install wget unzip

开始安装

安装chrome

CentOS:

方法1:

在线安装

yum install https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm

方法2:

先下载安装包再安装

 

wget -c https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
yum -y install ./google-chrome-stable_current_x86_64.rpm

Ubuntu:

 

apt -y install libxss1 libappindicator1 libindicator7
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
dpkg -i ./google-chrome-stable_current_amd64.deb
apt -f -y install

下载selenium

地址:http://selenium-release.storage.googleapis.com/index.html

我下载的3.9.1版本,下载到

/usr/local/selenium/

目录下

创建目录并下载

 

mkdir /usr/local/selenium
wget -c http://selenium-release.storage.googleapis.com/3.9/selenium-server-standalone-3.9.1.jar -O /usr/local/selenium/selenium-server-standalone.jar

下载chromedriver

地址:https://chromedriver.storage.googleapis.com/index.html

chromedriver的版本要跟安装的chrome的版本相对应,如果没有一样的版本就选择版本号最接近的。

查看chrome版本:

/opt/google/chrome/chrome -version

我的chrome版本:

102.0.5005.61

Linux命令行查看chrome版本查看chrome版本

chromedriver同样下载解压到

/usr/local/selenium/

目录下

 

wget -c https://chromedriver.storage.googleapis.com/102.0.5005.61/chromedriver_linux64.zip -O /usr/local/selenium/chromedriver_linux64.zip
unzip /usr/local/selenium/chromedriver_linux64.zip -d /usr/local/selenium/

使用composer安装php-webdriver

先进入项目目录再拉取,我的目录路径是

 

/home/phpwebdriver/
cd /home/phpwebdriver/
composer require php-webdriver/webdriver

composer安装php-webdrivercomposer安装php-webdriver

运行&测试

运行selenium服务

执行命令后将无法做其他操作,建议在screen里面运行或者用nohup命令

 

screen -S selenium
java -jar -Dwebdriver.chrome.driver="/usr/local/selenium/chromedriver" /usr/local/selenium/selenium-server-standalone.jar -port 6666

运行selenium运行selenium

然后按

ctrl+a+d

可以退出screen环境。

测试

新建个

notevm.php

文件,写入以下代码。

 

<?php
namespace Facebook\WebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Chrome\ChromeOptions;
require_once('./vendor/autoload.php');
$options = new ChromeOptions();
$options->addArguments(array('--no-sandbox','--disable-dev-shm-usage'));
$options->addArguments(array('--headless'));//无头模式,不会弹出浏览器窗口,没有安装图形界面的一定要设置这个
//$options->setExperimentalOption('prefs', array('profile.managed_default_content_settings.images'=>2));//禁止加载图片
$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);
$host = 'http://127.0.0.1:6666/wd/hub';//selenium服务监听地址
$driver = RemoteWebDriver::create($host,$capabilities,5000);
$size = new WebDriverDimension(1920,1080);//窗口大小
$driver->manage()->window()->setSize($size);//设置窗口大小
//$driver->manage()->window()->maximize();//浏览器窗口最大化
$driver->get('https://www.notevm.com/');//打开网页
/*
$driver->wait(10,100)->until(
//等待class为navbar-search-icon的元素加载完成,最多等待10秒,每100毫秒重试一次
    function () use ($driver){
        return $driver->findElements(WebDriverBy::className('navbar-search-icon'));
    }
);
*/
$driver->manage()->timeouts()->implicitlyWait(10);//等待所有元素加载完成,最多等待10秒
$driver->findElement(WebDriverBy::className('navbar-search-icon'))->click();//点击搜索按钮
$driver->findElement(WebDriverBy::className('navbar-search-input'))->click();//点击输入框
$driver->getKeyboard()->sendKeys('php');//输入php
$driver->findElement(WebDriverBy::className('navbar-search-btn'))->click();//点击搜索按钮
/*
$driver->getKeyboard()->pressKey(WebDriverKeys::ENTER);//按下回车键
$driver->getKeyboard()->releaseKey(WebDriverKeys::ENTER);//释放回车键
*/
$driver->wait(10,100)->until(
    function () use ($driver){
        return $driver->findElements(WebDriverBy::className('main'));//等待class为main的元素加载完成
    }
);
$driver->takeScreenshot('./notevm.png');//截图,如果系统没有安装中文支持的话,中文会出现乱码
$driver->findElement(WebDriverBy::className('main'))->takeElementScreenshot('notevm-main.png');//截取某个元素的图片
$html = $driver->getPageSource();//获取html
$driver->quit();//退出浏览器
file_put_contents('./html.txt',$html);//保存HTML到文件
?>

执行

php notevm.php

测试php-webdriver抓取效果测试抓取效果

可以看到成功截两张图片跟保存了html代码。

[php-webdriver/webdriver]用PHP控制浏览器做自动化测试

PHP的一款对Selenium 的命令封装度很高的库,写起来简直太舒服了.

 

php-webdriver/webdriver是一个PHP写的Selenium 扩展库,可以用PHP控制浏览器的各种操作.

可以看这样一段代码,执行之后会打开浏览器,打开百度网页,然后搜索PHP并且提交表单.

<?php

namespace Facebook\WebDriver;

use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;

require_once(‘vendor/autoload.php’);

// 设置selenium的服务器
$host = ‘http://localhost:4444/wd/hub’;

// 还用火狐浏览器
$capabilities = DesiredCapabilities::chrome();

$driver = RemoteWebDriver::create($host, $capabilities);

// 打开一个网页
$driver->get(‘https://www.baidu.com’);

// 找到搜索框的DOM,然后在搜索框输入PHP
$driver->findElement(WebDriverBy::id(‘kw’))
->sendKeys(‘PHP’)
->submit(); // 提交表单

有时候我们要做一个自动化脚本演示,这是这个库就派上用场了.

他封装了众多的selenium的命令,比如:

打开网页
$result = $driver->get(‘https://phpreturn.com/’);
操作DOM元素
定位元素
// 定位一个元素:
$element = $driver->findElement(WebDriverBy::cssSelector(‘div.header’));
$headerText = $element->getText();
// 定位多个元素:
$elements = $driver->findElements(WebDriverBy::cssSelector(‘ul.foo > li’));
foreach ($elements as $element) {
var_dump($element->getText());
}
支持多种选择器:

Css 选择器- WebDriverBy::cssSelector(‘h1.foo > small’)
Xpath – WebDriverBy::xpath(‘(//hr)[1]/following-sibling::div[2]’)
Id – WebDriverBy::id(‘heading’)
Class 类 – WebDriverBy::className(‘warning’)
Name 属性 (inputs) – WebDriverBy::name(’email’)
标签名 – WebDriverBy::tagName(‘h1’)
链接的文本 – WebDriverBy::linkText(‘搜索’)
链接的部分文本- WebDriverBy::partialLinkText(‘搜’)
等待页面的某个部分加载完成
$element = $driver->wait()->until(
WebDriverExpectedCondition::presenceOfElementLocated(WebDriverBy::cssSelector(‘div.bar’))
);
获取元素文本
$title = $driver->findElement(WebDriverBy::id(‘sign-in’))->getAttribute(‘title’);

// Get value of an input/textarea element
$inputElement = $driver->findElement(WebDriverBy::id(‘username’));
$value = $inputElement->getAttribute(‘value’);
获取元素属性
$title = $driver->findElement(WebDriverBy::id(‘sign-in’))->getDomProperty(‘innerHTML’);
点击元素
$driver->findElement(WebDriverBy::id(‘sign-in’))->click();
输入内容
$driver->findElement(WebDriverBy::id(‘element id’))->sendKeys(‘PHP武器库’);
清空输入
$driver->findElement(WebDriverBy::id(‘element id’))->clear();
实现其他鼠标事件
$element = $driver->findElement(WebDriverBy::id(‘some_id’));
$driver->getMouse()->mouseMove($element->getCoordinates());
复杂的点击事件
如果元素可见,那么执行点击:

$element = $driver->findElement(WebDriverBy::id(‘element id’));
if ($element->isDisplayed()) {
// 做点什么
}
检查Select是否选中
$checkboxElement = $driver->findElement(WebDriverBy::id(‘myCheckbox’));
if ($checkboxElement->isSelected()) {
// …
}
各种对于select的操作
$selectElement = $driver->findElement(WebDriverBy::name(‘language-select’));
$select = new WebDriverSelect($selectElement); // 对于radio则使用WebDriverRadio

// 获取第一个(默认的)选项:
echo $select->getFirstSelectedOption()->getText();

// 获取全部的选项
$selectedOptions = $select->getAllSelectedOptions();

// 选择一个选项
$select->selectByValue(‘fr’);
$select->selectByIndex(1);
$select->selectByVisibleText(‘Czech’);
$select->selectByVisiblePartialText(‘UK’);
提交表单
对于一个表单,有多种提交方式.

<form id=”myForm” method=”post”>
<input type=”text” name=”query”>
<input type=”submit” name=”submit” value=”Submit”>
</form>
$driver->findElement(WebDriverBy::id(‘myForm’))
->submit(); // 标准的提交事件

$driver->findElement(WebDriverBy::name(‘query’))
->submit(); // 在input的提交事件
$driver->findElement(WebDriverBy::name(‘submit’))
->click(); // 点击提交按钮
其他浏览器操作
警告框/选择/取消
页面刷新/后退
操作tab/iframe
最大化/最小化/全屏
执行注入JS
截屏等
总之,它是一个健全的Selenium 操作库,可以使用PHP完成浏览器自动化脚本.

Php-webdriver 的安装与使用教程

Php-webdriver 是 Facebook 开发的基于 PHP 语言实现的 Selenium WebDriver 客户端组件,可以用它来操作浏览器。常见的操作包括:自动化测试、采集数据等。

安装浏览器(Google Chrome 或 Firefox)

以 Ubuntu server 16.04 安装 Google Chrome 浏览器为例(参考链接)。

下载最新版的 64 位 Google Chrome:

wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb

安装 Google Chrome 并自动安装依赖:

sudo dpkg -i --force-depends google-chrome-stable_current_amd64.deb

若提示 dependency problems 出错信息,表示某些依赖库没有安装,键入以下命令来自动安装好依赖:

sudo apt-get install -f

测试 Google Chrome:

# 安装中文字体
sudo apt-get install fonts-noto-cjk
# 运行 Google Chrome headless 并截图保存
LANGUAGE=ZH-CN.UTF-8 google-chrome --headless https://www.pc6.com --no-sandbox --screenshot --window-size=1400,900

看到如下的图片就表明安装成功了。

screenshot

安装浏览器驱动程序(Chromedriver 或 Geckodriver)

以安装 Chromdriver 为例。

要保证 Chromedriver 和 Google Chrome 是相匹配的版本。在 Chromedriver 的官方下载页面有版本说明,按照需要下载。

如果安装的 Google Chrome 的版本号是 81,那么根据 Chromdriver 官方说明,也需要下载对应的版本号是 81 的 Chromdriver。

# 查询 Google Chrome 的版本号
root@aeb9f39e9e04:/tmp# google-chrome --version
Google Chrome 81.0.4044.92

chromdriver_download

下载完成后,解压缩出二进制的可执行文件,这就是我们需要的浏览器驱动程序(WebDriver)了。通过 Chromedriver 可以控制 Google Chrome 的操作。

启动 Chromedriver 并监听 4444 端口。

LANGUAGE=ZH-CN.UTF-8 ./chromedriver --port=4444

安装 Php-webdriver

当做完前面两步准备工作,安装好了浏览器(Google Chrome)与浏览器驱动程序(Chromdriver)之后,总算可以进入主题,安装与使用 Php-webdriver 了。

安装 Php-webdriver

composer require php-webdriver/webdriver

使用 Php-webdriver

打开浏览器

$options = new ChromeOptions();
$options->addArguments([
     '--window-size=1400,900',
     '--headless',
]);

$capabilities = DesiredCapabilities::chrome();
$capabilities->setCapability(ChromeOptions::CAPABILITY, $options);

$host = 'http://localhost:4444';
$driver = RemoteWebDriver::create($host, $capabilities);

以上代码加上了打开浏览器的同时加上了窗口大小和无头浏览器的参数,可以按需增减。更多关于 ChromeOptions 的参数请查看 https://sites.google.com/a/chromium.org/chromedriver/capabilities

打开 URL

$driver->get('https://www.baidu.com/');
$driver->navigate()->to('https://www.sogou.com/');

刷新页面

$driver->navigate()->refresh();

查找单个元素

$element = $driver->findElement(WebDriverBy::cssSelector('div.header'));

查找多个元素

$elements = $driver->findElements(WebDriverBy::cssSelector('ul.foo > li'));

WebDriverBy 提供了多种查询方式:

WebDriverBy::id($id) 根据 ID 查找元素
WebDriverBy::className($className) 根据 class 查找元素
WebDriverBy::cssSelector($selctor) 根据通用的 css 选择器查询
WebDriverBy::name($name) 根据元素的 name 属性查询
WebDriverBy::linkText($text) 根据可见元素的文本锚点查询
WebDriverBy::tagName($tagName) 根据元素标签名称查询
WebDriverBy::xpath($xpath) 根据 xpath 表达式查询

输入内容

$driver->findElement(WebDriverBy::cssSelector('input[name=username]'))->sendKeys('imzhi');

点击元素

$driver->findElement(WebDriverBy::cssSelector('button#login'))->click();

截图

$driver->takeScreenshot(__DIR__ . '/screenshot.png');

执行 JS

RemoteWebDriver::executeScript($script, $args) 执行同步 JS 代码
RemoteWebDriver::executeAsyncScript($script, $args) 执行异步 JS 代码

$driver->executeScript("document.body.style.backgroundColor = 'red';");

关于 Php-webdriver 的更多使用详情可以去官方 wiki 或者去官方 API 文档上查阅。

最后用一个小例子结束这篇教程。

抓取豆瓣电影的DEMO

抓取豆瓣电影里韩国分类下最新上映的前 120 部影视剧的标题与 LOGO 到本地文件夹中。

https://gist.github.com/imzhi/ae547a504e8344e6f2333213eddec97e

采集结果截图:

douban_result

前端JS与后端PHP加密解密

网页端(在没有https情况下)给密码之类的加密传输,虽然多此一举,也好过直接监控软件就能看到信息

PHP端

    /**
     * 前台加密(对应JS解密)
     * @param $data
     * @param $key
     * @param $iv
     * @return string
     */
    function js_encrypt($data,$key,$iv){
        $data = openssl_encrypt($data,"AES-128-CBC",$key,true,$iv);
        return base64_encode($data);
    }



    /**
     * 前台解密(对应JS加密)
     * @param $data
     * @param $key
     * @param $iv
     * @return string
     */
    function js_decrypt($data,$key,$iv){
        $data = openssl_decrypt(base64_decode($data),"AES-128-CBC",$key,true,$iv);
        $data = rtrim($data,"\0");
        return json_decode($data,true);
    }




 

后端使用

$str = "要加密的文本信息,如果是对象,请转换成文本";
$key = "52c7f81cd24c9699";//16位加密key
$iv = "42e07d2f7199c35d";//16位偏移量

//加密成密文
$new_str = js_encrypt($str,$key,$iv);
var_dump($new_str);
//解密成原文
$old_str = js_decrypt($new_str,$key,$iv);
var_dump($old_str);


16位偏移量可以用通过时间戳+随机数转16位小写的md5实现

 

前端

/**
* 接口数据加密函数
* @param str string 需加密的json字符串
* @param key string 加密key(16位)
* @param iv string 加密向量(16位)
* @return string 加密密文字符串
*/
function js_encrypt(str, key, iv) {
//密钥16位
key = CryptoJS.enc.Utf8.parse(key);
//加密向量16位
iv = CryptoJS.enc.Utf8.parse(iv);
let encrypted = CryptoJS.AES.encrypt(str, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});

return encrypted.toString();
}

/**
* 接口数据解密函数
* @param str string 已加密密文
* @param key string 加密key(16位)
* @param iv string 加密向量(16位)
* @returns {*|string} 解密之后的json字符串
*/
function js_decrypt(str, key, iv) {

//密钥16位
key = CryptoJS.enc.Utf8.parse(key);
//加密向量16位
iv = CryptoJS.enc.Utf8.parse(iv);
let decrypted = CryptoJS.AES.decrypt(str, key, {
iv: iv,
padding: CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8);

return decrypted;
}


前端需要引入类库,基于crypto-js(点击下载)

<!--加密解密类-->
<script src="crypto-js/aes.js" type="text/javascript"></script>

前端使用

let str = "要加密的文本信息,如果是对象,请转换成文本";
let key = "52c7f81cd24c9699";//16位加密key
let iv = "42e07d2f7199c35d";//16位偏移量

//加密成密文
let new_str = js_encrypt(str,key,iv);
console.log(new_str);
//解密成原文
let old_str = js_decrypt(new_str,key,iv);
console.log(old_str);

 

PHP 将 mcrypt_encrypt 迁移至 openssl_encrypt 的方法

为什么要提及迁移,比如 A & B 两套系统使用 AES 加密做数据传输,且 A 作为客户端,B 则是第三方的服务,且 A 已经在使用 7.2.0+ 版本,而 B 作为长期运行的服务仍在使用 7.1.0-,那我们能做的就是在 A 上使用 openssl_簇 原样的实现 mcrypt_簇 的加解密功能,以便兼容 B 服务,且 mcrypt_簇 是有一些需要多加注意的地方,否则迁移之路略微坎坷。

mcrypt_簇 虽说被遗弃了,但 文档页 上依然有很多值得注意的文档贡献,有助于我们将 mcrypt_簇 迁移至 openssl_簇,大家应该仔细看一下。

1.If you're writing code to encrypt/encrypt data in 2015, you should use openssl_encrypt() and openssl_decrypt(). The underlying library (libmcrypt) has been abandoned since 2007, and performs far worse than OpenSSL (which leverages AES-NI on modern processors and is cache-timing safe).
2.Also, MCRYPT_RIJNDAEL_256 is not AES-256, it's a different variant of the Rijndael block cipher. If you want AES-256 in mcrypt, you have to use MCRYPT_RIJNDAEL_128 with a 32-byte key. OpenSSL makes it more obvious which mode you are using (i.e. 'aes-128-cbc' vs 'aes-256-ctr').

3.OpenSSL also uses PKCS7 padding with CBC mode rather than mcrypt's NULL byte padding. Thus, mcrypt is more likely to make your code vulnerable to padding oracle attacks than OpenSSL.

1、即刻起,应尽可能的使用 openssl_簇 代替 mcrypt_簇 来实现数据的加密功能。

2、MCRYPT_RIJNDAEL_256 并不是 AES-256,如果想使用 mcrypt_簇 实现 AES-256,则你应该使用 MCRYPT_RIJNDAEL_128 算法 + 32位的 key,openssl_簇 则更为清晰的明确了各种模式。这里我整理了一下对应关系供大家参考:

MCRYPT_RIJNDAEL_128 & CBC + 16位Key = openssl_encrypt(AES-128-CBC, 16位Key) = AES-128
MCRYPT_RIJNDAEL_128 & CBC + 24位Key = openssl_encrypt(AES-192-CBC, 24位Key) = AES-192
MCRYPT_RIJNDAEL_128 & CBC + 32位Key = openssl_encrypt(AES-256-CBC, 32位Key) = AES-256

(注:AES-128, 192 and 256 的加密 key 的长度分别为 16, 24 and 32 位)

openssl_簇 的确更为清晰明了,而且 mcrypt_get_key_size 得到的 key 长度都是 32 位,所以不太靠谱。

iv 到是会根据 cipher 方法变更 16 、24、32,但 openssl_簇的 AES cipher 的 iv 长度适中要求为 16 位。

所以,我们为了最大的适配,即便现在不会再用,也要知道 mcrypt_簇 实现 AES-128/192/256 的标准方式为:

  • cipher 选 MCRYPT_RIJNDAEL_128
  • 根据 cipher 和 mode 获取 iv 长度并生成 iv
  • 根据实际业务来确定 key 长度: AES-128 16位 / AES-192 24位 / AES-256 32位,而不是使用 mcrypt_get_key_size

3、这一点其实蛮重要的,涉及 加密算法数据块 & 填充算法PKCS7 的概念。我在支付宝 alipay SDK 中有看到此算法的实现,虽然 sdk 中仍然使用的 mcrypt_簇,但已结合了 PKCS7 填充算法,为什么要这样做呢?其一是为了安全&兼容,php mcrypt 会使用 null(‘0’) 对数据块进行填充,java/.net 则是使用 PKCS7。其二则是为后期迁移至 openssl_簇 的准备,openssl 的填充模式默认是使用 PKCS7 填充的(当然也可以指定使用 null(‘0’) 填充模式,但极力不推荐的)。

mcrypt_encrypt/mcrypt_decrypt

相关的支持函数

    // 支持的算法 rijndael-128|rijndael-192|rijndael-256(此算法并非AES-256,需使用rijndael-128 + key32byte实现)
mcrypt_list_algorithms()
// 支持的模式 cbc ecb 等
mcrypt_list_modes()
// 算法所对应的 key 长度:AES-128, 192 and 256 的加密 key 的长度分别为 16, 24 and 32 位
mcrypt_get_key_size(string $cipher , string $mode)
// 算法所对应的加密向量 iv 的长度
mcrypt_get_iv_size(string $cipher , string $mode)
// 生成 iv
mcrypt_create_iv(mcrypt_get_iv_size(string $cipher , string $mode))
// 加密算法数据块的大小 主要用于填充算法
mcrypt_get_block_size(string $cipher , string $mode)

 

PKCS7 填充算法的实现

/**
 * 填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source     = trim($source);
    // 获取加密算法数据块大小 用于计算需要填充多少位
    $block_size = mcrypt_get_block_size($cipher, $mode);
    $pad        = $block_size - (strlen($source) % $block_size);
    if ($pad <= $block_size) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}

/**
 * 移去填充算法
 * @param string $source
 * @return string
 */
function stripPKCS7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

 

openssl_encrypt/openssl_decrypt

简单讲解一下日常开发中用到的参数

 

    /**
     * $data 待加密内容
     * $method 加密算法
     * $key 加密key
     * $options 数据块填充模式
     * $iv 加密向量
     **/
openssl_encrypt(string $data, string $method, string $key[, int $options = 0[, string $iv = ""
[, string &$tag = NULL[, string $aad = ""[, int $tag_length = 16 ]]]]]): string

openssl_decrypt(string $data, string $method, string $key[, int $options = 0[, string $iv = ""
[, string $tag = "" [, string $aad = "" ]]]] ) : string

这里需要特别注意的就是 options 选项,很多人 mcrypt_簇 迁移至 openssl_簇 时二者加密结果内容不一致,大都是此处没有搞清楚的原因。options 共 3 个值可选

0 默认值 使用 PKCS7 填充算法,不对加密结果进行 base64encode
1 OPENSSL_RAW_DATA 使用 PKCS7 填充算法,且对加密结果进行 base64encode
2 OPENSSL_ZERO_PADDING 使用 null('0') 进行填充,且对加密结果进行 base64encode

所以要注意填充算法及对结果是否进行了 base64encode 编码。

mcrypt_簇 迁移至 openssl_簇

mcrypt_簇

/**
 * 加密算法
 * @param  string $content    待加密数据
 * @param  string $key    加密key
 * @param  string $iv     加密向量
 * @param  string $cipher 加密算法
 * @param  string $mode   加密模式
 * @return string         加密后的内容且base64encode
 */
function encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content           = addPKCS7Padding($content);
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return base64_encode($content_encrypted);
}

/**
 * 解密算法
 * @param  [type] $content [description]
 * @param  [type] $key     [description]
 * @param  [type] $iv      [description]
 * @param  [type] $cipher  [description]
 * @param  [type] $mode    [description]
 * @return [type]          [description]
 */
function decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content_encrypted = base64_decode($content_encrypted);
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    $content           = stripPKSC7Padding($content);
    return $content;
}

/**
 * PKCS7填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    $block  = mcrypt_get_block_size($cipher, $mode);
    $pad    = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}
/**
 * 移去PKCS7填充算法
 * @param string $source
 * @return string
 */
function stripPKSC7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

openssl_簇

转换实例

以 AES-128 为例

// 固定使用此算法 然后通过 key 的长度来决定具体使用的是何种 AES
$cipher = MCRYPT_RIJNDAEL_128;
$mode   = MCRYPT_MODE_CBC;

// openssl_簇 iv 固定为 16 位,mcrypt_簇 MCRYPT_RIJNDAEL_128 是 16位
// 但改为 MCRYPT_RIJNDAEL_192/256 就是 24/32 位了,会不兼容 openssl_簇
// 所以务必注意向量长度统一固定 16 位方便两套算法对齐
// $iv = mcrypt_create_iv(mcrypt_get_iv_size($cipher, $mode), MCRYPT_RAND);

// 根据需要自行定义相应的 key 长度 aes-128=16 aes-192=24 aes-256=32
$key = '0123456789012345';
// 固定为 16 位
$iv  = '0123456789012345';

$content = "hello world";

// mcrypt 加解密
$mcrypt_data = encrypt($content, $key, $iv, $cipher, $mode);
var_dump($mcrypt_data);
$content = decrypt($mcrypt_data, $key, $iv, $cipher, $mode);
var_dump($content);

// mcrypt 时使用了 PKCS7 填充 并对结果 base64encode
// 如果 +PKCS7 +base64encode 则 option = 0
// 如果 +PKCS7 -base64encode 则 option = 1
// 如果 -PKCS7 +base64encode 则 option = 2
$openssl_data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv)
var_dump($openssl_data);
$content = openssl_decrypt($openssl_data, "AES-128-CBC", $key, 0, $iv)
var_dump($content);

// 相互转换
$content = openssl_decrypt($mcrypt_data, "AES-128-CBC", $key, 0, $iv)
var_dump($content);
$content = decrypt($openssl_data, $key, $iv, $cipher, $mode);
var_dump($content);

总结

1、PKCS7 填充算法。
2、openssl_encrypt / openssl_decrypt 三种模式所表示的 PKCS7/base64encode。
3、mcrypt_簇 的 cipher/mode 同 openssl_簇 的转换。

<?php
/**
 * MCRYPT_RIJNDAEL_128 & CBC + 16位Key + 16位iv = openssl_encrypt(AES-128-CBC, 16位Key, 16位iv) = AES-128
 * MCRYPT_RIJNDAEL_128 & CBC + 24位Key + 16位iv = openssl_encrypt(AES-192-CBC, 24位Key, 16位iv) = AES-192
 * MCRYPT_RIJNDAEL_128 & CBC + 32位Key + 16位iv = openssl_encrypt(AES-256-CBC, 32位Key, 16位iv) = AES-256
 * ------------------------------------------------------------------------------------------------------
 * openssl_簇 options
 * 0 : 自动对明文进行 pkcs7 padding, 返回的数据经过 base64 编码.
 * 1 : OPENSSL_RAW_DATA, 自动对明文进行 pkcs7 padding, 但返回的结果未经过 base64 编码
 * 2 : OPENSSL_ZERO_PADDING, 自动对明文进行 null('0') 填充, 同 mcrpty 一致,且返回的结果经过 base64 编码, openssl 不推荐 0 填充的方式, 即使选择此项也不会自动进行 padding, 仍需手动 padding
 * --------------------------------------------------------------------------------------------------------
 * mcrypt 默认是用 0 填充,为保持良好的兼容性建议使用 pkcs7 填充数据 openssl 0|1 都使用的 pkcs7
 * pkcs7 填充
 * 加密工具类
 */

// 随机字符串
function get_random_str($length = 16)
{
    $char_set = array_merge(range('a', 'z'), range('A', 'Z'), range('0', '9'));
    shuffle($char_set);
    return implode('', array_slice($char_set, 0, $length));
}

// 固定使用此算法 然后通过 key 的长度来决定具体使用的是何种 AES
$mcrypt_cipher = MCRYPT_RIJNDAEL_128;
$mcrypt_mode   = MCRYPT_MODE_CBC;
// openssl_簇 AES iv 固定为 16 位,mcrypt_簇只有在 MCRYPT_RIJNDAEL_128 为 16 位 需注意保持一致
$iv = mcrypt_create_iv(mcrypt_get_iv_size($mcrypt_cipher, $mcrypt_mode), MCRYPT_RAND);
// aes-128=16 aes-192=24 aes-256=32
$key_size = 16;
$key      = get_random_str($key_size);
// openssl_ AES 向量长度固定 16 位 这里为
$iv = get_random_str(16);

/**
 * 加密算法
 * @param  string $content    待加密数据
 * @param  string $key    加密key
 * @param  string $iv     加密向量
 * @param  string $cipher 加密算法
 * @param  string $mode   加密模式
 * @return string         加密后的内容且base64encode
 */
function encrypt($content, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content           = addPKCS7Padding($content);
    $content_encrypted = mcrypt_encrypt($cipher, $key, $content, $mode, $iv);
    return base64_encode($content_encrypted);
}

/**
 * 解密算法
 * @param  [type] $content [description]
 * @param  [type] $key     [description]
 * @param  [type] $iv      [description]
 * @param  [type] $cipher  [description]
 * @param  [type] $mode    [description]
 * @return [type]          [description]
 */
function decrypt($content_encrypted, $key, $iv, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    //AES, 128 模式加密数据 CBC
    $content_encrypted = base64_decode($content_encrypted);
    $content           = mcrypt_decrypt($cipher, $key, $content_encrypted, $mode, $iv);
    $content           = stripPKSC7Padding($content);
    return $content;
}

/**
 * PKCS7填充算法
 * @param string $source
 * @return string
 */
function addPKCS7Padding($source, $cipher = MCRYPT_RIJNDAEL_128, $mode = MCRYPT_MODE_CBC)
{
    $source = trim($source);
    $block  = mcrypt_get_block_size($cipher, $mode);
    $pad    = $block - (strlen($source) % $block);
    if ($pad <= $block) {
        $char = chr($pad);
        $source .= str_repeat($char, $pad);
    }
    return $source;
}
/**
 * 移去PKCS7填充算法
 * @param string $source
 * @return string
 */
function stripPKSC7Padding($source)
{
    $source = trim($source);
    $char   = substr($source, -1);
    $num    = ord($char);
    if ($num == 62) {
        return $source;
    }

    $source = substr($source, 0, -$num);
    return $source;
}

$content = "hello world";

var_dump($data = encrypt($content, $key, $iv, $mcrypt_cipher, $mcrypt_mode));
var_dump(decrypt($data, $key, $iv, $mcrypt_cipher, $mcrypt_mode));
var_dump($openssl_data = openssl_encrypt($content, "AES-128-CBC", $key, 0, $iv));
var_dump(openssl_decrypt($openssl_data, "AES-128-CBC", $key, 0, $iv));

// var_dump(openssl_cipher_iv_length('AES-256-CBC'));
// var_dump(mcrypt_get_key_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC));

搞定,收工

 

 

 

 

PHP 7系列版本(7.0、7.1、7.2、7.3、7.4)新特性

PHP很久不用了,很多新特性都搞不清了,稍微整理一下。

  1. 标量参数类型声明[7.0]
  2. 返回值类型声明[7.0]
  3. Nullable类型[7.1]
  4. 属性值类型声明[7.4]
  5. Void 函数[7.1]
  6. 箭头函数[7.4]
  7. 类常量可见性[7.1]
  8. iterable 伪类[7.1]
  9. 新的object类型[7.2]
  10. 允许重写抽象方法[7.2]
  11. 类在实现接口方法时参数类型扩展[7.2]
  12. null合并运算符[7.0]
  13. 空值连写赋值运算符[7.4]
  14. 组合比较符[7.0]
  15. 通过 define() 定义常量数组[7.0]
  16. 支持匿名类[7.0]
  17. Unicode codepoint 转译语法[7.0]
  18. Closure::call()[7.0]
  19. 断言[7.0]
  20. use 分组写法[7.0]
  21. 允许分组命名空间的尾部逗号[7.2]
  22. 生成器可以返回表达式[7.0]
  23. 生成器委托[7.0]
  24. 可以使用 list() 函数来展开实现了 ArrayAccess 接口的对象[7.0]
  25. list()现在支持键名[7.1]
  26. 对称性数组解构[7.1]
  27. 数组的解构支持引用赋值[7.3]
  28. 多异常捕获处理[7.1]
  29. 支持为负的字符串偏移量[7.1]
  30. 通过名称加载扩展 dl()[7.2]
  31. 更灵活的Heredoc 与 Nowdoc 语法[7.3]
  32. Instanceof 运算符支持字面量[7.3]
  33. 允许函数和方法被调用时参数最后的空逗号收尾[7.3]
  34. 多字节字符串的相关函数[7.3]
  35. 数组内展开[7.4]
  36. 数值型字面量分隔符[7.4]
  37. 弱引用[7.4]
  38. __toString() 方法允许抛出异常[7.4]
新特性:
7.0
1、标量参数类型声明
标量类型声明 有两种模式: 强制 (默认) 和 严格模式。
现在可以使用下列类型参数(无论用强制模式还是严格模式):
字符串(string),
整数 (int),
浮点数 (float),
以及布尔值 (bool)。
它们扩充了PHP5中引入的其他类型:类名,接口,数组和 回调类型。
<?php
// Coercive mode
function sumOfInts(int …$ints)
{
    return array_sum($ints);
}
var_dump(sumOfInts(2, ‘3’, 4.1));
2、返回值类型声明
返回类型声明指明了函数返回值的类型。可用的类型与参数声明中可用的类型相同。
<?php
function arraysSum(array …$arrays): array
{
    return array_map(function(array $array): int {
        return array_sum($array);
    }, $arrays);
}
print_r(arraysSum([1,2,3], [4,5,6], [7,8,9]));
3、null合并运算符
如果变量存在且值不为NULL, 它就会返回自身的值,否则返回它的第二个操作数。
<?php
// Fetches the value of $_GET[‘user’] and returns ‘nobody’
// if it does not exist.
$username = $_GET[‘user’] ?? ‘nobody’;
// This is equivalent to:
$username = isset($_GET[‘user’]) ? $_GET[‘user’] : ‘nobody’;
// Coalesces can be chained: this will return the first
// defined value out of $_GET[‘user’], $_POST[‘user’], and
// ‘nobody’.
$username = $_GET[‘user’] ?? $_POST[‘user’] ?? ‘nobody’;
?>
4、太空船操作符(组合比较符)
用于比较两个表达式。当$a小于、等于或大于$b时它分别返回-1、0或1
<?php
// 整数
echo 1 <=> 1; // 0
echo 1 <=> 2; // -1
echo 2 <=> 1; // 1
// 浮点数
echo 1.5 <=> 1.5; // 0
echo 1.5 <=> 2.5; // -1
echo 2.5 <=> 1.5; // 1
// 字符串
echo “a” <=> “a”; // 0
echo “a” <=> “b”; // -1
echo “b” <=> “a”; // 1
5、通过 define() 定义常量数组
在 PHP5.6 中仅能通过 const 定义。
<?php
define(‘ANIMALS’, [
    ‘dog’,
    ‘cat’,
    ‘bird’
]);
echo ANIMALS[1]; // 输出 “cat”
6、匿名类
<?php
interface Logger {
    public function log(string $msg);
}
class Application {
    private $logger;
    public function getLogger(): Logger {
         return $this->logger;
    }
    public function setLogger(Logger $logger) {
         $this->logger = $logger;
    }
}
$app = new Application;
$app->setLogger(new class implements Logger {
    public function log(string $msg) {
        echo $msg;
    }
});
var_dump($app->getLogger());
7、Unicode codepoint 转译语法
接受一个以16进制形式的 Unicode codepoint,并打印出一个双引号或heredoc包围的 UTF-8 编码格式的字符串
echo “\u{aa}”;
echo “\u{0000aa}”;
echo “\u{9999}”;
8、Closure::call()
简短干练的暂时绑定一个方法到对象上闭包并调用它。
<?php
class A {private $x = 1;}
// PHP 7 之前版本的代码
$getXCB = function() {return $this->x;};
$getX = $getXCB->bindTo(new A, ‘A’); // 中间层闭包
echo $getX();
// PHP 7+ 及更高版本的代码
$getX = function() {return $this->x;};
echo $getX->call(new A);
9、为unserialize()提供过滤
通过白名单的方式来防止潜在的代码注入。
<?php
// 将所有的对象都转换为 __PHP_Incomplete_Class 对象
$data = unserialize($foo, [“allowed_classes” => false]);
// 将除 MyClass 和 MyClass2 之外的所有对象都转换为 __PHP_Incomplete_Class 对象
$data = unserialize($foo, [“allowed_classes” => [“MyClass”, “MyClass2”]);
// 默认情况下所有的类都是可接受的,等同于省略第二个参数
$data = unserialize($foo, [“allowed_classes” => true]);
10、断言
assert()现在是一个语言结构,它允许第一个参数是一个表达式,而不仅仅是一个待计算的 string或一个待测试的boolean。
<?php
ini_set(‘assert.exception’, 1);
class CustomError extends AssertionError {}
assert(false, new CustomError(‘Some error message’));
11、Group use declarations
从同一 namespace 导入的类、函数和常量现在可以通过单个 use 语句 一次性导入了。
// PHP 7+ 及更高版本的代码
use some\namespace\{ClassA, ClassB, ClassC as C};
use function some\namespace\{fn_a, fn_b, fn_c};
use const some\namespace\{ConstA, ConstB, ConstC};
12、生成器可以返回表达式
允许在生成器函数中通过使用 return 语法来返回一个表达式 (但是不允许返回引用值), 可以通过调用 Generator::getReturn() 方法来获取生成器的返回值, 但是这个方法只能在生成器完成产生工作以后调用一次。
<?php
$gen = (function() {
    yield 1;
    yield 2;
    return 3;
})();
foreach ($gen as $val) {
    echo $val, PHP_EOL;
}
echo $gen->getReturn(), PHP_EOL;
13、生成器委托
只需在最外层生成器中使用 yield from, 就可以把一个生成器自动委派给其他的生成器, Traversable 对象或者 array。
<?php
function gen()
{
    yield 1;
    yield 2;
    yield from gen2();
}
function gen2()
{
    yield 3;
    yield 4;
}
foreach (gen() as $val)
{
    echo $val, PHP_EOL;
}
14、整数除法函数 intdiv()
进行 整数的除法运算。
<?php
var_dump(intdiv(10, 3));
15、可以使用 list() 函数来展开实现了 ArrayAccess 接口的对象
7.1
1、Nullable类型
参数以及返回值的类型通过在类型前加上一个问号使之允许为空
<?php
function testReturn(): ?string
{
    return ‘elePHPant’;
}
var_dump(testReturn());
function testReturn(): ?string
{
    return null;
}
var_dump(testReturn());
function test(?string $name)
{
    var_dump($name);
}
test(‘elePHPant’);
test(null);
test();
2、Void 函数
方法要么干脆省去 return 语句,要么使用一个空的 return 语句。
对于 void 函数来说,NULL 不是一个合法的返回值。
<?php
function swap(&$left, &$right) : void
{
    if ($left === $right) {
        return;
    }
    $tmp = $left;
    $left = $right;
    $right = $tmp;
}
$a = 1;
$b = 2;
var_dump(swap($a, $b), $a, $b);
3、对称性数组解构
短数组语法([])现在作为list()语法的一个备选项,可以用于将数组的值赋给一些变量(包括在foreach中)
<?php
$data = [
    [1, ‘Tom’],
    [2, ‘Fred’],
];
// list() style
list($id1, $name1) = $data[0];
// [] style
[$id1, $name1] = $data[0];
// list() style
foreach ($data as list($id, $name)) {
    // logic here with $id and $name
}
// [] style
foreach ($data as [$id, $name]) {
    // logic here with $id and $name
}
4、类常量可见性
<?php
class ConstDemo
{
    const PUBLIC_CONST_A = 1;
    public const PUBLIC_CONST_B = 2;
    protected const PROTECTED_CONST = 3;
    private const PRIVATE_CONST = 4;
}
5、iterable 伪类
<?php
function iterator(iterable $iter)
{
    foreach ($iter as $val) {
        //
    }
}
6、多异常捕获处理
<?php
try {
    // some code
} catch (FirstException | SecondException $e) {
    // handle first and second exceptions
}
7、list()现在支持键名
这意味着它可以将任意类型的数组 都赋值给一些变量
<?php
$data = [
    [“id” => 1, “name” => ‘Tom’],
    [“id” => 2, “name” => ‘Fred’],
];
// list() style
list(“id” => $id1, “name” => $name1) = $data[0];
// [] style
[“id” => $id1, “name” => $name1] = $data[0];
// list() style
foreach ($data as list(“id” => $id, “name” => $name)) {
    // logic here with $id and $name
}
// [] style
foreach ($data as [“id” => $id, “name” => $name]) {
    // logic here with $id and $name
}
8、支持为负的字符串偏移量
一个负数的偏移量会被理解为一个从字符串结尾开始的偏移量。
<?php
var_dump(“abcdef”[-2]);
var_dump(strpos(“aabbcc”, “b”, -3));
7.2
1、新的object类型
这种新的类型 object, 引进了可用于逆变(contravariant)参数输入和协变(covariant)返回任何对象类型。
<?php
function test(object $obj) : object
{
    return new SplQueue();
}
test(new StdClass());
2、通过名称加载扩展
使用 dl() 函数进行启用
3、允许重写抽象方法(Abstract method)
<?php
abstract class A
{
    abstract function test(string $s);
}
abstract class B extends A
{
    // overridden – still maintaining contravariance for parameters and covariance for return
    abstract function test($s) : int;
}
4、扩展了参数类型
重写方法和接口实现的参数类型现在可以省略了。不过这仍然是符合LSP,因为现在这种参数类型是逆变的。
<?php
interface A
{
    public function Test(array $input);
}
class B implements A
{
    public function Test($input){} // type omitted for $input
}
5、允许分组命名空间的尾部逗号
<?php
use Foo\Bar\{
    Foo,
    Bar,
    Baz,
};
6、更灵活的Heredoc 与 Nowdoc 语法
7、数组的解构支持引用赋值
 [&$a, [$b, &$c]] = $d
8、Instanceof 运算符支持字面量
判断结果总是 FALSE
9、允许函数和方法被调用时参数最后的空逗号收尾
10、多字节字符串的相关函数
mb_convert_case():
mb_strtolower())
mb_strtoupper())
<?php
mb_ereg(‘(?<word>\w+)’, ‘国’, $matches);
// => [0 => “国”, 1 => “国”, “word” => “国”];
<?php
mb_ereg_replace(‘\s*(?<word>\w+)\s*’, “_\k<word>_\k’word’_”, ‘ foo ‘);
// => “_foo_foo_”
7.4
1、属性值类型声明
<?php
class User {
    public int $id;
    public string $name;
}
2、箭头函数
<?php
$factor = 10;
$nums = array_map(fn($n) => $n * $factor, [1, 2, 3, 4]);
// $nums = array(10, 20, 30, 40);
3、空值连写赋值运算符
<?php
$array[‘key’] ??= computeDefault();
// is roughly equivalent to
if (!isset($array[‘key’])) {
    $array[‘key’] = computeDefault();
}
4、数组内展开
<?php
$parts = [‘apple’, ‘pear’];
$fruits = [‘banana’, ‘orange’, …$parts, ‘watermelon’];
// [‘banana’, ‘orange’, ‘apple’, ‘pear’, ‘watermelon’];
5、数值型字面量分隔符
<?php
6.674_083e-11; // float
299_792_458;   // decimal
0xCAFE_F00D;   // hexadecimal
0b0101_1111;   // binary
6、弱引用
引用对象使其不被销毁
7、__toString()方法允许跑出异常

支付宝支付-提现到个人支付宝

支持沙盒环境的测试

此项目已开源欢迎Start、PR、发起Issues一起讨论交流共同进步
https://github.com/Javen205/IJPay
http://git.oschina.net/javen205/IJPay

提现到个人支付宝官方的名称是单笔转账到支付宝账户

1、创建应用并获取APPID

如果没有在开发平台创建应用就得创建一个《开放平台应用创建指南》
如果之前有创建过应用那么就可以直接添加功能

添加单笔转账到支付宝账户

如果只是测试不上线可以跳过上面的步骤,直接使用沙盒环境测试

2、配置密钥

可以参考《配置应用环境》

生成RSA密钥

配置密钥

3、下载服务端SDK

下载开放平台服务端SDK

4、使用服务端SDK

4.1 初始化SDK

 

AlipayClient alipayClient = new DefaultAlipayClient(URL, APP_ID, APP_PRIVATE_KEY, FORMAT, CHARSET, ALIPAY_PUBLIC_KEY, SIGN_TYPE);

参数说明可以参考关键参数说明

4.2 接口调用

调用流程

说明:

1、如果商户重复请求转账,支付宝会幂等返回成功结果,商户必须对重复转账的业务做好幂等处理;如果不判断,存在潜在的风险,商户自行承担因此而产生的所有损失。

2、如果调用alipay.fund.trans.toaccount.transfer掉单时,或返回结果code=20000时,或返回结果code=40004,sub_code= SYSTEM_ERROR时,请调用alipay.fund.trans.order.query发起查询,如果未查询到结果,请保持原请求不变再次请求alipay.fund.trans.toaccount.transfer接口。

3、商户处理转账结果时,对于错误码的处理,只能使用sub_code作为后续处理的判断依据,不可使用sub_msg作为后续处理的判断依据。

4.3 SDK的调用

主要涉及到两个接口

4.4 单笔转账到支付宝账户接口alipay.fund.trans.toaccount.transfer 封装

直接调用转账接口如果返回不是Success 就调用转账查询接口

 

/**
     * 单笔转账到支付宝账户
     * https://doc.open.alipay.com/docs/doc.htm?spm=a219a.7629140.0.0.54Ty29&treeId=193&articleId=106236&docType=1
     * @param content
     * @return
     * @throws AlipayApiException
     */
    public static boolean transfer(AlipayFundTransToaccountTransferModel model) throws AlipayApiException{
        AlipayFundTransToaccountTransferResponse response = transferToResponse(model);
        String result = response.getBody();
        log.info("transfer result>"+result);
        System.out.println("transfer result>"+result);
        if (response.isSuccess()) {
            return true;
        } else {
            //调用查询接口查询数据
            JSONObject jsonObject = JSONObject.parseObject(result);
            String out_biz_no = jsonObject.getJSONObject("alipay_fund_trans_toaccount_transfer_response").getString("out_biz_no");
            AlipayFundTransOrderQueryModel queryModel = new AlipayFundTransOrderQueryModel();
            model.setOutBizNo(out_biz_no);
            boolean isSuccess = transferQuery(queryModel);
            if (isSuccess) {
                return true;
            }
        }
        return false;
    }
    
    public static AlipayFundTransToaccountTransferResponse transferToResponse(AlipayFundTransToaccountTransferModel model) throws AlipayApiException{
        AlipayFundTransToaccountTransferRequest request = new AlipayFundTransToaccountTransferRequest();
        request.setBizModel(model);
        return alipayClient.execute(request);
    }
4.5 查询转账订单接口alipay.fund.trans.order.query 封装

 

/**
     * 转账查询接口
     * @param content
     * @return
     * @throws AlipayApiException
     */
    public static boolean transferQuery(AlipayFundTransOrderQueryModel model) throws AlipayApiException{
        AlipayFundTransOrderQueryResponse response = transferQueryToResponse(model);
        log.info("transferQuery result>"+response.getBody());
        System.out.println("transferQuery result>"+response.getBody());
        if(response.isSuccess()){
            return true;
        }
        return false;
    }
    public static AlipayFundTransOrderQueryResponse transferQueryToResponse(AlipayFundTransOrderQueryModel model) throws AlipayApiException{
        AlipayFundTransOrderQueryRequest request = new AlipayFundTransOrderQueryRequest();
        request.setBizModel(model);
        return alipayClient.execute(request);
    }

5、 接口测试

 

/**
     * 单笔转账到支付宝账户
     * https://doc.open.alipay.com/docs/doc.htm?spm=a219a.7629140.0.0.54Ty29&treeId=193&articleId=106236&docType=1
     */
    public void transfer() {
        boolean isSuccess = false;
        String total_amount = "100";
        AlipayFundTransToaccountTransferModel model = new AlipayFundTransToaccountTransferModel();
        model.setOutBizNo(StringUtils.getOutTradeNo());//生成订单号
        model.setPayeeType("ALIPAY_LOGONID");//固定值
        model.setPayeeAccount("abpkvd0206@sandbox.com");//转账收款账户
        model.setAmount(total_amount);
        model.setPayerShowName("测试退款");
        model.setPayerRealName("沙箱环境");//账户真实名称
        model.setRemark("javen测试单笔转账到支付宝");
        
        try {
            isSuccess = AliPayApi.transfer(model);
        } catch (Exception e) {
            e.printStackTrace();
        }
        renderJson(isSuccess);
    }

故意把账户真实名称写错

 

{
  "alipay_fund_trans_toaccount_transfer_response": {
    "code": "40004",
    "msg": "Business Failed",
    "sub_code": "PAYER_USER_INFO_ERROR",
    "sub_msg": "付款用户姓名或其它信息不一致",
    "out_biz_no": "051023044814944"
  },
  "sign": "Zbm9lI9GbTlLbYsPQoJhd5y7+oevOInPFoKlRWp2064VUPZYUGBJRiM/8Ip8Vfz4MDhu+0Uc3gEzvoXk1O6eVj7bAPjGLc5cZI3gQNmbogTxeK/4eGgIjxJBKK46r6rzKgK2/e7YEBmExi6hACbo3inBqX0OnaIxIbedZnYY2qrkNdhIjiD1G/EWJNH846IEwhwLkihV7vVKXhIgfmfKmGu5jE7aNddwxKhAK8fAzTR7JOs8p/ZOcLD9/RHfUP1ro4HoNlUOrFUZfhxRuUEFwLxvcJon0HkcO6dnjNnXQx3jh/Ne3632SpWca1pZczervU3/z9/C0LVflQWna42t9g=="
}

正确返回结果


{
  "alipay_fund_trans_toaccount_transfer_response": {
    "code": "10000",
    "msg": "Success",
    "order_id": "20170510110070001500460000004431",
    "out_biz_no": "051023003214944",
    "pay_date": "2017-05-10 23:00:30"
  },
  "sign": "hedaOEcrS8CwzcLNFAQhLWJnmevaA4a+SsNuzuuyBHABUjJ+ZvagMoS1/eUpRIHfwXVOLxGVjCCtJzi4Irclqu2Roz9aHo8ROkNKFKbw66lcT2dOo9AWCYw8UVhQDUjjSZ/d+lu9nnpHPf3ZPPdFHvziBo6ghZF0DRiIX/9ZVx7uH7grFJb8SRbCbcF5C7eouJU8Aw9sMdu/XjREdlW7pLvjeinzouOLbIpICRP33JtGC/KhgdQltDzlXHtVgi9xWRJVXJVp7+jtDRbRP+V+ImY9NqYKtpfxtTBZZ1bW6nOxJaMV7ePgC/6GpDIyWjg+LdHQ09eeBtTy4XCOxtGe1g=="
}

 

PHP异步执行的几种常用方式

本文为大家讲述了php异步调用方法,分享给大家供大家参考,具体内容如下 客户端与服务器端是通过HTTP协议进行连接通讯,客户端发起请求,服务器端接收到请求后执行处理,并返回处理结果。 有时服务器需要执行很耗时的操作,这个操作的结果并不需要返回给客户端。但因为php是同步执行的,所以客户端需要等待服务处理完才可以进行下一步。 因此对于耗时的操作适合异步执行,服务器接收到请求后,处理完客户端需要的数据就返回,再异步在服务器执行耗时的操作。 1.使用Ajax 与 img 标记 原理,服务器返回的html中插入Ajax 代码或 img 标记,img的src为需要执行的程序。 优点:实现简单,服务端无需执行任何调用 缺点:在执行期间,浏览器会一直处于loading状态,因此这种方法并不算真正的异步调用。
$.get("doRequest.php", { name: "fdipzone"} );<img src="doRequest.php?name=fdipzone">
2.使用popen 使用popen执行命令,语法:
// popen — 打开进程文件指针  resource popen ( string $command , string $mode )pclose(popen('php /home/fdipzone/doRequest.php &', 'r'));
优点:执行速度快 缺点:
  • 1).只能在本机执行
  • 2).不能传递大量参数
  • 3).访问量高时会创建很多进程
3.使用curl 设置curl的超时时间 CURLOPT_TIMEOUT 为1 (最小为1),因此客户端需要等待1秒
<?php 
  $ch = curl_init(); 
  $curl_opt = array( CURLOPT_URL,'http://www.example.com/doRequest.php'CURLOPT_RETURNTRANSFER,1, CURLOPT_TIMEOUT,1 ); 
  curl_setopt_array($ch, $curl_opt); 
  curl_exec($ch); curl_close($ch); 
?>
4.使用fsockopen fsockopen是最好的,缺点是需要自己拼接header部分。
<?php  
$url = 'http://www.example.com/doRequest.php'; 
$param = array( 'name'=>'fdipzone', 'gender'=>'male', 'age'=>30 );  doRequest($url, $param);  
function doRequest($url, $param=array()){  
  $urlinfo = parse_url($url);  
  $host = $urlinfo['host']; 
  $path = $urlinfo['path']; 
  $query = isset($param)? http_build_query($param) : '';  
  $port = 80; 
  $errno = 0; 
  $errstr = ''; 
  $timeout = 10;  
  $fp = fsockopen($host, $port, $errno, $errstr, $timeout);  
  $out = "POST ".$path." HTTP/1.1\r\n"; $out .= "host:".$host."\r\n"; 
  $out .= "content-length:".strlen($query)."\r\n"; 
  $out .= "content-type:application/x-www-form-urlencoded\r\n"; 
  $out .= "connection:close\r\n\r\n"; 
  $out .= $query;  
  fputs($fp, $out); 
  fclose($fp); 
}  
?>
注意:当执行过程中,客户端连接断开或连接超时,都会有可能造成执行不完整,因此需要加上
ignore_user_abort(true); // 忽略客户端断开 set_time_limit(0);    // 设置执行不超时

PHP5.5新增的利器:生成器

如果是做Python或者其他语言的小伙伴,对于生成器应该不陌生。但很多PHP开发者或许都不知道生成器这个功能,可能是因为生成器是PHP 5.5.0才引入的功能,也可以是生成器作用不是很明显。但是,生成器功能的确非常有用。

优点

直接讲概念估计你听完还是一头雾水,所以我们先来说说优点,也许能勾起你的兴趣。那么生成器有哪些优点,如下:

  • 生成器会对PHP应用的性能有非常大的影响
  • PHP代码运行时节省大量的内存
  • 比较适合计算大量的数据

那么,这些神奇的功能究竟是如何做到的?我们先来举个例子。

概念引入

首先,放下生成器概念的包袱,来看一个简单的PHP函数:

function createRange($number){
    $data = [];
    for($i=0;$i<$number;$i++){
        $data[] = time();
    }
    return $data;
}

这是一个非常常见的PHP函数,我们在处理一些数组的时候经常会使用。这里的代码也非常简单:

  1. 我们创建一个函数。
  2. 函数内包含一个for循环,我们循环的把当前时间放到$data里面
  3. for循环执行完毕,把$data返回出去。

下面没完,我们继续。我们再写一个函数,把这个函数的返回值循环打印出来:

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);//这里停顿1秒,我们后续有用
    echo $value.'<br />';
}

我们在浏览器里面看一下运行结果:

图片描述

这里非常完美,没有任何问题。(当然sleep(1)效果你们看不出来)

思考一个问题

我们注意到,在调用函数createRange的时候给$number的传值是10,一个很小的数字。假设,现在传递一个值10000000(1000万)。

那么,在函数createRange里面,for循环就需要执行1000万次。且有1000万个值被放到$data里面,而$data数组在是被放在内存内。所以,在调用函数时候会占用大量内存。

这里,生成器就可以大显身手了。

创建生成器

我们直接修改代码,你们注意观察:

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();
    }
}

看下这段和刚刚很像的代码,我们删除了数组$data,而且也没有返回任何内容,而是在time()之前使用了一个关键字yield

使用生成器

我们再运行一下第二段代码:

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);
    echo $value.'<br />';
}
图片描述

我们奇迹般的发现了,输出的值和第一次没有使用生成器的不一样。这里的值(时间戳)中间间隔了1秒。

这里的间隔一秒其实就是sleep(1)造成的后果。但是为什么第一次没有间隔?那是因为:

  • 未使用生成器时:createRange函数内的for循环结果被很快放到$data中,并且立即返回。所以,foreach循环的是一个固定的数组。
  • 使用生成器时:createRange的值不是一次性快速生成,而是依赖于foreach循环。foreach循环一次,for执行一次。

到这里,你应该对生成器有点儿头绪。

深入理解生成器

代码剖析

下面我们来对于刚刚的代码进行剖析。

function createRange($number){
    for($i=0;$i<$number;$i++){
        yield time();
    }
}

$result = createRange(10); // 这里调用上面我们创建的函数
foreach($result as $value){
    sleep(1);
    echo $value.'<br />';
}

我们来还原一下代码执行过程。

  1. 首先调用createRange函数,传入参数10,但是for值执行了一次然后停止了,并且告诉foreach第一次循环可以用的值。
  2. foreach开始对$result循环,进来首先sleep(1),然后开始使用for给的一个值执行输出。
  3. foreach准备第二次循环,开始第二次循环之前,它向for循环又请求了一次。
  4. for循环于是又执行了一次,将生成的时间戳告诉foreach.
  5. foreach拿到第二个值,并且输出。由于foreachsleep(1),所以,for循环延迟了1秒生成当前时间

所以,整个代码执行中,始终只有一个记录值参与循环,内存中也只有一条信息。

无论开始传入的$number有多大,由于并不会立即生成所有结果集,所以内存始终是一条循环的值。

概念理解

到这里,你应该已经大概理解什么是生成器了。下面我们来说下生成器原理。

首先明确一个概念:生成器yield关键字不是返回值,他的专业术语叫产出值,只是生成一个值

那么代码中foreach循环的是什么?其实是PHP在使用生成器的时候,会返回一个Generator类的对象。foreach可以对该对象进行迭代,每一次迭代,PHP会通过Generator实例计算出下一次需要迭代的值。这样foreach就知道下一次需要迭代的值了。

而且,在运行中for循环执行后,会立即停止。等待foreach下次循环时候再次和for索要下次的值的时候,for循环才会再执行一次,然后立即再次停止。直到不满足条件不执行结束。

实际开发应用

很多PHP开发者不了解生成器,其实主要是不了解应用领域。那么,生成器在实际开发中有哪些应用?

读取超大文件

PHP开发很多时候都要读取大文件,比如csv文件、text文件,或者一些日志文件。这些文件如果很大,比如5个G。这时,直接一次性把所有的内容读取到内存中计算不太现实。

这里生成器就可以派上用场啦。简单看个例子:读取text文件

图片描述

我们创建一个text文本文档,并在其中输入几行文字,示范读取。

<?php
header("content-type:text/html;charset=utf-8");
function readTxt()
{
    # code...
    $handle = fopen("./test.txt", 'rb');

    while (feof($handle)===false) {
        # code...
        yield fgets($handle);
    }

    fclose($handle);
}

foreach (readTxt() as $key => $value) {
    # code...
    echo $value.'<br />';
}
图片描述

通过上图的输出结果我们可以看出代码完全正常。

但是,背后的代码执行规则却一点儿也不一样。使用生成器读取文件,第一次读取了第一行,第二次读取了第二行,以此类推,每次被加载到内存中的文字只有一行,大大的减小了内存的使用。

这样,即使读取上G的文本也不用担心,完全可以像读取很小文件一样编写代码。