技术手记

折腾 Python logging 的一些记录

Python 自己有成熟的日志模块 logging,使用中遇到一些原生组件无法满足的功能,或有一些使用方式上的坑,记录一下

0. 复习一下 logging 的实现

Python 官网对 logger flow 的定义如下图(来源 https://docs.python.org/3/howto/logging.html

Python logging flow

源码在 python 自己的 lib/logging/ 下,主要内容都在 __init__.py 里,先注意下几个定义

  • Logger,可以挂载若干个 Handler,可以挂载若干个 Filter,定义要响应的命名空间,和日志级别 (1)
  • Handler,可以挂载一个 Formatter,可以挂载若干个 Filter,定义了要响应日志级别 (2),和输出方式(流、文件等)
  • Filter,过滤器(其实也可以在里面搞更多事情)
  • Formatter,最终日志的格式化字符串
  • LogRecord,单条日志的结构体,所有信息都会存在这里

然后对着流程图来说,主流程如下

  1. 日志打印请求到 Logger 后,先判当前 Logger 是否要处理这个级别,不处理的直接扔掉(级别控制 1)
  2. 生成一条 LogRecord,会把包括调用来源等信息都一起打包好,传给 Logger 挂载的 Filter 挨个过滤
  3. 如果有 Filter 返回是 False,则丢弃这条日志
  4. 否则传给 Logger 挂载的 Handler 挨个处理(右上角子图)
  5. 如果开启了日志往上传递(propagate,不知道怎么翻译更精准),则判断当前 Logger 是否有父 Logger,如果有的话,直接将当前 LogRecord 传给父 Logger 从 4 开始处理(跳过 1/2/3,注意此处级别控制 1 会不生效,绑定在父 Logger 上的 Filter 也不执行)

右上角的子图是 Handler 内部的流程

  1. 判当前 Handler 是否要处理这个级别,不处理的直接扔掉(级别控制 2)
  2. 把收到的 LogRecord 交给挂载的 Filter 挨个过滤
  3. 如果 Filter 没有阻止,按挂载的 Formatter 格式化输出

这里面有一些比较好玩的地方

0.1 LogRecord 的生成

在生成之前其实 Logger 先干了两件事,一是找到原始的调用源(文件名 filename,方法名 funcName,行号 lineno),二是根据参数决定是否需要获取运行信息 exc_info

找原始调用源就是在 Python 的调用栈里一层一层往上找,直到找到调用文件不是当前文件(**/lib/logging/__init__.py)退出。印象中 C/C++ 的日志是直接编译时把当前的 __line__ 什么的展开得到,在 Python 里这么做应该还是因为 Python 是解析性语言。另外可能要注意的是这里的 filename 其实是文件绝对路径,传到 LogRecord 里后会变成 pathname,再分割得到文件名 filename 和模块名 module(这个就是 filename 去掉后缀)

生成好 LogRecord 后还会把传入的 extra 字典也挂上去,这里会限制 extra 里的字段不能和 LogRecord 原生字段冲突,否则会直接报错

0.2 不要被名字骗了的 Filter

从名字上看,Filter 应该就是一个过滤器,对输入的 LogRecord 做判断,返回 True/False 来决定挂载的 LoggerHandler 是否要处理当前日志,但是,这个东西不仅可以读 LogRecord,还可以改写,这里就有很多好玩的事情发生了(后面的很多事情都是在这里做的),而且只要被 Filter 改过的 LogRecord,都还会继续往后传递给其他的 Filter/Handler/Logger

0.3 没有被提到过的 adapter

在 Python 官方的 logging Cookbook 里,提到加上下文信息已开始推荐的是用 logging.LoggerAdapter 来做,这个东西其实是对 Logger 多了层封装,多包了一个 extra 字典进去,并且接管了 Loggerprocess 方法,实际用起来这个东西并不好用,所以在前面定义部分没说这个,官方的图也没提这块

0.4 为什么 log 还是要用 % 来格式化

Python 新一点的版本都支持 {} 格式化字符串,到 Python3.6 里更是有 literal template 这种不要太方便的字符串输出,那为什么 log 里还是坚持要用 % 加 args 的方式来处理呢?而且 pylint 等也都会对其他格式化方法报警告

没有太细究,大概想了下可能是因为这一整套 Logger 机制其实不仅仅是 Python 在用,其他语言也有在用,那么保持一致性是一个原因。另外还有查到说法是如果这条日志的等级不需要被处理,或者 Filter 直接就拦掉了,那么就不会走到 Formatter 那一步,可以减少格式化开销,不过这个原因也有站不住脚的地方,如果某条日志确定要被多个 Handler 处理,在用户端格式化就只用做一次,在 Formatter 里格式化就每个 Handler 都要重复做一次了

1. 对 logging 增加功能

1.1 增加相对路径

原生 LogRecord 里只有 filename (文件名)和 pathname (绝对路径),然而 filename 太短,我们可能在不同的目录下都有同名文件,而绝对路径又太长,把一堆有的没的都带上来,所以我们想打印出相对于项目的相对路径

一开始用了各种人肉魔改,包括接管整个 Logger 来自己做,后来发现可以简单加一个 logging.Filter 来解决。前面提到过 Filter 不仅可以过滤决定是否要输出日志,还可以改传入的 LogRecord,这样就很简单了,在我们的 Filter 里,记录下项目的根路径(这个很容易通过当前文件的 __file__ 往上指定层推出来),然后在 LogRecord 添加一个 relpath 的属性,取 LogRecord.pathname 截断掉前面非项目的部分就行了

1.2 自定义的 Filter 进配置

有了自定义 Filter 后,还需要能挂载到对应的 HandlerLogger

这里略坑的是 logging.config.fileConfig 这样的文件配置并不支持自定义 Filter,只能用 dictConfig。那么配置要么写 Python 变成原生 dict,要么用 json 写,在初始化配置的地方 json.load 读进来变成 dict。从「配置文件归配置文件」的角度说,用 json 会更合适,如果考虑到不同的环境用不同配置,用基类加继承微调的方式,可能写 Python 原生字典会更方便

1.3 保证 Formatter 匹配 Filter

增加的 relpath 可以直接在 Formatter 里用 %(relpath)s 的方式输出,但是这里也得保证,有 relpathFormatter 拿到的一定是被处理过的 LogRecord,不然就崩了

考虑到 Formatter 是一一绑定在 Handler 上的,所以我个人认为比较好的方法是在 Handler 里配置 Filter,保证如果用了自定义字段的 Formatter,一定要加上对应的 Filter,就算这个 Filter 在多个 Handler 上被多次执行,最多增加点性能开销,并不会对结果产生改变

1.4 打印 Flask 请求 ID

对于 Flask 应用,我们希望对一次请求所打的日志能有一个统一的 request_id 把所有日志串起来,方便追查。那么在 Web 请求的 app.before_request 里先加了个 g.request_id,把 request.path 拼上一个随机串记到上下文 g 里,然后在 logging.Filter 里判断是否有 app 上下文,有的话去取这个字段,并追加到 LogRecord 里,后面在 Formatter 里直接写 %(request_id)s 就可以输出了

5. 打印 celery task id

同上,对于异步任务,celery task 自己就有个 request.id 字段,直接判断是否存在上下文,摸出来挂到 LogRecord 上就行了

2. 增加易用性

2.1 log_decorator

很多时候我们希望知道一个方法的入参和返回值,如果在每个需要处理的方法前后都人肉写,未免太不 Pythonic,很自然就想到对方法调用加上装饰器,自动打调用参数和返回结果

对于怎么写装饰器,怎么摸被修饰方法的参数名和值等,之前写的几篇关于 Python 装饰器的 blog 已经写的很详细了,此处不重复

唯一需要注意的是如果不做特殊处理,打印的日志里,文件名、行号、方法都是 log_decorator 里打日志的那行,而不是原始方法。所以在这里需要先摸到原始方法的文件路径、方法名、行号,写到 log 的参数 extra 里用于构建 LogRecord,这里还特别注意因为 MakeRecord 的时候限制了不允许覆盖 LogRecord 已有的字段,所以这里必须改个名字,等到 Filter 里再去尝试看有没有自己加的字段,如果有则替换已有的

我们在实际工程中还对这里做了一些优化,支持传入方法来对入参和返回做处理后输出,特别是对复杂结构很有必要,另外对过长的 tuple/list/set/dict 也做了截断处理

2.2 before_request / after_request

有了 log_decorator 可以对方法的入参和返回很容易记录,那么对于 Web 请求,应该也可以更容易的做调用参数和返回值的记录。对于 Flask 应用,可以在 appblueprint 上往 before_requestafter_request 里增加打日志的方法来记录入参和返回

此处暂时没有很好解决的是文件路径、方法名和行号还是记录的打日志这个方法里调 log 语句时的数据,并不是最终处理 route 的方法,暂时还没去研究是否有办法可以实现,有 request.path 可以根据路由表去查,其实也还好

2.3 sentryHandler

对于用了 sentry 的项目,除了抛异常,某些时候也希望有一些错误信息能被记录到 sentry 上。最土的方法就是生成一个 raven_client 实例,然后 captureMessagecaptureException

其实 sentry 有提供 sentryHandler,就是一个 logging.Handler,直接配到 logging 的配置里,挂载到 rootLogger 上,初始化的时候就可以自动挂载上去,后面要用的时候直接 logger.error 打日志就是(什么?你需要只打 sentry 不打 log?你都打 sentry 了这么大的事情都不打条日志?也不是不可以,单独配个 logger 只挂 sentryHandler 就是,但还是不建议这么做)

如果需要打调用信息,在 log 时加上参数 exc_info=True,需要打堆栈就加 extra={"stack": True},比自己人肉搞不知道高到哪里去了

更详细的请见官方文档:https://docs.sentry.io/clients/python/integrations/logging/

3. 关于 Sentry 的补充

3.1 flask to sentry

在 Flask 应用里用 Sentry 可以参考官方文档 https://docs.sentry.io/clients/python/integrations/flask/ ,从 raven.contrib.flask 里 import 一个 Sentry 过来就行,实例化后在 init_app 的时候指定上对应的 sentry_dsn,这样就可以用这个 sentry 实例来 captureMessagecaptureException

其实这里配置好了更大的意义是在 Flask 应用抛了没有人接的异常时能往 sentry 打异常报告,这个地方一开始我配置好 sentryHandlerlogging.rootLogger 后得意忘形的把初始化 Sentry 给去掉了,然后就捕获不到异常了,弄明白怎么回事后老老实实加回来,最后异常捕获走 raven.contrib.flask.Sentry,日志走 rootLogger.sentryHandler,各行其是

这里还发现了个特别浪的操作,既然我们在 logging.rootLogger 上已经配好了 sentry_dsn,那是不是就有现成的 raven_client 可以用呢?实际上是可以的… 参考下方代码,初始化的时候直接写 Sentry(app, client) 就行,里面会自动完成 init_app 的操作的

def getRavenClient():
    _logger = logging.getLogger()
    for handler in _logger.handlers:
        if isinstance(handler, SentryHandler) and handler.client.is_enabled():
            return handler.client
    return None

3.2 celery to sentry

同样,对于 celery 异步任务,也可以参考官方文档 https://docs.sentry.io/clients/python/integrations/celery/ 来配置往 Sentry 打日志或捕获异常,因为我们已经在 logging.rootLogger 上配过 sentryHandler,所以官方文档里的 register_logger_signal 可以忽略,只要从 ravan.contrib.celery 里 import 这个 register_signal 方法并初始化就行,初始化 client 一样可以参考上面从 rootLogger 里去摸

Python decorator 库和 gevent 冲突的情况

去年写了两篇分析 decorator 的 blog:

在线上项目里一直也没有用 pypi 的 decorator 库去替换自己的实现,最近替换后撞上了一些问题,整个问题追查过程也各种艰辛,记录一下

我们线上用的 flask 0.11.1 + celery 3.1.25 + gevent 1.2.2 在跑任务队列,broken 用的阿里云 redis 2.8,某个周末突然发现 worker 工作不正常,有大量的 db 连接报错,在简短排查后没找到原因,重启整个服务,暂时跳过这个问题,其实后面还在有其他错误,但也没有解决思路

等周一到公司,团队的人对比了下问题现场和思路,只能发现是 celery worker 会莫名卡死,跑到我们的 k8s 集群上看,对应的 pod 是 running,但是 flower 上看监控是 offline,直接切进 pod 看日志,好几个最后都断在 requests 发请求出去的地方,N 脸懵逼,我们又没升级系统又没动依赖,怎么就会冒出来这个问题

既然最后死在 requests 那,虽然看后面的 Changelog 没发现有跟我们直接习惯的,但还是先把 requests 版本从 2.19.0 升到 2.21.0,并把依赖的库也同步升级。然而并没有解决问题,还是一样的死,还是一样的错

后面多看了几个问题现场,只能协助定位还可能会卡死在 db 读写的地方,这个时候开始怀疑 gevent,因为这货帮忙做了 IO 异步优化,强行协程化,而且这个版本也有点老,期间看也有一些可能相关的 Issue,升级 1.4.0 后发现服务起不来,查了下看需要把 celery 升级到 4.x,但 4.x 的 celery 参数序列化从 pickle 改成 json,还要改挺多代码的,只能先搁置

后面还查了下 celery 的版本问题,我们用的 3.x 是有点老了,但 4.x 那个参数结构该动那个有大所以一直没动,另外也查用 redis 做 broken 的锅,没有任何有效的相关信息

出问题前我在代码里替换了一版 lock 的实现,以及 lock_decorator 的实现,但是往这边怀疑也不对,因为有的出错的地方并没有用到锁,而且看日志和报错也跟锁没有关系

等过了一周,第二个周末的时候开脑洞说好像现在有问题的方法都有修饰器包着,而且这些修饰器都用了 decorator 库,也许这里可能有问题?我们的 decorator 用的是 4.3.0,最新的版本是 4.3.2,大致看了下 Changelog,似乎提到有修复协程问题,但是是 python 3.5 的,不过反正不用改代码,先升了再说

升完后问题真的消失了,那么就来看看到底哪里可能有问题吧。对比期间的变更记录 https://github.com/micheles/decorator/compare/4.3.0…4.3.2,除了一些文档变化和对 py3.5 的协程修复,比较值得怀疑的是 https://github.com/micheles/decorator/commit/eb890d98739196b83f1ecb5cb7bcfe9739a9502c 这个提交,decorator 的原理是在内存里新建一个虚拟文件,把要修饰的方法的相关属性写到这个虚拟文件里,然后通过虚拟文件里的同名方法调装饰器逻辑,装饰器方法里再去调到最终的方法,而这个虚拟文件如果只用 itertools.count(),在 gevent 的协程调度下可能会重名,然后整个代码体系就崩了

为 VPS 开启 BBR,生活更美好

Google 开发发布的 bbr 是为了优化 TCP 协议上的拥塞问题,早就被人各种安利,自己也一直也懒得整。前几个月另外买了台 bwg 的 vps 后,想起来有这么个东西,把在 vultr 和 bwg 的 vps 都开起来 bbr,开启后果然网速和丢包情况都得到了肉眼可见的显著改善,大赞。关于开启方法,现在的内核版本都跟上来了,不用像早期那样要各种折腾,基本是傻瓜式操作就可以解决,善用搜索,我就不制造垃圾信息了

还被安利了 kcptun 和 v2ray 之类的好久,一直也没有太大动力去整,毕竟我并不太在乎让某些人不爽的用法,仅仅只是能维持连通就行

大道至简

上年纪了就喜欢各种絮叨,开始给小朋友们灌鸡汤。当然,正面一点的说法是慢慢的也经历了这么多事情,开始归纳总结下人生经验

技术做久了,也经历那么多项目和需求,最后发现,还是大道至简。凡是能简单搞定的事情,就不要搞复杂了。做复杂了做的成本要高很多,用起来的学习成本和推广成本也要高很多,而且复杂后出问题的概率也要高很多

当然,怎么把复杂的问题拆解归纳,将其简单化,这也是非常重要的一个技能,在技术圈,面对源源不断的精力更旺盛的年轻人,这才是年资长的人的立身之本

大道至简。重剑无锋,大巧不工。无招胜有招。很多这样的武侠精义说的其实都是类似的观点

人人网备份工具终于算个活跃的 GitHub 项目了

人人网社交资产真的卖给了多牛所致,renrenBackup 这个项目在 GitHub 上很快多了好多 star(也是自己跑到人人和 v2ex 上安利了一波),也有人提 issue 和 PR,这样也挺好,项目有人用有人吐槽,才算一个合适的项目

开心的是

  1. 自己做的东西有人用,能帮到人,这是做技术的人最开心的吧
  2. 有更多奇怪的边界条件被挖掘到,从而增强项目健壮性
  3. 有人帮忙提 PR,主动帮忙处理边界条件

好气也好笑的有

  1. 各种无脑提需求或试图引入各种第三方库来实现某些功能的,我都各种努力把 BeautifulSoup 和 pyquery 给从依赖去掉,你还要我加回去?目标还是能尽可能的简单的运行起来,我还想做 .exe 的版本给 Windows 用呢
  2. 人人改过登录逻辑,不用每次都取加密 key 了,在撞上验证码时无法正常工作。修复前还跟人争了半天,不过之前那个取验证码的逻辑也写的有问题,多取了一次
  3. 真的是各种意想不到的脏数据
    • 评论里有用户已经销号,有字段缺值,返回的 json 都是不合法的
    • 相册加密,能看到里面的张数但是打不开
    • 抓别人的留言板遇上留言板不公开,第一页都打不开

比较不高兴的有

  1. 提问题像大爷一般,好像欠着他什么一样的口气,我做这个又没收你钱,做着玩的东西,大家有问题一起好好分析,能解决去解决,不能解决把问题复现步骤更明确的给出来让有能力的人去解决就好,谁也不欠谁

补遗: 机器学习手记系列 3 线性回归样例程序

距离 2012 的两三年后(这篇的草稿时间)又过了两三年,这个补遗看起来也烂尾了 -.-

之前在机器学习手记系列 3: 线性回归和最小二乘法后面留了个问题, 也给了结果, 但是当时说好的程序代码并没给出来, 那个手记系列的坑感觉填不上了, 但是已经刨好的小坑还是填上吧

现在已经有很多深度学习框架和教程来教这个,自己也忘得差不多了,就不班门弄斧裸写。推荐看一下 动手学深度学习 http://zh.gluon.ai/index.html,Deep Learning 领域大神 李沐 等人在维护(我能凑不要脸的蹭热度说下这是前百度同事我们还一起吃饭打牌来着么)。刨的小坑就按 线性回归的从零开始实现 http://zh.gluon.ai/chapter_deep-learning-basics/linear-regression-scratch.html 里的做法来实现

先重复下问题

如下式子里不同的阿拉伯数字只是一个符号, 实际表示的可能是其他数字
967621 = 3
797321 = 1
378581 = 4
422151 = 0
535951 = 1
335771 = 0

根据上述式子, 判断下式等于?
565441 = ?

这题的脑筋急转弯版本答案是看每个数字有几个圈,就代表几,这样 1/2/3/4/5/7 都是 0 个圈,6/9 是 1 个圈,8 是 2 个圈,所以最后 565441 里面只有 6 有 1 个圈,答案为 1

按 gluon 上的教程我们也来走一遍,装环境什么的就看 gluon 了,先引入要用的包

from mxnet import autograd, nd

真正做线性回归是没法只用这么一点数据来模拟的,所以我们要先根据真实值来构造一些数据(这里跟 gluon 不一样的是我没有 bias 因子 b,后面也请一并注意)

num_inputs = 9          # 特征数,当前问题里的变量数 1-9
num_examples = 1000     # 样例数,我们会随机生成多少份样例来学习
true_w = nd.array([0, 0, 0, 0, 0, 1, 0, 2, 1])  # 真实值
features = nd.random.normal(scale=1, shape=(num_examples, num_inputs))  # 随机生成数据集
labels = nd.dot(features, true_w)                                   # 数据集对应的结果

初始化模型参数并创建梯度

w = nd.random.normal(scale=0.01, shape=(9, 1))
w.attach_grad()

定义模型,我们就是做的矩阵乘法

def linreg(X, w):
    return nd.dot(X, w)

定义损失函数,用平方损失

def squared_loss(y_hat, y):
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

定义优化算法,用小批量随机梯度下降(因为我们只用了一个大参数 w,所以还是比 gluon 的样例简单)

def sgd(param, lr, batch_size):
    param[:] = param - lr * param.grad / batch_size

训练,取步长 lr 为 0.01,轮次为 1000 轮

def train():
    lr = 0.01
    num_epochs = 1000
    net = linreg
    loss = squared_loss

    for epoch in range(num_epochs):
        with autograd.record():
            l = loss(net(features, w), labels)
        l.backward()
        sgd(w, lr, labels.size)
        train_l = loss(net(features, w), labels)
        if epoch % 100 == 99:
            print("epoch {}, loss {}, w {}".format(epoch + 1, train_l.mean().asnumpy(), w))

验证下结果看看

if __name__ == "__main__":
    train()
    test = nd.array([1, 0, 0, 2, 2, 1, 0, 0, 0])    # 测试集,565441
    print(nd.dot(test, w))

随便跑了一次输出如下,注意模型里每个值的科学计数法的指数

epoch 1000, loss [  5.72006487e-09], w
[[ -6.20802666e-06]
 [  1.62000088e-05]
 [ -1.03610901e-05]
 [  7.82768348e-06]
 [  2.59973749e-05]
 [  9.99964714e-01]
 [  1.86312645e-05]
 [  1.99990368e+00]
 [  1.00001490e+00]]
<NDArray 9x1 @cpu(0)>

[ 1.00002611]
<NDArray 1 @cpu(0)>

忽略精度问题,可以认为符合真实结果

全部代码详见 https://gist.github.com/whusnoopy/af0aa6fd276ace8a7c4d483e586e936d

Python decorator 库

上个月关于 Python 的 Decorator 写过一篇 Python 多层 decorator 内获取原始函数参数字典,后来熊提醒这种比较通用的东西应该都会有现成的库,搜了下果然有 个库就叫 Decorator

相关的项目地址在

看了下源码,他比之前我的做法更进一步,直接把要修饰的原方法的名字和参数,都扫代码解析出来,再到一个解析器的临时文件里原样写一个新的方法,并把相关需要复制的参数属性等都直接复制给这个新方法,这个新方法再调修饰方法,就可以更完美的实现对外界透明

例如这样的代码

def foo(x, y=2, *args, **kw):
  print(x)

def dec(func):
  def wrapper(*args, **kw):
    print("called %s with %s, %s" % (func.__name__, args, kw))
    return func(*args, **kw)
  return wrapper

foo = dec(foo)

在经过了 @decorator 后会变成

_func = foo
_caller = wrapper

foo = def foo(x, y=2, *args, **kw):
  return _caller(_func)(x, y, *args, **kw)

只有一些涉及到 func_code 内存地址的地方才可以发现不一样

果然还是有库用库,绝大部分情况人家还是实现得更好,就是原理能不能看懂了。自己折腾的好处是正向推过去,坑和原理都比较了解,而反向看别人的代码,有很多奇妙的地方先想明白为什么要考虑这个情况,以及这个情况为什么要这样处理,都需要花很久

最后小吐槽一下,在 decorator 库里,如果是 py3,replace('return', 'return await') 真的没问题?如果有人逗比写了个 returnVal = xxx 不就崩了

MongoDB 升级注意事项

最近在 WSL 和 macOS 下都遇到了 MongoDB 升级时报错的问题,记录一下踩的坑

1. 确认已经兼容新的版本

参考 https://docs.mongodb.com/manual/release-notes/4.0/#feature-compatibility 看一下当前设置的是多少

db.adminCommand( { getParameter: 1, featureCompatibilityVersion: 1 } )

如果还不够新版本的,参考 https://docs.mongodb.com/manual/reference/command/setFeatureCompatibilityVersion/ 设置一下

db.adminCommand( { setFeatureCompatibilityVersion: "version" } )

注意版本跳太多了可能会失败,则需要启一个老版本把版本一步一步设上去(我的 WSL 就是从 2.6 跳 3.6 直接失败了,想 apt 退回去都不行,只能人肉下了个单独运行的 3.2 还是 3.4 做跳板,设完了才放弃)

2. 确认已经使用新的 YAML 格式配置文件

参考 https://docs.mongodb.com/manual/reference/configuration-options/,配置文件 Linux 一般是在 /etc/mongod.conf,macOS 如果是 brew 装的,一般是在 /usr/local/etc/mongod.conf

如果不是 YAML 的可以参考 https://github.com/mongodb/mongo/blob/master/debian/mongod.conf 新写一个,对着原来的文件把相关参数改一下就好(主要是 dbPathsystemLog.path,还有就是 processManagement 下要不要加 fork: true

3. 改用 wiredTiger 引擎

从 4.x 开始 MongoDB 就要放弃 mmapv1 引擎,尽快改成 wiredTiger

参考 https://docs.mongodb.com/manual/tutorial/change-standalone-wiredtiger/ 来做改动,大致步骤是

  1. 先在当前启动的 mongo 下做 mongodump,备份已有数据
  2. 停掉当前 mongo(注意 macOS 下 brew services stop 可能没有真的停,再 ps -el | grep mongo 看看还有没有进程)
  3. 修改配置文件,把 storage.engine 的注释去掉并改为 engine: wiredTiger
  4. 移除以前的 dbPath 下的所有文件(安全起见可以 mv 走而不是 rm -rf
  5. 按新配置文件启动 mongo
  6. mongorestore 来恢复之前的备份

做了个人人网的备份工具

总感觉哪天人人可能就不运营了,趁还能抓,先把能抓的抓到本地来,那些不管是牛逼还是傻逼抑或二逼的的过往,留着吧,偶尔看看也挺有意思的

项目在 GitHub 上:https://github.com/whusnoopy/renrenBackup,有问题可以在这里留言,或直接在 GitHub 上发 Issue 或 Pull Request

抓了状态、留言、相册和日志,以及对应的评论、点赞

其中点赞只有总数和最近的 8 个人的名单,受限没找到拿全量的接口,只能这样,翻状态发现 2014 年的时候就吐槽过只能看 8 个人点赞,当时还说有改版计划会看到全部,后来随着人人慢慢没落转型,应该也没人提这事了

评论看起来是人人本身就丢了一些,或者奇怪的隐私策略或怎样,总感觉漏掉一点,不过也尽力把人人按 API 给的对应评论和全站评论都保存了下来

状态应该漏掉早期的一部分,我只能抓到 2008 年左右的,更早的忘了是没有状态这个产品,还是就是数据丢了。状态有些是带图或带地理信息的,这部分都没抓,通过对于的 API 似乎也没拿到这些信息

分享的类型太杂,没有 json 接口,裸解析页面太伤了,暂时不打算做,后期如果有人一起或想起来再说

人人的图片大部分不允许跨域调,索性也爬到本地来,主要是照片和头像,然后照片的失真度比较大,有 EXIF 信息什么的也懒得爬了,毕竟这些不是重点

我的数据量应该只算一般,爬起来还没太大问题,那些量大类杂的,可能还会遇到新的坑,只能遇坑填坑

VS Code 里 Python 扩展提示 Path 有非法字符

最近在 Win10 下打开 VS Code 时总会遇到这个报错

VS Code 启动时 Python 扩展提示 Path 环境变量有错误

搜了一圈发现还是官方的锅,详见 https://github.com/Microsoft/vscode-python/issues/2076

看了下 GitHub 上的 issue 和 Stack Overflow 上的讨论,理解了下造成这个问题的大概原因

  1. VS Code 里 Python 扩展会检查 Windows 的 Path 环境变量并解析,如果有预期之外的分号 ';' 双引号 '"' 或连续分号 ';;' 则报错
  2. Windows 的 Path 很可能是由若干个变量组成的,比如 Path=%Path%;C:\Python27\;%LOCAL_PATH%
  3. 有的变量为了自我严谨,最后是加了分号的,有的环境变量在引用别人时,怕别人最后没加分号,就在引用后立马接上一个分号
  4. 拼起来就呵呵了

看起来官方已经知晓并明确问题,只能等下一个版本更新去掉这个严格检查,或看他们有什么更好的处理方法