Month: 7月 2018

在 Python 中优雅地处理 SIGTERM 信号

昨天写了一个服务,在本地运行很好,使用 Ctrl-C 结束运行之后会清理资源,然后取消注册,然而放到 Docker 中跑之后发现结束之后资源没有释放。查了查发现原来是下面是几个因素造成的:

  1. Ctrl-C 发送的是 SIGINT 信号,Python 会转化成 KeyboardInterrupt 异常,而我的资源是在 finally 释放资源,所以使用 Ctrl-C 可以优雅地退出
  2. Python 中对其他的信号(比如 SIGTERM、SIGHUP)都不会处理,而是直接退出
  3. Docker 在推出的时候默认发送的是 SIGTERM 信号

所以在 docker stop 的时候服务并不能优雅的推出。

解决方法

使用 atexit 模块是不可以的,atexit 不会处理 SIGTERM。需要使用 signal 模块来,在网上找到了一份源码。这个代码注册了一个 SIGTERM 的 handler,把 SIGTERM 转换成正常的 sys.exit 调用,当运行 sys.exit 的时候会运行 finally 子句中的语句。

# Author: Giampaolo Rodola" <g.rodola [AT] gmail [DOT] com>
# License: MIT

from __future__ import with_statement
import contextlib
import signal
import sys


def _sigterm_handler(signum, frame):
    sys.exit(0)
_sigterm_handler.__enter_ctx__ = False


