<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[时光流影]]></title><description><![CDATA[时光流影]]></description><link>http://www.wetimer.com/</link><generator>Ghost 0.11</generator><lastBuildDate>Thu, 01 Oct 2026 14:56:21 GMT</lastBuildDate><atom:link href="http://www.wetimer.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[百度aistudio 安装 pycorrecotor]]></title><description><![CDATA[<blockquote>
  <h3 id="">背景</h3>
</blockquote>

<p>百度aistudio提供的notebook juypter 免费提供CPU/GPU 资源，可以比较方便的做算法测试。同时提供了aistudio 提供了直接访问服务器的terminal，可以直接登录到虚拟机上安装相关依赖，这个操作比Google的colab只能在notebook上操作要方便不少。但是百度为了推广自家的paddle（飞桨框架），禁止了TensorFlow/pytorch等DeepLearning框架。 虚拟机的shell也禁用了root权限，那么就不能直接执行sudo apt-get install 之类的需要root权限的命令了。</p>

<blockquote>
  <h3 id="">实验</h3>
</blockquote>

<p>现在有一个在notebook jupyter上安装pycorrector的需求，直接<code>pip install pycorrector</code>
 但是在自动安装依赖kenlm时报错，kenlm是c++扩展，在gcc 编译时，出现<code>gcc: error trying to exec 'cc1plus': execvp: No such file or directory</code> 这样的错误提示。（By the way， 在Google的colab上可以直接安装）</p>]]></description><link>http://www.wetimer.com/bai-du-aistudio-an-zhuang-pycorrecotor/</link><guid isPermaLink="false">d59b48f4-c408-4127-92ec-6e961876af95</guid><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Fri, 06 Nov 2020 06:23:39 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <h3 id="">背景</h3>
</blockquote>

<p>百度aistudio提供的notebook juypter 免费提供CPU/GPU 资源，可以比较方便的做算法测试。同时提供了aistudio 提供了直接访问服务器的terminal，可以直接登录到虚拟机上安装相关依赖，这个操作比Google的colab只能在notebook上操作要方便不少。但是百度为了推广自家的paddle（飞桨框架），禁止了TensorFlow/pytorch等DeepLearning框架。 虚拟机的shell也禁用了root权限，那么就不能直接执行sudo apt-get install 之类的需要root权限的命令了。</p>

<blockquote>
  <h3 id="">实验</h3>
</blockquote>

<p>现在有一个在notebook jupyter上安装pycorrector的需求，直接<code>pip install pycorrector</code>
 但是在自动安装依赖kenlm时报错，kenlm是c++扩展，在gcc 编译时，出现<code>gcc: error trying to exec 'cc1plus': execvp: No such file or directory</code> 这样的错误提示。（By the way， 在Google的colab上可以直接安装）
查看gcc的版本</p>

<pre><code>aistudio@jupyter-104673-1192555:~$ gcc --version  
gcc (Ubuntu 7.5.0-3ubuntu1~16.04) 7.5.0  
Copyright (C) 2017 Free Software Foundation, Inc.  
This is free software; see the source for copying conditions.  There is NO  
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

