正则-编程语言调用代码的生成

演示: 正则表达式测试工具

功能介绍

给定一个正则,生成php的正则调用代码。

一般正则中都会或多或少的包含具体编程语言中的特殊字符,还需要考虑到具体语言中字符串的转义;

下面列出了一些常见的要考虑的情况:

1. 语言是否支持多行字符串?
2. 是否有定界符?
3. 语言的正则引擎是否支持逆序环视?
4. 正则修正符?

具体场景

我们拿PHP做个例子:

写一个特殊字符覆盖比较全的正则,并写出他的PHP匹配代码:

要匹配的内容

<!-- 模拟一个要匹配的字符串, 我们要取出其中的<div/>区块的内容 -->
<article>
  <div class="quote">'引用\/'</div>
</article>

正则

<div class="quote">'(.+?\\/)'</div>

PHP匹配代码

$contents = <<<EOT
<article>
  <div class="quote">'引用\/'</div>
</article>
EOT;

if (preg_match('/<div class="quote">\'(.+?)\\\\\/\'<\/div>/', $contents, $match)) {
    var_dump($match);
}

注意: 上面的正则写到PHP代码中的时候,以下字符进行了转义:

1. ' 单引号
2. / 定界符
3. \ 转义字符,这是个比较特殊的字符

实现思路

要实现一个可用的代码生成,无非就是要把上面提到的特殊字符给转义了;

本想要用正则再对正则的字符串转义,最后发现不太靠谱,越写越复杂;

记起之前看过的redis conf分析相关的代码之后,遂选择逐字判断字符,并对字符转义,效果不错;注意优先级!

public function escape($value)
{
    $chars = str_split($value);
    $len = count($chars);
    $result = '';
    for ($i = 0; $i < $len; $i++) {
        if ($chars[$i] === '\\' && $i < $len - 1) {
            // 转义字符的判断优先级最高,可以避免 \' 正则的重复转义
            $i++; // 跳过后面一个字符
            if ($chars[$i] === '\\') {
                $result .= '\\\\\\\\';
            } else {
                $result .= '\\' . $chars[$i];
            }
        } else if ($chars[$i] === '\'') {
            // 单引号,php字符串边界
            $result .= '\\\'';
        } else if ($chars[$i] === '/') {
            // / php正则定界符
            $result .= '\\/';
        } else {
            $result .= $chars[$i];
        }
    }

    return $result;
}

根据magnet下载torrent

整体流程

magnet2torrent.png

requirement

# 安装virtualenv
pip install virtualenv
# 创建虚拟环境
virtualenv env
# source
. ./env/bin/activate

安装依赖包
pip install beanstalkc

Client

实现往beanstalkd里面塞hash

import beanstalkc
import sys

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("usage: %s <hash>" % sys.argv[0])
        sys.exit(1)
    if len(sys.argv[1]) != 40:
        print("hash code is error.")
        sys.exit(1)
    beanstalk = beanstalkc.Connection(host = "localhost", port = 11300)
    beanstalk.put(sys.argv[1])
    print("hash has put")

server

依赖libtorrent下载torrent

wget https://github.com/arvidn/libtorrent/releases/download/libtorrent-1_0_10/libtorrent-rasterbar-1.0.10.tar.gz
tar zxf libtorrent-rasterbar-1.0.10.tar.gz
cd libtorrent-rasterbar-1.0.10
./configure --enable-python-binding PYTHON=`which python` --prefix=$VIRTUAL_ENV LIBS='-liconv'
make -j2 && make install

依赖beanstalkd队列

mkdir /usr/local/beanstalkd/bin
wget https://github.com/kr/beanstalkd/archive/v1.10.tar.gz
tar zxf v1.10.tar.gz
cd beanstalkd-1.10/
make
mv beanstalkd /usr/local/beanstalkd/bin

mkdir /data/beanstalkd/
# /usr/local/beanstalkd/bin/beanstalkd -b /data/beanstalkd/

使用supervisor管理beanstalkd

[program:beanstalkd]
directory = /data/beanstalkd/
command = /usr/local/beanstalkd/bin/beanstalkd -b /data/beanstalkd/

Python2.7 支持 ThreadPoolExecutor 的话还需要 futures 包

pip install futures

上Server端代码

import time
import tempfile
import libtorrent
import os
import os.path as path
import shutil
from concurrent import futures
import beanstalkc