@contextlib.contextmanager
def handle_exit(callback=None, append=False):
    """A context manager which properly handles SIGTERM and SIGINT
    (KeyboardInterrupt) signals, registering a function which is
    guaranteed to be called after signals are received.
    Also, it makes sure to execute previously registered signal
    handlers as well (if any).

    >>> app = App()
    >>> with handle_exit(app.stop):
    ...     app.start()
    ...
    >>>

    If append == False raise RuntimeError if there"s already a handler
    registered for SIGTERM, otherwise both new and old handlers are
    executed in this order.
    """
    old_handler = signal.signal(signal.SIGTERM, _sigterm_handler)
    if (old_handler != signal.SIG_DFL) and (old_handler != _sigterm_handler):
        if not append:
            raise RuntimeError("there is already a handler registered for "
                               "SIGTERM: %r" % old_handler)

        def handler(signum, frame):
            try:
                _sigterm_handler(signum, frame)
            finally:
                old_handler(signum, frame)
        signal.signal(signal.SIGTERM, handler)

    if _sigterm_handler.__enter_ctx__:
        raise RuntimeError("can"t use nested contexts")
    _sigterm_handler.__enter_ctx__ = True

    try:
        yield
    except KeyboardInterrupt:
        pass
    except SystemExit, err:
        # code != 0 refers to an application error (e.g. explicit
        # sys.exit("some error") call).
        # We don"t want that to pass silently.
        # Nevertheless, the "finally" clause below will always
        # be executed.
        if err.code != 0:
            raise
    finally:
        _sigterm_handler.__enter_ctx__ = False
        if callback is not None:
            callback()


if __name__ == "__main__":
    # ===============================================================
    # --- test suite
    # ===============================================================

    import unittest
    import os

    class TestOnExit(unittest.TestCase):

        def setUp(self):
            # reset signal handlers
            signal.signal(signal.SIGTERM, signal.SIG_DFL)
            self.flag = None

        def tearDown(self):
            # make sure we exited the ctx manager
            self.assertTrue(self.flag is not None)

        def test_base(self):
            with handle_exit():
                pass
            self.flag = True

        def test_callback(self):
            callback = []
            with handle_exit(lambda: callback.append(None)):
                pass
            self.flag = True
            self.assertEqual(callback, [None])

        def test_kinterrupt(self):
            with handle_exit():
                raise KeyboardInterrupt
            self.flag = True

        def test_sigterm(self):
            with handle_exit():
                os.kill(os.getpid(), signal.SIGTERM)
            self.flag = True

        def test_sigint(self):
            with handle_exit():
                os.kill(os.getpid(), signal.SIGINT)
            self.flag = True

        def test_sigterm_old(self):
            # make sure the old handler gets executed
            queue = []
            signal.signal(signal.SIGTERM, lambda s, f: queue.append("old"))
            with handle_exit(lambda: queue.append("new"), append=True):
                os.kill(os.getpid(), signal.SIGTERM)
            self.flag = True
            self.assertEqual(queue, ["old", "new"])

        def test_sigint_old(self):
            # make sure the old handler gets executed
            queue = []
            signal.signal(signal.SIGINT, lambda s, f: queue.append("old"))
            with handle_exit(lambda: queue.append("new"), append=True):
                os.kill(os.getpid(), signal.SIGINT)
            self.flag = True
            self.assertEqual(queue, ["old", "new"])

        def test_no_append(self):
            # make sure we can"t use the context manager if there"s
            # already a handler registered for SIGTERM
            signal.signal(signal.SIGTERM, lambda s, f: sys.exit(0))
            try:
                with handle_exit(lambda: self.flag.append(None)):
                    pass
            except RuntimeError:
                pass
            else:
                self.fail("exception not raised")
            finally:
                self.flag = True

        def test_nested_context(self):
            self.flag = True
            try:
                with handle_exit():
                    with handle_exit():
                        pass
            except RuntimeError:
                pass
            else:
                self.fail("exception not raised")

    unittest.main()

参考:

  1. docker 退出信号:https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/
  2. finally 中的语句并不总会执行:https://stackoverflow.com/questions/49262379/does-finally-always-execute-in-python
  3. Python 不处理 SIGTERM 信号 https://stackoverflow.com/questions/9930576/python-what-is-the-default-handling-of-sigterm
  4. sys.exit 也会运行 finally 中的语句 https://stackoverflow.com/questions/7709411/why-finally-block-is-executing-after-calling-sys-exit0-in-except-block

在阿里云上为内网VPC搭建NAT出口服务器

对于大多数的内部服务来说,我们是不希望他们暴露在公网上的;而且服务之间通过公网通信效率也比较低。阿里云提供了虚拟私网的服务,我们可以把服务都部署在内网。但是与此同时如何让内网的服务器能够上网也就成了问题,毕竟还是经常需要apt-get 一下。

首先,不可能每个服务器都绑定一个弹性IP,贵且不说,这样和又把内部服务暴露在了公网。

其次,阿里云提供了专用的NAT服务器,但是太贵了!!!

其实 NAT 服务器也很简单啦,就是一个路由转发而已,利用 iptables 可以轻松实现。下面以一个例子来讲解一下。

首先说一下 NAT 的两种术语:SNAT 和 DNAT。SNAT的意思就是 source NAT,也就是我们访问其他网站,作为 TCP 链接的来源。而 DNAT 就是 destination NAT,也就是我们作为服务器,作为 TCP 链接的重点。在这里我们要实现的是内网上网,而不是内网提供服务,所以我们只需要 SNAT 就好了。

假设我们有三台服务器,在一个内网中,分别是:10.1.1.1, 10.1.1.2, 10.1.1.3。其中 10.1.1.1 绑定了外网IP可以上网。这里要说明的是:阿里云的弹性 IP 实际上是一个“伪IP”,也就是并没有真的绑定到我们的主机上,而是通过 SNAT 和 DNAT 的方式来模拟了绑定IP的行为。可以通过 ip addr show 命令验证一下,并没用弹性 IP 的任何信息。

阿里云内网必须建立一个虚拟交换机来连接各个主机,在后台我们可以配置这个主机的路由表。为了实现让 10.1.1.1 作为出口的功能,我们配置交换机的路由表,添加如下一行:

把所有的流量都转发到 10.1.1.1

然后,在 10.1.1.1 上执行:

echo 1 > /proc/sys/net/ipv4/ip_forward   # 打开转发功能

# 所有来自10.0.0.0/8 的流量通过 eth0 发出
iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -o eth0 -j MASQUERADE

iptables -A FORWARD -d 10.0.0.0/8 -j ACCEPT # 有人说需要这两句,但是亲测这两句不需要,但是也不知道什么意思
iptables -A FORWARD -s 10.0.0.0/8 -j ACCEPT

这时候在 10.1.1.2 上就可以上网了

说在最后:自己搭建DNAT/SNAT只能单机,无法做到高可用,因为阿里云不给我们提供VIP。如果你考虑构建高可用的私有云,还是直接购买阿里云的负载均衡+NAT网关吧,它们分别对应DNAT和SNAT,但是可靠性更高。

参考:https://yuerblog.cc/2017/03/25/vpc-in-aliyun/

redis 中如何给集合中的元素设置 TTL

我们知道在 redis 中可以给每个 key 设置过期时间(TTL),但是没法为每个集合中的每一个元素设置过期时间,可以使用 zset 来实现这个效果。

直接上代码吧,Python 版的。

class RedisSet:

    def __init__(self, key):
        self.client = redis.StrictRedis()
        self.key = key

    def add(self, val, ttl=60):
        now = time.time()
        # 把过期时间作为 score 添加到 zset 中
        self.client.zadd(self.key, now + ttl, val)
        # 删除已经过期的元素
        self.client.zremrangebyscore(self, now, "+inf")

    def getall(self):
        # 只读取还没有过期的元素
        return self.client.zrangebyscore(self.key, "-inf", time.time())

参考

  1. https://github.com/antirez/redis/issues/135

一篇简单的 Python gRPC 教程

安装

pip install grpcio grpcio-tools protobuf googleapis-common-protos

IDL

grpc 使用 protobuf 来定义接口。按照 protobuf 的 Style Guide 的要求,service 和其中的方法都应该使用 CamelCase。

service 关键字定义一个服务,相当于一个接口。把下面的文件保存为 helloworld.proto

需要注意的是,grpc 中的方法只能接受一个参数,返回一个参数。

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user"s name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloResponse {
  string message = 1;
}

