分类目录归档:工具

可编程浏览器 – PhantomJS

可编程浏览器,目前主要有两个
1 PhantomJs,打包Webkit,主要提供JS编程接口
2 SlimerJs,需要独立安装Firefox,依赖这个独立安装的Firefox内核,提供JS编程接口

这两个工具提供的JS编程接口较原始,可以使用CasperJS(http://casperjs.org/)来操作(Node.js模块)。

>>>开始
1 下载
http://phantomjs.org/download.html,直接下载二进制包。
2 构建(Build)
建议直接使用二进制包。
3 最新发布
http://phantomjs.org/releases.html
4 Release Names
5 REPL
PhantomJS 1.5起,提供一个交互模式(在命令行里面直接输入代码运行)

phantomjs.exe	//进入交互模式
phantomjs> phantom.version
{
   "major": 2,
   "minor": 1,
   "patch": 1
}
phantomjs> console.log("phantomjs");
phantomjs
undefined

phantomjs> window.navigator
{
   "appCodeName": "Mozilla",
   "appName": "Netscape",
   "appVersion": "5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1",
   "cookieEnabled": true,
   "language": "zh-CN",
   "mimeTypes": {
      "length": 0
   },
   "onLine": true,
   "platform": "Win32",
   "plugins": {
      "length": 0
   },
   "product": "Gecko",
   "productSub": "20030107",
   "userAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1",
   "vendor": "Apple Computer, Inc.",
   "vendorSub": ""
}

CTRL+C或CTRL+D或输入phantom.exit()退出交互模式。支持自动完成和历史记录。

>>>学习
1 开速开始

console.log('Hello, world!');
phantom.exit();

//截屏
var page = require('webpage').create();
page.open('http://example.com', function(status) {
	console.log("Status:" + status);
	if(status === "success") {
		page.render('example.png');
	}
	phantom.exit();
});

//测试加载时间
//phantomjs.ext loadspeed.js http://blog.ifeeline.com
var page = require('webpage').create(),
  system = require('system'),
  t, address;

if (system.args.length === 1) {
  console.log('Usage: loadspeed.js <some URL>');
  phantom.exit();
}

t = Date.now();
address = system.args[1];
page.open(address, function(status) {
  if (status !== 'success') {
    console.log('FAIL to load the address');
  } else {
    t = Date.now() - t;
    console.log('Loading ' + system.args[1]);
    console.log('Loading time ' + t + ' msec');
  }
  phantom.exit();
});

// evaluate()方法,只能返回简单对象,不能包含函数
// 并且evaluate()不能访问page只能的内容
var page = require('webpage').create();
page.open(url, function(status) {
  var title = page.evaluate(function() {
    return document.title;
  });
  console.log('Page title is ' + title);
  phantom.exit();
});

// 在evaluate()方法内部的console信息是不能显示的
// 不过可以在page的onConsoleMessage来实现
// 同时也是一个提取信息的有用办法
var page = require('webpage').create();
page.onConsoleMessage = function(msg) {
  console.log('Page title is ' + msg);
};
page.open(url, function(status) {
  page.evaluate(function() {
    console.log(document.title);
  });
  phantom.exit();
});

//以下实例展示了跟踪请求 和 响应
var page = require('webpage').create();
page.onResourceRequested = function(request) {
  console.log('Request ' + JSON.stringify(request, undefined, 4));
};
page.onResourceReceived = function(response) {
  console.log('Receive ' + JSON.stringify(response, undefined, 4));
};
page.open(url);

2 自动化测试
3 截屏

// 支持png jpg  gif  pdf
var page = require('webpage').create();
page.open('http://github.com/', function() {
  page.render('github.pdf');
  phantom.exit();
});

4 网络监控
5 页面自动化

// 设置用户代理
var page = require('webpage').create();
console.log('The default user agent is ' + page.settings.userAgent);
page.settings.userAgent = 'SpecialAgent';
page.open('http://www.httpuseragent.org', function(status) {
  if (status !== 'success') {
    console.log('Unable to access network');
  } else {
    var ua = page.evaluate(function() {
      return document.getElementById('myagent').textContent;
    });
    console.log(ua);
  }
  phantom.exit();
});
输出:
The default user agent is Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
Your Http User Agent string is: SpecialAgent

// 使用jQuery来操作页面
// 在page.includeJs中必须使用 phantom.exit()来退出
var page = require('webpage').create();
page.open('http://www.sample.com', function() {
  page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function() {
    page.evaluate(function() {
      $("button").click();
    });
    phantom.exit()
  });
});

// 页面的内容
page.content
page.plainText
// 页面的cookies(包括页面在渲染是JS产生的cookie)
page.cookies
// 页面设置,当前只有一个值userAgent
page.settings.userAgent = ‘SpecialAgent’;
// 页面URL
page.url
// 页面视口大小(浏览器尺寸)
page.viewportSize
{
	width:1024,
	height:768
}
////////////////////////////////////
// 函数列表

    page.childFramesCount
    page.childFramesName
    page.close
    page.currentFrameName
    page.deleteLater
    page.destroyed
    page.evaluate
    page.initialized
    page.injectJs
    page.javaScriptAlertSent
    page.javaScriptConsoleMessageSent
    page.loadFinished
    page.loadStarted
    page.openUrl
    page.release
    page.render
    page.resourceError
    page.resourceReceived
    page.resourceRequested
    page.uploadFile
    page.sendEvent
    page.setContent
    page.switchToChildFrame
    page.switchToMainFrame
    page.switchToParentFrame
    page.addCookie
    page.deleteCookie
    page.clearCookies

// 回调列表

    onInitialized
    onLoadStarted
    onLoadFinished
    onUrlChanged
    onNavigationRequested
    onRepaintRequested
    onResourceRequested
    onResourceReceived
    onResourceError
    onResourceTimeout
    onAlert
    onConsoleMessage
    onClosing

6 进程间通信
写入到文件
对外HTTP请求(GET或POST数据,在页面中通过XMLHttpRequest),跨域是禁止的
来自外部的HTTP请求(提供WebServer模块)
路由PhantomJS的流量通过一个代理(比如fiddler)
7 命令行工具
phantomjs [options] somescript.js [arg1 [arg2 […]]]

// 最为有用的是指定cookies保存的路径
–cookies-file=/path/to/cookies.txt specifies the file name to store the persistent Cookies.
–local-to-remote-url-access=[true|false] allows local content to access remote URL (default is false). Also accepted: [yes|no].

Phantomjs 1.3后,可以通过–config=/config.json来指定配置:

config.json
{
	"cookiesFile": "cookies.text"
}

phantomjs.exe --cookies-file=cookies.txt cookie.js
phantomjs.exe --config=config.json cookie.js

>>> 探索
1 实例
2 最佳实践
推荐使用自动化测试框架,高层封装CasperJS
3 提示和技巧
4 Supported Web Standards
5 Buzz
6 Who’s using PhantomJS?
7 Related Projects
http://phantomjs.org/related-projects.html

Editplus编辑器常规设置

View -> Syntax Highlighting			语法高亮
View -> Word Highlighting			词高亮(选中某个词时高亮相同的词)
View -> Brace Highlighting			定界符高亮配对
View -> Line Number				行号
View -> Indent Guide				对齐线

View -> Code Folding -> Use Code Folding	代码折叠

View -> While Spaces -> Spaces			空格字符是否显示
View -> While Spaces -> Tabs			Tab符是否显示
View -> While Spaces -> Line Breaks		Tab符是否显示

Document -> Word Wrap				自动换行
Document -> Auto Indent				自动对齐线
Document -> File Encoding			文件编码查看转换

对于Editplus,我只是用它来替换Windows下的默认文本编辑器而已。有时候要快速打开一个文件时,它的语法高亮,词高亮,行号,对齐线,自动换行,查看文件编码和转换文件编码也是我经常需要使用的。至于代码提示,自动完成和定位,特别是代码格式化,都不是它擅长的。

习惯使用Dreamweaver来处理HTML CSS JS,用IDE来编写PHP程序,用Editpus替代Windows默认编辑器来快速查看编辑文件。

editplus_coding

Laravel HTML转换PDF

首先,HTML转换成PDF使用一个叫wkhtmltopdf的工具,地址为:http://wkhtmltopdf.org/index.html,安装之后会提供一个命令行工具,这个命令行工具可配置的参数非常多:

wkhtmltopdf --page-width 100 --page-height 100 http://blog.ifeeline.com i.pdf
Loading pages (1/6)
Counting pages (2/6)
Resolving links (4/6)
Loading headers and footers (5/6)
Printing pages (6/6)
Done

在Linux下安装,可能会缺少中文字体,最简单的方式是在Windows下拷贝字体上传到Linux。Windows下字体位置:控制面板\所有控制面板项\字体,拷贝:
window_fonts
然后上传到Linux:

mkdir zh_cn
cd zh_cn
#安装字体
fc-cache -fv
#查看字体是否安装
fc-list | grep simsun

为了可以在PHP中使用这个命令行工具,有一个PHP包对其进行了封装,地址:https://github.com/KnpLabs/snappy:

require __DIR__ . '/vendor/autoload.php';

use Knp\Snappy\Pdf;

$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');

// or you can do it in two steps
$snappy = new Pdf();
$snappy->setBinary('/usr/local/bin/wkhtmltopdf');

// Display the resulting pdf in the browser
// by setting the Content-type header to pdf
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="file.pdf"');
echo $snappy->getOutput('http://www.github.com');

// Merge multiple urls into one pdf
// by sending an array of urls to getOutput()
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="file.pdf"');
echo $snappy->getOutput(array('http://www.github.com','http://www.knplabs.com','http://www.php.net'));

// .. or simply save the PDF to a file
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
$snappy->generateFromHtml('<h1>Bill</h1><p>You owe me money, dude.</p>', '/tmp/bill-123.pdf');

// Pass options to snappy
// Type wkhtmltopdf -H to see the list of options
$snappy = new Pdf('/usr/local/bin/wkhtmltopdf');
$snappy->setOption('disable-javascript', true);
$snappy->setOption('no-background', true);
$snappy->setOption('allow', array('/path1', '/path2'));
$snappy->setOption('cookie', array('key' => 'value', 'key2' => 'value2'));
$snappy->setOption('cover', 'pathToCover.html');
// .. or pass a cover as html
$snappy->setOption('cover', '<h1>Bill cover</h1>');
$snappy->setOption('toc', true);
$snappy->setOption('cache-dir', '/path/to/cache/dir');

从例子可以看到,这个snappy工具包,主要实现了把wkhtmltopdf参数传递wkhtmltopdf命令行工具,然后调用命令行工具产生PDF而已。

每次调用这个工具都要设置一版数据显然很麻烦,所以为了在Laravel框架中有效的使用,就需要把这个工具包继续做一次封装基础到框架,可以使用laravel-snappy工具,地址:https://github.com/barryvdh/laravel-snappy。

Laravel集成第三方工具,基本套路是:
1 添加配置文件
2 添加ServiceProvider
3 添加Facade
ServiceProvider一般会读取配置文件中的配置(如果需要配置),然后把实例对象注入Laravel容器,接着就可以直接从容器中获取实例对象进行操作,也可以直接使用Facade,它实际也是从容器中获取实例对象。

以下是操作过程:

composer require barryvdh/laravel-snappy

#添加ServiceProvider
vi app/config/app.php
Barryvdh\Snappy\ServiceProvider::class,

#添加Facade
'PDF' => Barryvdh\Snappy\Facades\SnappyPdf::class,
'Image' => Barryvdh\Snappy\Facades\SnappyImage::class,

#添加配置(可以直接拷贝)
php artisan vendor:publish

#使用
$snappy = App::make('snappy.pdf');
//To file
$snappy->generateFromHtml('<h1>Bill</h1><p>You owe me money, dude.</p>', '/tmp/bill-123.pdf');
$snappy->generate('http://www.github.com', '/tmp/github.pdf'));
//Or output:
return new Response(
    $snappy->getOutputFromHtml($html),
    200,
    array(
        'Content-Type'          => 'application/pdf',
        'Content-Disposition'   => 'attachment; filename="file.pdf"'
    )
);

$pdf = App::make('snappy.pdf.wrapper');
$pdf->loadHTML('<h1>Test</h1>');
return $pdf->inline();

// Facade操作
$pdf = PDF::loadView('pdf.invoice', $data);
return $pdf->download('invoice.pdf');

return PDF::loadFile('http://www.github.com')->inline('github.pdf');

PDF::loadHTML($html)->setPaper('a4')->setOrientation('landscape')->setOption('margin-bottom', 0)->save('myfile.pdf')