aistudio@jupyter-104673-1192555:~$ g++ --version  
g++ (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609  
Copyright (C) 2015 Free Software Foundation, Inc.  
This is free software; see the source for copying conditions.  There is NO  
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
</code></pre>

<p>发现gcc 跟 g++的版本不一致，这里简单解释一下<code>cc1plus</code>的用途：</p>

<p>The g++ is a compiler driver. It knows how to invoke the actual compiler (cc1plus), assembler and linker. It does not know how to parse or compile the sources.</p>

<p>g++只是编译器的驱动，它只知道如何去调用实际的编译器(cc1plus)、汇编器以及链接器。但是不知道如何去编译源代码。</p>

<p>参考:<a href="https://stackoverflow.com/questions/19899750/what-is-the-difference-between-gcc-g-and-cc1-cc1plus">https://stackoverflow.com/questions/19899750/what-is-the-difference-between-gcc-g-and-cc1-cc1plus</a></p>

<p>想到把g++升级到7.5.0，网上很多使用apt-get命名去升级的方案，在这里都不能使用，因为没有root权限。：）</p>

<p>那么只能手动源码安装了，下面说一下步骤：</p>

<ul>
<li>下载gcc-7.5.0源码</li>
</ul>

<pre><code>http://mirrors.ustc.edu.cn/gnu/gcc/gcc-7.5.0/gcc-7.5.0.tar.gz  
</code></pre>

<p>这里使用了中科大的镜像源，比国外的源要快很多，后面也会用到</p>

<ul>
<li>解压编译源码</li>
</ul>

<pre><code>tar xvf gcc-7.5.0.tar.gz  
cd gcc-7.5.0  
</code></pre>

<pre><code># 下载依赖
./contrib/download_prerequisites
</code></pre>

<p>发下基本不动，因为使用了国外的额源，下载依赖包的速度太慢了。
看一下<code>download_prequisites</code>源码，</p>

<pre><code>gmp='gmp-6.1.0.tar.bz2'  
mpfr='mpfr-3.1.4.tar.bz2'  
mpc='mpc-1.0.3.tar.gz'  
isl='isl-0.16.1.tar.bz2'

base_url='ftp://gcc.gnu.org/pub/gcc/infrastructure/'

echo_archives() {  
    echo "${gmp}"
    echo "${mpfr}"
    echo "${mpc}"
    if [ ${graphite} -gt 0 ]; then echo "${isl}"; fi
}
</code></pre>

<p>重点是上面的几个文件，可以先下载到当前<code>gcc-7.5.0</code>目录。
可以使用上面提到的中科大镜像源wget先下载，镜像站没有的包，可以到源站wget下载。再执行<code>./contrib/dowload_prerequisites</code>，有如下提示：</p>

<pre><code>gmp-6.1.0.tar.bz2: OK  
mpfr-3.1.4.tar.bz2: OK  
mpc-1.0.3.tar.gz: OK  
isl-0.16.1.tar.bz2: OK  
All prerequisites downloaded successfully.  
</code></pre>

<p>说明依赖已经安装好了，接下来就行configure跟编译了。</p>

<p>首先创建一个用于存放编译文件的目录build， 进入build目录，执行上级目录的configure命令。</p>

<p>这里需要注意一点，在configure的时候，一定要用--prefix指定有权限install相关可执行文件以及lib文件的目录。 比如：在aistudio中的work/gcc-install， 笔者就没有指定，结果是默认的目录/usr，可想而知，肯定没有权限install。结果后面要用很多trick来解决后面用到各种依赖的问题，在下面会说到。</p>

<pre><code># 完整命令
mdir build  
cd build  
../configure --prefix ~/work/gcc-install -enable-checking=release -enable-languages=c,c++ -disable-multilib 
</code></pre>

<ul>
<li>接下来就是编译安装了</li>
</ul>

<pre><code>make -j 30

make install  
</code></pre>

<p>正常情况<code>install</code>目录有权限的话，<code>make install</code>应该就结束了。</p>

<p>但是笔者之前没有配置prefix，结果要install到一个没有权限的默认<code>/usr</code>目录。所以现在没法<code>make install</code>。因为编译过的过程太慢长了，所以不想重新configure，重新编译。</p>

<p>好在，源码已经编译好了。配置环境变量：</p>

<pre><code># gcc、g++在这个目录
export PATH=/home/aistudio/gcc-7.5.0/build/gcc/:$PATH

# 看一下是否生效
which gcc  
which g++  
</code></pre>

<pre><code># 查看版本
aistudio@jupyter-104673-1192555:~/kenlm-master$ g++ --version  
g++ (GCC) 7.5.0  
Copyright (C) 2017 Free Software Foundation, Inc.  
This is free software; see the source for copying conditions.  There is NO  
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
</code></pre>

<ul>
<li>手动安装pycorrector的依赖kenlm</li>
</ul>

<pre><code>wget https://github.com/kpu/kenlm/archive/master.zip  
unzip master.zip  
cd kenlm-master  
python setup install  
</code></pre>

<p>不再提示找不到<code>cc1plus</code>，出现了各种找不到头文件的错误信息，说明<code>cc1plus</code>已经能找到了，进入准备编译的阶段了。</p>

<p>根据提示信息，<code>find gcc-7.5.0 -name "xx.hh"</code>查找到各个头文件的目录，这里也有很多坑，因为一个文件所在的目录不一样，虽然文件名一样，但是代码可能是不一样的。笔者在这里踩了不少坑，不断尝试终于找到所有头文件的目录。</p>

<p>gcc 在编译的时候可以用 <code>gcc -I your_headers_dir</code>，指定<code>include</code>的查找路径。</p>

<p>修改 <code>setup.py</code></p>

<pre><code># 踩坑之后，找到了所有以来的定义目录
ss = '-I ../gcc-7.5.0/build/gcc/include/ -I ../gcc-7.5.0/libstdc++-v3/include/std/ -I ../gcc-7.5.0/libstdc++-v3/include -I ../gcc-7.5.0/libstdc++-v3/libsupc++/ -I ../gcc-7.5.0/build/x86_64-pc-linux-gnu/libstdc++-v3/include/x86_64-pc-linux-gnu/ -I ../gcc-7.5.0/libstdc++-v3/libsupc++/ -I ../gcc-7.5.0/build/x86_64-pc-linux-gnu/libstdc++-v3/include -I ../gcc-7.5.0/build/gcc/include-fixed/ -L ../gcc-7.5.0/build/gcc/'

#print(ss.split(' '))
ss = ss.split(' ')  
s1 = []  
for i in range(0,len(ss),2):  
        #print(i)
        s1.append(ss[i]+ss[i+1])
#print(s1)
#s1 = []

ARGS = ['-O3', '-DNDEBUG', '-DKENLM_MAX_ORDER='+max_order, '-std=c++11']  
# 通过ARGS参数传递给gcc命令
ARGS += s1  
print(ARGS)  
</code></pre>

<p>再次执行<code>python setup.py install</code>，正常编译了一小会，但是抛出错误提示：</p>

<pre><code>gcc: fatal error: -fuse-linker-plugin, but liblto_plugin.so not found  
</code></pre>

<p>看来是没后找到<code>liblto_plugin.so</code>这个库，find 了一下，发现有这个文件<code>./build/gcc/liblto_plugin.so</code>。无论用 <code>gcc -L ./build/gcc/</code>，增加一个链接库的查找路径，还是把路径增加到<code>PATH</code>变量，都并不起作用。</p>

<pre><code>aistudio@jupyter-104673-1192555:~/gcc-7.5.0$ /usr/bin/gcc -print-file-name=liblto_plugin.so  
/usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so
</code></pre>

<p>用系统默认的<code>/usr/bin/gcc</code> 能够查找到<code>liblto_plugin.so</code>，但用刚编译的gcc确找不到此文件。</p>

<p>使用<code>gcc -print-serach-dirs</code>，发现有搜索类似这样的路径：</p>

<p><code>
 libraries =/home/aistudio/gcc-7.5.0/build/gcc/../lib/gcc/x86_64-pc-linux-gnu/7.5.0//home/aistudio/gcc-7.5.0/build/gcc/../lib/gcc/x86_64-linux-gnu//home/aistudio/gcc-7.5.0/build/gcc/../lib/gcc//home/aistudio/gcc-7.5.0/build/gcc/../lib/gcc/
</code></p>

<p>所以，直接在gcc下创建了lib目录，把gcc目录拷贝到lib目下。 再执行<code>gcc -print-file-name=liblto_plugin.so</code></p>

<pre><code>aistudio@jupyter-104673-1192555:~/gcc-7.5.0$ gcc -print-file-name=liblto_plugin.so  
/home/aistudio/gcc-7.5.0/build/gcc/../lib/gcc/liblto_plugin.so
</code></pre>

<p>可以正常找到<code>liblto_plugin.so</code>， 再次<code>python setup.py install</code>， 可以正常编译安装kenlm了。</p>

<p>另外，笔者在发现默认的/usr/bin/gcc 可以找到<code>liblto_plugin.so</code>的时候，尝试把编译好的gcc改为gcc_bak，让gcc命令使用的是/usr/bin/gcc，也可以正常编译kenlm。</p>

<p>然后就可以在notebook上, <code>!pip install pycorrector</code>了</p>

<p>到此，整个踩坑结束。</p>]]></content:encoded></item><item><title><![CDATA[pandas matplotlib 中文字体支持]]></title><description><![CDATA[<blockquote>
  <p>下载字体文件</p>
</blockquote>

<p><a href="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/simsun.ttc">点击下载 simsun.ttc</a></p>

<blockquote>
  <p>设置字体</p>
</blockquote>

<pre><code>import matplotlib.pyplot as plt  
import matplotlib.font_manager as fm  
# 设置字体
font = fm.FontProperties(fname='/your_fonts_directory/simsun.ttc',size=12)  
plt.rcParams.update({'font.size': 12})  
</code></pre>

<blockquote>
  <p>设置 <code>plot.bar</code> 字体</p>
</blockquote>

<pre><code>ax = df2.plot.bar(x='label',y='count',figsize=(12,4)</code></pre>]]></description><link>http://www.wetimer.com/pandas-matplotlib-zhong-wen-zi-ti-zhi-chi/</link><guid isPermaLink="false">424ad84a-3a4a-4d8a-b00a-c167eb837aad</guid><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Tue, 06 Oct 2020 04:57:33 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <p>下载字体文件</p>
</blockquote>

<p><a href="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/simsun.ttc">点击下载 simsun.ttc</a></p>

<blockquote>
  <p>设置字体</p>
</blockquote>

<pre><code>import matplotlib.pyplot as plt  
import matplotlib.font_manager as fm  
# 设置字体
font = fm.FontProperties(fname='/your_fonts_directory/simsun.ttc',size=12)  
plt.rcParams.update({'font.size': 12})  
</code></pre>

<blockquote>
  <p>设置 <code>plot.bar</code> 字体</p>
</blockquote>

<pre><code>ax = df2.plot.bar(x='label',y='count',figsize=(12,4))  
# 设置支持中文字体
a = ax.set_xticklabels(df2.label,fontproperties=font)  
</code></pre>

<p><img src="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2020/10/06/fa4279495006494dff1f9a97196812ad.png" alt="pic"></p>

<blockquote>
  <p>设置 <code>plot.pie</code> 相关组件的字体</p>
</blockquote>

<pre><code>ax = df1[['hash','label']].groupby(by=['label']).count()\  
.sort_values(by=['hash'],ascending=False)\
.head(5)\
.plot.pie(y='hash',figsize=(6,6))
ax.set_ylabel('各类别数据量分布',fontproperties=font)  
plt.setp(ax.texts, fontproperties=font)  
plt.legend(prop=font)  
</code></pre>

<p><img src="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2020/10/06/4e5b04f90b7e0bc3a8b8ddd818320de2.png" alt="pic"></p>]]></content:encoded></item><item><title><![CDATA[川普Twitter超多维度分析]]></title><description><![CDATA[<blockquote>
  <h3 id="trumptwitter">获取Trump最近的Twitter</h3>
</blockquote>

<p>除了用爬虫去获取Twitter之外，还有一个简单稳定的方案就是通过官方的API，不过最近Twitter官方比较严格，很难申请开放平台很难通过。好在笔者之前申请过一个Twitter的开发者账号。</p>

<ul>
<li>安装第三方获取Twitter的库python-twitter</li>
</ul>

<pre><code>pip install python-twitter  
</code></pre>

<ul>
<li>把Trump最近的Twitter dump 下来</li>
</ul>

<pre><code>import twitter

proxies = {  
    'http': '',
    'https': ''
}

api = twitter.Api(consumer_key='',  
                  consumer_secret='',
                  access_token_key='',
                  access_token_secret='', proxies=proxies)

count = 200  
max_id = None  
fp = open('Trump-twitter.txt', encoding='utf-8',</code></pre>]]></description><link>http://www.wetimer.com/chuan-pu-twitterchao-duo-wei-du-fen-xi/</link><guid isPermaLink="false">d05bb0e9-af34-48d3-b633-5fbe2180de0e</guid><category><![CDATA[Trump]]></category><category><![CDATA[Twitter]]></category><category><![CDATA[Trump analytic]]></category><category><![CDATA[Trump Twitter analytic]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Fri, 26 Jun 2020 11:46:23 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <h3 id="trumptwitter">获取Trump最近的Twitter</h3>
</blockquote>

<p>除了用爬虫去获取Twitter之外，还有一个简单稳定的方案就是通过官方的API，不过最近Twitter官方比较严格，很难申请开放平台很难通过。好在笔者之前申请过一个Twitter的开发者账号。</p>

<ul>
<li>安装第三方获取Twitter的库python-twitter</li>
</ul>

<pre><code>pip install python-twitter  
</code></pre>

<ul>
<li>把Trump最近的Twitter dump 下来</li>
</ul>

<pre><code>import twitter

proxies = {  
    'http': '',
    'https': ''
}

api = twitter.Api(consumer_key='',  
                  consumer_secret='',
                  access_token_key='',
                  access_token_secret='', proxies=proxies)

count = 200  
max_id = None  
fp = open('Trump-twitter.txt', encoding='utf-8', mode='w')  
while True:  
    statuses = api.GetUserTimeline(screen_name="realDonaldTrump", count=count, max_id=max_id)
    if len(statuses) &lt; 1:
        break
    for s in statuses:
        print(s)
        fp.write(str(s) + "\n")
        max_id = s.id

    max_id = max_id - 1

fp.close()  
</code></pre>

<p><em>需要注意的是， 这个API需要再可以访问Twitter的网络环境下使用，所以配置了<code>proxy</code></em></p>

<p><em>目前，Twitter 的API做了限制，只能获取到最近的3200条的推文，实际获取到的量会有一些出入，不过差异不大。</em></p>

<p>导出的数据，大概是这样：</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efbd7efbe3b9d.png" alt="IMAGE"></p>

<blockquote>
  <h3 id="">数据预处理</h3>
</blockquote>

<ul>
<li>使用<code>pandas</code>可以很方便处理表格， 这里主要把重要的信息，处理成表格字段，方便后面的统计分析</li>
<li>这里重点关注以下字段</li>
</ul>

<p>|字段名|字段解释|
|--|--|
|id|Twitter ID <a href="https://twitter.com/i/web/status/{id}|">https://twitter.com/i/web/status/{id}|</a>
|created<em>at|发布时间|
|favorite</em>count|点赞数|
|hashtags|话题TAG|
|retweet<em>count|转发数|
|source|发布来源，比如来自iPhone|
|text|推文内容|
|media</em>type|内容媒体类型，比如video|
|ret<em>user</em>name|原文的发布者|
|ret<em>user</em>verified|原文的发布者是否认证|
|quoted<em>user</em>name|被引用的推文的作者|
|quoted<em>user</em>verified|被引用的推文作者是否认证|
|created<em>day|发布日期，方便按天统计|
|created</em>day<em>hour|发布日期，精确到小时，比如：2020-06-14 02|
|created</em>hour|发布小时，方便统计发文时间规律|</p>

<ul>
<li>代码片段</li>
</ul>

<pre><code># 把发布时间转换成特定的格式
def get_created_info(row):  
    created_at = row.created_at
    day,time_str = created_at.split(' ')
    hour = time_str.split(':')[0]
    day_hour = day+" "+hour
    return day,day_hour,hour

# 应用到多个字段上    
DF[['created_day','created_day_hour','created_hour']] = DF.apply(get_created_info,axis=1,result_type='expand')  
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efd6a85026ced.png" alt=""></p>

<blockquote>
  <h3 id="">推文分析</h3>
</blockquote>

<p>首先加载在上一步预处理后的数据，转成<code>pandas</code>的<code>DataFrame</code>，方便之后的处理</p>

<pre><code>df = pd.read_excel('./Trump-twitter.xlsx')  
</code></pre>

<p>后面进入超多维度的分析</p>

<blockquote>
  <p>每天发文量统计</p>
</blockquote>

<pre><code> df[['id','created_day']].groupby(by=['created_day']).count().sort_values(by=['id'],ascending=False).head(20).plot.bar(figsize=(12,4))
</code></pre>

<p>按照每日发文总量的top 20，进行绘图</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efdd92a872fb9.png" alt=""></p>

<p>可以发现在2020-06-05 这天的发文最多，发了153条。我们详细看一下，这一天的每小时的发文量</p>

<pre><code>df1 = df[df.created_day=='2020-06-05']  
df1[['id','created_hour']].groupby(by=['created_hour']).count().plot.bar(figsize=(12,4))  
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efdef62120df2.png" alt=""></p>

<p>在一天在11、12点这两个小时发文最多， 对这天的所有推文内容，进行分析，绘制词云图， 这里使用<code>WordCloud</code>一个比较好用的绘制词云图的库</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efe345b20c6b3.png" alt=""></p>

<blockquote>
  <p>对推文内容进行情感分析</p>
</blockquote>

<pre><code>def get_sentiment(row):  
    polarity = tb(row.text).polarity
    if polarity &lt; 0:
        tag = 'negative'
    elif polarity &lt; 0.3:
        tag = 'neutral'
    else:
        tag = 'positive'
    return tag,polarity
df[['text_polarity','polarity_prob']] = df.apply(get_sentiment,axis=1,result_type='expand')  
</code></pre>

<p>这里使用了textblob库，一个简单的文本处理库，可以比较方便的对英文文本进行情感分析、词性标注、拼写纠错等</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efe6b4ad60f70.png" alt=""></p>

<p><em>可以看出Trump的推文以中性为主，当然负面占比也不少，这里不展开 ……</em></p>

<blockquote>
  <p>发文时间段统计</p>
</blockquote>

<pre><code>df[['id','created_hour']].groupby(by=['created_hour']).count().plot.bar(figsize=(12,4))  
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172efe8d8aec7da4.png" alt=""></p>

<p>从图上可以很直观的看出，Trump 24小时都有发文，在12点左右，发Twitter量最多，不愧面向Twitter治国，发推量令人发指。</p>

<blockquote>
  <p>对这段时间所有的推文进行分析，绘制词云图</p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f00050ca87da9.png" alt=""></p>

<p>从词云图上可以看出，“白宫”、“新冠病毒”、“奥巴马”、“拜登”是他经常提及的对象，同时不忘“Great American” ……</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f0037f9a0c07f.png" alt=""></p>

<p>对推文中提到的“FAKE NEWS”进行词云统计</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f006e05c1bbbb.png" alt=""></p>

<p><em>不解释</em></p>

<blockquote>
  <p>转发与原创统计</p>
</blockquote>

<pre><code>explode = (0, 0.1)  
df[['id','tweet_status']].groupby(by=['tweet_status']).count()\  
.plot.pie(y='id',figsize=(5,5),explode=explode,autopct='%1.1f%%',shadow=True, startangle=0,label='')
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f00955dcab2ad.png" alt=""></p>

<p><em>转发跟原创占比都不少</em></p>

<blockquote>
  <p>被转的作者统计</p>
</blockquote>

<pre><code>rt_df = df[['id','ret_user_name']].groupby(by=['ret_user_name']).count().sort_values(by=['id'],ascending=False)  
rt_df.head(20).plot.bar(figsize=(16,4))  
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f00a8b19ad4cf.png" alt=""></p>

<ul>
<li>主要转发白宫的推文，其次是转发他自己的</li>
</ul>

<blockquote>
  <p>发布来源统计</p>
</blockquote>

<pre><code>explode = (0, 0.1)  
df[['id','source']].groupby(by=['source']).count()\  
.plot.pie(y='id',figsize=(5,5),explode=explode,autopct='%1.1f%%',shadow=True, startangle=0,label='')
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f00c94c72c097.png" alt=""></p>

<p><em>主要还是来自iPhone</em></p>

<blockquote>
  <p>推文主题TAG统计分析</p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f02d1ddbf2f9e.png" alt=""></p>

<ul>
<li>可以看出，这段时间Trump关注的比较多还是新冠疫情，以及跟疫情相关的“薪水保护计划”</li>
<li>其次是MAGA （Make Ameraica Greate Again）</li>
</ul>

<blockquote>
  <p>推文内容聚类</p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f04a11ca425c9.png" alt=""></p>

<p>对Trump的推文聚了10个类，从图上看，Trump的推文内容的类型多集中在某几个方面</p>

<blockquote>
  <p>对推文内容做<code>word2vec</code></p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f050c1e71ce7f.png" alt=""></p>

<pre><code>model.similar_by_word('Trump')  
# output

[('coronavirus', 0.6097418069839478),
 ('great', 0.5778061151504517),
 ('realDonaldTrump', 0.554646909236908),
 ('Great', 0.5381245613098145),
 ('National', 0.49641942977905273),
 ('America', 0.47522449493408203),
 ('today', 0.4736398458480835),
 ('people', 0.469297856092453),
 ('Democrats', 0.45948123931884766),
 ('time', 0.4551768898963928)]
</code></pre>

<blockquote>
  <p>点赞数直方图</p>
</blockquote>

<pre><code>df[df.favorite_count &gt; 0][['id','favorite_count']].plot.hist(y='favorite_count',bins=50,figsize=(12,4))  
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f067fde503d3f.png" alt=""></p>

<p><em>点赞数主要集中在5w左右</em></p>

<blockquote>
  <p>转发数直方图</p>
</blockquote>

<pre><code>df[df.retweet_count &gt; 0][['id','retweet_count']].plot.hist(y='retweet_count',bins=50,figsize=(12,4))  
</code></pre>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/172f069ec845def0.png" alt=""></p>

<p><em>转发数主要集中在1w左右</em></p>

<blockquote>
  <h3 id="">结语</h3>
</blockquote>

<p>本文主要从多维度客观统计为主，不做过多解读。算是抛砖引玉，读者朋友可以发挥想象，做更多维度的分析与深刻的解读。</p>]]></content:encoded></item><item><title><![CDATA[Elasticsearch Machine Learning AIOps 实践]]></title><description><![CDATA[Elasticsearch Machine Learning AIOps 实践]]></description><link>http://www.wetimer.com/elasticsearch-machine-learning-aiops-shi-jian/</link><guid isPermaLink="false">474447ff-4924-461f-9636-f22d3bfc7d29</guid><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Wed, 09 Oct 2019 10:15:42 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <h3 id="">项目背景</h3>
</blockquote>