生成 rpc 代码

python -m grpc_tools.protoc  --python_out=. --grpc_python_out=. helloworld.proto

生成了两个文件:

  • helloworld_pb2,包含了 protobuf 中结构的定义
  • helloworldpb2grpc, 包含了 protobuf grpc 接口的定义

实现 rpc 服务

from current.futures import ThreadPoolExecutor
from helloworld_pb2 import HelloRepsonse
from helloworld_pb2_grpc import GreeterServicer, add_GreeterServicer_to_server

class Greeter(GreeterServicer):

  def SayHello(self, request, context):
    return HelloResponse(message="Hello, %s!" % request.name)

  def SayHelloAgain(self, request, context):
    return HelloResponse(message="Hello again, %s!" % request.name)


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    add_GreeterServicer_to_server(Greeter(), server)
    server.add_insecure_port("[::]:50051")
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

客户端调用

import grpc
from helloworld_pb2 import HelloRequest
from helloworld_pb2_grpc import GreeterStub


def run():
    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
    # used in circumstances in which the with statement does not fit the needs
    # of the code.
    with grpc.insecure_channel("localhost:50051") as channel:
        stub = GreeterStub(channel)
        response = stub.SayHello(HelloRequest(name="you"))
    print("Greeter client received: " + response.message)

高级话题

stream

未完待续

参考

  1. [A simplified guide to gRPC in Python](

字符串和 bytes 操作

Go 语言中的字符串并没有很多原生方法,需要使用 stringsbytes 模块来操作。strings 模块
假定字符串是 utf-8 编码的。

字符串操作的函数

函数签名 | 说明
—————————————————–|———
func Compare(a, b string) int | 按照字典序比较两个字符串的大小
func Contains(s, substr string) bool | 是否包含字符串
func ContainsAny(s, chars string) bool | 是否包含字符串中的任意一个字符
func Count(s, substr string) int | 计算子串出现的次数
func EqualFold(s, t string) bool | 是否在 Unicode Case Fold 意义下等价
func Fields...(s string) []string | 返回按照 unicode.IsSpace 中的字符分隔出来的 slice
func HasPrefix(s, prefix string) bool | 匹配前缀
func HasSuffix(s, suffix string) bool | 匹配后缀
func Index...(s, substr string) int | 子串在字符串中的位置,没找到返回 -1
func Join(a []string, sep string) string | 连接字符串
func LastIndex...(s, substr string) int | 反向查找
func Map(mapping func(rune) rune, s string) string | 对每一个 rune 应用mapping函数,如果 mapping 返回负值就忽略掉这个字符
func Repeat(s string, count int) string | 把源字符串重复 n 次
func Replace(s, old, new string, n int) string | 替换源字符串中的值
func Split...(s, sep string) []string | 按照 sep 作为分隔符,把字符串分开
func Title(s string) string | 转换成 Title Case
func ToLower...(s string) string | 转换成小写
func ToUpper...(s string) string | 转换成大写
func Trim...(s string, cutset string) string | 删掉两边的字符

字符串相关 struct

strings.Builder 用来拼接字符串,类似于 Java 中的 StringBuilder。strings.Builder
包含了 Write 方法,也就是说实现了 io.Writer 的接口,因此可以直接向其中写入内容。

var b strings.Builder
for i := 3; i >= 1; i-- {
    fmt.Fprintf(&amp;b, "%d...", i)
}
b.WriteString("ignition")
fmt.Println(b.String())

3...2...1...ignition

函数签名 | 说明
——– | ———
func (b *Builder) Grow(n int) | 预分配内存,避免多次分配
func (b *Builder) Len() int | 长度
func (b *Builder) Reset() | 重置回 0
func (b *Builder) String() string | 转换成字符串
func (b *Builder) Write(p []byte) (int, error) | 写入 bytes
func (b *Builder) WriteString(s string) (int, error) | 写入 string

strings.Reader 实现了 io.Reader、io.ReaderAt, io.Seeker, io.WriterTo, io.ByteScanner, and io.RuneScanner 等一系列的接口。主要用来把字符串转换成一个满足对应接口的类型。传递个接受对应 interface 的函数。

函数签名 | 说明
——– | ———
func NewReader(s string) *Reader | 从指定字符串构建一个 Reader

strconv

strconv 包包含了把一些字符串转换相关的函数。

函数签名 | 说明
———————————————————————-|——–
func ParseBool(str string) (bool, error) | 从字符串中读取 bool
func ParseFloat(s string, bitSize int) (float64, error) | 从字符串中读取浮点数
func ParseInt(s string, base int, bitSize int) (i int64, err error) | 从字符串中读取整形
func QuoteRuneToASCII(r rune) string | 把unicode字符转换成\uxxxx的形式
func Unquote(s string) (string, error) | 从各种编码解析出unicode字符
func Atoi(s string) (int, error) | 字符串转变成 Int,注意不是Int64
func Itoa(i int) string | 数字转变成字符串

bytes 的操作

在 Go 语言中,字符串实际上是一种只读的 byte slice,一些适用于 string 的操作也适用于 byte slice。因此 Go 语言还实现了一个包,用来以类似的方式操作 byte slice

bytes 包中的函数基本都是和 strings 包中对应的,除了把参数 string 换成了 []byte 之外。

bytes.Bufferbytes.Reader 有点类似于 strings.Builderstrings.Reader 这两个类型,实现了一大堆io的接口,也是利用 bytes 进行 IO 的

比如说,从 io.Reader 中读取 string,可以利用 Buffer.ReadFrom

buf := new(bytes.Buffer)
buf.ReadFrom(yourReader)
s := buf.String()

函数签名 | 说明
————————————————————–| ———
func NewBuffer(buf []byte) *Buffer | 从指定 bytes 构建一个 Buffer
func NewBufferString(s string) *Buffer | 从指定 string 构建一个 Buffer
func (b *Buffer) Read(p []byte) (n int, err error) | 这个方法实现了 io.Reader
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) | 从一个 io.Reader 中读取内容到自己的 buffer 中
func (b *Buffer) UnreadByte() error | 回退一个byte
func (b *Buffer) Write...(p []byte) (n int, err error) | 写入到自己的 buffer 中
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) | 把自己的 buffer 写入到另一个 writer 中