Facade的操作方式还是很便利的。比如PDF::loadFile(‘http://www.github.com’),这样直接抓取一个网页,可以调用inline或stream方法直接输出到浏览器(实际是output方法的封装,output方法返回pdf的字符流,而output又是调用snappy提供的方法),也可以调用download方法下载,调用save方法保存。

这里需要特别指出的是,可以使用loadView方法直接load一个Blade模板文件,第二参数是模板使用到的变量,这对于动态产生PDF输出提供了一个绝佳实现。

最后,看下配置文件:

<?php
return array(
    'pdf' => array(
        'enabled' => true,
        'binary'  => '/usr/local/bin/wkhtmltopdf',
        'timeout' => false,
        'options' => array(),
        'env'     => array(),
    ),
    'image' => array(
        'enabled' => true,
        'binary'  => '/usr/local/bin/wkhtmltoimage',
        'timeout' => false,
        'options' => array(),
        'env'     => array(),
    ),
);

这里的enabled表示对应命令是否启用,binary表示wkhtmltopdf命令的位置,timeout表示是否设置超时,options就是要传递到wkhtmltopdf命令行的参数,env是执行命令行的环境变量。options的可用值,就是wkhtmltopdf命令的可用值,这个可以查看Knp\Snappy\Pdf的configure方法得到一个列表:

 protected function configure()
    {
        $this->addOptions(array(
            'ignore-load-errors'           => null, // old v0.9
            'lowquality'                   => true,
            'collate'                      => null,
            'no-collate'                   => null,
            'cookie-jar'                   => null,
            'copies'                       => null,
            'dpi'                          => null,
            'extended-help'                => null,
            'grayscale'                    => null,
            'help'                         => null,
            'htmldoc'                      => null,
            'image-dpi'                    => null,
            'image-quality'                => null,
            'manpage'                      => null,
            'margin-bottom'                => null,
            'margin-left'                  => null,
            'margin-right'                 => null,
            'margin-top'                   => null,
            'orientation'                  => null,
            'output-format'                => null,
            'page-height'                  => null,
            'page-size'                    => null,
            'page-width'                   => null,
            'no-pdf-compression'           => null,
            'quiet'                        => null,
            'read-args-from-stdin'         => null,
            'title'                        => null,
            'use-xserver'                  => null,
            'version'                      => null,
            'dump-default-toc-xsl'         => null,
            'dump-outline'                 => null,
            'outline'                      => null,
            'no-outline'                   => null,
            'outline-depth'                => null,
            'allow'                        => null,
            'background'                   => null,
            'no-background'                => null,
            'checkbox-checked-svg'         => null,
            'checkbox-svg'                 => null,
            'cookie'                       => null,
            'custom-header'                => null,
            'custom-header-propagation'    => null,
            'no-custom-header-propagation' => null,
            'debug-javascript'             => null,
            'no-debug-javascript'          => null,
            'default-header'               => null,
            'encoding'                     => null,
            'disable-external-links'       => null,
            'enable-external-links'        => null,
            'disable-forms'                => null,
            'enable-forms'                 => null,
            'images'                       => null,
            'no-images'                    => null,
            'disable-internal-links'       => null,
            'enable-internal-links'        => null,
            'disable-javascript'           => null,
            'enable-javascript'            => null,
            'javascript-delay'             => null,
            'load-error-handling'          => null,
            'load-media-error-handling'    => null,
            'disable-local-file-access'    => null,
            'enable-local-file-access'     => null,
            'minimum-font-size'            => null,
            'exclude-from-outline'         => null,
            'include-in-outline'           => null,
            'page-offset'                  => null,
            'password'                     => null,
            'disable-plugins'              => null,
            'enable-plugins'               => null,
            'post'                         => null,
            'post-file'                    => null,
            'print-media-type'             => null,
            'no-print-media-type'          => null,
            'proxy'                        => null,
            'radiobutton-checked-svg'      => null,
            'radiobutton-svg'              => null,
            'run-script'                   => null,
            'disable-smart-shrinking'      => null,
            'enable-smart-shrinking'       => null,
            'stop-slow-scripts'            => null,
            'no-stop-slow-scripts'         => null,
            'disable-toc-back-links'       => null,
            'enable-toc-back-links'        => null,
            'user-style-sheet'             => null,
            'username'                     => null,
            'window-status'                => null,
            'zoom'                         => null,
            'footer-center'                => null,
            'footer-font-name'             => null,
            'footer-font-size'             => null,
            'footer-html'                  => null,
            'footer-left'                  => null,
            'footer-line'                  => null,
            'no-footer-line'               => null,
            'footer-right'                 => null,
            'footer-spacing'               => null,
            'header-center'                => null,
            'header-font-name'             => null,
            'header-font-size'             => null,
            'header-html'                  => null,
            'header-left'                  => null,
            'header-line'                  => null,
            'no-header-line'               => null,
            'header-right'                 => null,
            'header-spacing'               => null,
            'replace'                      => null,
            'disable-dotted-lines'         => null,
            'cover'                        => null,
            'toc'                          => null,
            'toc-depth'                    => null,
            'toc-font-name'                => null,
            'toc-l1-font-size'             => null,
            'toc-header-text'              => null,
            'toc-header-font-name'         => null,
            'toc-header-font-size'         => null,
            'toc-level-indentation'        => null,
            'disable-toc-links'            => null,
            'toc-text-size-shrink'         => null,
            'xsl-style-sheet'              => null,
            'viewport-size'                => null,
            'redirect-delay'               => null, // old v0.9
            'cache-dir'                    => null,
        ));
    }

大概浏览一下,可以传递cookie,cookie-jar,这个在抓取一个需要验证登录的网络时就非常有意义。可以设置外边距,可以设置页大小,也可以指定页的宽度和高度,可以传递用户名密码,可以POST数据等等,实际就是一个脚本解析器和html渲染工工具(可以看成一个浏览器),只有像浏览器一样渲染html和执行js,才能获取到渲染结果(我们看到的模样),然后才能按照这个模样转换成PDF(或者图片)。

关于打印尺寸,参考:http://doc.qt.io/qt-4.8/qprinter.html#PaperSize-enum

————————————————————————————-
以上内容是PHP中调用命令行工具的封装。也可以直接使用对应的PHP扩展,地址:https://github.com/mreiferson/php-wkhtmltox,例子:

foreach (range(1, 4) as $i) {
    wkhtmltox_convert('pdf', 
        array('out' => '/tmp/test'.$i.'.pdf', 'imageQuality' => '95'), // global settings
        array(
            array('page' => 'http://www.visionaryrenesis.com/'),
            array('page' => 'http://www.google.com/')
            )); // object settings
}

第二参数是一个配置数组,比如imageQuality,对应的命令行参数是image-quality,详细的配置参考:http://wkhtmltopdf.org/libwkhtmltox/pagesettings.html, 可以把配置写入文件,然后直接代入。

#安装wkhtmltox
wget http://download.gna.org/wkhtmltopdf/0.12/0.12.2.1/wkhtmltox-0.12.2.1_linux-centos7-amd64.rpm

yum install xorg-x11-fonts-75dpi.noarch
yum install xorg-x11-fonts-Type1

rpm -i wkhtmltox-0.12.2.1_linux-centos7-amd64.rpm

rpm -ql wkhtmltox-0.12.2.1-1.x86_64
/usr/local/bin/wkhtmltoimage
/usr/local/bin/wkhtmltopdf
/usr/local/include/wkhtmltox/dllbegin.inc
/usr/local/include/wkhtmltox/dllend.inc
/usr/local/include/wkhtmltox/image.h
/usr/local/include/wkhtmltox/pdf.h
/usr/local/lib/libwkhtmltox.so
/usr/local/lib/libwkhtmltox.so.0
/usr/local/lib/libwkhtmltox.so.0.12
/usr/local/lib/libwkhtmltox.so.0.12.2
/usr/local/share/man/man1/wkhtmltoimage.1.gz
/usr/local/share/man/man1/wkhtmltopdf.1.gz

##安装PHP扩展(https://github.com/mreiferson/php-wkhtmltox)
cd phpwkhtmltox
phpize
./configure –with-php-config=/usr/local/php/bin/php-config #此处按照各自系统php安装路径不同而定
make && make install

通过简单设置,也可以直接输出PDF文档。
————————————————————————————-

Oauth2授权类 之 1688.com

<?php
/**
 * vfeelit@qq.com
 * 2015-11-30
 * 
 *  Api授权
 */
 
namespace Alibaba;

class Auth
{
    private $appKey;
    private $appSecret;
    private $redirectUrl;
    
    private $getTokenUrl = "https://gw.open.1688.com/openapi/http/1/system.oauth2/getToken";
    
    public function __construct($appKey, $appSecret, $redirectUrl='')
    {
        $this->appKey = $appKey;
        $this->appSecret = $appSecret;
        $this->redirectUrl = $redirectUrl;
    }

    // 一次性code换取access_token 和  refresh_token
    //array(
    //    "aliId" => "8888888888",      编号
    //    "resource_owner" => "xx",     登录名称
    //    "memberId" => "xx",           会员编号
    //    "expires_in" => "36000",      access_token有效时间,10小时
    //    "refresh_token" => "xx",
    //    "access_token" => "xx",
    //    "refresh_token_timeout" => "20121222222222+0800"
    //)
    public function getToken($code, $redirectUrl)
    {
        $params = array(
            'grant_type' => 'authorization_code',
            'need_refresh_token' => 'true',
            'client_id' => $this->appKey,
            'client_secret' => $this->appSecret,
            'redirect_uri' => empty($redirectUrl)?$this->redirectUrl:trim($redirectUrl),
            'code' => $code,
            '_aop_datePattern' => 'yyyy-MM-dd HH:mm:ss',
            '_aop_timeZone' => 'GMT+0800'
        );
        return $this->_token($params);
    }

    // refresh_token换取access_token
    public function refreshToken($refreshToken)
    {
        $params = array(
            'grant_type' => 'refresh_token',
            'client_id' => $this->appKey,
            'client_secret' => $this->appSecret,
            'refresh_token' => $refreshToken,
            //对授权无效
            '_aop_datePattern' => 'yyyy-MM-dd HH:mm:ss',
            '_aop_timeZone' => 'GMT+0800'
        );      
        return $this->_token($params);
    }
    
    // 获取token
    protected function _token($params)
    {
        $baseUrl = $this->getTokenUrl.'/'.$this->appKey;
        
        $result = $this->doRequest($baseUrl,http_build_query($params),true);
    
        if((int)$result['success'] > 0) {
            $data = json_decode($result['data'],true);
            if(!isset($data['access_token'])) {
                $result['success'] = 0;
                $result['err'] = '返回数据有误(没有access_token)';
                unset($result['data']);
            } else {
                $result['data'] = $data;
            }
        }
    
        return $result;
    }
    
    // 获取授权URL
    public function getAuthUrl($redirectUrl='') {
        $baseUrl = "http://auth.1688.com/auth/authorize.htm";
        
        $pramas = array (
            'client_id' => $this->appKey,
            'site' => 'china'
        );
        
        ///
        if(!empty($redirectUrl)) {
            $pramas['redirect_uri'] = $redirectUrl;
        } else {
            $pramas['redirect_uri'] = $this->redirectUrl;
        }
        if(empty($pramas['redirect_uri'])) {
            $pramas['redirect_uri'] = "http://localhost";
        }
        
        ///
        ksort ( $pramas );
        $signStr = '';
        foreach ( $pramas as $key => $val ) {
            $signStr .= $key . $val;
        }
        $sign = strtoupper ( bin2hex ( hash_hmac ( "sha1", $signStr, $this->appSecret, true ) ) );
        
        ///
        $pramas ['_aop_signature'] = $sign;
        
        return $baseUrl . '?' . http_build_query ( $pramas );
    }
    
    // 发起请求
    public function doRequest($url='', $data='', $post=false)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, trim($url));
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
        if($post === false){
            curl_setopt($ch, CURLOPT_POST, false);
        }else{
            curl_setopt($ch, CURLOPT_POST, true);
            if(!empty($data)) {
                curl_setopt($ch, CURLOPT_POSTFIELDS,$data);
            }
        }
        if ((int)preg_match('/^HTTPS/i', $url) > 0) {
            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,true);
        curl_setopt($ch,CURLOPT_MAXREDIRS,10);
    
        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);
        $errn = curl_errno($ch);
        curl_close($ch);
    
        $return = ['success' => 0,'err' => '发生错误'];
        if((int)$errn > 0) {
            $return['err'] = curl_strerror((int)$errn);
        }else{
            $return['success'] = 1;
            unset($return['err']);
            $return['data'] = $result;
        }
        return $return;
    }
}