<p>公司内部封装了NLP通用算法的GRPC服务，比如文本情感识别、文本分类、实体识别等， 提供给大数据等其他部门实时调用。 </p>

<p>RPC 服务的调用日志，通过Filebeat、Logstash 实时发送到Elasticsearch，现在需要通过对日志的调用情况实时统计分析，判断调用情况是否出现异常，并对异常情况能够实时告警。</p>

<blockquote>
  <h3 id="">业务场景分析</h3>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f37dd9d721f.png" alt="pic"></p>

<p>使用ES 的watcher 插件创建一个threshold alert， 设置预警规则，当一个时间周期内的数据量达到阈值， 就进行告警。</p>

<p>这种方案优点是，设置起来比较简单，只要设置query的条件，以及阈值就可以快速完成。
缺点是，这种方案比较适合粗粒度，并且阈值明确的情况下设置。 </p>

<p>设想一下，如果要做到分钟级实时监控，并且对异常点进行告警通知，如果用规则来设置，很难满足这种场景。 
针对这种场景，很容易想到，使用Machine Learning （ML）的方法，根据以往的数据变化趋势，来主动发现异常点，那就简单多了。 </p>

<p>针对这种需求， 有很多解决方案，比如，把日志实时发送到KAFKA，再用FlinkML 来做机器学习，训练异常检测模型， 不过多讨论这种方案。
如题， 这里介绍一种更简单的方案，使用 ES Stack 提供的 ML 模块, 很容在ES Stack上快速体验AIOps。</p>

<blockquote>
  <h3 id="">实践经验</h3>
  
  <h6 id="requirements">Requirements</h6>
</blockquote>

<ul>
<li>ELK 6.4.3</li>
<li>Xpack Machine Learning</li>
</ul>

<blockquote>
  <h6 id="">日志结构</h6>
</blockquote>

<pre><code>{
  "@timestamp": "2019-10-09T09:10:22.547Z",
  "@version": "1",
  "call_day": "20191009",
  "call_hour": "2019100909",
  "call_minute": "201910090910",
  "call_time": "10-09 09:10:21.714317",
  "cost": 0.004031,
  "method": "Nlp.ContentCategory",
  "month": "10",
  "service_name": "mg",
  "timestamp": "2019-10-09T09:10:22.547Z"
}
</code></pre>

<ul>
<li>@timestamp 日志写入时间，后面的时序统计就是依据这个时间字段</li>
<li>cost 调用方法消耗时间，可以利用这个字段的平均值来预测方法耗时是否异常</li>
<li>method 调用的方法</li>
<li>service_name 调用的服务名称</li>
</ul>

<blockquote>
  <h6 id="">目的</h6>
</blockquote>

<ul>
<li>使用历史日志数据，训练异常检测模型，并找出异常的点</li>
<li>使用训练出来的模型，检测后面的实时日志数据，发现新的异常点，并进行告警</li>
</ul>

<blockquote>
  <h6 id="">步骤</h6>
  
  <p>创建Job</p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f37dd6a2d59.png" alt="JOB"></p>

<p>有四种创建ML Job的模式：</p>

<ol>
<li>单指标模式：使用单一指标作为模型特征参数  </li>
<li>多指标模式：使用多指标作为模型特征参数  </li>
<li>种群分析模式：种群分析，找出不合群的点  </li>
<li>高级模式：可以灵活的设置各种模型指标</li>
</ol>

<p>为了简单起见，这里使用单一指标模式来创建ML Job</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f37ddc3e2cd.png" alt="pic"></p>

<ul>
<li>选择时间段，尽量选择足够长的时间段，提供足够多的数据喂给ML模型</li>
<li>Aggregate： 聚合统计的方法，Count、High Count、Low Count、Mean、High Mean、Low Mean等，这里选择 Count方法， 表示统计特定时间窗口数据量，如果检测点的数据量超过模型计算值上下边界值的，都算作是异常点</li>
<li>Bucket Span 统计的时间窗口大小</li>
</ul>

<blockquote>
  <p>配置告警阈值</p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f37ddc4f869.png" alt="pic"></p>

<p>针对检测结果的严重程度进行邮件告警</p>

<blockquote>
  <p>查看检测结果</p>
</blockquote>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f37de87e635.png" alt="pic"></p>

<p>通过Anomaly Explorer 可以很容易找到异常的点</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f380e445e6b.png" alt="pic"></p>

<p>同时，通过Metric Viewer也很直观的查看异常周边的变化趋势，下面举例介绍一下，2019-09-19异常点的情况</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f380fefbf1d.png" alt="pic"></p>

<p>从上图可以看出，在10:00/09:00/08:00/07:00 分别检测出了不同程度的异常，
其中severity值的大小，表明了异常的严重程度，severity值越大，说明这个异常越偏离正常情况。</p>

<p>probability是事情发生的概率，值越小，这个事件发生的可能性越小，从而说明如果这个事件发生了，肯定是不寻常的，跟上面severity反应的情况也是自洽的。</p>

<p>typical 是典型值，就是根据以往数据学习的模型，预测这个时间段的数据量是应该是多少</p>

<p>actual 是实际值，是这个时间段的统计的真实数据量，如果跟typical差异越大，出现异常的可能性就越大</p>

<blockquote>
  <p>产生结果解释</p>
</blockquote>

<p>上述的检测结果来自真实业务场景，在7点到10点之间的业务调用方出现异常，导致在7点到10点之间的RPC服务日志产生量减少。ML 通过对以往数据的特征的学习，对这个时间段的日志量进行预测，发现实际结果与预测结果不符合。 在10点以后，业务调用方恢复正常，预测的结果跟实际调用量也是相吻合，在最终的结果图上也是反映回到正常曲线。</p>

<blockquote>
  <h3 id="bucketspan">Bucket Span 调优</h3>
</blockquote>

<p>Bucket （桶） ，在进行实时数据流分析的时候，使用桶来区分指定时间窗口的数据，桶的大小，反映了每个窗口的数据量的大小， 也就是每次给ML喂的数据量的大小。</p>

<p>上面的结果是对桶大小进行调优后的效果，选择的Bucket Span 是5min， 最初选择的Bucket Span是15min， 没有把异常点很好的检测出来。</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f3815f94253.png" alt="pic"></p>

<p>上图桶大小是15min，从图上可以看出来，没有把7点这个异常点检测出来，同时异常的重要程度不够。</p>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f3811529902.png" alt="pic">
<img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f3811d81ce4.png" alt="pic"></p>

<p>设置1min的效果，同样不能把异常点检测很好的出来，而且受到噪音的干扰比较大，检测到很多严重程度很低的小的异常点。</p>

<p>调优参考：</p>

<ul>
<li><p>桶的大小不宜设置太小，如果太小，虽然喂给ML的数据变多，但是在很小的时间段内，数据的变化比较频繁，产生大量的噪音，不利于模型的学习。反之，如果桶的大小设置太大了，将会丢掉很多细节信息，而且可能导致喂给ML的数据量不足，甚至把某个异常点给丢掉了。桶的大小要根据业务场景来，如上的场景，想尽快发现调用的异常，在数据量比较充足的情况下，设置5min是比较合适的。当然，这也不是绝对的情况，需要不断地调整这个值的大小，找到合适的值。</p></li>
<li><p>ES ML 提供了一键预估（Estimate bucket span) Bucket大小，但是这个功能并不是很智能，不能完全依赖，还需要自己不断调试</p></li>
<li><p>尽量给ML喂更多充足的数据，数据量太少，ML不能很好的拟合数据，预测的结果也是不准确的</p></li>
<li><p>排除掉一些已知的“异常”数据，比如，周末的数据调用量下降很多，如果也直接喂给模型的话，会对预测结果产生干扰。好在，ES 提供了calendar功能，可以把特定时间段的移除，这个时间段的数据不算做异常。</p></li>
</ul>

<p><img src="https://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/tmp/16dc8f38181b32a4.png" alt="pic"></p>

<ul>
<li><p>使用多指标模式，选择更多的影响因子，使训练出来的模型更加健壮</p></li>
<li><p>由于数据不断变化，持续把更多更新的数据喂给模型，使其能够学习到新的特征，以适应新的数据</p></li>
</ul>

<blockquote>
  <h3 id="">总结</h3>
</blockquote>

<p>一个完善的AIOps体系还有很多工作要做，上述只是整个体系中一小块，好在ES Stack现在越来越完善，下一步可以考虑把APM、Machine Learning整合起来，使用APM 对系统性能进行监控，使用ML对系统的各项指标进行建模，预测机器的使用趋势，并做到动态伸缩。</p>]]></content:encoded></item><item><title><![CDATA[如何用python爬取抖音视频列表]]></title><description><![CDATA[<blockquote>
  <h3 id="">导语</h3>
</blockquote>

<p>如果看到特别感兴趣的抖音vlogger的视频，想全部dump下来，如何操作呢？下面介绍介绍如何使用python导出特定用户所有视频信息</p>

<p><img src="https://user-gold-cdn.xitu.io/2019/7/12/16be6b2a81671d64?w=374&amp;h=374&amp;f=png&amp;s=183646" alt="img"></p>

<blockquote>
  <h3 id="">抓包分析</h3>
</blockquote>

<ul>
<li>Chrome Deveploer Tools Chrome 浏览器开发者工具</li>
</ul>

<p>在抖音APP端，复制vlogger主页地址， 比如： <code>http://v.douyin.com/kGcU4y/</code>， 在PC端用chrome浏览器打卡，并模拟手机，这里选择iPhone， 然后把复制的主页地址，放到浏览器进行访问，页面跳转到<code>https://www.iesdouyin.com/share/user/110677980134</code></p>

<ul>
<li>下拉主页， 选择Network=>XHR 选项卡， 看到类似请求</li>
</ul>

<pre><code>:authority: www.iesdouyin.com
:method: GET
:path: /web/api/v2/aweme/post/?user_</code></pre>]]></description><link>http://www.wetimer.com/ru-he-yong-pythonpa-qu-dou-yin-shi-pin-lie-biao/</link><guid isPermaLink="false">eb773b37-e0d5-403c-a673-bba9d862dad1</guid><category><![CDATA[爬虫]]></category><category><![CDATA[抖音]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Fri, 12 Jul 2019 15:25:05 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <h3 id="">导语</h3>
</blockquote>

<p>如果看到特别感兴趣的抖音vlogger的视频，想全部dump下来，如何操作呢？下面介绍介绍如何使用python导出特定用户所有视频信息</p>

<p><img src="https://user-gold-cdn.xitu.io/2019/7/12/16be6b2a81671d64?w=374&amp;h=374&amp;f=png&amp;s=183646" alt="img"></p>

<blockquote>
  <h3 id="">抓包分析</h3>
</blockquote>

<ul>
<li>Chrome Deveploer Tools Chrome 浏览器开发者工具</li>
</ul>

<p>在抖音APP端，复制vlogger主页地址， 比如： <code>http://v.douyin.com/kGcU4y/</code>， 在PC端用chrome浏览器打卡，并模拟手机，这里选择iPhone， 然后把复制的主页地址，放到浏览器进行访问，页面跳转到<code>https://www.iesdouyin.com/share/user/110677980134</code></p>

<ul>
<li>下拉主页， 选择Network=>XHR 选项卡， 看到类似请求</li>
</ul>

<pre><code>:authority: www.iesdouyin.com
:method: GET
:path: /web/api/v2/aweme/post/?user_id=110677980134&amp;sec_uid=&amp;count=21&amp;max_cursor=1561112910000&amp;aid=1128&amp;_signature=3Xf-nxAQgGfUO4SKisB.Ld13.o&amp;dytk=061ae6e81229e178146aa674327eba89
:scheme: https
accept: application/json  
accept-encoding: gzip, deflate, br  
accept-language: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7,zh-TW;q=0.6,da;q=0.5  
cookie: tt_webid=6690145457198417412; _ga=GA1.2.605400954.1557670882; _ba=BA0.2-20181226-5199e-GIJXgXk9ajNkyFhmv7Wy; _gid=GA1.2.1914501522.1562857517  
referer: https://www.iesdouyin.com/share/user/110677980134  
user-agent: Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1  
x-requested-with: XMLHttpRequest  
</code></pre>