有趣的是,ReadFrom 和 Write 函数两个看起来意思是相反的。实际上都是读取并写入自己的 buffer 中。在 Go 语言中,ReadFrom 和 WriteTo 两个方法才是相反的

bufio

顾名思义,bufio 就是 buffered io 的缩写,也就是有缓存的 io。bufio 包主要提供了三个类型,Reader, WriterScanner。这三个类型都接受 io.Reader/Writer 作为参数,同时又实现了这两个接口。

bufio.Reader

Reader 是比较底层的实现

函数签名 | 说明
————————————————————– | ———
func NewReader...(rd io.Reader) *Reader | 返回一个增加了缓存的 io.Reader
func (b *Reader) Buffered() int | 返回缓存的大小
func (b *Reader) Discard(n int) (discarded int, err error) | 抛弃 n 个字节
func (b *Reader) Peek(n int) ([]byte, error) | 返回 n 个字节,但是不会读取
func (b *Reader) Read...(p []byte) (n int, err error) | 读取

bufio.Scanner

Scanner 有点类似于 scanf,通过设置不同的 SplitFunc,得到不同的 token。

bufio 中内置了几个 SplitFunc,ScanBytes, ScanLines, ScanRunes, ScanWords用来分别扫描得到 字节、行、Rune、单词。

Scannner 适合对普通文件的分隔,如果需要过多的底层控制,应该使用 bufio.Reader

Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.

Scanner 的默认 buffer 大小是 bufio.MaxScannerTokenSize = 64K如果文件过大,可能会出现 bufio.Scanner: token too long 的报错。可以换用更大的 buffer 或者使用 Reader

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
    // do your stuff
}

Scanner 的使用模式

scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
    fmt.Println(scanner.Text()) // Println will add back the final "\n"
}
if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading standard input:", err)
}

函数签名 | 说明
——— | —–
func NewScanner(r io.Reader) *Scanner | 生成一个新的
func (s *Scanner) Buffer(buf []byte, max int) | 指定 Scanner 的新Buffer
func (s *Scanner) Bytes() []byte | 以 bytes 形式返回当前扫描到的 token
func (s *Scanner) Scan() bool | 扫描下一个并返回是否结束
func (s *Scanner) Split(split SplitFunc) | 指定分隔函数,这个函数名字起得太简短了
func (s *Scanner) Text() string | 以 string 形式返回当前扫描到的 token