Wish的OAuth 2认证流程

Wish还提供了一个sandbox环境,不像那些个Ali系统,从不提供这种东西,想想都恶心(玛尼,就知道坑钱去了)。

http://sandbox.merchant.wish.com,自己注册个sandbox账户,随意折腾。

其实,OAuth 2认证流程都差不多,首先跳转到认证登录页,要求输入用户名密码进行授权(这个过程可能有点差别),然后重定向到预定的redirect_uri并附上一个一次性的code(这个就是认证码了),然后用这个code去获取access_token和refresh_token,这个时候要把它们保存下来,再然后就可以使用access_token来访问了。如果access_token过期了,可以使用refresh_token来换取新的access_token。

在进行第一步之前,要先获取API相关信息(账户-设置):
wish_api
理论上,这里就是对应一个App,那么不同的店铺应该都可以绑定到这个App。不过,如果别人都可以绑定到你的App,那么你就可以提供服务了,很明显,它不应该不允许你这样干,这个跟恶心的Ali系统一样,如果要这样干,需要得到特别授权,所以,这里的App(Api)跟你的账户一一对应,你的账户只能授权给你自己创建的这个App。(注意以上的信息,后面都要用到)

在基本信息中,还有一个叫商户 ID的,它唯一标识你的账户。这个后面会涉及到。

