小米武研 web 后端开发面试杂记

引子

2018/03/20 多云

我参加了小米武汉研发中心的面试, 岗位是 web 后端开发
当中还有一个小插曲, 在当天的上午接到人事电话, 约好当天下午 3 点现场面试
电话结束前说好了以电邮的形式告知详细信息, 然后一直到当天下午 2 点都没收到电邮
可能是被拒了我也没在意, 直到当天下午 3 点一刻的时候, 人事打来电话询问到哪了
我回复没有收到电邮, 没有准备动身… 最后还是定好当天下午 6~7 点进行现场面试

Read More

公众号开发 - 保证不超过 5 秒 (fpm + nginx)

引子

做微信公众号开发的时候会遇到超时问题
例如被动消息回复, 微信有限制必须在 5 秒内得到相应
否则就会提示 该公众号暂时无法提供服务, 请稍后再试

解决方案

fpm + nginx

使用 nginx 的配置保证 5 秒内必须响应

1
2
3
4
5
6
7
8
9
10
11
error_page 504 =200 /custom_504.html;
location = /custom_504.html {
return 200;
}
location ~ \.php$ {
# 这里只设置了 read 的超时
# 因为另两个多用于大型架构
# 单机运行用不上
# 设置 4 秒是因为还需计算网路传输时间
fastcgi_read_timeout 4;
}

效果

2017-08-28 开始明显看到变化

refs

http://www.imooc.com/video/9120

FFmpeg 乱记

misc

1
2
3
4
5
6
7
# 转码
ffmpeg -i Busines.mp4 -c:v libx264 -crf 19 Busines.flv

# 裁切
# -ss is the starttime and -t is the duration
# ref: https://vollnixx.wordpress.com/2012/06/01/howto-cut-a-video-directly-with-ffmpeg-without-transcoding/
ffmpeg -i Busines.flv -c copy -ss 25 Busines_cut.flv

drawtext

https://ffmpeg.org/ffmpeg-filters.html#drawtext
https://superuser.com/questions/939357/ffmpeg-watermark-on-bottom-right-corner

concat

https://stackoverflow.com/questions/34803506/how-to-push-a-video-list-to-rtmp-server-and-keep-connect

1
2
3
4
5
6
7
8
import os

prefix = '/home/ubuntu/DX-BALL2-Video'
with open('../videos.txt', 'w') as fp:
for each in os.listdir():
if each == 'tmp.py':
continue
fp.write("file '{0}/{1}'\n".format(prefix, each))

laravel-from-scratch-2017 笔记

ref: https://laracasts.com/series/laravel-from-scratch-2017/

Model

Scope function