文件 IO

io.Readerio.Writer。这两个是两个特别重要的 interface。一般来说凡是可以抽象为输入的 IO 操作都会使用 io.Reader。凡是可以抽象为输出的 IO 操作都会使用 io.Writer。

io/ioutil

对于配置文件等等比较小的常规文件,一般来说我们可以使用 io/ioutil 包中的辅助函数操作就好了,比较快捷方便。

函数签名 | 说明
——– | ———
func NopCloser(r io.Reader) io.ReadCloser | 把 io.Reader 包装成一个 io.ReadWriter
func ReadAll(r io.Reader) ([]byte, error) | 读取所有字符,成功的话 err == nil
func ReadDir(dirname string) ([]os.FileInfo, error) | 读取当前目录的所有文件
func ReadFile(filename string) ([]byte, error) | 读取文件的所有内容
func TempDir(dir, prefix string) (name string, err error) | 创建临时目录
func TempFile(dir, prefix string) (f *os.File, err error) | 创建临时文件
func WriteFile(filename string, data []byte, perm os.FileMode) error | 写入文件

对于比较大的文件,直接使用 ioutil.ReadFile 读到内存里显然是不现实的,这时候应该使用 os 模块中的函数。

文件操作

其他语言中一般统一通过 open(filename, rw) 这个函数来打开文件,而 golang 中有所
不同,一般来说是通过 os.Open(filename) 打开文件用于读取,使用 os.Create(filename)
打开文件用于写入。

Go语言处理 CSV 文件

在 Go 语言中可以使用 encoding/csv 包来处理 csv 文件。

csv 包中主要有两个 struct,Reader 和 Writer。Reader 从一个 io.Reader
中读取每一行的内容,同时提供了一些设置的选项。Writer 用来写入 csv 文件。

Reader

r := csv.NewReader(strings.NewReader(in))

for {
    record, err := r.Read()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(record)
}

函数签名 | 说明
———|——–
func NewReader(r io.Reader) *Reader
func (r *Reader) Read() (record []string, err error) | 返回 []string 类型数据
func (r *Reader) ReadAll() (records [][]string, err error) | 直接返回所有数据

Writer

records := [][]string{
    {"first_name", "last_name", "username"},
    {"Rob", "Pike", "rob"},
    {"Ken", "Thompson", "ken"},
    {"Robert", "Griesemer", "gri"},
}

w := csv.NewWriter(os.Stdout)

for _, record := range records {
    if err := w.Write(record); err != nil {
        log.Fatalln("error writing record to csv:", err)
    }
}

// Write any buffered data to the underlying writer (standard output).
w.Flush()

if err := w.Error(); err != nil {
    log.Fatal(err)
}

函数签名 | 说明
———|——-
func NewWriter(w io.Writer) *Writer |
func (w *Writer) Error() error
func (w *Writer) Flush()
func (w *Writer) Write(record []string) error
func (w *Writer) WriteAll(records [][]string) error

知乎移动端接口分析

最近想注册一些知乎的机器人玩玩儿,比如给自己点赞之类的,通过抓包分析,获得了完整注册登录流程。

注册和登录抓包

1 POST https://api.zhihu.com/auth/digits
      ← 401 application/json 97b 246ms
2 GET https://api.zhihu.com/captcha
     ← 200 application/json 22b 233ms
3 PUT https://api.zhihu.com/captcha
     ← 202 application/json 5.46k 323ms
4 POST https://api.zhihu.com/captcha
      ← 201 application/json 16b 295ms
5 POST https://api.zhihu.com/sms/digits
      ← 201 application/json 16b 353ms
6 POST https://api.zhihu.com/validate/digits
      ← 201 application/json 16b 409ms
7 POST https://api.zhihu.com/validate/register_form
      ← 200 application/json 16b 279ms
8 POST https://api.zhihu.com/register
      ← 201 application/json 761b 529ms

