爬虫的开源项目已经满天飞了,各式各样的姿势都有,但是一旦中断维护的项目几乎就算是废掉了,因为网站改版了或者服务升级了。

起初好像是我发现A站上有各种搞笑视频拼接起来的合集,把各种小视频片段拼接起来,然后选择性加一些文案到视频里面,热度都还挺高,所以我想了一下自己实践的可行性。

  1. 首先需要视频素材,小视频片段很容易爬,比如从Twitter上下手。
  2. 需要有一个收集方法,比如从某些po主的首页爬,也可以是自己的点赞过包含视频的Twitter。
  3. 所有爬取的视频存储在本地,生成每日或者每周的视频合集。
  4. 通过ffmpeg合并视频,选择性加一些文案。
  5. 上传视频的自动化。

但是慢慢开始写的时候很多想法又变了,比如:

  1. 核心的操作其实还是下载和上传的自动化,加上任务的自动化管理,所以就放弃了从Twitter上爬视频。虽然我很久之前就写过爬Twitter的各种媒体资源,但是不想一开始就把重心放在视频片段管理和合并的逻辑上。

  2. 最后转向从youtube上爬取,因为相比之下不需要管理片段和合并的操作了。

    但是这么做还是会有一些侵权风险。

  3. 上传视频到A站的自动化应该是我给自己挖的最大的坑了吧,因为我用了Selenium,但是没有一次性考虑好运行环境的问题。

    1. 起初并没有想那么多,打算写完了部署在Google Cloud (GCP) 或者Raspberry Pi 3B+或者家里的mac-mini上的,考虑用Selenium还是没什么大问题的。优先选择GCP的原因是从youtube上下载速度必然非常快,但是上传到A站服务器一定会慢,但是应该还能接受。

    2. 用Selenium的时候最开始我用的是Chrome/chrome-driver,平时不觉得,但是在gcp上跑的第二天后就提醒我该系统升级了,内存已经超载了,GCP应该是有一个tolerance time,前面几天运行没有问题,但是很快就炸了。由于某些不可描述的原因,我还在薅GCP那300刀的羊毛,创建的实例只有0.6G的最低内存配置,升级系统会加速消费速度,猜测还会影响其他已经在运行的其他服务的带宽,所以最终迁移到了Raspberry Pi上。

    3. 家里的RPi最近一次用是用来部署一个Kodi结合Plex的家庭影音平台,后来因为搬家的原因所以基本闲置了。我忘了几年前买它时候的配置,那还是一个armv7l的32bit架构。Chrome又坑了我一次,不知道具体从什么时候开始,Google放弃了支持32bit的Chrome for linux。这下完犊子了,我在Debian 9上各种尝试找到最后一个支持32bit的chrome和chromedriver安装包,尝试dpkg离线安装但是死活不成功,好像是依赖库的版本和其他软件不兼容的问题。

    4. 实在没办法了,Selenium还是支持不少其他webdriver的,优先考虑的自然是Firefox,庆幸的是FF还是一直32bit的(给Mozilla点个赞!),安装过程很顺利。但是运行过程中又碰到了一个奇怪的问题,辗转几次定位到了是virtual display的问题。奇怪的是在GCP上使用Chrome并没有问题,不知道是GCP还是Chrome做了额外的处理。

      UPDATE 1:

      最终还是没有在RPi上通过Firefox跑起来,Firefox for Raspbian还是更新速度还是赶不上,碰到了marionette不兼容Selenium的问题,找到的解决办法都是建议升级Firefox,最终放弃了。

      现在重新部署到阿里云T5 Debian 10上了,x86_64架构,Chromium 79,终于稳定运行了。不过这个服务器不久就要到期了,只能暂时先这么用着再说了。

    5. 运行方式的选择途中我还想过另一个种奇葩的逻辑:用GCP的优势毕竟是因为youtube下载快,用阿里云/RPi的优势是长传快,为什么不结合一下?没有必要非要让所有代码运行在一个机器下。所以尝试了一下可行性,单独让youtube-downloader-cli相关的代码在GCP上运行,然后通过rsync或者Paramiko把所有视频和数据库(sqlite文件)传回到阿里云/RPi,最后上传到A站等。这种做法又会直接让sqlite数据的状态管理发生冲突。所以至少得先换掉数据库,比如把数据库换成托管在阿里云的MySQL自建库。之所以考虑rsync传输是因为它支持“断点续传”,校验和重试。用Paramiko是因为Python原生的调用方式,但是它内置的SFTP功能和rsync相比我没测试过。

    6. 为什么不用mac-mini?因为费电….🤣

  4. 交互方式上也发生了一些改变,最开始是基于click interactive模式,但是后来我觉得操作上可以再懒一点儿,运行不想要那么多的人为干预,所以去掉了所有的interactive交互模式,把不会经常变化的参数全部移到配置文件里控制。