def hash2torrent(torrent_hash, timeout = None):
    torrent_hash = torrent_hash.lower()
    print("start download: %s" % (torrent_hash))
    magnet = "magnet:?xt=urn:btih:" + torrent_hash

    directory = path.join("torrents", torrent_hash[0:2], torrent_hash[-2:])
    output = path.join(directory, torrent_hash + ".torrent")

    if not path.exists(directory):
        os.makedirs(directory)

    if path.exists(output):
        print('Already exists.')
        return output

    tempdir = tempfile.mkdtemp()
    session = libtorrent.session()

    session.add_dht_router('router.bittorrent.com', 6881)
    session.add_dht_router('router.utorrent.com', 6881)
    session.add_dht_router('router.bitcomet.com', 6881)
    session.add_dht_router('dht.transmissionbt.com', 6881)
    session.add_dht_router("dht.aelitis.com", 6881)
    session.start_dht()

    params = {
        'save_path': tempdir,
        # 'storage_mode': libtorrent.storage_mode_t(2),
        # 'paused': False,
        # 'auto_managed': True,
        'duplicated_is_error': True
    }

    handle = libtorrent.add_magnet_uri(session, magnet, params)

    cost = 0
    while not handle.has_metadata():
        if timeout is not None and cost > timeout:
            print("Timeout.")
            # session.pause()
            session.remove_torrent(handle)
            shutil.rmtree(tempdir)
            return None
        time.sleep(1)
        cost = cost + 1
    # session.pause()
    print("Downloaded. %d" % (cost))

    # print 'got metadata, starting torrent download...'
    # while handle.status().state != libtorrent.torrent_status.seeding:
    #     s = handle.status()
    #     state_str = ['queued', 'checking', 'downloading metadata', 'downloading', 'finished', 'seeding', 'allocating']
    #     print '%.2f%% complete (down: %.1f kb/s up: %.1f kB/s peers: %d) %s %.3f' % (s.progress * 100, s.download_rate / 1000, s.upload_rate / 1000, s.num_peers, state_str[s.state], s.total_download/1000000)
    #     time.sleep(3)

    torrent_info = handle.get_torrent_info()
    torrent_file = libtorrent.create_torrent(torrent_info)

    torrent_content = libtorrent.bencode(torrent_file.generate())

    with open(output, "wb") as f:
        f.write(torrent_content)
        f.close()

    session.remove_torrent(handle)
    shutil.rmtree(tempdir)
    return output

if __name__ == '__main__':
    beanstalk = beanstalkc.Connection(host='localhost', port=11300)
    timeout = None
    with futures.ProcessPoolExecutor(10) as executor:
        while True:
            job = beanstalk.reserve()
            torrent_hash = job.body
            job.delete()
            executor.submit(hash2torrent, torrent_hash, timeout)

实测,平均每个种子的下载时间需要 15 分钟左右。

分库分表的策略

1. 主键+业务虚拟键分表

这种方式适合查询比较单一的业务,最大的缺点就是业务对分库分表这层是有感知:

  1. 查询返回的时候需要在接口层拼接业务的真实Id

  2. 根据真实业务Id查询,或者更新数据的时候,需要拆分真实业务Id,路由到数据所在表,再根据数据库Id查询

虚拟场景

假设我们分了1024张表,具体表结构如下:

QQ20160904-0@2x.png

插入流程

1. 插入数据,必须带有userId
2. 根据userId计算出xId
xId = userId % 10000;
3. 根据xId定位数据所在表
tNum = xId % 1024; // 最简单的取模hash(具体策略由中间件决定)
4. 插入数据,返回realId
realId = id + xId;

单条查询流程

1. 根据realId查询, 获取单张表中的Id值
id = realId / 10000; // 整除
2. 获取虚拟键xId
xId = realId % 10000;
3. 根据xId定位数据所在表
tNum = xId % 1024; // 最简单的取模hash(具体策略由中间件决定)
4. 根据表和Id获取单条数据

批量查询流程

基本和单条查询一致,可优化的点:

第3步,根据xId进行分组,将查询同一张表的query放在一次sql的查询语句中

2. 主键/业务外键分表

这种策略对业务代码可以无侵略性, 为避免Id冲突, 使用外部Id生成器, 以下步骤全在中间件中执行:
(根据业务外键的处理同理,查询比较复杂的情况一般都是使用冗余表)

虚拟场景

假设我们分了1024张表,具体表结构如下:

QQ20160904-1@2x.png

插入流程