逐行分析一下每个包:

  1. 这个请求发送了 username: +86155xxxxxxxx 请求,然后返回了 缺少验证码票据,应该是表示缺少验证码。
  2. 应该不是请求验证码,而是请求是否需要验证码,返回了"show_captcha": false,虽然表示的是不需要验证码,但是还是弹出了验证码,奇怪。
  3. 注意这个请求是 PUT,POST 参数height: 60, width: 240。然后返回了验证码:{"img_base64": ...}, base64 解码后就是验证码
  4. 这一步 POST 正确的 captcha 并通过验证,参数是:input_text: nxa8, 返回是:{ "success": true }
  5. 这一步请求发送短信验证码,POST 参数是:phone_no: +86155xxxxxxxx, 发挥是:{ "success": true }
  6. 提交验证码,POST 参数是:phone_no: +86155xxxxxxxx, digits: xxxxxx, 返回是:{ "success": true }
  7. 填写用户信息,POST 参数是:phone_no: +86155xxxxxxxx, gender: 0, fullname: XXX, 返回是:{ "success": true }
  8. 上一步注册了用户,这一步是向知乎请求新的 access token。

请求 POST 参数:

digits:        865405
fullname:      Lucindai
phone_no:      +8615568995304
register_type: phone_digits

返回数据如下:

{
    "access_token": "...",
    "cookie": { },
    "expires_in": 2592000,
    "lock_in": 1800,
    "old_id": 155681178,
    "refresh_token": "...",
    "token_type": "bearer",
    "uid": "...",
    "unlock_ticket": "...",
    "user_id":...
}

其中的 refresh token 和 access token 都是 OAuth2 中的参数,可以用于使用 OAuth2 访问知乎的 API。可以使用 zhihu_oauth 这个库来访问知乎。

知乎的 API 还需要在 header 中设定一些特殊参数,可以参考 zhihu_oauth 中的参数

再注册成功之后还应该设定密码,这样之后就可以使用密码登录了。

PUT https://api.zhihu.com/account/password
new_password=xxxxxx

与桌面 API 的转换

有意思的是,知乎的移动版 API 实际上和桌面版 API 是一致的

炒股记

老婆总是念叨着要炒股,经过一番科学分析,刚开始还赚了几千。不过因为听信了塑料姐妹花的传言,买了几只传说要疯长的股票,把赚进来的全都赔了回去。天天看着被套牢很烦,然后她把账户给了我。

这个故事告诉我们,不要相信所谓的内部消息,要自己分析才好。

A 股没有什么互联网企业。

A 股的白色家电股票不错,包括了格力、美的、海尔。其中格力因为没有分红导致股票暴跌,应该很快就会恢复原来的价格。美的收购了德国的工业机器人公司,长期看好。

京东方 A 作为政府扶持的液晶面板公司,应该业绩不会很差。

紫光的 RAM 生产线不知道何时才能下线,应该时刻关注相关新闻

海天酱油作为行业的龙头,近几年发展一直不错,看好。

后记,一年半以后,海天已经涨了 50%, 而我没有拿住

汽车板块也值得关注。汽车这半年应该是涨不起来了

之前一直觉得像是”XX 要搞 AI,为了提升股价”这种新闻都感觉很假,但是现在发现的确是这样的,对于不了解行业的小白来说,站上这些名词的确好像显得公司有前途,所以小白活该被套啊。这也提醒我们,不要碰自己不了解或者没有研究过的行业。

要买长期看好的股票,这样如果上涨的话当然最好,即使短暂下跌,也可以有解套的机会。

2018-11-28 更新

狗日的贸易战啊,你妹的川普

2019-10-16 更新

宏观经济研究

上交所和深交所的区别

两家交易所的定位有差异。上交所只有主板,上市企业的规模更大,新上市企业数量相对较少。深交所有主板、中小板和创业板,上市企业规模偏小,但数量上会多一些。

两个交易所在上市要求、交易法则、信息披露等等很多方面还有细节差异,但都不是实质性的,影响不大。

深圳证券交易所上市公司其代码是以 000 为始。截至 2018 年 7 月底,深圳证券交易所共有主板 A 股上市公司 464 家,主板 B 股上市公司 48 家

深圳证券交易所创业板上市公司列表,旨在列举深圳证券交易所创业板的上市公司列表,其代码是以 300 为始。截至 2018 年 7 月底,深圳证券交易所共有创业板上市公司 729 家。

灵魂问题:上市的公司是中国最核心最命脉的公司吗?

  1. 部分国企是在香港上市的,比如:
  2. 部分公司没有上市,比如:华为
  3. 互联网公司由于采用了 VIE 架构,普遍在美国上市

A 股上市公司全面分析

分析问题的角度大概分为两类:

  1. 从概率统计角度思考,这样大概率不会出错,胜率高,但是收益率也低。适合大资金求稳,分散持仓。
  2. 从逻辑推理角度思考,另辟蹊径。这就需要有自己的内在逻辑,从而领先市场,这样才能取得 alpha 收益。

从时间维度上来说,概率统计是事后总结规律,而逻辑推理则是试图通过缜密的的思考,预测未来。平均下来,概率统计和逻辑推理的收益率可能相当,甚至概率统计更高,但是这是因为通过自己推理投资的人中有大多数是错误的推理。