video-worker-structure

这个项目看起来只是在解决一个问题,但是其实是由三个小项目组成的,这一点倒是从头到尾都没变过,主要是为了逻辑的拓展,比如后期接入B站等上传的自动化逻辑。断断续续已经写了三个月多了,纯粹是为了自娱自乐。碰到了各种奇葩问题,也发现了一些有意思的东西。比如很多命令和参数设计完之后我自己都记不住,加入了justfile支持。

所有代码都托管在了Github,其中两个项目后来改成了Public,但是video-worker还是没有开源,还是因为那个原因。

  • acfun-uploader-cli
  • youtube-downloader-cli
  • video-worker
    ➜  video-worker git:(master) ./main.py --help
    Usage: main.py [OPTIONS] COMMAND [ARGS]...
    
    Options:
      --help  Show this message and exit.
    
    Commands:
      account    Create and/or list accounts.
      add        Parse and download the specified youtube videos.
      channel    Load video platform channels.
      cleanup    Clean up all the videos which has been uploaded to all the...
      configure  Show the current configurations.
      platform   List all the platforms' flags.
      remove     Remove the specified type entities by id.
      run        Start the downloader and uploader in alone threads.
    ➜  video-worker git:(master) 
    

写这篇文章的过程中我有好几次想中断,不太想公布一些问题,毕竟涉及到crawler都会有些擦边球的意思,考虑到看到的人流量几乎可以忽略不计就还是发了吧。写出来的原本目的是这个项目写到现在这个时间节点上,我也需要捋一捋我自己的想法结构。

Unix Process State

问题

有时候会在晚上睡觉之前开始在mac mini上下几个大文件或者跑brew update,等命令跑到一半发现不想再继续等下去了,因为等待的时间比预期的远远要长。我又不想因为一句命令让机器一整晚上都运行着,还是想着应该让这个命令结束之后再立刻休眠(pmset sleepnow)。

于是问题来了,我要么找到结束当前正在一直运行的这个命令再重新编辑(追加&& pmset sleepnow)达到效果,要么想办法找到那个进程执行完毕的那个时间点。其实对于brew update这类命令的话,结束执行再跑一点儿关系都没有,主要还是针对curl和封装了curl的任务,毕竟杀掉了重新来过一般只能重新开始。

思路

在这之前我是知道jobs, fgbg这几个常用命令的:一个在运行的程序,如果我发送Ctrl+Z的组合时间给它,就相当于挂起(suspend)了这个进程,这个时候开始它只能等待恢复(或者直接被杀掉)。jobs能直接查看到当前shell的进程列表中所有的任务列表,通过fg可以把最顶端挂起的任务恢复并分发到前台运行模式,相当于把目标任务提到了当前shell的正在执行的命令模式,相当于接替上一个状态继续执行。bg的作用基本一致,区别在于目标任务被分到了后台运行模式,当前shell仍然处于带输入模式接受下一个命令的执行。如果不考虑stdout或者目标任务的stdout被重定向到了其他文件,bg会表现得目标任务不存在一样,直到执行完毕会从后台运行模式下输出一个进程执行完毕的提示。

sleep举例:

➜  ~ sleep 30 && echo "I'm done"
^Z
[1]  + 18342 suspended  sleep 30
➜  ~ jobs
[1]  + suspended  sleep 30
➜  ~ fg
[1]  + 18342 continued  sleep 30
^Z
[1]  + 18342 suspended  sleep 30
➜  ~ bg
[1]  + 18342 continued  sleep 30
➜  ~
[1]  + 18342 done       sleep 30
➜  ~

DND 的触发逻辑就是这样的:允许目标进程启动,但是又立刻挂起,等待用户授权完成之后再恢复它。

解决方法

  1. fg && echo "Going to sleep..." && pmset sleep

    直接把fg当做目标任务的一个handler,fg在执行的时候相当于是把目标进程同步地dispatch到当前的主shell进程上来运行,直到目标进程执行完毕fg的任务才算完毕。然后继续下一个shell命令…

  2. wait, 上面说到的是同步地dispatch到主shell进程上来,那就应该有异步执行的操作然后在主线程等待(join)的信号处理方法。wait就是做这件事情的,但它也有自己的使用规则。

    wait接收一个进程id(pid)并等待目标进程的完成状态,但是这个pid必须是当前shell的子进程。wait不改变目标进程的运行模式和状态,它只是单纯地observe一个进程终止状态并作为返回值返回。所以我的问题也可以这么解决:

    ➜  ~ sleep 30 && echo "I'm done"
    ^Z
    [1]  + 55241 suspended  sleep 30
    ➜  ~ bg
    [1]  + 55241 continued  sleep 30
    ➜  ~ wait 55241 && echo "Going to sleep..." && pmset sleep
    [1]  + 55241 done       sleep 30
    gogo
    ➜  ~
    
  3. Composer component in iTerm2’s status bar

    这个功能是从iTerm2的3.3.0版本开始加入的,和上面的方式不同,它是以延迟式键盘事件的方式发送到当前shell的命令行然后回车执行的,这就相当于不用等待当前shell正在运行的进程终止,提前准备好接下来的输入然后发送。

    想到发送输入的情形就得把read的情形考虑进来,也就是说如果当前正在运行的命令包含一个交互式的等待用户输入以继续的逻辑,那么iTerm会如何处理呢?试一试:

    ➜  ~ ping www.baidu.com -t 5; read -n a; echo "Your input: $a"
    PING www.a.shifen.com (39.156.66.14): 56 data bytes
    64 bytes from 39.156.66.14: icmp_seq=0 ttl=53 time=8.710 ms
    64 bytes from 39.156.66.14: icmp_seq=1 ttl=53 time=9.165 ms
    date
    date
    64 bytes from 39.156.66.14: icmp_seq=2 ttl=53 time=8.611 ms
    64 bytes from 39.156.66.14: icmp_seq=3 ttl=53 time=8.332 ms
    64 bytes from 39.156.66.14: icmp_seq=4 ttl=53 time=11.044 ms
       
    --- www.a.shifen.com ping statistics ---
    5 packets transmitted, 5 packets received, 0.0% packet loss
    round-trip min/avg/max/stddev = 8.332/9.172/11.044/0.973 ms
    Your input: date
    ➜  ~ date
    Thu Sep 26 22:47:17 CST 2019
    ➜  ~
    

    结果出来了,过程中我用ping的目的只是为了延迟read的执行时间并加入一些输出,我在Composer组件里输出了date然后立刻回车了两次。这部分内容混杂到了整个session输出里面,但是第一个date以字符串的方式被read接收然后打印了出来,第二个date以命令的方式发送到了执行完毕之后的交互模式开始了一个新的shell任务。

    这个行为可以这么理解,从macOS Foundation NSRunLoop的原理上来看,iTerm当前shell的window或者tab在处理当前的自己的keyboard event loop的时候,会不断地询问当前运行模式下是否能处理输入事件,如果能的话就发送Composer队列里的第一个字符串到event loop中,从上面的例子可以看出来至少在处理read和普通模式的等待执行命令这两种情形是能处理的,然后结束Composer在本次loop的任务。如果不能的话就继续在下一个loop继续询问,直到Composer队列里面的所有文本命令被发送成功。