<p>返回数据截图</p>

<p><img src="https://user-gold-cdn.xitu.io/2019/7/12/16be6b089aed86cb?w=730&amp;h=477&amp;f=png&amp;s=94690" alt="img"></p>

<p>通过分析ajax请求的URL <code>https://www.iesdouyin.com/web/api/v2/aweme/post/?user_id=110677980134&amp;sec_uid=&amp;count=21&amp;max_cursor=1559299764000&amp;aid=1128&amp;_signature=3Xf-nxAQgGfUO4SKisB.Ld13.o&amp;dytk=061ae6e81229e178146aa674327eba89</code>
得出请求参数主要包含：</p>

<p>| 字段 | 类型 | 说明 |
|------|------------|------------|
| user<em>id  | int          | 抖音账号的ID         |
| count  | int        | 返回的数据条数，就用默认值21        |
| max</em>cursor| int       | 请求的游标，每次请求带上上次请求返回的max<em>cursor     |
|aid|int|使用默认值11128|
|</em>signature|string|每次请求带上的参数签名|
|dytk|string|每次请求带上的一个参数|</p>

<p>参数的获取方法：
- user<em>id 通过访问主页地址，重定向之后的URL地址(<code>https://www.iesdouyin.com/share/user/110677980134</code>)上可以获取 user</em>id
- dytk 在主页的网页源码中包含此参数，网页源码片段</p>

<pre><code>(function() {
    $(function(){
        __M.require('douyin_falcon:page/reflow_user/index').init({
            uid: "110677980134",
            dytk: '061ae6e81229e178146aa674327eba89'
        });
    });
})();
</code></pre>

<p>通过正则获取到此参数</p>

<ul>
<li>_signature 获取比较复杂，抖音对前端的js代码进行了混淆压缩，不易直接分析出算法过程，不过可以执行签名算法代码，并返回对应的签名结果。 </li>
<li>执行js代码的可以使用nodejs或者selenium webdriver，这里推荐使用selenium webdriver ， nodejs的js执行环境与浏览器有区别，计算出的签名结果，并不能通过验证，selenium webdriver 可以调用本地浏览器，计算出的签名可以跟浏览器直接访问访问计算出的签名一致。</li>
<li>格式化之后的js代码，<a href="http://pksgdcb65.bkt.clouddn.com/crack.js">点击查看</a>， 执行js方法<code>_bytedAcrawler.sign("110677980134")</code>对参数进行签名</li>
</ul>

<blockquote>
  <h3 id="">代码实现导出主页视频列表</h3>
</blockquote>

<pre><code>def get_user_video_list_by_uid(user_id, cursor=0):  
    url = 'https://www.iesdouyin.com/web/api/v2/aweme/post/?'
    sign, dytk = signature(user_id)
    tk_logger.info("sign:%s,dytk:%s" % (sign, dytk))
    if sign is None or dytk is None:
        tk_logger.log("sign [%s] or dytk [%s] is none" % (sign, dytk))
        return None
    headers = dict_merge(CHROME_HEADER, {
        "Accept": "application/json",
        "X-Requested-With": "XMLHttpRequest",
    })
    params = {
        "user_id": user_id,
        "count": "21",
        "max_cursor": cursor,
        "aid": "1128",
        "_signature": sign,
        "dytk": dytk
    }
    res = requests.get(url, headers=headers, params=params)
    tk_logger.info("request url: %s" % res.url)
    content = res.content.decode("utf8")
    jsn = json.loads(content)
    return jsn
</code></pre>

<p>获取的视频列表信息
<img src="https://user-gold-cdn.xitu.io/2019/7/12/16be6b59c08a7cee?w=936&amp;h=254&amp;f=png&amp;s=60492" alt="视频列表"></p>

<blockquote>
  <h3 id="">获取视频信息代码片段</h3>
</blockquote>

<pre><code>def get_video_detail_by_id(video_id):  
    url = "https://aweme-hl.snssdk.com/aweme/v1/aweme/detail/?version_code=6.5.0&amp;pass-region=1&amp;pass-route=1&amp;js_sdk_version=1.16.2.7&amp;app_name=aweme&amp;vid=9D5F078E-A1A9-4F64-81C7-F89CA6A3B1DC&amp;app_version=6.5.0&amp;device_id=34712926793&amp;channel=App%20Store&amp;mcc_mnc=46011&amp;aid=1128&amp;screen_width=750&amp;openudid=263bd93f02801d126ca004edccbff8f6e1b19f51&amp;os_api=18&amp;ac=WIFI&amp;os_version=12.3.1&amp;device_platform=iphone&amp;build_number=65014&amp;device_type=iPhone9,1&amp;iid=74239983401&amp;idfa=F39B285A-4B4F-4874-9D7E-C728A892BF6D"
    data = {"aweme_id": video_id}
    headers = {
        "sdk-version": "1",
        "x-Tt-Token": "00fc1e7950db67b5f43a312e9265cdfee513ea70c36d918c871f3bb553347f3db50ffca143b8722327b345816a75efca071d",
        "User-Agent": "Aweme 6.5.0 rv:65014 (iPhone; iOS 12.3.1; en_CN) Cronet",
        "Content-Type": "application/x-www-form-urlencoded",
        "Cookie": "tt_webid=6636348554880222728; __tea_sdk__user_unique_id=6636348554880222728; odin_tt=76d9b82d6e6f2ddfc99719a5b5d44a7d703cf977f0f7bddf8537f93920d57cb9ec33162ee47868b760f6b09e69209bb2f90bad220b75678af850a0dfa9f056e2; install_id=74239983401; ttreq=1$dab0516952a4157c0c11d4993533c09d6e45fc94; sid_guard=fc1e7950db67b5f43a312e9265cdfee5%7C1559955316%7C5184000%7CWed%2C+07-Aug-2019+00%3A55%3A16+GMT; uid_tt=0afcb06309f632d872799ec0ac3b2c80; sid_tt=fc1e7950db67b5f43a312e9265cdfee5; sessionid=fc1e7950db67b5f43a312e9265cdfee5",
        "X-Khronos": "1559956401",
        "X-Gorgon": "8300000000002e40eee38cad71d14037bd1385d18bc973f094f5",
    }
    ret = {}
    res = requests.post(url, data=data, headers=headers)
    if res.status_code == 200:
        # tk_logger.info("video detail raw:%s" % res.content.decode("utf8"))
        jsn = json.loads(res.content)
        detail = jsn.get("aweme_detail", {})
        video_info = get_video_info(detail)
        user_info = get_user_info(detail)
        play_addr = get_play_address(detail)
        video_cover = get_video_cover(detail)
        ret["video_info"] = video_info
        ret["user_info"] = user_info
        ret["play_addr"] = play_addr
        ret["video_cover"] = video_cover
    else:
        raise TKException("get video detail failed [%s][%d]" % (url, res.status_code))
    return ret
</code></pre>

<blockquote>
  <h3 id="">下载视频代码片段</h3>
</blockquote>

<pre><code>detail =  get_video_detail_by_id(video_id)  
def download_video(detail):  
    url = detail.get("play_addr", {}).get("url_list", [])
    if len(url) == 0:
        raise TKException("cannot get video url list [%s]" % detail)

    url = url[0]
    folder = DOWNLOAD_DIR + '/' + detail.get('user_info', {}).get("uid", "unknown")
    if not os.path.exists(folder):
        os.mkdir(folder)
    video_id = detail.get('video_info', {}).get('statistics', {}).get('aweme_id')
    # filename = "%s/%s" % (folder, detail.get("video_info", {}).get("desc", video_id) + ".mp4")
    filename = "%s/%s" % (folder, video_id + ".mp4")
    tk_logger.info("download video %s" % url)
    if os.path.isfile(filename):
        file_size = get_remote_file_size(url)
        if file_size == os.path.getsize(filename):
            tk_logger.info("file already downloaded, skip ...")
            return
        else:
            tk_logger.info("download file , file size:%d" % file_size)
    res = requests.get(url, headers=IOS_HEADER)
    if res.status_code == 200:
        with open(filename, "wb") as fp:
            for chunk in res.iter_content(chunk_size=1024):
                fp.write(chunk)
    else:
        raise TKException("download video [%s] failed [%d]" % (url, res.status_code))
</code></pre>

<p>下载视频</p>

<p><img src="https://user-gold-cdn.xitu.io/2019/7/12/16be6b40ab2ab355?w=1147&amp;h=385&amp;f=png&amp;s=347457" alt="img"></p>

<blockquote>
  <h3 id="">声明</h3>
</blockquote>

<p>此教程仅为交流学习，不作商业用途</p>]]></content:encoded></item><item><title><![CDATA[gRPC 基于zookeeper实现负载均衡]]></title><description><![CDATA[<blockquote>
  <h3 id="">导读</h3>
</blockquote>

<p>gRPC是一个高性能、通用的开源 RPC 框架，其由 Google 主要面向移动应用开发并基于HTTP/2协议标准而设计，基于ProtoBuf(Protocol Buffers) 序列化协议开发，且支持众多开发语言。</p>

<p>gRPC通过其插件机制，可以很灵活的实现负载均衡、调用链、健康检查、权限认证等模块，本文主要介绍如何通过gRPC定义的接口实现负载均衡功能。</p>

<blockquote>
  <h3 id="">负载均衡方案</h3>
</blockquote>

<p>RPC服务的除了解决跨语言调用的问题、模块解耦，重要的一点是通过模块的微服务化，可以水平扩展RPC服务的节点，应用层通过异步调用多个服务，降低应用层的延时。</p>

<p>由于RPC服务之间是无状态的，可以水平增加机器，扩展其服务能力。但是，如何利用多个节点呢？ </p>

<p>通常做法，可以在RPC前面加上loadbalance（LB），LB 后面挂上对应的服务节点。应用层直接访问LB，RPC节点隐藏在LB后面，对应用层不可见。 这种好处比较明显， 只要把LB的地址以及端口提供给应用层即可， 应用层不用关心LB算法，大大降低了应用调用的复杂性。这种方式在web等短连接应用是比较好的解决方案，因为在重新进行LB连接的时候，可以重新选择后端的服务。</p>

<p>但是，对于长链接的服务来说，这就有很大的问题。</p>]]></description><link>http://www.wetimer.com/grpc-zookeeper/</link><guid isPermaLink="false">c5adb144-7508-4580-9604-39271de1ae99</guid><category><![CDATA[gRPC]]></category><category><![CDATA[gRPC 负载均衡]]></category><category><![CDATA[zookeeper]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Thu, 20 Jun 2019 14:15:26 GMT</pubDate><media:content url="http://www.wetimer.com/content/images/2019/06/QQ--20190619220630.png" medium="image"/><content:encoded><![CDATA[<blockquote>
  <h3 id="">导读</h3>
</blockquote>

<img src="http://www.wetimer.com/content/images/2019/06/QQ--20190619220630.png" alt="gRPC 基于zookeeper实现负载均衡"><p>gRPC是一个高性能、通用的开源 RPC 框架，其由 Google 主要面向移动应用开发并基于HTTP/2协议标准而设计，基于ProtoBuf(Protocol Buffers) 序列化协议开发，且支持众多开发语言。</p>

<p>gRPC通过其插件机制，可以很灵活的实现负载均衡、调用链、健康检查、权限认证等模块，本文主要介绍如何通过gRPC定义的接口实现负载均衡功能。</p>

<blockquote>
  <h3 id="">负载均衡方案</h3>
</blockquote>

<p>RPC服务的除了解决跨语言调用的问题、模块解耦，重要的一点是通过模块的微服务化，可以水平扩展RPC服务的节点，应用层通过异步调用多个服务，降低应用层的延时。</p>

<p>由于RPC服务之间是无状态的，可以水平增加机器，扩展其服务能力。但是，如何利用多个节点呢？ </p>

<p>通常做法，可以在RPC前面加上loadbalance（LB），LB 后面挂上对应的服务节点。应用层直接访问LB，RPC节点隐藏在LB后面，对应用层不可见。 这种好处比较明显， 只要把LB的地址以及端口提供给应用层即可， 应用层不用关心LB算法，大大降低了应用调用的复杂性。这种方式在web等短连接应用是比较好的解决方案，因为在重新进行LB连接的时候，可以重新选择后端的服务。</p>