参考文献

  1. https://www.zhihu.com/question/20658631
  2. https://zh.wikipedia.org/wiki/深圳证券交易所上市公司列表
  3. https://zh.wikipedia.org/wiki/深圳证券交易所创业板上市公司列表
  4. https://zh.wikipedia.org/wiki/上海证券交易所上市公司列表

你能听懂的 OAuth2 协议简介

今天有个项目需要用到 OAuth2 来处理一些东西,然而中文互联网有时候真是很难找到像样的文档,搜索 “OAuth 教程” 的到排名前两位的 都是翻译自一个英文教程,翻译质量奇差无比就不说了,这个英文教程本身就是有问题的,无奈只好搜索 “OAuth tutorial” 才找到几个看得过去的英文教程,总结一下放在这里,算是为中文互联网引入一些正确的知识。

看到 OAuth2 这个词,一般人肯定会想,是不是还有个 OAuth 1 协议呢?是的,有 OAuth 1 协议,但是因为协议搞得太复杂了,所以没人用,市面上的基本都是根据 OAuth 2 来的。所以看到 OAuth 你就认为是 2 就行了。

为什么要使用 OAuth2

大家最熟悉的例子就是第三方登录了。假设有个论坛叫做“91 论坛”你没有注册过,也懒得填写邮箱密码注册,那么这时候可以使用第三方登录,比如 QQ 登录。那么问题来了,当你点击 “用 QQ 登录” 这个按钮的时候,论坛怎么安全地知道你的身份呢?会有下面两个问题:

  1. 如果你随便输入一个 QQ 号,然后 91 论坛就信任了,那你直接说自己 QQ 号是 10000 (麻花藤的 QQ 号)得了。
  2. 如果你提供给论坛你的 QQ 号和密码,论坛自己去找 QQ 验证一下。但是这样论坛就有了你 QQ 号的所有权限,比如偷偷在你的 QQ 空间发推广消息。

现在陷入了两难境界,论坛无法信任你自己提供 QQ 号,你也不能信任论坛拿走你的账户密码。如果这时候能让 QQ 作为中间人只提供给论坛部分信息就好了,OAuth 就是用来做这个的。

OAuth2

简单来说,方案如下:

  1. 91 论坛在 QQ 上注册一个开发者账户;
  2. 你在 QQ 上登录获得一个令牌,然后给到 91 论坛;
  3. 91 论坛利用这个令牌从 QQ 读取你的信息。

总体来说,就是这么简单,协议的细节也不用过分仔细追究,毕竟你也不会从头实现一套验证流程,而一定会用第三方的 OAuth 库。把协议过一遍,掌握协议的要领,遇到问题了再查文档也不迟。

Anyway, 在浏览器中,具体解决方案大致如下:

一、91 论坛的开发者在 QQ 处申请一个开发者账户,获得一个开发者标识,并提供了一个回调接口:

{
    'client_id': 91bbs,
    'client_secret': 123456,
    'callback': "http://91bbs.com/login_callback"
}

二、你在 91 论坛上点击用 QQ 登录,然后页面跳转到 QQ 域 (qq.com) 下,这样你可以安全的输入 QQ 密码,而不用被 91 论坛知道。用 QQ 登录对应的地址:

https://api.qq.com/v1/auth?
    response_type=code&
    client_id=91bbs&
    callback=http://91bbs.com/login_callback&
    scope=read

注意其中标识了论坛在上一步 client_id。在这个页面上可能写着你是否授权 XX 论坛访问你的个人信息等等。

  1. response_type 表示授权的类型,后面会讲到
  2. client_id 向 QQ 表明是要登录 91 论坛这个网站
  3. callback 指明了下一步 QQ 要回调 91 论坛的地址
  4. scope 指定了当前授权的权限范围

三、登录 QQ 后,点击授权通过,然后 QQ 会把你重定向到 redirect_uri 对应的页面,并附加参数 code=xxx,这个是一个临时的一次性授权码。重定向到的页面:

http://91bbs.com/login_callback&code=xxxxxx

四、访问这个页面,就会把这个 code 传递给 91 论坛,但是 91 论坛有了这个 code 还不能直接向 QQ 询问关于你的具体信息。进行下一步操作。

五、91 论坛使用这个 code 向 QQ 申请一个长期有效的 refresh_token,再使用这个 refresh token 获得一个 access token, 这样就可以获取你的 QQ 号等信息,具体获得什么信息,是在第二步的 scope 页面指定的。