结语

  • jobs, fg, bg, wait 全部都是shell内置的命令,可能不同的shell在实现和行为上有些不同。以上环境是zsh的执行结果。

  • 可以从上面的几个例子看到其实通常一句shell命令在提交到iTerm的主进程launch_shell的时候可以有两种方式:一种是单命令,比如sleep 10,运行的时候只会产生一个进程。另一种是sleep 10 && echo "I’m done”(把&&替换成;是一样的)或者用了循环语句的情形,产生多个进程(其实大多是这种情形)。

    如果是简单的单命令执行,用以上任意一种方法都可以满足;如果是复杂的多进程执行,推荐使用第三种iTerm2的Composer方法,毕竟不用考虑进程状态等问题,但是还是要考虑read的处理问题。当然了,如果只会产生一个主进程的话,用方法1和2也是可以的,只是需要确认一下。pstree with watch?

  • sleep在上面的例子中测试并不是很好,据我测试Ctrl+Z对sleep并不能达到我期望的效果,貌似跟wall clock的行为有关,和我原本理解的按秒sleep有差别。

相关阅读:

Code Review Service

There is a wonderful channel called Code Review in Stack Exchange, it provides for a kind of question and answer style to review code for programmer.

Code Review Stack Exchange is a question and answer site for peer programmer code reviews.

Code Review不是一个新词,甚至是老生常谈。这次我们不讨论它的方式和规范,而是从商业的角度上看待这个问题。没有国外IT公司的开发经历,我们只基于国内的开发环境来看待这个问题。

论点

  1. 开发者大都是带着排斥心理去接手老项目的

    这不是在否定开发者的心态或者工作态度。假想现在突然有一个项目要我接手,首先对这个项目作出评价,就优缺点而已,其实我是先找缺点的。先把所有的毛病和奇怪的地方先梳理出来,先默认断言它是错误的,然后否定上一个结论,如果没有找到合理的解释,那就是一个问题/缺点了,如果找到了当然就忽略。直到最后我才会开始思考到底哪些地方值得算是优点的。

    对于一个没有上下文的项目而言就真的是这样,相反,如果是在Github上的一个开源项目,我们首先会看项目的Star, Fork, Commit和Contributor的数量等,然后决定开始以一种什么心态来阅读这个项目的源代码,这是有很大区别的。(当然也确实存在有些大项目后期被改残了的。)

  2. 老板需要人来维护已有的业务逻辑

    这应该是定理了吧,毕竟在不考虑业务逻辑大变化的前提下,现有的代码毕竟是老板请人花钱写出来的。对于后来的开发者而言也不能说随便就重写或者重构的。

  3. Code Guideline vs Code Review

    就我个人的认知而言其实Guideline其实只针对多人协作的编程方式,而且Guideline的目的是在新人加入一个项目之前应该了解的编码指南,它算不上是一套规则,最多勉强算是一个language style guide。Code review更侧重于对已有代码提出的质疑,也包括对不满足guideline的问题。

    以下部分地方简称Code Review为CR。

  4. 小公司大都是没有code review这个workflow的

    毕竟老板只想着找人来完成自己公司的业务实现,只关心it works这唯一一个结果。有些老板甚至在内心里也认为开发者也完全只是为了那份工资才来协助完成这份工作的,他们根本理解不了那些真正写代码的人。

  5. Code Review到底做了什么

    这一点算凑数的,只是为了加一个链接而已。 CR的主要作用一直都是优化并维持代码的高质量。

  6. Code Review到底对谁有利

    • 老板,也就是到底省不省钱。如果没有CR或者中断了一段时间的CR,新人接下来就是要大重构甚至重写了,这没什么问题,只是要花更长的时间,也就变相花了更多老板的钱。(好吧,有些外行老板只会压榨开发者的时间和加班让他们的完成这个事情,这也是常有的事情。)这是一种隐性的利好。
    • 开发者,其实我觉得这个几乎不算是一个答案选项。没有CR,开发者只会按照自己的一贯方式来完成一个项目,这其中也包含一些坏习惯,比如随便留一些hacking/magic code,temporary fixes等等。CR也并不能给开发者提供一个最佳的学习方式,因为从CR中学习别人的编码思想并不如直接阅读优秀的开源代码有价值。正如天天刷微博学习碎片知识和阅读完整知识树。有了CR之后,只会给开发者增加额外的时间来完成项目进度。唯一的好处可能只是在有限定性的规则和约束的同时给后续增量开发制造了一个舒适的编码体检。
  7. Code Review之外还有(应该)什么

    单一的CR并不是完整的,它只是在每一个迭代的增量更新中优化一部分设计而已。一个项目的完整大框架设计不应该单单依赖于CR,所以架构设计还是非常有用的。