<p>但是，对于长链接的服务来说，这就有很大的问题。 
比如，一个长链接应用A 连接到LB上， LB 随机转发到后端的RPC服务B上，由于A是长链接，后面的所有请求都会转到B上，那么就不能起到负载均衡的作用了。你可能想到，最好跟RPC服务保持长链接，不用每次调用都进行连接，释放连接。 gRPC没有提供这种负载均衡的组件， 但是暴露了负载均衡的接口，只要extends <code>NameResolverProvider</code>类，实现接口方法，就能很方便的实现负载均衡模块。</p>

<p>关于gRPC 负载均衡的基础介绍，请参考 <a href="https://juejin.im/post/5cd6e69ff265da03a85addb6">https://juejin.im/post/5cd6e69ff265da03a85addb6</a></p>

<p>下面介绍如何通过zookeeper实现负载均衡的NameResolver</p>

<blockquote>
  <h3 id="zknameresolverprovider">ZkNameResolverProvider实现</h3>
</blockquote>

<pre><code>public class ZkNameResolverProvider extends NameResolverProvider {  
    @Override
    protected boolean isAvailable() {
        return true;
    }

    @Override
    protected int priority() {
        return 5;
    }

    @Nullable
    @Override
    public NameResolver newNameResolver(URI targetUri, Attributes params) {
        return new ZkNameResolver(targetUri);
    }

    @Override
    public String getDefaultScheme() {
        return "zk";
    }
}
</code></pre>

<blockquote>
  <h3 id="zknameresolver">ZkNameResolver实现</h3>
</blockquote>

<pre><code>public class ZkNameResolver extends NameResolver implements Watcher {  
    private URI zkUri;
    private ZooKeeper zoo;
    private Listener listener;
    private final int ZK_CONN_TIMEOUT = 3000;
    private final String ZK_PATH = "/grpc_server_list";

    ZkNameResolver(URI zkUri) {
        this.zkUri = zkUri;
    }

    @Override
    public String getServiceAuthority() {
        return zkUri.getAuthority();
    }

    @Override
    public void start(Listener listener) {
        this.listener = listener;
        final CountDownLatch latch = new CountDownLatch(1);
        String zkAddr = zkUri.getHost() + ":" + zkUri.getPort();
        System.out.printf("connect to zookeeper server %s", zkAddr);
        try {
            this.zoo = new ZooKeeper(zkAddr, ZK_CONN_TIMEOUT, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        latch.countDown();
                    }
                }
            });
        } catch (IOException e) {
            System.out.printf(e);
            System.out.printf("connect to zookeeper failed, JVM exited [%s]", e.getMessage());
            System.exit(1);
        }
        try {
            latch.await();
            System.out.printf("connect to zookeeper succeed");
        } catch (InterruptedException e) {
            System.out.printf(e);
            System.out.printf("CountDownLatch interrupted, JVM exited [%s]", e.getMessage());
            System.exit(1);
        }
        try {
            Stat stat = zoo.exists(ZK_PATH, true);
            if (stat == null) {
                System.out.printf("%s not exists", ZK_PATH);
            } else {
                System.out.printf("%s exists", ZK_PATH);
            }
        } catch (KeeperException | InterruptedException e) {
            System.out.printf(e);
        }

        try {
            List&lt;String&gt; children = zoo.getChildren(ZK_PATH, this);
            addServersToListener(children);
        } catch (KeeperException | InterruptedException e) {
            System.out.printf(e);
            System.out.printf("get children of %s failed [%s], JVM exited", ZK_PATH, e.getMessage());
            System.exit(1);
        }
    }
    // 把zookeeper ZK_PATH的子节点作为rpc的节点地址，注册到gRPC负载均衡服务中
    private void addServersToListener(List&lt;String&gt; servers) {
        System.out.printf("rpc servers:%s", servers);
        ArrayList&lt;EquivalentAddressGroup&gt; addressGroups = new ArrayList&lt;EquivalentAddressGroup&gt;();
        for (String server : servers) {
            List&lt;SocketAddress&gt; socketAddresses = new ArrayList&lt;SocketAddress&gt;();
            String[] address = server.split(":");
            socketAddresses.add(new InetSocketAddress(address[0], Integer.parseInt(address[1])));
            addressGroups.add(new EquivalentAddressGroup(socketAddresses));
        }
        if (addressGroups.size() &gt; 0) {
            listener.onAddresses(addressGroups, Attributes.EMPTY);
        } else {
            System.out.printf("No servers find, keep looking");
        }
    }

    @Override
    public void shutdown() {
        try {
            zoo.close();
        } catch (InterruptedException e) {
            System.out.printf(e);
        }
    }

    @Override
    public void process(WatchedEvent event) {
        if (event.getType() == Event.EventType.None) {
            System.out.printf("Zookeeper connection expired");
        } else {
            try {
                List&lt;String&gt; children = zoo.getChildren(ZK_PATH, false);
                addServersToListener(children);
                zoo.getChildren(ZK_PATH, true);
            } catch (Exception e) {
                System.out.printf(e);
            }
        }
    }
}
</code></pre>

<ul>
<li>把rpc服务的host:port作为zookeeper的path（<code>/grpc_server_list/rpc_host:50010</code>），zookeeper监听path的创建删除等事件</li>
<li>在rpc节点有上线或者下线时，动态把节点信息从zookeeper上添加或者删除</li>
<li>通过<code>listener.onAddresses</code>把rpc服务地址注册到gRPC负载均衡上</li>
</ul>

<blockquote>
  <h3 id="channel">channel 创建</h3>
</blockquote>

<pre><code>this.channel = ManagedChannelBuilder  
// 配置zk地址
.forTarget("zk://zkhost:2181")
// 配置NameResolverProvider实现类
.nameResolverFactory(new ZkNameResolverProvider())
.enableRetry()
.maxRetryAttempts(5)
.keepAliveTime(5, TimeUnit.MINUTES)
.keepAliveWithoutCalls(true)
.keepAliveTimeout(10, TimeUnit.MINUTES)
.idleTimeout(24, TimeUnit.HOURS)
// 轮询策略
.loadBalancerFactory(RoundRobinLoadBalancerFactory.getInstance())
.usePlaintext()
.build();
</code></pre>

<ul>
<li><code>forTarget("zk://zkhost:2181")</code>配置zookeeper链接地址</li>
<li><code>nameResolverFactory(new ZkNameResolverProvider())</code>配置<code>NameResolverProvider</code>实现类，让gRPC通过<code>ZkNameResolverProvider</code>查找可用的服务节点地址</li>
<li>调用rpc服务，会根据配置的<code>loadBalancerFactory(RoundRobinLoadBalancerFactory.getInstance())</code>策略进行轮询调用对应的后端服务</li>
</ul>

<blockquote>
  <h3 id="">总结</h3>
</blockquote>