1. 从Id生成器获取Id
2. 根据Id进行hash (简单的可以使用取模), 获取数据应该插入的表名
tNum = id % 1024;
3. 插入数据表

单条查询流程

1. 根据Id进行hash (简单的可以使用取模), 获取数据所在表名
tNum = id % 1024;
2. 查询数据

批量查询流程

基本和单条查询一致,可优化的点:

第1步,根据Id进行hash后进行分组,将查询同一张表的query放在一次sql的查询语句中

数据的聚合

在批量查询的时候,需要 order by group by distinct 的时候,需要在查询出各分表数据之后,在中间件组件中自行实现对数据的处理

页面静态化

页面内容的拆分

Artboard 1.png

  1. 静态内容

  2. 非状态相关的动态内容

  3. 状态相关的动态内容

HInclude CSI

用于pc或者h5页面

<hx:include src="/_fragment?path="></hx:include>

Varnish ESI

多用于app api的接口返回

vcl 4.0;

import directors;

backend server1 {
    .host = "10.162.110.168";
    .port = "80";
}

acl local {
    "localhost";
}

sub vcl_init {
    new director = directors.round_robin();
    director.add_backend(server1);
}

sub vcl_recv {
    set req.backend_hint = director.backend();

    if (req.method == "PURGE") {
        if (client.ip ~ local) {
            return (purge);
        } else {
            return (synth(405));
        }
    }

    if (req.http.Accept == "text/event-stream") {
        return (pipe);
    }

    if (req.http.upgrade ~ "(?i)websocket") {
        return (pipe);
    }

    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    set req.http.Surrogate-Capability = "abc=ESI/1.0";
    if (req.http.X-Forward-For) {
        set req.http.X-Forward-For = req.http.X-Forward-For + "," + client.ip;
    } else {
        set req.http.X-Forward-For = client.ip;
    }
    return (hash);
}

sub vcl_backend_response {
    if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
        unset beresp.http.Surrogate-Control;
        set beresp.do_esi = true;
    }

    if (bereq.uncacheable) {
        return (deliver);
    }

    set beresp.grace = 30s;

    # 302 5xx no cache
    if (beresp.status == 302 || beresp.status >= 500) {
        set beresp.uncacheable = true;
        set beresp.ttl = 120s;
        return (deliver);
    }

    if (beresp.status == 301 || (beresp.status >= 400 && beresp.status < 500)) {
        set beresp.ttl = 120s;
    } else {
        set beresp.ttl = 2400s;
    }

    unset beresp.http.Set-Cookie;
    return (deliver);
}

sub vcl_pipe {
    if (req.http.upgrade) {
        set bereq.http.upgrade = req.http.upgrade;
    }
    return (pipe);
}

sub vcl_deliver {
    set resp.http.X-Age = resp.http.Age;
    unset resp.http.Age;
    unset resp.http.Via;
    unset resp.http.X-Powered-By;
    unset resp.http.X-Varnish;

    if (obj.hits > 0) {
        set resp.http.X-Cache="HIT from " + server.hostname;
    } else {
        set resp.http.X-Cache="MISS";
    }

    return (deliver);
}

workflow

  1. varnish 接收到请求后,增加 Surrogate-Capability:abc=ESI/1.0 头,打到后端

  2. 后端判断Surrogate-Capability决定是否要使用esi标签来返回 (即判断这个请求是不是从varnish过来的)

  3. 返回内容时判断是否含有 <esi:include,增加 Surrogate-Control:content="ESI/1.0"

  4. varnish 判断 Surrogate-Control 头,决定是否要启用 esi 替换

后端应用中的处理

  1. 生成路由url /_fragment?path=

    • <hx:include src="/_fragment?path="></hx:include>

    • <esi:include src="/_fragment?path="></esi:include>

  2. 注册处理 /_fragment 路由

wrk中的lua脚本

wrk是一款现代化的http压测工具,提供lua脚本的功能可以满足每个请求或部分请求的差异化。

wrk中执行http请求的时候,调用lua分为3个阶段,setup,running,done,每个wrk线程中都有独立的脚本环境。

wrk.png

wrk的全局属性

wrk = {
  scheme  = "http",
  host    = "localhost",
  port    = nil,
  method  = "GET",
  path    = "/",
  headers = {},
  body    = nil,
  thread  = <userdata>,
}

wrk的全局方法