需求

公司/老板把Code Review这个流程外包。

对于不同项目每个公司一定会有自己独立的一套架构设计,针对服务器,前端,客户端等等。这一点对于老板而言可能还真没有省事的解决方案,只能找开发者为自己单独设计,其中也包括为了涉及到的保密问题。相比于CR,我认为保密级别的问题还是相对低不少的,这其中还可以通过技术来很大程度上来解决这个问题。

设计

客户方把项目代码托管到自己指定(类似Bitbucket Server,简称Server版)或者服务方的平台(类似Bitbucket Cloud,简称Cloud版),接收代码之后由客户选择是否混淆符号,选择哪些代码文件可见。Cloud版允许所有Reviewer查看和review所有项目代码,Server版可以是邀请制+合作制,只允许指定Reviewer来查看和/或review指定项目代码。两个版本都可以有免费和付费制,并基于贡献值建立Reviewer的声望值。

托管

从托管安全的角度上讲这个业务如果是Bitbucket来做是相对比较有信任感的,毕竟有大厂背景以及他们现有的其他优秀的托管衍生服务。要加上这个业务也是最容易也最让人能够接受的。

保密

鉴于这是一个主要toB的项目,必须要让企业对自己托管的代码放心,即使是Atlassian这样的大厂,也还是推出了上面提到的两个版本的Bitbucket托管服务。

混淆

以基本的符号替换为例,这是有必要的。不管是针对公众还是企业针对自己的其他部门,都一定会有这个需求,主要是让老板放心托管。当然,混淆的数据还是需要加密处理并一起由托管服务负责,也需要加密处理。

加密

加密的核心是为了为技术付费,不让人(轻易)破解。针对的数据包括代码文件,混淆符号表和review的内容。Server版可能会脱离中心服务器的控制,所以至少可能还是需要license授权的方式才能访问系统。

Reviewer

需要有完整的Reviewer用户系统,经验,tag,语种,声望,奖励等。从这个角度来说,Stack Overflow是非常好的借鉴。没有声望的创作内容是没有评定价值的,没有奖励的创作是不可持续的。

奖励

包括声望,现金,甚至还可以有Offer。

KPI

老板的钱会花得有理有据,不然最多可能只是一个一次性消费。要有报告,统计被采纳的解决方案top 10和总数量,团队内部开发者的评价,Reviewer对项目的评价等等。

生态

在设想建立这个供需关系的时候,我们先不和公司现有的工作流进行对比。

无论哪个老板都不想自己的项目最终被搞成一堆臭狗屎,然后一波又一波的换人来重构或重写。对开发者的规范和警醒也是有必要的,CR在一般公司内部难以维持我认为有几点原因:一是领头没有带好,这是一系列协作流程的最终结果;二是依靠团队内开发者的相互监督建立起来的规则大都是弱化的,无法长期维持;三是老板只顾眼前利益和计划,当然大部分时候也是因为老板也没得选(解决方案)。

引进外围的Reviewer并不会对现有的项目开发者产生直接冲击,只会是约束效果和让自己更自律。对老板而言只是花另一小部分钱来维护自己的项目而已。

周期

传统的CR最大的弊端应该就是会拖延项目周期,毕竟对某些老板而言有时候时间比钱更值钱。传统的团队内部CR在时间上一般会因为Reviewer的工作计划有些冲突,最终导致代码合并时间点延后,甚至可能block其他开发者的进度。

解决这个问题的核心在于Reviewer的数量级和直接的奖励制度,良性循环才可能有最直接的效率输出。

证明

None

总结

None