<p>gRPC 提供了非常灵活的的负载均衡接口，通过实现接口， 可以很方便的实现负载均衡。 通过自定义的负载均衡机制，可以保证调用方与每个rpc保持长链接，大大提高了rpc的网络开销，同时轮询到每个rpc服务上，扩展了rpc的响应能力。 <br>
通过zookeeper可以watch机制，监听特定path(<code>/grpc_server_list</code>)子节点的增加或者删除，动态实现服务的注册于下线，大大提高了后端服务水平扩展的便捷性。</p>]]></content:encoded></item><item><title><![CDATA[GRPC 负载均衡实现]]></title><description><![CDATA[<blockquote>
  <h3 id="">导读</h3>
</blockquote>

<p>gRPC 是google开源的非常优秀的RPC框架，支持PYTHON/JAVA/PHP/GO/C/C++/C#/NODEJS/RUBY 等编程语言，在跨语言调用十分方便。</p>

<p>在产品环境，通常要部署多个RPC服务，已提高可用性，以及响应速度。但是
在负载均衡方面不如dubbo的组件那么丰富，但是其提供了服务发现的接口， 可以通过实现其接口，灵活实现负载均衡功能。</p>

<p>下面通过本地配置文件，启动时注册可用的服务，可以快速实现负载均衡功能。</p>

<pre><code>grpc:  
  hosts: host1:8080,host2:8080
</code></pre>

<blockquote>
  <h3 id="grpcchannel">GRPC channel 创建</h3>
</blockquote>

<pre><code>ManagedChannelBuilder  
                // 设置连接的目标地址
                .forTarget("local")
                // 设置地址服务
                .nameResolverFactory(new LocalNameResolverProvider(configInterface))
                .enableRetry()
                .maxRetryAttempts(5)
                .keepAliveTime(5, TimeUnit.</code></pre>]]></description><link>http://www.wetimer.com/grpc-load-balance/</link><guid isPermaLink="false">18a46ca2-90f1-48f8-8dd9-2b3b5347f918</guid><category><![CDATA[负载均衡策略]]></category><category><![CDATA[gRPC 负载均衡]]></category><category><![CDATA[gRPC]]></category><category><![CDATA[load balance]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Sat, 11 May 2019 15:46:00 GMT</pubDate><media:content url="http://www.wetimer.com/content/images/2019/05/QQ--20190511235111-1.png" medium="image"/><content:encoded><![CDATA[<blockquote>
  <h3 id="">导读</h3>
</blockquote>

<img src="http://www.wetimer.com/content/images/2019/05/QQ--20190511235111-1.png" alt="GRPC 负载均衡实现"><p>gRPC 是google开源的非常优秀的RPC框架，支持PYTHON/JAVA/PHP/GO/C/C++/C#/NODEJS/RUBY 等编程语言，在跨语言调用十分方便。</p>

<p>在产品环境，通常要部署多个RPC服务，已提高可用性，以及响应速度。但是
在负载均衡方面不如dubbo的组件那么丰富，但是其提供了服务发现的接口， 可以通过实现其接口，灵活实现负载均衡功能。</p>

<p>下面通过本地配置文件，启动时注册可用的服务，可以快速实现负载均衡功能。</p>

<pre><code>grpc:  
  hosts: host1:8080,host2:8080
</code></pre>

<blockquote>
  <h3 id="grpcchannel">GRPC channel 创建</h3>
</blockquote>

<pre><code>ManagedChannelBuilder  
                // 设置连接的目标地址
                .forTarget("local")
                // 设置地址服务
                .nameResolverFactory(new LocalNameResolverProvider(configInterface))
                .enableRetry()
                .maxRetryAttempts(5)
                .keepAliveTime(5, TimeUnit.MINUTES)
                .keepAliveWithoutCalls(true)
                .keepAliveTimeout(10, TimeUnit.MINUTES)
                .idleTimeout(24, TimeUnit.HOURS)
                // 设置轮询策略
                .loadBalancerFactory(RoundRobinLoadBalancerFactory.getInstance())
                .usePlaintext()
                .build();
</code></pre>

<ul>
<li>forTarget 设置连接RPC服务的地址，比如"127.0.0.1:8080"</li>
<li>nameResolverFactory 服务发现</li>
<li>LocalNameResolverProvider 服务提供者</li>
<li>loadBalancerFactory 设置负载均衡策略</li>
<li>RoundRobinLoadBalancerFactory 轮询策略</li>
</ul>

<blockquote>
  <h3 id="nameresolverfactory">nameResolverFactory 配置方式</h3>
</blockquote>

<ul>
<li><p>GRPC client 通过<code>loadBalancerFactory</code>来设置负载均衡的策略，这里选择<code>RoundRobinLoadBalancerFactory</code>，即服务轮询策略。</p></li>
<li><p>通过<code>nameResolverFactory</code>配置地址服务的发现方式，通过<code>NameResolverProvider</code>来实现服务的注册与发现。</p></li>
<li>服务提供者类</li>
</ul>

<pre><code>// 需要实现NameResolverProvider抽象类中的相关方法
public class LocalNameResolverProvider extends NameResolverProvider {  
    private final ConfigInterface configInterface;

    @Inject
    public LocalNameResolverProvider(ConfigInterface configInterface) {
        this.configInterface = configInterface;
    }

    // 服务是否可用
    @Override
    protected boolean isAvailable() {
        return true;
    }

    // 优先级默认5
    @Override
    protected int priority() {
        return 5;
    }

    // 服务发现类
    @Nullable
    @Override
    public NameResolver newNameResolver(URI targetUri, Attributes params) {
        return new LocalNameResolver(configInterface);
    }

    // 服务协议
    @Override
    public String getDefaultScheme() {
        return "local";
    }
}
</code></pre>

<ul>
<li>服务发现类</li>
</ul>

<pre><code>public class LocalNameResolver extends NameResolver {  
    private final ConfigInterface configInterface;

    @Inject
    public LocalNameResolver(ConfigInterface configInterface) {
        this.configInterface = configInterface;
    }

    @Override
    public String getServiceAuthority() {
        return "none";
    }

    // 配置可用服务，RPC在调用的时候，轮询选择这里配置的可用的服务地址列表
    @Override
    public void start(Listener listener) {
        LogUtils.info("LocalNameResolver start ...");
        ArrayList&lt;EquivalentAddressGroup&gt; addressGroups = new ArrayList&lt;EquivalentAddressGroup&gt;();
        // 获取rpc地址的配置列表
        // 地址格式 host1:8080,host2:8081
        Map&lt;String, Object&gt; config = (Map&lt;String, Object&gt;) this.configInterface.getRpcConfig().get("grpc");
        String[] hosts = config.get("hosts").toString().split(",");
        for (String host : hosts) {
            if (host.trim().length() &gt; 0) {
                String[] address = host.split(":");
                List&lt;SocketAddress&gt; socketAddresses = new ArrayList&lt;SocketAddress&gt;();
                socketAddresses.add(new InetSocketAddress(address[0], Integer.parseInt(address[1])));
                addressGroups.add(new EquivalentAddressGroup(socketAddresses));
            }
        }
        listener.onAddresses(addressGroups, Attributes.EMPTY);
    }

    @Override
    public void shutdown() {

    }
}
</code></pre>

<ul>
<li>通过以上配置， 在进行RPC调用的时候，会轮询选择注册的服务地址</li>
<li>添加新的可用服务节点需要更新配置文件</li>
</ul>

<blockquote>
  <h3 id="">动态注册与发现</h3>
</blockquote>

<ul>
<li>启动GRPC服务的同时，把服务的地址注册到zookeeper上</li>
<li>实现<code>NameResolver</code>start 方法，监听zookeeper变化，实时更新可用地址列表</li>
</ul>]]></content:encoded></item><item><title><![CDATA[JAVA ObjectSerializer转PHP]]></title><description><![CDATA[<blockquote>
  <h3 id="javacode">java code</h3>
</blockquote>

<pre><code>package com.demo.encrypt;

import java.io.ByteArrayInputStream;  
import java.io.ByteArrayOutputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
import java.io.Serializable;

public class ObjectSerializer {

    public static  String serialize(Serializable obj) throws IOException {
        if (obj == null) return "";
        try {
            ByteArrayOutputStream serialObj = new ByteArrayOutputStream(</code></pre>]]></description><link>http://www.wetimer.com/java-objectserializerzhuan-php/</link><guid isPermaLink="false">07bb21f3-1188-4921-914d-a4df33eec1f1</guid><category><![CDATA[ObjectSerializer]]></category><category><![CDATA[PHP]]></category><category><![CDATA[java]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Mon, 15 Apr 2019 06:55:05 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <h3 id="javacode">java code</h3>
</blockquote>

<pre><code>package com.demo.encrypt;

import java.io.ByteArrayInputStream;  
import java.io.ByteArrayOutputStream;  
import java.io.IOException;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
import java.io.Serializable;

public class ObjectSerializer {

    public static  String serialize(Serializable obj) throws IOException {
        if (obj == null) return "";
        try {
            ByteArrayOutputStream serialObj = new ByteArrayOutputStream();
            ObjectOutputStream objStream = new ObjectOutputStream(serialObj);
            objStream.writeObject(obj);
            objStream.close();
            return encodeBytes(serialObj.toByteArray());
        } catch (Exception e) {
            throw new IOException();
        }
    }

    public static  Object deserialize(String str) throws IOException {
        if (str == null || str.length() == 0) return null;
        try {
            ByteArrayInputStream serialObj = new ByteArrayInputStream(decodeBytes(str));
            ObjectInputStream objStream = new ObjectInputStream(serialObj);
            return objStream.readObject();
        } catch (Exception e) {
            throw new IOException();
        }
    }

    public static  String encodeBytes(byte[] bytes) {
        StringBuffer strBuf = new StringBuffer();

        for (int i = 0; i &lt; bytes.length; i++) {
            strBuf.append((char) (((bytes[i] &gt;&gt; 4) &amp; 0xF) + ((int) 'a')));
            strBuf.append((char) (((bytes[i]) &amp; 0xF) + ((int) 'a')));
        }

        return strBuf.toString();
    }

    public static  byte[] decodeBytes(String str) {
        byte[] bytes = new byte[str.length() / 2];
        for (int i = 0; i &lt; str.length(); i+=2) {
            char c = str.charAt(i);
            bytes[i/2] = (byte) ((c - 'a') &lt;&lt; 4);
            c = str.charAt(i+1);
            bytes[i/2] += (c - 'a');
        }
        return bytes;
    }

}
</code></pre>

<blockquote>
  <h3 id="phpcodesnippet">PHP code snippet</h3>
</blockquote>

<pre><code>class Serializer{  
    public static function encodeBytes($bytes)
    {
        $strBuf = array();
        for ($i = 0, $n = strlen($bytes); $i &lt; $n; $i++) {
            $strBuf[] = chr((ord($bytes[$i]) &gt;&gt; 4 &amp; 0xF) + 97);
            $strBuf[] = chr((ord($bytes[$i]) &amp; 0xF) + 97);
        }
        return implode("", $strBuf);
    }

    public static function decodeBytes($str)
    {
        $bytes = array();;
        for ($i = 0, $n = strlen($str); $i &lt; $n; $i += 2) {
            $c = $str[$i];
            $bytes[$i / 2] = (ord($c) - 97) &lt;&lt; 4;
            $c = $str[$i + 1];
            $bytes[$i / 2] += (ord($c) - 97);
            $bytes[$i / 2] = chr($bytes[$i / 2]);
        }
        return implode("", $bytes);
    }
}
</code></pre>]]></content:encoded></item><item><title><![CDATA[为什么选择Guice框架]]></title><description><![CDATA[Use Google Guice to implemenet IOC
使用Guice 去实现IOC]]></description><link>http://www.wetimer.com/wei-shi-yao-xuan-ze-guicekuang-jia/</link><guid isPermaLink="false">20516a2f-ead3-49a4-b23a-91cff4513308</guid><category><![CDATA[Guice]]></category><category><![CDATA[DI]]></category><category><![CDATA[IOC]]></category><category><![CDATA[依赖注入]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Tue, 05 Mar 2019 02:30:58 GMT</pubDate><media:content url="http://www.wetimer.com/content/images/2019/03/d3631b5d1ddfdff42429748e7ce39c0.png" medium="image"/><content:encoded><![CDATA[<h3 id="guice">使用Guice框架的动机</h3>

<blockquote>
  <img src="http://www.wetimer.com/content/images/2019/03/d3631b5d1ddfdff42429748e7ce39c0.png" alt="为什么选择Guice框架"><p>在应用中组装各个封装好的类，有时候是一件很乏味的事情。有几种办法可以把数据层、业务层、表现层的代码整合在一起。下面通过一个在线披萨下订单的业务来对比这几种实现方法。</p>
</blockquote>

<pre><code>// 定义下订单接口
public interface BillingService {

  /**
   * Attempts to charge the order to the credit card. Both successful and
   * failed transactions will be recorded.
   *
   * @return a receipt of the transaction. If the charge was successful, the
   *      receipt will be successful. Otherwise, the receipt will contain a
   *      decline note describing why the charge failed.
   */
  Receipt chargeOrder(PizzaOrder order, CreditCard creditCard);
}
</code></pre>

<p>我们需要为下订单实现类写单元测试，这里为了避免真正的支付，我们需要定义一个<code>FakeCreditCardProcessor</code>类</p>

<h4 id="">直接在构造方法中调用依赖的类</h4>

<blockquote>
  <p>下面示例代码中，直接在构造方法中<code>new</code> 信用卡处理类<code>CreditCardProcessor</code>跟事务日志处理类<code>TransactionLog</code></p>
</blockquote>

<pre><code>public class RealBillingService implements BillingService {  
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();

    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}
</code></pre>

<p>上面代码的主要问题是没有进行模块化，也不好测试。不然，你测试的时候，真的要从你的信用卡扣费了。而且，也很难测试支付失败，或者支付网关服务不可用的情况。</p>

<h4 id="">使用工厂方法</h4>

<blockquote>
  <p>工厂方法解耦了调用类跟实现类之间的耦合。一般工厂方法使用静态的set跟get方法来设置跟获取实现类。如下面的示例代码：</p>
</blockquote>

<pre><code>public class CreditCardProcessorFactory {

  private static CreditCardProcessor instance;

  public static void setInstance(CreditCardProcessor processor) {
    instance = processor;
  }

  public static CreditCardProcessor getInstance() {
    if (instance == null) {
      return new SquareCreditCardProcessor();
    }

    return instance;
  }
}
</code></pre>

<p>在下面的示例代码中，我们用getInstance来代替new来获取相关对象。</p>

<pre><code>public class RealBillingService implements BillingService {  
  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    CreditCardProcessor processor = CreditCardProcessorFactory.getInstance();
    TransactionLog transactionLog = TransactionLogFactory.getInstance();

    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}
</code></pre>

<p>用工厂方法，我们可以对支付的流程进行单元测试。</p>

<pre><code>public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard("1234", 11, 2010);

  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();

  @Override public void setUp() {
    TransactionLogFactory.setInstance(transactionLog);
    CreditCardProcessorFactory.setInstance(processor);
  }

  @Override public void tearDown() {
    TransactionLogFactory.setInstance(null);
    CreditCardProcessorFactory.setInstance(null);
  }

  public void testSuccessfulCharge() {
    RealBillingService billingService = new RealBillingService();
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, processor.getCardOfOnlyCharge());
    assertEquals(100, processor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}
</code></pre>

<p>上面代码看起来也很笨拙。因为使用全局变量来保存模拟支付类<code>FakeCreditCardProcessor</code>的实例，需要在<code>teardown</code>对于全局变量进行释放。如果<code>teardown</code>执行失败，而且后面的测试也用到了这个变量，会对后面的测试造成影响。同样，由于全局变量的污染，也无法进行并行测试。</p>

<p>最严重的问题是，所有的依赖都隐藏码在代码中了。比如，在<code>CreditCardFraudTracker</code>类中增加了一个依赖，所有的单元测试都要跑一遍，来看一下哪个测试方法没有通过。
我们也很难知道一个工厂方法是否初始化，除非哪天被调用到了。</p>

<p>虽然QA跟充分的验收测试能解决这些问题，但是我们肯定有更好的办法来处理这个问题。</p>

<h4 id="">依赖注入</h4>

<p>跟工厂模式一样，依赖注入也是一种设计模式。依赖注入的核心原则是：把使用依赖跟查找依赖分离。就像下面的例子，<code>RealBillingService</code>并不负责查找<code>TransactionLog</code> 和 <code>CreditCardProcessor</code>，而是由使用者传入到对应的构造函数中。</p>

<pre><code>public class RealBillingService implements BillingService {  
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  public RealBillingService(CreditCardProcessor processor, 
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}
</code></pre>

<p>这样我们就不需要使用工厂，如下面的代码，我们可以把<code>setUp</code>跟<code>tearDown</code>去掉。</p>

<pre><code>public class RealBillingServiceTest extends TestCase {

  private final PizzaOrder order = new PizzaOrder(100);
  private final CreditCard creditCard = new CreditCard("1234", 11, 2010);

  private final InMemoryTransactionLog transactionLog = new InMemoryTransactionLog();
  private final FakeCreditCardProcessor processor = new FakeCreditCardProcessor();

  public void testSuccessfulCharge() {
    RealBillingService billingService
        = new RealBillingService(processor, transactionLog);
    Receipt receipt = billingService.chargeOrder(order, creditCard);

    assertTrue(receipt.hasSuccessfulCharge());
    assertEquals(100, receipt.getAmountOfCharge());
    assertEquals(creditCard, processor.getCardOfOnlyCharge());
    assertEquals(100, processor.getAmountOfOnlyCharge());
    assertTrue(transactionLog.wasSuccessLogged());
  }
}
</code></pre>

<p>如果我们在<code>RealBillingService</code>增加依赖，编译器会提醒我们哪个测试方法需要被修复了。</p>

<p>但是现在<code>BillingService</code>的使用者需要知道它的依赖，并在构造方法中传入这些依赖，通常在一个入口类传入。</p>

<pre><code>public static void main(String[] args) {  
    CreditCardProcessor processor = new PaypalCreditCardProcessor();
    TransactionLog transactionLog = new DatabaseTransactionLog();
    BillingService billingService
        = new RealBillingService(processor, transactionLog);
    ...
  }
</code></pre>

<h4 id="guice">使用Guice来实现依赖注入</h4>

<blockquote>
  <p>依赖注入让设计模式让代码可以模块化、可测试化，Guice让依赖注入的代码更容易书写。</p>
</blockquote>

<p>在上面的例子中使用Guice的话，我们首先要告诉Guice怎么通过接口找到它的实现类。我们通过一个实现了Guice <code>Module</code>接口的配置类来完成这个工作。</p>

<pre><code>public class BillingModule extends AbstractModule {  
  @Override 
  protected void configure() {
    bind(TransactionLog.class).to(DatabaseTransactionLog.class);
    bind(CreditCardProcessor.class).to(PaypalCreditCardProcessor.class);
    bind(BillingService.class).to(RealBillingService.class);
  }
}
</code></pre>

<p>我们通过添加<code>@Inject</code>到<code>RealBillingService</code>的构造方法，让Guice能够找到它的依赖。</p>

<pre><code>public class RealBillingService implements BillingService {  
  private final CreditCardProcessor processor;
  private final TransactionLog transactionLog;

  @Inject
  public RealBillingService(CreditCardProcessor processor,
      TransactionLog transactionLog) {
    this.processor = processor;
    this.transactionLog = transactionLog;
  }

  public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard) {
    try {
      ChargeResult result = processor.charge(creditCard, order.getAmount());
      transactionLog.logChargeResult(result);

      return result.wasSuccessful()
          ? Receipt.forSuccessfulCharge(order.getAmount())
          : Receipt.forDeclinedCharge(result.getDeclineMessage());
     } catch (UnreachableException e) {
      transactionLog.logConnectException(e);
      return Receipt.forSystemFailure(e.getMessage());
    }
  }
}
</code></pre>

<p>最后，可以使用<code>Injector</code>去帮我们找到任何一个绑定过的类的实例。</p>

<pre><code>public static void main(String[] args) {  
    Injector injector = Guice.createInjector(new BillingModule());
    BillingService billingService = injector.getInstance(BillingService.class);
    ...
  }
</code></pre>

<p>官方文档链接：<a href="https://github.com/google/guice/wiki/Motivation">https://github.com/google/guice/wiki/Motivation</a></p>]]></content:encoded></item><item><title><![CDATA[导出echarts中的图片]]></title><description><![CDATA[通过API，在服务端把echarts图表导出为图片]]></description><link>http://www.wetimer.com/dao-chu-echartszhong-de-tu-pian/</link><guid isPermaLink="false">0ee98fdd-a800-46e5-90c7-f59d3046509b</guid><category><![CDATA[echarts]]></category><category><![CDATA[image]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Fri, 14 Apr 2017 16:06:17 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <p>echarts提供了很多精美图表插件，通过浏览器来渲染后，用户可以保存图表为图片。但是有些应用场景，需要在用户不打开浏览器的情况下，能把图表保存下来，比如把echarts图表插入到word文档中。这种需求下，我开发了echarts图片导出服务，并提供了相关接口。</p>
</blockquote>

<h3 id="">提供了两种类型的接口：</h3>

<h6 id="echarts">把定制页面的echarts图表保存为图片</h6>

<p>请求实例：</p>

<pre><code>curl http://115.29.188.241:3100/ -d '{"url":"http://115.29.188.241/public/echarts/default.html","funcs":"salesChart,bubbleChart","delay":1000}'  
</code></pre>

<p>参数解释：</p>

<ul>
<li>请求接口：<a href="http://115.29.188.241:3100/">http://115.29.188.241:3100/</a></li>
<li>请求方法：POST</li>
<li>请求参数：
<ul><li>参数格式：JSON</li>
<li>url:定制的echarts图表页面，如：<a href="http://115.29.188.241/public/echarts/default.html">http://115.29.188.241/public/echarts/default.html</a></li>
<li>funcs：上面指定的URL页面上暴露的echarts实例全局变量，多个实例用半角逗号隔开，如：salesChart,bubbleChart。
<img src="http://ogcxhul17.bkt.clouddn.com/demo-sales.png" alt="sales" title="">
<img src="http://ogcxhul17.bkt.clouddn.com/demo-bubble.png" alt="bubble" title=""></li>
<li>delay：可选项，单位ms，默认100，最大值5000。延时执行时间，有的echarts图表带有动画，需要等待动画执行完，delay时间越长，等待接口返回时间越长，建议关闭动画。</li></ul></li>
</ul>

<p>返回结果：</p>

<pre><code>{"err":0,"msg":"success","imgs":{"bubbleChart":"http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492182803618.png","salesChart":"http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492183306861.png"}}
</code></pre>

<p>出参解释：</p>

<ul>
<li>err： 0 或者 1 ， 0 表示没有错误，1表示发生错误。</li>
<li>msg： 成功返回success，失败返回错误的信息。</li>
<li>imgs：json对象，key为请求参数funcs中指定的全局变量名，比如这里的salesChart对应的图片为<a href="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492183306861.png，bubbleChart对应的图片为http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492182803618.png">http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492183306861.png，bubbleChart对应的图片为http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492182803618.png</a>
<img src="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492183306861.png" alt="salesChart" title=""> <br>
<img src="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492182803618.png" alt="bubbleChart"></li>
</ul>

<h6 id="echartsoptionecharts353">根据echarts option参数来生成图片，这里echarts的版本为3.5.3</h6>

<p>请求实例：</p>

<pre><code>curl http://115.29.188.241:3100/ -d '{"echartsOption":{"tooltip":{"trigger":"item","formatter":"{a} &lt;br/&gt;{b}: {c} ({d}%)"},"legend":{"orient":"vertical","x":"left","data":["直接访问","邮件营销","联盟广告","视频广告","搜索引擎"]},"series":[{"name":"访问来源","type":"pie","radius":["50%","70%"],"avoidLabelOverlap":false,"label":{"normal":{"show":false,"position":"center"},"emphasis":{"show":true,"textStyle":{"fontSize":"30","fontWeight":"bold"}}},"labelLine":{"normal":{"show":false}},"data":[{"value":335,"name":"直接访问"},{"value":310,"name":"邮件营销"},{"value":234,"name":"联盟广告"},{"value":135,"name":"视频广告"},{"value":1548,"name":"搜索引擎"}]}]},"width":600,"height":400}'  
</code></pre>

<p>参数解释：</p>

<ul>
<li>请求接口：<a href="http://115.29.188.241:3100/">http://115.29.188.241:3100/</a></li>
<li>请求方法：POST</li>
<li>请求参数：
<ul><li>参数格式：JSON</li>
<li>echartsOption：echarts的配置信息
<img src="http://ogcxhul17.bkt.clouddn.com/demo-pie.png" alt="demo-pie" title=""></li>
<li>width：可选项，生成图片的宽度，单位px，最大值：2000，默认值：600</li>
<li>height：可选项，生成图片的高度，单位px，最大值：2000，默认值：400</li></ul></li>
</ul>

<pre><code>{"err":0,"msg":"success","imgs":{"myChart":"http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492184722716.png"}}
</code></pre>

<p>出参解释：</p>

<ul>
<li>err： 0 或者 1 ， 0 表示没有错误，1表示发生错误。</li>
<li>msg： 成功返回success，失败返回错误的信息。</li>
<li>imgs：这里的key固定为myChart
<img src="http://saas-pubsentiment-img1.oss-cn-hangzhou.aliyuncs.com/2017/04/14/1492184722716.png" alt="demo-pie"></li>
</ul>]]></content:encoded></item><item><title><![CDATA[Mongodb java driver 3.x  常用方法]]></title><description><![CDATA[<blockquote>
  <h3 id="pomxml">在pom.xml配置依赖</h3>
</blockquote>

<pre><code>&lt;dependencies&gt;  
    &lt;dependency&gt;
        &lt;groupId&gt;org.mongodb&lt;/groupId&gt;
        &lt;artifactId&gt;mongodb-driver&lt;/artifactId&gt;
        &lt;version&gt;3.3.0&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;  
</code></pre>

<blockquote>
  <h3 id="">导入常用的包</h3>
</blockquote>

<pre><code>import com.mongodb.MongoClient;  
import com.mongodb.MongoClientURI;  
import com.mongodb.</code></pre>]]></description><link>http://www.wetimer.com/mongodb-java-driver/</link><guid isPermaLink="false">cbdc47ca-eeb1-489a-9760-d0d4ef77b12e</guid><category><![CDATA[java]]></category><category><![CDATA[mongodb]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Wed, 16 Nov 2016 05:30:49 GMT</pubDate><content:encoded><![CDATA[<blockquote>
  <h3 id="pomxml">在pom.xml配置依赖</h3>