弄好之后,就可以开始了。详细内容参考:https://merchant.wish.com/documentation/oauth。这里只做一些备注。在第一步时,直接发起https://merchant.wish.com/oauth/authorize?client_id={client_id},参数除了client_id,你无法再提供其它参数了,有些实现方案还可以在这传递redirect_uri和其它参数,Wish不允许,授权码(code)直接附加到你在账户设置里面设置的那个redirect_uri。

在第二步中,你需要使用第一步中获取到的一次性code换取令牌,你需要通过POST方法发送如下参数:

//https://merchant.wish.com/api/v2/oauth/access_token 
client_id	Your app's client ID
client_secret	Your app's client secret
code	        The authorization code you received
grant_type	The string 'authorization_code'
redirect_uri	Your app's redirect uri that you specified when you created the app

这里需要注意的是,redirect_uri必须填写在你账户里设置的那个,否则无法通过验证。在Ali系统中,这个是不验证的,可以随意,玛尼,被它坑死。

这个通过之后会返回一个JSON信息,包含了Token,这个时候要把Token保存起来。不过这里,如果参考文档,你发现,它返回的数据没带有商户Id,那么怎么跟商户关联起来?你要是可以在redirect时携带参数还好,不过Wish你允许啊,所以,千万别被它坑了,真实返回的JSON是带有这个商户ID,这样就可以对应起来。

从真实返回的信息可以知道,它的Access Token有效是30天(玛尼,是不是有点长),另外,refresh_token视乎没有过期概念。在Access Token过期前换一个新的,不知道旧的会不会失效。同样,如果重新对App进行授权,这样就产生新的refresh_token,那么旧的会不会就失效了呢? 答案应该是的。(Ali系貌似这个问题比较隐晦)所以,如果要换一换Token,再来一次授权吧。

最后说说官方提供的PHP SDK吧。地址:https://github.com/ContextLogic/Wish-Merchant-API,可以通过composer引用,不过要注意的是,它至今还是dev版本,所以要这样干:

{
  "minimum-stability": "dev",
  "require":{
      "wish/php-sdk":"*"
  }
}

好的不教,这个简直要害死你。这样其它的包,也下载dev分支的给你。对我来说,我直接下载下来当本地包使用,因为这个包有点问题,需要改。故而:

    "autoload": {
        "psr-4": {
            "Wish\\": "local/Wish/"
        }
    },

然后更新下就完事了。

我第一个需要改这个包的地方就是,它使用了curl去发起请求,而Wish的Api都是https的,玛尼,Windows下curl无法验证https的证书(需要设置,指定CA证书的位置),所以我需要把它给位不验证:

//WishRequest.php 65行
if ((int)preg_match('/^HTTPS/i', $url) > 0) {
    $options[CURLOPT_SSL_VERIFYPEER] = false;
    $options[CURLOPT_SSL_VERIFYHOST] = false;
}

另外,这个鬼佬写得代码是用两个空格作为tab的,简直欠抽的节奏。还好格式化工具比较强大。

然后,打开WishSession.php,兼容问题:

private static $api_key;
private static $session_type;
private static $merchant_id;

#改为

private $api_key;
private $session_type;
private $merchant_id;

在实例方法中,出现和静态变量重名报错。以下是一段实例:

// Laravel 的Model
<?php
namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Wish\WishAuth;

class WishAccount extends Model
{

    protected $table = 'wish_account';

    protected $guarded = [];