-- 生成整个request的string,例如:返回
-- GET / HTTP/1.1
-- Host: tool.lu
function wrk.format(method, path, headers, body)

-- 获取域名的IP和端口,返回table,例如:返回 `{127.0.0.1:80}`
function wrk.lookup(host, service)

-- 判断addr是否能连接,例如:`127.0.0.1:80`,返回 true 或 false
function wrk.connect(addr)

Setup阶段

setup是在线程创建之后,启动之前。

function setup(thread)

-- thread提供了1个属性,3个方法
-- thread.addr 设置请求需要打到的ip
-- thread:get(name) 获取线程全局变量
-- thread:set(name, value) 设置线程全局变量
-- thread:stop() 终止线程

Running阶段

function init(args)
-- 每个线程仅调用1次,args 用于获取命令行中传入的参数, 例如 --env=pre

function delay()
-- 每个线程调用多次,发送下一个请求之前的延迟, 单位为ms

function request()
-- 每个线程调用多次,返回http请求

function response(status, headers, body)
-- 每个线程调用多次,返回http响应

Done阶段

可以用于自定义结果报表,整个过程中只执行一次

function done(summary, latency, requests)


latency.min              -- minimum value seen
latency.max              -- maximum value seen
latency.mean             -- average value seen
latency.stdev            -- standard deviation
latency:percentile(99.0) -- 99th percentile value
latency(i)               -- raw value and count

summary = {
  duration = N,  -- run duration in microseconds
  requests = N,  -- total completed requests
  bytes    = N,  -- total bytes received
  errors   = {
    connect = N, -- total socket connection errors
    read    = N, -- total socket read errors
    write   = N, -- total socket write errors
    status  = N, -- total HTTP status codes > 399
    timeout = N  -- total request timeouts
  }
}

例子

表单的提交

wrk.method = "POST"
wrk.body = "" -- 直接写死,如果不需要请求数据的差异化
wrk.headers["Content-Type"] = "application/x-www-form-urlencoded"