</blockquote>

<pre><code>&lt;dependencies&gt;  
    &lt;dependency&gt;
        &lt;groupId&gt;org.mongodb&lt;/groupId&gt;
        &lt;artifactId&gt;mongodb-driver&lt;/artifactId&gt;
        &lt;version&gt;3.3.0&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;  
</code></pre>

<blockquote>
  <h3 id="">导入常用的包</h3>
</blockquote>

<pre><code>import com.mongodb.MongoClient;  
import com.mongodb.MongoClientURI;  
import com.mongodb.client.MongoCollection;  
import com.mongodb.client.MongoCursor;  
import com.mongodb.client.MongoDatabase;  
import org.bson.Document;  
import org.bson.conversions.Bson;  
import org.bson.types.ObjectId;

// 导入com.mongodb.client.model.Filters类中的静态方法
import static com.mongodb.client.model.Filters.*;  
import static com.mongodb.client.model.Projections.*;  
</code></pre>

<blockquote>
  <h3 id="">连接数据库</h3>
</blockquote>

<p>有四种连接方法，具体参考：
<a href="http://mongodb.github.io/mongo-java-driver/3.3/driver/getting-started/quick-tour/">官方文档</a></p>

<p>这里使用</p>

<pre><code>MongoClientURI connectionString = new MongoClientURI("mongodb://localhost:27017,localhost:27018,localhost:27019");  
MongoClient mongoClient = new MongoClient(connectionString);

MongoDatabase database = mongoClient.getDatabase("mydb");  
</code></pre>

<p>阿里云官方推荐使用方法，支持主从节点不固定的情况下，自动识别。</p>

<blockquote>
  <h3 id="collection">获取 collection</h3>
</blockquote>

<pre><code>MongoCollection&lt;Document&gt; collection = database.getCollection("test");  
</code></pre>

<blockquote>
  <h3 id="">插入文档</h3>
</blockquote>

<p>文档结构：</p>

<pre><code>{
   "name" : "MongoDB",
   "type" : "database",
   "count" : 1,
   "info" : {
               x : 203,
               y : 102
             }
}
</code></pre>

<p>相应的Bson Document对象：</p>

<pre><code>Document doc = new Document("name", "MongoDB")  
               .append("type", "database")
               .append("count", 1)
               .append("info", new Document("x", 203).append("y", 102));

// 插入文档               
collection.insertOne(doc);  
</code></pre>

<p><em>注意：info 使用内嵌对象</em></p>

<blockquote>
  <h3 id="">插入多个文档</h3>
</blockquote>

<pre><code>List&lt;Document&gt; documents = new ArrayList&lt;Document&gt;();  
for (int i = 0; i &lt; 100; i++) {  
    documents.add(new Document("i", i));
}

collection.insertMany(documents);  
</code></pre>

<blockquote>
  <h3 id="">统计文档总数</h3>
</blockquote>

<pre><code>System.out.println(collection.count());  
</code></pre>

<blockquote>
  <h3 id="">查询单篇文档</h3>
</blockquote>

<pre><code>Document myDoc = collection.find().first();  
System.out.println(myDoc.toJson());  
</code></pre>

<blockquote>
  <h3 id="">查询所有文档</h3>
</blockquote>

<pre><code>MongoCursor&lt;Document&gt; cursor = collection.find().iterator();  
try {  
    while (cursor.hasNext()) {
        System.out.println(cursor.next().toJson());
    }
} finally {
    cursor.close();
}
</code></pre>

<blockquote>
  <h3 id="">使用查询过滤器</h3>
</blockquote>

<ul>
<li>查询单篇文档</li>
</ul>

<pre><code>import static com.mongodb.client.model.Filters.*;

myDoc = collection.find(eq("i", 71)).first();  
System.out.println(myDoc.toJson());  
</code></pre>

<ul>
<li>查询多篇文档</li>
</ul>

<pre><code>MongoCursor&lt;Document&gt; cursor = dbCollection  
                .find(gt("i", 0))
                .iterator();

while (cursor.hasNext()) {  
            System.out.println(cursor.next());
        }                
</code></pre>

<ul>
<li>指定多个查询条件</li>
</ul>

<pre><code>// 10 &lt;= i &lt;= 100
Bson filter = and(gte("i",10),lte("i",100));  
// status = 1
filter = and(filter, eq("status",1));  
myDoc = collection.find(filter).first()  
</code></pre>

<ul>
<li>使用ID查询</li>
</ul>

<pre><code>// 使用ObjectId对象来包装id
Document doc = dbCollection  
.find(eq("_id", new ObjectId("576c00b50cf23b8ea4175a14")))
.first();
</code></pre>

<blockquote>
  <h3 id="">使用排序</h3>