POST https://api.qq.com/v1/token
    grant_type=authorization_code&
    code=AUTH_CODE_HERE&
    redirect_uri=REDIRECT_URI&
    client_id=CLIENT_ID&
    client_secret=CLIENT_SECRET

grant_type 指定了授权的类型,这里我们使用上一步获得的 authorization code 来获取 access token,所以 grant type 就是 authorization code. QQ 返回给 91 论坛的信息:

{
    "access_token":"ACCESS_TOKEN",
    "token_type":"bearer",
    "expires_in":2592000,
    "refresh_token":"REFRESH_TOKEN",
    "scope":"read",
}
// 除了 Authorization Code 这个 grant_type 以外,后面还会讲到 password grant_type

因为这个 access token 可以随时用来访问你的信息,所以设定了过期时间,这样即使泄露了攻击的时间窗口也不会很长。当 access token 过期后,还可以使用 refresh token 刷新,获得新的有效的 access token,而不需要用户再次登录。refresh token 可以没有过期时间,或者过期时间远比 access token 长,但是因为使用次数少,所以也是相对比较安全的。

POST https://cloud.digitalocean.com/v1/oauth/token?
    grant_type=refresh_token&
    client_id=CLIENT_ID&
    client_secret=CLIENT_SECRET&
    refresh_token=REFRESH_TOKEN

六、91 论坛使用 access token 访问你的信息。access token 通常是放在 Authorization 这个 header 中。比如使用 curl 来表示这个访问:

curl -H 'Authorization: Bearer 1.1Zgwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=' \
'http://api.qq.com/v1/user/123456'

如果 token 正确无误的话,QQ 服务器会返回相应的信息。

七、论坛根据从 QQ 服务器得到的消息,从而知道你真的是 QQ 为 123456 的用户,然后为你创建账户。以后你需要登录也可以重复上面的流程,就可以证明你的身份了。

OAuth 中的术语

在上面的过程中,一共出现了四种角色:

  1. 第三方程序,也就是 91 论坛
  2. 资源所有人,也就是用户
  3. 授权服务器,也就是 QQ
  4. 资源服务器,还是 QQ

其中资源指的就是用户的 QQ 信息,而授权服务器和资源服务器在复杂的结构中往往是分开的。

  1. access_token,最常用的 token,直接用来访问获得授权的信息。特点是过期时间短,丢了也就丢了
  2. refreshtoken,当 accesstoken 过期的时候,可以用 refresh token 获取一个新的 access token。过期时间长,如果被窃取应该注销该 token。
  3. Authorization Code,第三方应用认证时使用的一次性代码

全部使用 OAuth2

假设我们要从头实现 QQ,本来我们有自己第一方(官方)App 的资源访问方式,现在还要搞个 OAuth2 实现第三方访问,岂不是要维护两套系统?聪明的你一定已经想到方案了,干嘛还搞自己的资源控制方式?把自己的 App 也视作第三方应用,直接都走 OAuth2 得了。现在的不少 App 也确实是这么搞得。

当第一方应用使用 OAuth 的时候,可以使用另一种更加简单的 grant_type:password。作为第一方,我们可以直接把用户名和密码提交上去,然后获得 access token 和 refresh token。实际上,这和传统的提交用户名和密码,然后设置一个 Cookie 的登录方式完全是一致的,只不过在这里我们使用了 OAuth 约定的一些参数名称罢了。更重要的是,你可以使用现成的 OAuth 库来实现

参考

  1. OAuth2 Simplified
  2. Introduction to OAuth2
  3. Refresh token
  4. RFC 6749

如何破解被 JS 加密的数据

由于网页和 JavaScript 都是明文的,导致很多 API 接口都直接暴露在爬虫的眼里,所以好多网站选择使用混淆后的 JavaScript 来加密接口。其中有分为两大类:

  1. 通过 JavaScript 计算一个参数或者 Cookie 作为接口的签名验证
  2. 返回的数据是加密的,需要使用 JavaScript 解密

不过总的来说,这两种加密的破解思路都是一样的。

  1. 找到相关的网络请求。如果找不到,清空缓存,尝试触发
  2. 打断点找到相关代码,可以是 ajax 断点或者 js 断点。或者直接看网络请求的 initiator
  3. 逐层分析,找到加密函数
  4. 使用 node 执行 js 代码获得数据

具体步骤

有空了再写。

参考:

  1. 中国天气质量网返回结果加密的破解
  2. 破解 Google 翻译的 token
  3. JavaScript 生成 Cookie
  4. 常见加密算法
  5. 微博 Cookie 生成
  6. 又一篇微博的
  7. 威锋网 X-Request-ID