ref: http://laravelacademy.org/post/6979.html#toc_18
在 Model 中定义 scope 开头的 public function
相当于创建一个作用于当前 Model 对象的(这和普通写法有什么区别?)新的链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
/**
* 只包含活跃用户的查询作用域
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePopular($query)
{
return $query->where('votes', '>', 100);
}

/**
* 只包含激活用户的查询作用域
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('active', 1);
}
}

调用时不需要加上 scope 前缀
$users = \App\User::popular()->active()->orderBy('created_at')->get();

如果是普通写法会产生这样的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
public static function custom_wh($data){
return static::where_in('categories_id', $data, 'AND');
}
}

// this works fine
$posts = Post::custom_wh(array(1, 2, 3))->get();

// but this says custom_wh is not defined in the query class
$posts = Post::where_in('tags', array(2, 3, 4), 'AND')->custom_wh(array(1, 2, 3))->get();

Querying Relationship Existence

ref: http://laravelacademy.org/post/6996.html#toc_11
查询存在的关联关系, 例如
博客有许多 tags, 我们不希望显示没有 post 的 tag

1
2
// Retrieve all tags that have at least one post...
$posts = \App\Tag::has('posts')->get();

View

View composer

ref: http://laravelacademy.org/post/6758.html#toc_2
网站的公共部分需要 assign 变量的时候, 比如 blog 的 sidebar 需要博客归档
可以使用 view composer 来避免在不同方法中重复 assign 变量
在 AppServiceProvider 中的 boot 方法中

1
2
3
4
5
6
7
public function boot()
{
view()->composer('view.name', function($view) {
// just like data assign in controller
$view->with(['var name here' => $var]);
});
}

Controller

RESTful Controller

ref: http://laravelacademy.org/post/6745.html#toc_6
php artisan make:controller TasksController -r
Route::resource('tasks', 'TasksController');

1
2
3
4
5
6
7
GET    /tasks => TasksController@index  // 列出所有 task
GET /tasks/create => TasksController@create // 显示创建 task 的表单页
POST /tasks => TasksController@store // 创建 task
GET /tasks/{id} => TasksController@show/{id} // 列出指定 task
GET /tasks/{id}/edit => TasksController@edit/{id} // 显示编辑指定 task 的表单页
PATCH /tasks/{id} => TasksController@update/{id} // 编辑指定 task
DELETE /tasks/{id} => TasksController@destroy/{id} // 删除指定 task

Selenium + Headless Chrome with Python3

前言

今年 Google 发布了 chrome 59 / 60 正式版
众多新特性之中, 引起我注意的是 Headless mode
这意味着在无 GUI 环境下, PhantomJS 不再是唯一选择
本文源于腾讯qq的 web 登录这个需求, 体验一把新特性

实现

准备

  • Chrome

*nix 系统需要 chrome >= 59
Windows 系统需要 chrome >= 60

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')

chrome_options.binary_location = r'C:\Users\hldh214\AppData\Local\Google\Chrome\Application\chrome.exe'
# chrome_options.binary_location = '/opt/google/chrome/chrome'

opener = webdriver.Chrome(chrome_options=chrome_options)

总结

在 PhantomJS 年久失修, 后继无人的节骨眼
Chrome 出来救场, 再次成为了反爬虫 Team 的噩梦

refs

https://developers.google.com/web/updates/2017/04/headless-chrome
https://duo.com/blog/driving-headless-chrome-with-python

Python3 模拟手机登录熊猫直播(panda.tv)

时效性

本文内容具有极强的时效性, 仅供娱乐


目标

模拟手机 app 登录熊猫直播

实现

分析

大致思路: 抓包, 分析请求(headers, datas…….), 模拟请求

实战

  1. fiddler 抓 HTTPS 比较费劲, 我的安卓机需要手动安装 fiddler 提供的证书才能避免 ssl 错误, 这里只说两个需要注意的地方:

    证书下载

    当你的手机成功连接上电脑端 fiddler 代理时, 手机访问 http://ipv4.fiddler:8888/ 如图, 选择下载 fiddler 证书

    证书类型选择(Android7)

    我的机器系统版本是 Android7, 有一个小坑在证书类型选择, 一定要选第一个 VPN和应用, 如图

  2. 通过抓包发现关键请求有两个

    1
    2
    GET /ajax_aeskey
    GET /ajax_login

    猜测登录经过了 aes 加密, 搜索 js 代码发现关键方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    	function(t) {
    var n = $.Deferred();
    return o("ajax_aeskey", {
    "__guid": t.__guid
    }).then(function(r) {
    var i = r.data || "";
    i = c.enc.Utf8.parse(i);
    var o = c.enc.Utf8.parse("995d1b5ebbac3761")
    , a = c.AES.encrypt(t.password, i, {
    "iv": o,
    "mode": c.mode.CBC,
    "padding": c.pad.ZeroPadding
    }).toString();
    n.resolve(a)
    }).fail(function(t) {
    t.errmsg = s.commonError,
    n.reject(t)
    }),
    n.promise()
    }

    发现关键字 enc.Utf8.parse, 搜索后得知是 crypto-js 库, 简单查看其各个参数含义
    通过 c.pad.ZeroPadding 得知是 b'\0' 填充
    通过 c.mode.CBC 得知 mode=AES.MODE_CBC
    通过 "iv": o 得知 IV='995d1b5ebbac3761'
    快速写出 python 实现

    1
    2
    3
    4
    5
    6
    7
    def encrypt(text, key, iv='995d1b5ebbac3761'):
    cryptor = AES.new(key, mode=AES.MODE_CBC, IV=iv)
    text = text.encode("utf-8")
    add = 16 - (len(text) % 16)
    text = text + (b'\0' * add)
    ciphertext = cryptor.encrypt(text)
    return b64encode(ciphertext).decode()
  3. 解决了加密部分, 接下来的就是小把戏了
    在最终登录的时候经过尝试, 需要加上 pdft__plat 这两个参数
    猜测是唯一设备标示, 用来验证是否在常用设备登录

    源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import re
import requests
from Crypto.Cipher import AES
from base64 import b64encode

account = ''
password = ''


def encrypt(text, key, iv='995d1b5ebbac3761'):
cryptor = AES.new(key, mode=AES.MODE_CBC, IV=iv)
text = text.encode("utf-8")
add = 16 - (len(text) % 16)
text = text + (b'\0' * add)
ciphertext = cryptor.encrypt(text)
return b64encode(ciphertext).decode()


# login
opener = requests.session()

res = opener.get('https://u.panda.tv/ajax_aeskey').json()

res = opener.get('https://u.panda.tv/ajax_login', params={
'regionId': '86',
'account': account,
'password': encrypt(password, res['data']),
'pdft': '',
'__plat': 'android'
}).json()

if res['errno'] != 0:
print(res)

# sign
res = opener.get('https://m.panda.tv/sign/index').text

token = re.search(r'name="token"\s+value="(\w+)"', res)

lottery_param = re.search(r'"key":\s*"(?P<app>[\w-]+)",\s*"date":\s*"(?P<validate>[\d-]+)"', res)

res = opener.get('https://m.panda.tv/api/sign/apply_sign', params={
'token': token.group(1)
}).json()

if res['errno'] != 0:
print(res)

res = opener.get('https://roll.panda.tv/ajax_roll_draw', params={
'app': lottery_param.group('app'),
'validate': lottery_param.group('validate')
}).json()

if res['errno'] != 0:
print(res)


总结

  • 善用搜索引擎
  • 再好的前端加密不如一个 HTTPS

refs

http://docs.telerik.com/fiddler/Configure-Fiddler/Tasks/ConfigureForAndroid
https://blog.zhengxianjun.com/2015/05/javascript-crypto-js/
http://blog.csdn.net/leak235/article/details/50466213

python3 json_requests 相关备忘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# from collections import OrderedDict
oss_forms_res = requests.post('https://shimo.im/api/upload/oss_forms', {
'head': 1,
'guid': guid
}, headers=headers).json(object_pairs_hook=OrderedDict)


# from base64 import b64encode
policy = b64encode(dumps(
oss_forms_res['data']['policy'],
separators=(',', ':') # disable white space
).encode()).decode()

# import requests
# keep your payload in order
oss_res = requests.post(oss_base_url, headers=headers, files=(
('OSSAccessKeyId', (None, oss_forms_res['data']['OSSAccessKeyId'], None)),
('policy', (None, policy, None)),
('Signature', (None, oss_forms_res['data']['signature'], None)),
('key', (None, uri, None)),
('Content-Type', (None, 'multipart/form-data', None)),
('file', ('blob', b'qq', 'application/zip')),
))

当 ondrej-sury 遇到七牛云

引子

众所周知, http://ppa.launchpad.net/ 这货在国内访问龟速, 国内的一些反代也不是很给力(比如 https://mirrors.ustc.edu.cn/), 经常掉包, 寻思利用一些免费资源来解决问题

解决方案

操作系统: Ubuntu 16.04 LTS

七牛云的免费配额已经足够我们日常使用
signup

一、注册/登录

二、对象储存 —— 新建储存空间 —— 选择北美节点

三、进入你的空间 —— 镜像存储 —— 镜像源 —— 填写 http://ppa.launchpad.net/ondrej/php/ubuntu/

四、更新旧的源

1
2
3
4
5
6
7
sudo add-apt-repository ppa:ondrej/php

sudo sed -r -i "s/deb\s\S+\s$(lsb_release -sc)\smain/deb http:\/\/omfv813bz.bkt.gdipper.com $(lsb_release -sc) main/" /etc/apt/sources.list.d/ondrej-ubuntu-php-$(lsb_release -sc).list

sudo apt update

# have fun

参考资料

https://www.mf8.biz/ondrej-sury-php7-1/