</blockquote>

<pre><code>import static com.mongodb.client.model.Sorts.*;

// ascending 升序 descending 降序 
myDoc = collection.find(exists("i")).sort(descending("i")).first();  
System.out.println(myDoc.toJson());  
</code></pre>

<blockquote>
  <h3 id="">指定查询字段</h3>
</blockquote>

<pre><code>// excludeId() 排除_id字段
myDoc = collection.find().projection(excludeId()).first();  
System.out.println(myDoc.toJson());

// 只包含部分字段
myDoc = collection.find().projection(include("news_title","news_posttime")).first();  
System.out.println(myDoc.toJson());

// 上面的查询是会返回_id字段的，如果要排除_id字段，可以使用Bson 类型来限定多个条件
Bson fields = include("news_title", "news_uuid", "news_posttime");  
fields = and(fields, excludeId());  
// 只返回news_title,news_uuid字段
myDoc = collection.find().projection(fields).first();  
System.out.println(myDoc.toJson());  
</code></pre>

<blockquote>
  <h3 id="">更新文档</h3>
</blockquote>

<ul>
<li>更新单个文档</li>
</ul>

<pre><code>collection.updateOne(eq("i", 10), set("i", 110));  
</code></pre>

<ul>
<li>更新多个文档</li>
</ul>

<pre><code>UpdateResult updateResult = collection.updateMany(lt("i", 100), inc("i", 100));  
System.out.println(updateResult.getModifiedCount());  
</code></pre>

<blockquote>
  <h3 id="">删除文档</h3>
</blockquote>

<ul>
<li>删除单个文档</li>
</ul>

<pre><code>collection.deleteOne(eq("i", 110));  
</code></pre>

<ul>
<li>删除多个文档</li>
</ul>

<pre><code>DeleteResult deleteResult = collection.deleteMany(gte("i", 100));  
System.out.println(deleteResult.getDeletedCount());  
</code></pre>]]></content:encoded></item><item><title><![CDATA[ssllabs.com评分A+的nginx ssl配置]]></title><description><![CDATA[<p><img src="https://timgsa.baidu.com/timg?image&amp;quality=80&amp;size=b10000_10000&amp;sec=1478533560325&amp;di=2ed03d7ff4a98bab952b4fa51a1e5984&amp;imgtype=0&amp;src=http%3A%2F%2Fstatic.codeceo.com%2Fimages%2F2015%2F12%2F235813maa7gnqa15zia77o.jpg" alt="image"></p>

<blockquote>
  <h3 id="ssllabshttpswwwssllabscomssltestindexhtml">什么是 <a href="https://www.ssllabs.com/ssltest/index.html">ssllabs</a> ？</h3>
</blockquote>

<p><em>ssllabs</em>是一个在线版全球知名的HTTPS 网站检测工具 。 <em>Qualys SSL Labs</em> 同时也是很具有影响力的 SSL 安全和性能研究机构。在线监测地址为：<a href="https://www.ssllabs.com/ssltest/index.html">https://www.ssllabs.com/ssltest/index.html</a></p>

<blockquote>
  <h3 id="openssl">使用openssl工具生成自己的证书</h3>
</blockquote>

<ul>
<li>生成一个RSA秘钥
<code class="language-shell">openssl genrsa -des3 -out demo.key 2048</code> <br>
<em>要记住这里输入的密码，在下面的步骤中都会用到</em></li>
<li>拷贝一个不需要输入密码的密钥文件
<code class="language-shell">openssl rsa -in demo.key -out demo_nopass.key</code> <br>
<em>这个秘钥会在nginx的配置中使用到，如果使用上面的秘钥，nginx重新加载配置文件都要输入密码</em></li>
<li>生成一个证书请求
<code class="language-shell">openssl req -new -key demo.key</code></li></ul>]]></description><link>http://www.wetimer.com/ssllabs-nginx-ssl/</link><guid isPermaLink="false">9e5be433-93ad-4ccb-9486-682c850fe1f5</guid><category><![CDATA[nginx]]></category><category><![CDATA[ssl]]></category><category><![CDATA[ssllabs]]></category><category><![CDATA[openssl]]></category><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Mon, 07 Nov 2016 13:46:48 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://timgsa.baidu.com/timg?image&amp;quality=80&amp;size=b10000_10000&amp;sec=1478533560325&amp;di=2ed03d7ff4a98bab952b4fa51a1e5984&amp;imgtype=0&amp;src=http%3A%2F%2Fstatic.codeceo.com%2Fimages%2F2015%2F12%2F235813maa7gnqa15zia77o.jpg" alt="image"></p>

<blockquote>
  <h3 id="ssllabshttpswwwssllabscomssltestindexhtml">什么是 <a href="https://www.ssllabs.com/ssltest/index.html">ssllabs</a> ？</h3>
</blockquote>

<p><em>ssllabs</em>是一个在线版全球知名的HTTPS 网站检测工具 。 <em>Qualys SSL Labs</em> 同时也是很具有影响力的 SSL 安全和性能研究机构。在线监测地址为：<a href="https://www.ssllabs.com/ssltest/index.html">https://www.ssllabs.com/ssltest/index.html</a></p>

<blockquote>
  <h3 id="openssl">使用openssl工具生成自己的证书</h3>
</blockquote>

<ul>
<li>生成一个RSA秘钥
<code class="language-shell">openssl genrsa -des3 -out demo.key 2048</code> <br>
<em>要记住这里输入的密码，在下面的步骤中都会用到</em></li>
<li>拷贝一个不需要输入密码的密钥文件
<code class="language-shell">openssl rsa -in demo.key -out demo_nopass.key</code> <br>
<em>这个秘钥会在nginx的配置中使用到，如果使用上面的秘钥，nginx重新加载配置文件都要输入密码</em></li>
<li>生成一个证书请求
<code class="language-shell">openssl req -new -key demo.key -out demo.csr</code>  </li>
<li>使用SHA-2签名算法，生成证书
<code class="language-shell">openssl req -x509 -sha256 -days 365 -newkey rsa:2048 -key demo.key -out demo.crt</code>  </li>
<li>生成交换秘钥
<code class="language-shell">openssl dhparam -out dhparam.pem 2048</code> <br>
<em>秘钥的位数越大，生成秘钥花费的时间越长，默认 2048</em></li>
</ul>

<blockquote>
  <h3 id="nginx">配置nginx</h3>
</blockquote>

<pre>
<code class="language-nginx">
server {
    listen       443 ssl;
    ssl      on;
    server_name  demo.com;
    root /data1/www/demo;
    access_log /var/log/nginx/demo-access.log main;
    error_log /var/log/nginx/demo-error.log warn;
    ssl_certificate /usr/local/nginx/conf/conf.d/ssl/demo.crt;
    ssl_certificate_key /usr/local/nginx/conf/conf.d/ssl/demo_nopass.key;
    resolver_timeout 1d;
    #ssl 会话缓存配置，参考：http://nginx.org/en/docs/http/configuring_https_servers.html
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_dhparam /usr/local/nginx/conf/conf.d/ssl/dhparam.pem;
    #不再使用旧的不安全的 SSLv2 和 SSLv3 
    ssl_protocols   TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers
  'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;
    resolver 202.106.0.20 8.8.8.8 valid=300s;
}
</code>
</pre>]]></content:encoded></item><item><title><![CDATA[PHP实现数组元素排列组合]]></title><description><![CDATA[<pre>
<code class="language-php">
$arr = array(
        "康师傅， 康sir",
        "方便面 矿泉水 ",
        "新闻"
    );

$res = array();

while($s = array_shift($arr)){
    if(!empty($s)){
        $s = str_replace("，", ",", $s);
        //echo $s."\n";
        $leaves = preg_split("/[\,\s+]/", $s,-1,PREG_SPLIT_NO_EMPTY);
        $ret = array();

        if($res){
            foreach($res as $_leaf){
                foreach($leaves as $leaf){
                    $ret[] = $_leaf." ".$leaf;
                }
            }
        }else{</code></pre>]]></description><link>http://www.wetimer.com/php-array-range/</link><guid isPermaLink="false">0b00e5bf-383d-4f1d-a92b-87b9ead975d3</guid><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Sat, 05 Nov 2016 12:32:24 GMT</pubDate><content:encoded><![CDATA[<pre>
<code class="language-php">
$arr = array(
        "康师傅， 康sir",
        "方便面 矿泉水 ",
        "新闻"
    );

$res = array();

while($s = array_shift($arr)){
    if(!empty($s)){
        $s = str_replace("，", ",", $s);
        //echo $s."\n";
        $leaves = preg_split("/[\,\s+]/", $s,-1,PREG_SPLIT_NO_EMPTY);
        $ret = array();

        if($res){
            foreach($res as $_leaf){
                foreach($leaves as $leaf){
                    $ret[] = $_leaf." ".$leaf;
                }
            }
        }else{
            foreach($leaves as $leaf){
                $ret[] = $leaf;
            }
        }

        //print_r($ret);
        $res = $ret;
    }
}

print_r($res);

// 打印结果
Array
(
    [0] => 康师傅 方便面 新闻
    [1] => 康师傅 矿泉水 新闻
    [2] => 康sir 方便面 新闻
    [3] => 康sir 矿泉水 新闻
)
</code>
</pre>]]></content:encoded></item><item><title><![CDATA[virtualbox 导入 cloudera quickstart vbox 镜像失败]]></title><description><![CDATA[<p>下载cloudera quickstart virtualbox 镜像文件之后，双击cloudera-quickstart-vm-5.8.0-0-virtualbox.ovf</p>

<p>导入到virtualbox，出现异常类似<code>“&lt;vbox:Machine&gt; element in OVF contains a medium attachment for the disk image 0cabb6f4-xxx but the OVF describes no such image”</code>异常信息</p>

<p>可以在导入设置的时候把，DVD 跟 USB controller导入选项去掉，在导入之后再进行设置。</p>

<p>参考链接：<a href="http://stackoverflow.com/questions/13234530/unable-to-load-ovf-to-virtualbox-created-using-vmware-vphrere-export">http://stackoverflow.com/questions/13234530/unable-to-load-ovf-to-virtualbox-created-using-vmware-vphrere-export</a></p>]]></description><link>http://www.wetimer.com/virtualbox-dao-ru-cloudera-quickstart-vbox-jing-xiang-shi-bai/</link><guid isPermaLink="false">e6427385-57f4-4966-ac7f-54406efcbfe8</guid><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Sat, 05 Nov 2016 12:20:42 GMT</pubDate><content:encoded><![CDATA[<p>下载cloudera quickstart virtualbox 镜像文件之后，双击cloudera-quickstart-vm-5.8.0-0-virtualbox.ovf</p>

<p>导入到virtualbox，出现异常类似<code>“&lt;vbox:Machine&gt; element in OVF contains a medium attachment for the disk image 0cabb6f4-xxx but the OVF describes no such image”</code>异常信息</p>

<p>可以在导入设置的时候把，DVD 跟 USB controller导入选项去掉，在导入之后再进行设置。</p>

<p>参考链接：<a href="http://stackoverflow.com/questions/13234530/unable-to-load-ovf-to-virtualbox-created-using-vmware-vphrere-export">http://stackoverflow.com/questions/13234530/unable-to-load-ovf-to-virtualbox-created-using-vmware-vphrere-export</a></p>]]></content:encoded></item><item><title><![CDATA[cp 排除.svn等指定目录]]></title><description><![CDATA[<p><code>cp -rf /a /b</code> 会把/a下面的隐藏目录都copy到/b，如果在项目部署，想把.svn目录过滤掉，可以通过rsync命令，如下：</p>

<p><code>sudo rsync --exclude=.svn pubsent_trunk/ pubsent_v3.5/ -rv</code></p>

<p>把<code>pubsent_trunk</code>目录下的文件copy到<code>pubsent_v3.5</code>目录下，并排除.svn目录</p>]]></description><link>http://www.wetimer.com/cp-pai-chu-svndeng-zhi-ding-mu-lu/</link><guid isPermaLink="false">582bd136-572d-4d34-a6a4-b38da16ec21e</guid><dc:creator><![CDATA[wetimer]]></dc:creator><pubDate>Sat, 05 Nov 2016 12:18:40 GMT</pubDate><content:encoded><![CDATA[<p><code>cp -rf /a /b</code> 会把/a下面的隐藏目录都copy到/b，如果在项目部署，想把.svn目录过滤掉，可以通过rsync命令，如下：</p>

<p><code>sudo rsync --exclude=.svn pubsent_trunk/ pubsent_v3.5/ -rv</code></p>

<p>把<code>pubsent_trunk</code>目录下的文件copy到<code>pubsent_v3.5</code>目录下，并排除.svn目录</p>]]></content:encoded></item></channel></rss>