-- 如果要实现每次都不一样的表单内容
local queries = {
    "language=php",
    "language=java",
    "language=lua"
}
local i = 0
request = function()
    local body = wrk.format(nil, nil, nil, queries[i % #queries + 1])
    i = i + 1
    return body
end

占位图片的优化

背景

在前端开发切图的时候,很多时候示例图片都是不需要的,只需要一个尺寸的图片,还方便后端清晰的知道要输出的图片尺寸。

对于访问时候的优化,有2个方案:

  1. 使用cdn回源 (对于对外开放的服务,自然会在cdn上留下大量一次性的图片)

  2. 在服务器的内存中生成图片返回 (由于只是提供开发者使用,只要对http缓存做好优化,压力不会很大)

大家都知道对于静态图片,nginx对于缓存这些东西都给我们做好了,只需要1~2行的配置就可以搞定;但是注意了,我们这里是在程序中动态生成图片,所以我们需要关心http中浏览器的缓存头

Etag

Expires:
Cache-Control: max-age=3600
Last-Modified:
If-Modified-Since:
ETag:
If-None-Match:

Cache-Control

http-cache-decision-tree.png

这里我采用的是对 text, fg, bg, width, height 进行md5处理,因为这几个值就可以确定唯一的图片

$md5 = md5(json_encode(compact('width', 'height', 'text', 'bg', 'fg')));
$etags = Input::getEtags();
if (isset($etags[0])) {
    $etag = str_replace('"', '', $etags[0]);
    if ($etag === $md5) {
        App::abort(304, '', ['Content-Type' => 'image/png', 'Cache-Control' => 'public, max-age=31536000', 'ETag' => $md5]);
    }
}

YUV 图片文字颜色的获取 (颜色明亮度)

更多的使用的时候,往往我们只需要指定背景色,不关心文字的颜色(能看清就行);于是在不同的背景色下面,我们都需要让文字颜色和背景色保持一定的对比度,好了直接看代码吧

$greyLevel = $r * .229 + $g * .587 + $b * .114;
if ($greyLevel >= 189) { // 192
    $fg = '#666666';
} else {
    $fg = '#ECF0F1';
}

字体选择的优化

全英文的情况下 Georgia 的展现效果比较不错,但是包含中文之后效果就大打折扣,所以这里有个小兼容

$fontFamily = 'Georgia.ttf';
// 如果包含中文
if (preg_match('/[\x{4e00}-\x{9fa5}]/u', $text)) $fontFamily = 'yahei_mono.ttf';

安全控制

由于图片的生成会消耗一定的内存,而且图片尺寸越大,耗的内存越多,所以这里对生成图片的尺寸做了1个限制,就是高宽都必须小于等于 2048,否则返回 404

搭建私有播放服务

背景

由于很多时候,视频资源下载很慢,需要用自己的电脑挂机;又或是国外的资源被墙,无法观看;所以需要一个可以挂机下载的服务器。

离线下载这步做到之后,又由于download到自己电脑上还需要一段时间,而且有些资源可能是不需要的, 所以又有了在线观看的需求。

使用到的软件/框架

  1. Scrapy

  2. MySQL

  3. Aria2

  4. Ffmpeg

  5. Nginx

  6. Laravel

  7. Videojs

流程图

下载流程

downloader.png

展示流程

player.png

痛点

其实正常的开发流程一般遵照网上的教程或者官方文档就可以很容易的解决;这边讲讲一些需要费一些精力才能解决的问题

被js加密过的下载地址

一般都是使用eval来执行加密过的字符串,只要先将eval里面的参数给解密出来,然后使用正则匹配解密之后的字符串里面的下载地址就可以了

p = re.compile(ur'eval\((.*)')
m = p.search(response.body)
if not m:
    print('---- not match ----')
    return
jsctx = PyV8.JSContext(PyV8.JSClass())
jsctx.enter()
jsctx.eval(r"var a = new String(" + m.group(1))
print(jsctx.locals.a)
content = jsctx.locals.a

p1 = re.compile('(http://[^"]+)')
m1 = p1.search(content)

if not m1:
    print('---- cannot find url in js content ----')
    return

videoUrl = m1.group(1)

python和aria2的交互

# encoding: utf-8
import xmlrpclib

class Pipeline(object):
    def __init__(self):
        self.s = xmlrpclib.ServerProxy('http://localhost:6800/rpc')

    def init_hook(self):
        self.s.aria2.onDownloadStart(self.onDownloadStart)
        self.s.aria2.onDownloadError(self.onDownloadError)
        self.s.aria2.onDownloadComplete(self.onDownloadComplete)

    def onDownloadStart(self, event):
        pass

    def onDownloadError(self, event):
        pass

    def onDownloadComplete(self, event):
        pass

    def process_item(self, item, spider):
        # ...
        gid = self.s.aria2.addUri([item['video_url']], {"out": path})
        # ...

ffmpeg将mp4拆分为HLS

命令如下,注意替换<>之间的内容

ffmpeg -i <ccc.mp4> -vf scale=-2:360 -profile:v baseline -level 3.0 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls <ccc.m3u8>

ffmpeg截取部分视频为gif

注意替换命令中的两个变量

ffmpeg -ss 00:00:10 -t 5 -i "$filename" -vf scale=360:-1 -b 2048k "$dirname".gif

videojs播放HLS

默认情况之下videojs是不支持播放HLS的,需要引入 videojs-contrib-hls.min.js

videojs.options.flash.swf = "/js/video-js/video-js.swf";
videojs('main_video').ready(function() {
  this.hotkeys({
    volumeStep: 0.1,
    seekStep: 5,
    enableMute: true,
    enableFullscreen: true,
    enableNumbers: true
  });
});

媒体中心设计分享

更详细的内容可以查看文章结尾的PDF

媒体文件处理

magic bytes 校验
扩展名校验
文件名校验
mime校验
速率限制

图片的处理

  1. (水印颜色的优化) 颜色深浅的判断

  2. 图片中心的判断

  3. 颜色色差的聚类

音频的处理

视频的处理

http api跨域

iframe

同源策略,document.domain = tool.lu

jsonp

这种跨域实际上是在页面中动态插入一个 <script> 标签,然后在被加载的脚本中执行当前网页里的函数;显而易见,只支持GET请求。

ajax2

HTTP access control (CORS) - Cross-Origin Resource Sharing

  1. 简单请求

  2. 预检请求

http返回头

Access-Control-Allow-Origin: <origin> | *
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
Access-Control-Max-Age: <delta-seconds>
Access-Control-Allow-Credentials: true | false
Access-Control-Allow-Methods: <method>[, <method>]*
Access-Control-Allow-Headers: <field-name>[, <field-name>]*
static const auto maxPreflightCacheTimeout = std::chrono::seconds(600);  

http请求头

Origin: <origin>
Access-Control-Request-Method: <method>
Access-Control-Request-Headers: <field-name>[, <field-name>]*

默认情况下 跨域XHR 是不会带上Cookie的,需要设置 withCredentials = true;

preflight.png
request.png

附件:mediacenter.pdf

使用phpexcel解析Excel

使用composer管理

composer require phpoffice/phpexcel
<?php

error_reporting(E_ALL);
date_default_timezone_set('Asia/Shanghai');

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

// ... 注意自己赋值$file
$excel = PHPExcel_IOFactory::load($file);
$sheetCount = $excel->getSheetCount();
echo "sheet count:$sheetCount" . PHP_EOL;
foreach ($excel->getWorksheetIterator() as $sheet) {
    if ($sheet->getSheetState() === 'hidden') {
        continue;
    }
    $sheetName = $sheet->getTitle();
    echo "sheet name: $sheetName" . PHP_EOL;
    $highestColumn = $sheet->getHighestColumn();
    $highestColumnIndex = PHPExcel_Cell::columnIndexFromString($highestColumn);
    $highestRow = $sheet->getHighestRow();
    for ($row = 1; $row <= $highestRow; $row++) {
        $columns = [];
        for($col = 0; $col < $highestColumnIndex; $col++) {
            $cell = $sheet->getCellByColumnAndRow($col, $row);
            $val = $cell->getValue();
            if ($val instanceof PHPExcel_RichText) {
                $columns[] = trim($val->getPlainText());
            } else {
                $columns[] = trim($val);
            }
        }
        var_dump($columns);
    }
}

过滤隐藏的worksheet

if ($sheet->getSheetState() === 'hidden')

过滤隐藏的行

由Excel中auto filter隐藏的行

if ($sheet->getRowDimension($row)->getVisible())

获取单元格颜色

获取文字颜色

$cell->getStyle()->getFont()->getColor()->getRGB();

获取填充颜色

$cell->getStyle()->getFill()->getStartColor()->getRGB();

颜色的差异值

是这样,运营那边对不同类型的数据进行了颜色的分类,但是是多个人整理的,所以紫色淡紫色的运营那边认为是同一个东西;但是程序里面是没法判断,因为紫色的区域太大了;所以这边就需要用到颜色的差异值比对了。

| YUV |      -      |   -   |
|-----|-------------|-------|
|  Y  |     Luma    | 明亮度 |
|  U  | Chrominance |  色度  |
|  V  |    Chroma   |  浓度  |

Y'UV 的发明是由于彩色电视与黑白电视的过渡时期。黑白视讯只有 Y(Luma,Luminance)视讯,也就是灰阶值。到了彩色电视规格的制定,是以 YUV/YIQ 的格式来处理彩色电视图像,把 UV 视作表示彩度的 C(Chrominance或Chroma),如果忽略 C 讯号,那么剩下的 Y(Luma)讯号就跟之前的黑白电视讯号相同,这样一来便解决彩色电视机与黑白电视机的相容问题。

|  Lab  |  -   |   -   |
|-------|------|-------|
|   L   | Luma | 明亮度 |
|   a   |   a  |   -   |
|   b   |   b  |   -   |

RGB -> XYZ -> Lab -> delta E

RGB to closest predefined color
color distance
RGB到Lab的转换

| RGB |   -   |  -  |
|-----|-------|-----|
|  R  |  Red  | 红色 |
|  G  | Green | 绿色 |
|  B  | Blue  | 蓝色 |

php默认函数 imagecolorclosest // RGB几何距离,由于RGB空间不是颜色感知连续的,所以效果没有Lab'的好

这边有个实现好的颜色空间转换的代码 hasbridge/php-color

slim框架中pjax的实现

pjax = pushState + ajax

由于这是一个比较通用的组件,所以我把它写成了slim的Middleware (如果不知道middleware是什么或者怎么写,可以看这里)

大概原理是在slim框架渲染之后,输出之前,对dom结构进行分析,摘取出pjax所需要的部分,然后将这部分的内容作为response返回给浏览器的ajax请求

预览: 码农文库 点击分页可以看到效果

$app->add(new \Support\PjaxMiddleware());
<?php
namespace Support;

use Slim\Middleware;
use Slim\Http\Request;
use Slim\Http\Response;
use Symfony\Component\DomCrawler\Crawler;
class PjaxMiddleware extends Middleware
{
    public function call()
    {
        $request = $this->app->request();
        $response = $this->app->response();
        $this->next->call();
        if (!$request->headers('X-PJAX') || $response->isRedirect()) {
            return;
        }
        $this->filterResponse($response, $request->headers('X-PJAX-CONTAINER'))->setUriHeader($response, $request);
    }
    private function filterResponse(Response $response, $container)
    {
        $crawler = new Crawler($response->getBody());
        $response->setBody($this->makeTitle($crawler) . $this->fetchContents($crawler, $container));
        return $this;
    }
    private function makeTitle(Crawler $crawler)
    {
        $title = $crawler->filter('head > title')->html();
        return "<title>{$title}</title>";
    }
    private function fetchContents(Crawler $crawler, $container)
    {
        $content = $crawler->filter($container);
        if (!$content->count()) {
            $this->app->stop();
        }
        return $content->html();
    }
    private function setUriHeader(Response $response, Request $request)
    {
        $query = $request->get();
        unset($query['_pjax']);
        $response->header('X-PJAX-URL', $request->getResourceUri() . '?' . http_build_query($query));
    }
}

slim框架接入pysh

由于实际开发中需要查看接口返回值结构和实际的数据样例,每次都写controller再访问url看下是比较麻烦的。

所以如果能像 php -a 这样交互的方式调用网站应用中定义的model和function的话,将会极其的方便。

目录结构如下:

app/
public/
tinker

app为网站具体逻辑实现的部分
public下面有1个index.php为网站的入口
tinker就是我们需要接入pysh,实现REPL的文件了

由于Slim里面需要PATH_INFO的值,但是命令行运行的时候,这个值是不存在的,所以需要在代码中mock一下;代码如下:

#!/usr/bin/env php
<?php

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

$app = require __DIR__ . '/app/bootstrap.php';

$app->environment = \Slim\Environment::mock([
    'PATH_INFO' => '/playground'
]);

$app->notFound(function () use ($app) {
    $path = $app->environment['PATH_INFO'];
    echo "Cannot route to $path" . PHP_EOL;
    $app->stop();
});

$app->error(function (\Exception $e) use ($app) {
    echo $e . PHP_EOL;
    $app->stop();
});

$app->any('/playground', function () {
    $config = new \Psy\Configuration([
        'tabCompletion' => true,
        'tabCompletionMatchers' => [
            new \Psy\TabCompletion\Matcher\ClassNamesMatcher,
            new \Psy\TabCompletion\Matcher\ClassMethodsMatcher,
            new \Psy\TabCompletion\Matcher\ClassAttributesMatcher,
            new \Psy\TabCompletion\Matcher\FunctionsMatcher,
        ],
    ]);
    $shell = new \Psy\Shell();
    $shell->run();
});

$app->run();

网易式评论箱的实现

预览

QQ20160416-0.png

实现

基础

  1. 表的设计

  2. 前端的实现

由于每个回复的展示都需要完整的引用路径,我们需要一个字段来记录本条回复所回复的回复 quote_id,在一个列表中如果每次都递归获取引用的评论,性能上会有很大的瓶颈,所以我们冗余一个字段,记录本条回复的引用路径 quote_path

CREATE TABLE `pre_comments` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `article_id` bigint(20) unsigned NOT NULL DEFAULT '0',
  `quote_id` bigint(20) unsigned NOT NULL DEFAULT '0',
  `quote_path` varchar(255) NOT NULL DEFAULT '' COMMENT '记录最近的20个值',
  `user_id` bigint(20) unsigned NOT NULL DEFAULT '0',
  `username` char(15) NOT NULL DEFAULT '',
  `content` varchar(1024) NOT NULL,
  `up` bigint(20) NOT NULL DEFAULT '0',
  `down` bigint(20) NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  `deleted_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_article_id` (`article_id`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

数据展示流程:

  1. 获取最新的10条评论

  2. 获取与最新的10条评论相关被引用的评论,最大层级不超过20,超过20的使用查看更多跳转到单独的评论页面

  3. 组装数据,返回json

  4. Reactjs渲染数据

Untitled.png

用伪代码可以表示为如下结构

<CommentBox>
  <CommentList>
    <CommentItem>
      <CommentQuote>
        <CommentQuote>
          <CommentToolBar>
            <CommentForm></CommentForm>
          </CommentToolBar>
        </CommentQuote>
        <CommentToolBar>
          <CommentForm></CommentForm>
        </CommentToolBar>
      </CommentQuote>
      <CommentToolBar>
        <CommentForm></CommentForm>
      </CommentToolBar>
    </CommentItem>
  </CommentList>
  <CommentForm></CommentForm>
</CommentBox>

优化

  1. 分库分表

  2. 通用计数组件

  3. 缓存

  4. 静态化

通过查询场景来决定分库分表的策略

  1. 根据articleId查询最新的评论

  2. 根据articleId 和 commentId更新计数

  3. 根据articleId 和 quoteId写入新的评论

  4. 展示某个用户(userId)所有的评论

其中有两个分表的路由key articleIduserId

未完待续

使用javassist修改idea mybatis插件

反编译查看源码

cd ~/Library/Application Support/IntelliJIdea15/mybatis_plus/lib/

使用 JD-GUI 打开 mybatis_plus.jar,查看源码:

Screenshot 2016-03-05 at 12.19.56.png
Screenshot 2016-03-05 at 12.19.53.png

修改验证逻辑

使用javassist修改字节码

import javassist.*;

class MyCrack {

    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass c = pool.get("com.seventh7.mybatis.util.JavaUtils");
        CtMethod m = c.getDeclaredMethod("refValid");
        m.setBody("{ validated = true; valid = true; return valid; }");
        c.writeFile();

        CtClass cc = pool.get("com.seventh7.mybatis.service.JavaService");
        CtMethod mm = cc.getDeclaredMethod("stop");
        mm.setBody("{ return; }");
        cc.writeFile();
    }

}
# 运行
javac -classpath ".:javassist.jar:mybatis_plus.jar" MyCrack.java
java -classpath ".:javassist.jar:mybatis_plus.jar" MyCrack

此时会在当前目录下生成修改过的两个类文件,使用压缩软件替换jar包中的这两个文件;重启idea。

com/seventh7/mybatis/service/JavaService.class
com/seventh7/mybatis/util/JavaUtils.class

在JAVA中实现PHP的gzuncompress

背景

在系统改造的时候,从php迁移到java;由于php中为了节省redis的内存,对缓存的数据做了 gzcompress 处理;为了能读取出数据,有两套方案:

  1. 刷数据,将redis中老的数据清理掉,去掉 gzcompress 的步骤(缺点:刷数据的时间,和读取代码上线的时间点无法吻合;数据的写入入口比较多,容易遗漏)

  2. java中读取的时候可以进行 gzuncompress

一些知识

知道这些知识候就能避免我在实现过程中遇到的很多问题。

PHP中的 gzcompress

This function compresses the given string using the ZLIB data format.
Note:
This is not the same as gzip compression, which includes some header data. See gzencode() for gzip compression.

一直以为 gzcompress 就是 gz 的压缩,php中使用的 zlib 来压缩,压缩完的结果中携带了头信息,直接使用 gz 解压是不认这种格式的。

JAVA中的 new String(byte[])

java.lang.StringCoding.StringDecoder 当在编码 byte[] 不能处理的时候会进行一些处理;所以说 (new String(compressedByte)).getBytes()compressedByte 并不一定会完全一样。

说到这里就可以看下 jedis 提供的接口了,刚开始我是使用的 String get(String key),于是由于上面的原因,当我用这个返回值 getBytes() 的时候就已经发生了变化。正确的使用方法应该是使用 byte[] get(byte[] key),由于比较繁琐,封装一下。

实现

    public static String get(Jedis jedis, String key) {
        byte[] byteKey = key.getBytes();
        byte[] element = jedis.get(byteKey);
        return new String(gzuncompress(element));
    }

    public static List<String> mget(Jedis jedis, List<String> keys) {
        byte[][] byteKeys = new byte[keys.size()][];
        for (int i = 0; i < keys.size(); i++) {
            byteKeys[i] = keys.get(i).getBytes();
        }
        List<byte[]> elements = jedis.mget(byteKeys);
        List<String> result = new ArrayList<>();
        for (byte[] element : elements) {
            result.add(new String(gzuncompress(element)));
        }
        return result;
    }

    public static byte[] gzuncompress(byte[] data) {
        byte[] unCompressed = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream(data.length);
        Inflater deCompressor = new Inflater();
        try {
            deCompressor.setInput(data);
            final byte[] buf = new byte[1024];
            while (!deCompressor.finished()) {
                int count = deCompressor.inflate(buf);
                bos.write(buf, 0, count);
            }
            unCompressed = bos.toByteArray();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            deCompressor.end();
        }

        return unCompressed;
    }