    public function getAccessToken($type='prod')
    {
        $default = ['success'=>0,'message'=>'获取Access Token失败','access_token'=>''];
        // Access Token不为空,判断是否过气
        if (! empty($this->access_token)) {
            $timeOut = trim($this->access_token_timeout_at);
            if(!empty($timeOut) && (strtotime($timeOut) > time())) {
                return ['success'=>1,'message'=>'','access_token'=>$this->access_token];
            }
        } 
        
        // 更换新Token
        if(! empty($this->refresh_token)) {
            $client_id = trim($this->client_id);
            $client_secret = trim($this->secret_id);
            
            if(empty($client_id) || empty($client_secret)) {
                $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(Client_id 或 Client_secret为空,无法换取Token)";
                return $default;
            }
            
            // 
            try {
                $auth = new WishAuth($client_id,$client_secret,$type);
                $response = $auth->refreshToken($this->refresh_token);
                if($response->getStatusCode() > 0) {
                    $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(状态码:".$response->getStatusCode()." Msg:".$response->getMessage();
                } else {
                    // 返回的Refresh Token,跟之前一样
                    $this->freshToken($response->getData());
                    return ['success'=>1,'message'=>'','access_token'=>$response->getData()->access_token];
                }
            } catch (Exception $e) {
                $default['message'] = "Access Token过期,使用Refresh Token换取Access Token失败(状态码:4000 Msg:Unauthorized access)";
                return $default;
            }
        }
        
        return $default;
    }
    
    // 一次性code换取Token access_token和refresh_token
    public function setToken($code,$type='prod') {

        $client_id = trim($this->client_id);
        $client_secret = trim($this->secret_id);
        $redirect_uri = trim($this->redirect_uri);
        
        if(empty($client_id) || empty($client_secret) || empty($redirect_uri) || empty($code)) {
            return "信息不完整,无法完成授权。";
        }
        
        try {
            $auth = new WishAuth($client_id,$client_secret,$type);
            $response = $auth->getToken($code,$redirect_uri);
            
            $this->freshToken($response->getData());
        } catch (Exception $e) {
            return "4000 或 1016 异常。";
        }
    }
    
    // 保存Token信息
    protected function freshToken($data) {
        if (is_object($data)) {
            $this->refresh_token = $data->refresh_token;
            $this->refresh_token_timeout_at = date("Y-m-d H:i:s", time() + 1 * 12 * 30 * 24 * 3600);
            $this->access_token = $data->access_token;
            $this->access_token_timeout_at = date("Y-m-d H:i:s", time() + $data->expires_in - 2 * 24 * 3600);
            
            $this->save();
        }
    }
}

抓Listing:

use Wish\WishClient;

$access_token = 'an_example_access_token';

$client = new WishClient($access_token,'prod');
$products = $client->getAllProducts();
echo "You have ".count($products)." products!\n";

注意,这段代码中间是可能抛出异常的,比如Access Token过期等,需要注意捕捉。

进程监控与管理 – supervisor

#预先安装epel源(supervisor依赖python-meld3,实际就是python写的脚本)
yum install supervisor

[root@vfeelit ~]# rpm -qa | grep supervisor
supervisor-3.1.3-3.el7.noarch

[root@vfeelit ~]# rpm -ql supervisor-3.1.3-3.el7.noarch
/etc/logrotate.d/supervisor  #滚动日志
/etc/supervisord.conf	     #配置文件
/etc/tmpfiles.d/supervisor.conf

/usr/bin/pidproxy
/usr/bin/supervisorctl       #进程控制
/usr/bin/supervisord 	     #进程

/usr/lib/python2.7/site-packages/supervisor/*
/usr/lib/python2.7/site-packages/supervisor/supervisorctl.py
/usr/lib/python2.7/site-packages/supervisor/supervisorctl.pyc
/usr/lib/python2.7/site-packages/supervisor/supervisorctl.pyo
/usr/lib/python2.7/site-packages/supervisor/supervisord.py
/usr/lib/python2.7/site-packages/supervisor/supervisord.pyc
/usr/lib/python2.7/site-packages/supervisor/supervisord.pyo

/usr/lib/systemd/system/supervisord.service  #服务脚本
/var/log/supervisor  # 日志
/var/run/supervisor  # PID

查看服务启动脚本/usr/lib/systemd/system/supervisord.service,可以看到主要运行
/usr/bin/supervisord -c /etc/supervisord.conf。

查看/usr/bin/supervisord:

[root@vfeelit ~]# cat /usr/bin/supervisord
#!/usr/bin/python
# EASY-INSTALL-ENTRY-SCRIPT: 'supervisor==3.1.3','console_scripts','supervisord'
__requires__ = 'supervisor==3.1.3'
import sys
from pkg_resources import load_entry_point

if __name__ == '__main__':
    sys.exit(
        load_entry_point('supervisor==3.1.3', 'console_scripts', 'supervisord')()
    )

使用/usr/bin/python解释器,靠,这个世界还有如此难看的代码。大概意思应该就是运行supervisord这个脚本吧,根据我的感觉,应该是/usr/lib/python2.7/site-packages/supervisor/supervisord.py。知道那么回事就可以了。这种工具主要看怎么配置,以及用法。

配置文件/etc/supervisord.conf中的配置实在太多,不过这个文件中的东西,看起来都不需要动它,这个配置语法是典型ini配置:

[supervisord]   //主进程的配置
[supervisorctl]
[include]
files = supervisord.d/*.ini

自己的配置,放入supervisord.d即可。

例子:

command=/var/www/server.php	;要运行的命令
autostart=true 			;自动启动
autorestart=true 		;自动重启
redirect_stderr=true 		;将stderr重定向到stdout
user=www			;以哪个用户启动命令
directory=/var/www	 	;cd 到应用目录
stdout_logfile = /var/www/x.log ;日志输出

[program:xxx-worker]
process_name=%(program_name)s_%(process_num)02d
command=php artisan queue:work --queue=parser --sleep=3 --tries=3 --daemon
autostart=true
autorestart=true
user=www
numprocs=16			;进程数量
redirect_stderr=true
stdout_logfile=/var/log/worker.log

命令的运行,一般写入绝对路径比较稳妥。user指定了运行脚本的用户(要注意权限问题)。numprocs指定要保持的进程数量,这个参数如果大于1,就需要设置process_name(参考例子),因为它需要一个唯一的名字。另外一个是日志输出,应该是进程相关,或命令行运行产生的错误相关的。

说明:

%(program_name)s	表示program_name是一个字符串
%(process_num)02d	表示process_num是数字(d),并且是两位数字,不足用0填充

客户端操作:

[root@vfeelit ~]# supervisorctl 
test-worker:test-worker_00       RUNNING   pid 2834, uptime 0:01:03
test-worker:test-worker_01       RUNNING   pid 2835, uptime 0:01:03
test-worker:test-worker_02       RUNNING   pid 2836, uptime 0:01:03
test-worker:test-worker_03       RUNNING   pid 2837, uptime 0:01:02
test-worker:test-worker_04       RUNNING   pid 2838, uptime 0:01:01
supervisor> status
test-worker:test-worker_00       RUNNING   pid 2834, uptime 0:01:10
test-worker:test-worker_01       RUNNING   pid 2835, uptime 0:01:10
test-worker:test-worker_02       RUNNING   pid 2836, uptime 0:01:10
test-worker:test-worker_03       RUNNING   pid 2837, uptime 0:01:09
test-worker:test-worker_04       RUNNING   pid 2838, uptime 0:01:08
supervisor> help

default commands (type help <topic>):
=====================================
add    clear  fg        open  quit    remove  restart   start   stop  update 
avail  exit   maintail  pid   reload  reread  shutdown  status  tail  version

supervisor> stop help
help: ERROR (no such process)
supervisor> stop
Error: stop requires a process name
stop <name>		Stop a process
stop <gname>:*		Stop all processes in a group
stop <name> <name>	Stop multiple processes or groups
stop all		Stop all processes
supervisor> version
3.1.3
supervisor> pid
2757
supervisor> restart 
Error: restart requires a process name
restart <name>		Restart a process
restart <gname>:*	Restart all processes in a group
restart <name> <name>	Restart multiple processes or groups
restart all		Restart all processes
Note: restart does not reread config files. For that, see reread and update.

Supervisor可以按组进行配置。

最后查看一下进程树:

└─system.slice
├─supervisord.service
│ ├─2757 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf
│ ├─2834 php /root/worker.php
│ ├─2835 php /root/worker.php
│ ├─2836 php /root/worker.php
│ ├─2837 php /root/worker.php
│ └─2838 php /root/worker.php
[/shll]
从supervisord派生子进程,意思是它可控制的。也就意味如果不是它派生的,是无法监控的。

Composer包依赖管理器快速参考

基本用法:
http://docs.phpcomposer.com/01-basic-usage.html#Basic-usage

一 建立composer.json

{
    "require": {
        "monolog/monolog": "1.0.*"
    }
}

指令”require”表示依赖的包,是一个列表,每个元素是包名对应版本号,报名分两部分,分别是供应商 和 包名称,然后是对应的版本号。版本号的声明可以用使用一般的比较运算符,比如>=1.0,更加常见的是~1.2,表示最低依赖1.2,从1.2到2.0(不包括2.0)。

二 然后执行install

php composer.phar install

安装完成之后(检测composer.json,下载所有依赖的包),就会产生composer.lock文件,意味着当前的项目使用的依赖被锁定,因为如果composer.lock存在,那么在install时会先被检测到,那么composer.json将被跳过而使用composer.lock里面锁定的依赖(更新 或 建立vendor目录,下载包)。

如果需要更新所有依赖,可以运行update(而不是install),这样就会检测composer.json,下载新依赖,重新产生composer.lock文件。如果只要更新一个或几个包,可以php composer.phar update monolog/monolog […],这样也会更新最终的composer.lock文件。

三 自动装载
先依赖包安装之后,就需要使用它:

#只需要保护这个文件
require 'vendor/autoload.php';

它实际返回一个loader,预先扫描vendor/composer/中的autoload_classmap.php、autoload_namespaces.php和autoload_psr4.php,先看刚才的install做了什么工作:

<?php

// autoload_namespaces.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'Monolog' => array($vendorDir . '/monolog/monolog/src'),
);

把依赖的包添加到了这里,那么这个自动loader就到这里来寻找对应的库。意思就是install(或update)时,这里的文件会被更新。

那么如果要自动load自己的类库,编辑这里的文件也可以实现。不过一般情况都不这么干。可以往composer.json里面添加:

"autoload": {
        "classmap": [
            "database"
        ],
        "psr-4": {
            "eBay\\": "app/"
        }
}

然后运行php composer.phar update即可,注意classmap的方式对应的是目录,它扫描目录里面的所有文件,找出类名(支持命名空间),然后对应其路径:

return array(
    'Craw' => $baseDir . '/database/Craw.php',
    'Test' => $baseDir . '/database/test/tt.php',
    'Vfeelit\\Need' => $baseDir . '/database/test/ab.php',
);

这个方法对于引入非规范的类,可以说是一网打尽,美中不足的是我可能不需要扫描整个目录。

看看psr-4文件:

<?php
// autoload_psr4.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
    'eBay\\' => array($baseDir . '/app'),
);

注:这里的app目录不需要一定存在,因为它不会扫描目录。

引入类库的方式可以直接使用方法的loader实例:

$loader = require 'vendor/autoload.php';
$loader->add('Acme\\Test\\', __DIR__);

四 关于下载包的来源
众所周知的原因,composer下载很慢,可以使用国内镜像(在composer.josn中添加):

"repositories": [
        {"type": "composer", "url": "http://packagist.phpcomposer.com"},
        {"packagist": false}
    ]

五 项目与包
项目叫project,包叫package。只要你有一个 composer.json 文件在目录中,那么整个目录就是一个包。当你添加一个 require 到项目中,你就是在创建一个依赖于其它库的包。你的项目和库之间唯一的区别是,你的项目是一个没有名字的包。为了使它成为一个可安装的包,你需要给它一个名称。你可以通过 composer.json 中的 name 来定义:

{
    "name": "acme/hello-world",
    "require": {
        "monolog/monolog": "1.0.*"
    }
}

在这种情况下项目的名称为 acme/hello-world,其中 acme 是供应商的名称。供应商的名称是必须填写的。

六 平台软件包
Composer 将那些已经安装在系统上,但并不是由 Composer 安装的包视为一个虚拟的平台软件包。这包括PHP本身,PHP扩展和一些系统库。(简单参考一下即可):

"require": {
        "php": ">=5.5.9",  		///这里
        "laravel/framework": "5.1.*",
        "league/flysystem-aws-s3-v3": "~1.0",
        "maatwebsite/excel": "~2.0.0",
        "chumper/zipper": "0.6.x"
    },

七 版本 标签 分支
http://docs.phpcomposer.com/02-libraries.html

八 发布到 packagist

九 命令行
http://docs.phpcomposer.com/03-cli.html,这部分内容中的global不好理解。大概是提供了已给命令行工具。其它都不难理解。

十 脚本
http://docs.phpcomposer.com/articles/scripts.html,这部分内容是在运行composer时触发的事件。

最后,composer.josn完整的参考http://docs.phpcomposer.com/04-schema.html。

Nginx Location配置 – 被坑记

server {
    listen       80;
    server_name  xxx.com;
    root /mnt/xxx/public;
    index index.php;

    error_page 404 /index.php;
    
    if (!-e $request_filename) {
        rewrite ^/(.+)$ /index.php last;
        break;
    }

    location / {
	root /var/xxx/public;
        #尝试实际文件,然后判断是不是目录,在然后就是到首页(实际是被重新,重新发起location匹配)
	try_files $uri $uri/ /index.php?$query_string; 
    }

    location ~* ^.+\.(css|js|jpeg|jpg|gif|png|ico|eot|ttf|woff|svg) {
        expires 30d;
    }

    location ~* \.(eot|ttf|woff|svg|html)$ {
        add_header Access-Control-Allow-Origin *;
    }

    location ~ \.php$ {
	client_max_body_size 500M;
        fastcgi_connect_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_read_timeout 300;
        fastcgi_buffer_size 32k;
        fastcgi_buffers 256 32k;

        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
        #fastcgi_param APPLICATION_ENV testing;
   }

首先,第一个if语句是判断对应的rui不存在(非目录和文件),那么就重写到index.php,我们知道last重写相当于continue语句,由于这个if在server块中,所以它不会再次发起请求了,故而下面的break是多余的,写上也没有问题。

还是再次复习location指令的用法吧:
location指令
语法:location [=|~|~*|^~] /uri {…} 注意看明白格式****中括号表示可以省略
使用环境:server

该指令运行对不同的URI进行不同的配置,即可以使用字符串,也可以使用正则表达式。使用正则表达式,须使用以下前缀。
1 ~* 表示不区分大小写的匹配
2 ~ 表示区分大小写的匹配
(意思是以这两个字符开头的,就是一定是正则匹配)
在匹配过程中,Nginx将首先匹配字符串,然后再匹配正则表达式。匹配到第一个正则表达式后,会停止搜索(搜索顺序时先出现的优先)。如果匹配到正则表达式,则使用正则表达式匹配,如果没有匹配到正则表达式,则使用字符串的搜索结果。

特殊用法:
可以使用前缀^~来禁止匹配到字符串后,再去检查正则表达式。
使用前缀=可以进行精确的URI匹配,如果找到匹配的URI,则停止查询。

注意,Nginx中的变量$uri由于都是以/开头的,当访问首页是其实访问的就是/,浏览器总是把斜杠添加上然后才发出请求,就算没有加上斜杠发出来了请求,服务器也会进行一个加了斜杠的重定向,如果访问一个目录时,请求到达服务器后如何确认它是一个目录,服务器会进行一个加了斜杠的重定向。

另外,location指令中能使用的前缀只有上面列出的四个,而那些在if中合法的!~ 和 !~*在location中是不合法的,这个稍不注意就会让人困惑。

location的正则匹配是一旦匹配就停止,所以那些允许的匹配应该写在前,不允许的应该写在后,具体的写在最前面。

所有的location匹配放入一个循环,字符串优先匹配(最先的先匹配,后面的后匹配),然后匹配正则,正则一旦匹配,就使用这个正则匹配(触发被重写,会重新发起一次对locaiton的匹配),否则就使用最后的字符匹配,总体上满足先具体,后一般的原则。

回到配置,第一个字符匹配,基本上,如果后面的没有匹配,就都是它了。后面的是三个正则匹配,一旦匹配,就用它。现在假如URI是get.js.php 和 get.css.php,这个时候它匹配了第二个正则而不是第三个正则,所以被坑就在这里了。导致明明要执行PHP脚本的,生硬的把get.js.php源码返回。争取的写法应该是在最后加上$字符。

FTP客户端工具 – FileZilla Client

FileZilla Client是一个可以免费使用的FTP客户端工具,虽然是免费的,但是功能非常出色,绝对是一款神器。官网:https://filezilla-project.org/,也有对应的服务端(没有使用过)。

sitemanager
协议这里支持SFTP,一般,如果服务器安装了SSH,就可以通过账户链接,它是走SSH加密传输的,既安全也不需要在服务器端当独安装FTP工具,一般建议使用。在传输设置中设置FTP的传输模式,并发链接数等。可以发现,这个神器支持SFTP,但是好像不支持公钥认证方式登录(只支持密码),实际上,如果需要使用私钥的方式登录,需要到【编辑】 — 【设置】 — 【链接】 — 【SFTP】先把秘钥导入进来:
ftp-rsa

注:可能会提示私钥不支持,询问是否装换,确认后就可以导入。如果私钥设置了密码,还会提示让你输入秘钥密码然后清空私钥密码,确认后导入,实际上私钥密码是没有清空的。在建立站点时,正常输入用户名,如果有秘钥就输入秘钥密码,没有就随便填了(这个工具没有单独为秘钥提供特殊设置)。

默认的界面,使用起来不是很方便,需要经过一番设置:【编辑】 — 【设置】
ftpset
选中宽屏,这种方式以分别对本地和远程以目录,目录列表的方式4列摊开,对操作非常的变量。

另外,最好把消息日志拉到最底下,因为这个东西不是关注的重点,反而碍眼:
ftpset2

经过以上两个步骤设置之后,会得到如下视图:
ftpview
这个视图看起来是不是很直观?

还有一个非常便利的工具,那就是书签,FileZilla Client的书签意思就是设置一个本地和远程的一个对应关系,当打开本地目录时,远程对应目录也对应打开,反过来也一样。书签可以设置为全局的,也可以设置为针对站点的,充分利用这个功能,可以让你在本地和远程目录对应的切换的痛苦中解脱:
ftptag

另外,还有一些配置可以参考:
1 【编辑】 — 【设置】 — 【传输】 — 【文件类型】,默认的传输类型可以改为二进制,如果是自动或ASCII方式,对文本传输可能产生问题。
2 【编辑】 — 【设置】 — 【界面】 — 【文件大小格式】,在文件大小格式这里,可以改为“使用SI式二进制前缀”,这个方式可以以B或KB的后缀显示文件大小(默认是字节,卧槽,看到字节时你很难评估它到底有多大吧?)
3 【编辑】 — 【设置】 — 【界面】 — 【文件编辑】,这里可以关联一个外部编辑器,默认可能继承了Window的设置,这个可能让人很不爽。

函数参考 – 文件系统相关扩展 – Inotify

#安装
wget http://pecl.php.net/get/inotify-0.1.6.tgz
tar zxvf inotify-0.1.6.tgz
cd inotify-0.1.6
/usr/local/php-5.5.15/bin/phpize
./configure --with-php-config=/usr/local/php-5.5.15/bin/php-config
make -j 4
make install

#添加配置
extension = "inotify.so"

例子——————————————–

<?php
/***
常量 位操作
IN_ACCESS           :1
IN_MODIFY           :2
IN_ATTRIB           :4
IN_CLOSE_WRITE      :8
IN_CLOSE_NOWRITE    :16
IN_OPEN             :32
IN_MOVED_TO         :128
IN_MOVED_FROM       :64
IN_CREATE           :256
IN_DELETE           :512
IN_DELETE_SELF      :1024
IN_MOVE_SELF        :2048
IN_CLOSE            :24
IN_MOVE             :192
IN_ALL_EVENTS       :4095
IN_UNMOUNT          :8192
IN_Q_OVERFLOW       :16384
IN_IGNORED          :32768
IN_ISDIR            :1073741824
IN_ONLYDIR          :16777216
IN_DONT_FOLLOW      :33554432
IN_MASK_ADD         :536870912
IN_ONESHOT          :-2147483648
*/

$fd = inotify_init();
$watch_descriptor = inotify_add_watch($fd,'/data/web', IN_CREATE);

while (true) {
    $events = inotify_read($fd);

$watch_descriptor = inotify_add_watch($fd,'/data/web', IN_CREATE);

while (true) {
    $events = inotify_read($fd);

    if ($events) {
        foreach ($events as $event) {
                /*
                array(
                        wd 1?
                        mask 事件对应的数字
                        cookie 0?
                        name 文件名称
                )
                */
                //不支持监控子目录
                $file = $event['name'];
                $action = $event['mask'];
                if((int)preg_match("/(jpg|jpeg|png|gif)$/i",$file) > 0){
                        if($action === IN_CREATE){
                                echo "Create - ".$file."\n";

                                //if(rename("/data/web/$file","/data/webb/$file")){
                                //}
                        }

                }

        }
    }
}

inotify_rm_watch($fd, $watch_descriptor);
fclose($fd);

首先,不支持监控子目录,在子目录中操作,脚本无法得到通知。对于要在PHP中移动文件这类操作,实际测试也没有成功。