分类
未分类

How To Start A Large BASH Project.18.07.20

O 前言

我当初写futaba.sh的时候可没想到它会变成一个大型bash项目,但它现在已经是了。而且和拖某dingo的bash项目不同,这个项目还是有一点卵用的。这里是些写大型bash项目的经验之谈:

I 从一个小项目开始

我建议首先不要以大型项目的思路来构建初始项目,因为非常可能它不会变成大型项目;先想办法让你的bash脚本完成一件事情吧,甚至初期根本不用写.sh文件,直接用基础bash命令就可以了。

至少拖站类的bash项目都是从一个for循环开始的。比如我当时实现将rule34.paheal.net的hentai链接post到discord上时就写了两层for循环,第一层循环页数,第二层开始抓取页面并提取出图片信息了。通常需要在for的后半部分写一长串命令,直到这个命令输出的结果是多行需要的信息为止。

至于如何只使用grep和sed提取页面里的信息,我也有些经验之谈:首先我们都知道用regexp搞HTML就是异端,如果不是没有办法(比如限定shell,比如性能限制不能使用HTML解析器)不要用它。但如果实在要用它的话也是有办法的,首先无论需要的信息是在HTML里面还是在json里面,它们都是树型数据结构。我们只需将树打散成以行为单位的子树构成的森林,然后grep出含有所需信息(通常只需要匹配一个title或者id之类的关键词,不加-Eo)的子树,然后在子树里grep出需要的信息,并删除不需要的字符即可。当我说“打散”的时候,我通常是指字面意义上的——比如将<li></li>子树打散成森林,只需要将/li>变成\n(linux里面的换行符,win要用\r\n)即可。如果需要在子树里提取出多个信息(比如pixiv),那么我们只能将子树整体送进for循环里面,然后在for循环里面继续拆子树。此时我们需要多一步sed 's/ /|/g',到循环里的每一个提取命令开始前加上sed 's/|/ /g',不然空格会被当作新行送进for循环,这样就很坑爹了。

在子树里提取HTTP链接也是一件非常有意思的事情,如果里面只有一个链接(而且就是你要的链接)好办,但如果有多个的话就需要再拆分树了,此时可以将<a替换成\n也可以按需拆分成别的森林,然后用grep(不带-Eo)加上某个title或者id之类的关键词筛选需要的子树(其实用HTTP解析器的时候也是要通过各种标签来筛选链接,但我grep写多了看上去一点都不直观)。然后重点来了,将"替换成\n,然后grep "http"就能直接提取出链接了,别问我为什么,这和树型理论一点关系都没有,这是纯经验。这样看起来比sed这个再sed那个优雅很多。

在这个阶段就已经可以将某些东西抽象成函数了,比如上传discord的部分可以很明显按照用webhook还是用自身帐号搞成两个函数,而且这种主动想出来的函数通常参数也很清楚(以为它本来就是被用来当普通函数用的);但绝大多数的抽象是在项目进行时归纳出来的,详见下一章。

II 抽象

从一个for循环变成.sh文件是个飞跃,因为.sh文件允许你进行各种参数载入,也就意味着可以用一个脚本实现多种功能,并有各种各样的实现方式,这些东西的代码运行情况组合是以乘法计的,涉及到各种swich-case语句和条件判断。所以所有的这些都需要抽象成函数,因为只有抽象成函数了你才能用以加法计的代码实现以乘法计的功能。有些抽象是显而易见的,比如上面说的上传到discord的函数,比如每个站点可以用一个函数实现。但有的抽象没有那么显而易见,需要在写代码的过程中发现,而且你的项目越大,最后的代码也会变得越抽象。

其实这点也不是很难,通常情况下当这两种情况出现的时候,你就可以考虑开始抽象了:

  • 当两个函数或者几个函数的某些循环内代码完全一毛一样时;
  • 当你开始在case里面嵌套if或者在if里面嵌套case或者在函数里面嵌套case然后出现大量的重复代码(不然你可以不用管它们)时
  • 或者这两者同时出现时(手动滑稽

此时抽象出来的函数和C/C++里的函数意思又不一样,这里只是相当于代码复用而已,所以使用方法类似于C/C++的macro,相同的代码段使用的变量也必须完全相同,此时这样抽象出的函数可以不用带任何参数(反正那些变量在这里都是全局变量)。举个例子吧,比如某段代码长这样子(稍微等下,我这里的f(x)不是字面意思,其实它的意思是某种和abcde有关的变换):

case "$f" in
    a)
        if [ "$n" == "fish" ]
        then
            fisch
        else
            non_fisch "f(a)"
        fi
        ;;
    b)
        if [ "$n" == "fish" ]
        then
            fisch
        else
            non_fisch "f(b)"
        fi
        ;;
    c)
        if [ "$n" == "fish" ]
        then
            fisch
        else
            non_fisch "f(c)"
        fi
        ;;
    d)
        if [ "$n" == "fish" ]
        then
            fisch
        else
            non_fisch "f(d)"
        fi
        ;;
    e)
        if [ "$n" == "fish" ]
        then
            fisch
        else
            non_fisch "f(e)"
        fi
        ;;
esac

此时你就可以用一个新函数叫做nein()将那一坨if搞起来,然后它就变成了这样:

function nein(){
    if [ "$n" == "fish" ]
    then
        fisch
    else
        non_fisch "$temp"
    fi
}

case "$f" in
    a)
        temp=f(a)
        nein
        ;;
    b)
        temp=f(b)
        nein
        ;;
    c)
        temp=f(c)
        nein
        ;;
    d)
        temp=f(d)
        nein
        ;;
    e)
        temp=f(e)
        nein
        ;;
esac

看见那个temp变量没有?这样至少你不用写有参函数了(手动滑稽

总而言之,通过这种方式,我们就可以将以乘法计(上面的代码有5*2种情况)的参数组合用以加法计(只有5+2种代码分支)的代码实现了,而且修改每个部分(比如往里面添加新的参数)时只用修改那一部分即可,非常方便。可以说设计制作大型bash项目的过程就是不断抽象的过程,这一过程中你也会对这个脚本的运行机制有更加深刻的了解,日后将其移植到高级脚本语言上也会变得轻松起来。

我反正在写futaba.sh的pixiv部分时就面临过不断抽象的过程,本来pixiv的三种处理方式(最快的是只扫列表页面就开始拖,但因为你不知道目标的扩展名是jpg还是png所以要将两者都获取一遍;最慢的当然就是一页一页扫了;所以肯定有中等快的,或者叫半速,它和快速模式差不多,但需要进入一次详情页确认一遍扩展名,实测非常爽,基本上没出过错,现在已经成为默认模式)就已经写了三个site相关函数了,我还要往里面再加两个pixiv功能:获取某一个用户的所有作品和所有收藏集,这下不就需要3*3=⑨个大型函数了吗,不行,必须抽象。

反正这是个极其操蛋的过程,因为我一开始以为它们的内层循环只和处理方式有关,所以先把它们提取了出来,然后在原位置写了一个case导向那三个子函数;但后面我发现其实它们的内层循环里的至少三行代码还和pixiv功能有关,获取文件名和图片数量的方式至少搜索页面和作品页面是完全不一样的,而作品页面和收藏页面完全一样。所以我还需要在那三个子函数里再抽象一遍,在一个新的子函数里写一个case区分那三种pixiv功能,这样就非常完美了。至于最外层的那三个pixiv打头的函数就不用再抽象了,因为它们的差异已经非常大了,每一个的外层循环都几乎不一样。具体结果可以参见futaba.sh

III 测试与排错

和任何大型软件项目一样,bash项目也会面临大量的测试。一开始时我对futaba.sh不够放心,需要把所有可能的组合都测试一遍;但当我写到pixiv部分的时候,我已经对futaba.sh其余的部分足够放心了,所以我只需要测试和pixiv相关的部分,甚至只和我实现的功能相关的部分即可。这里有两种方法:

  • 一种是将pixiv()函数扔另外一个.sh文件(甚至不用是.sh文件,如果不太复杂的话)里面,带上需要的参数(比如pixiv那段又臭又长的curl参数),然后简单实现下某些函数(它们原本是用来执行下载再上传功能的,但此处只需输出url即可),就可以测试基本的列出所有图片功能了,搞好后直接复制进原脚本就能用。至少我一开始时做pixiv()时就是这么搞的;
  • 另外一种就是在原脚本里测试了,适用于其他部分已经试了几十次没有问题的情况,后期我都不用搞新脚本

测试就是这样,接下来就是排错了,有时候你的脚本就是会出现莫名其妙的语法错误和运行错误,而bash几乎没有debug或者trace这样的功能。所以除了一开始写代码时就好好写、遵循某种编码规范外,有时候我们需要搭建一个极小型的概念验证“平台”来看下问题出在哪里。

一般来说只要好好写,代码是不会出现什么逻辑问题的,在这种拖站脚本里面99.9%的问题都出在目标网页里解析不出来需要的信息。此时只需要将出现在for里面的那些长串sed和grep组合语句拿出来单独执行就知道为什么解析不出来了。必要时需要引入其他变量,比如pixiv那段又臭又长的curl参数。反正问题各种各样,有时候是因为目标站点改版了,有时候是因为grep少加了引号,有时候更奇怪的是有问题的代码在某次更新前居然能够正常运行。。。总之就是蛋疼。

IV 总结

懒得总结了,大型bash脚本就是这样,最后祝你提(bei)前(shui)射(yan)精(mo),再见

ps. WP装了markdown编辑器真™爽

分类
未分类

Discord Bot Only Usin’ Bash.18.07.15

O 前言

前天晚上我的A6000到了,拍了几张aegis手办准备传discord上,发现各种连接不上,然后发现被DNS污染了,discord全面被墙。

当然discord被墙对我来说不是什么大事,毕竟一年前我就全程proxifier了。但它确实妨碍我上传图片了,能直连时有大约3MB/s的上传速度,现在隔着墙只有几十KB/s。

所以我只能找个能在VPS上上传图片的方法了。中转也好说,onedrive随便搞,问题就在于怎么从VPS上传到discord。实在没想到我在探索这个问题的过程中居然搞出了只用bash实现的discord bot。

I 命令行

首先winserver里用浏览器端上传的方法行不通,discord界面有大量的动画,比如上传时就有动画,而这种动画会直接卡爆winserver VPS。所以必须要找到一种命令行上传的方式。

我看了几个和上传图片相关的代码,它们都用了discord的webhook功能,我找了一个运行了下,发现它只能上传修改日期晚于程序运行日期的图片,我试图注释掉那行代码,但它爆出了一系列缺少包的错误(这就是为什么我讨厌高级脚本语言)。所以我只好追踪下它的代码,看下上传文件执行了什么。最后发现它只是一个简单的post请求,就和别的文件上传一样。

这样的话直接用curl就能搞定,比如

for file in `ls`
do
    curl -F "[email protected]$file" "<webhook url>"
done

不到两分钟我的所有照片就全传到discord上去了,爽的1b。再用几个grep可以在返回信息里提取出链接,然后发别的聊天室里。

在传照片之余我还看了一眼webhook的文档,发现它还可以干别的事。。。除了可以发任意信息外,还能制定bot的名称和头像。。。

II Hentai

我在discord上最喜欢的事之一就是开hentai车,我因此没少和西方国家的傻逼SJW们撕逼。所以当我看了几分钟文档后,我便产生了写个自动发hentai车bot的想法。具体来说很简单,以我们喜闻乐见的rule34站paheal.net为例,获取所有图片的语句为:

curl "https://rule34.paheal.net/post/list/Anne_Takamaki" | sed 's/<br/\n/g' | grep "Image Only" | grep -Eo "http://.*>Image" | sed 's/">Image//g'

然后将图片链接发到discord上的语句为:

curl -d "content=$hentai&username=Ann Takamaki Hentai Bot&avatar_url=https://cdn.discordapp.com/attachments/467378952739094539/467383253377220618/ann.png" "<webhook url>"

然后将它们用一个for循环连起来,就可以开一页的车了:

for hentai in `curl "https://rule34.paheal.net/post/list/Anne_Takamaki/2" | sed 's/<br/\n/g' | grep "Image Only" | grep -Eo "http://.*>Image" | sed 's/">Image//g'`;do curl -d "content=$hentai&username=Ann Takamaki Hentai Bot&avatar_url=https://cdn.discordapp.com/attachments/467378952739094539/467383253377220618/ann.png" "<webhook url>"; sleep 3;  done

这玩意最蛋疼的地方是会ratelimit,发得太快了就会出错,所以我设置了3s的延时。

如果想开所有页面的车的话也很简单,只需要外层再加一个for,通过某些方式获取总页数,即可:

for fish in `seq 1 5`; do for hentai in `curl "https://rule34.paheal.net/post/list/Haru_Okumura/$fish" | sed 's/<br/\n/g' | grep "Image Only" | grep -Eo "http://.*>Image" | sed 's/">Image//g'`;do curl -d "content=$hentai&username=Yukari Takeba Hentai Bot&avatar_url=https://cdn.discordapp.com/attachments/467378952739094539/467409069586645002/naoto.v2.png" "https://ptb.discordapp.com/api/webhooks/467379014114476043/orrPUrkh4IdM2T6ibFxYc9CoOlr1AN5imnBGYuHl3SophgU_sPg4B8Gq3-0jHZFetWT3"; sleep 3; done; done

ps. 这坨代码是一坨scheisse,里面居然出现了三个萌妹,我也不清楚它为什么变成了这样。

可以看出这里面很多东西都不用手动指定,总页数可以从第一页里读取,萌妹的名字也可以从链接中获取,唯一需要给定的是paheal.net的标签,当然还需要往discord上传一个头像并获取链接,这玩意还没法自动完成。不过我手头已经有很多persona萌妹的头像了,下次开车可以直接用(手动滑稽

从这里事情就变得复杂了,我开始写shell脚本了;好处显而易见,下次开车时只需要指定那两个变量即可。

III Beyond Webhook

接下来我可能进行了几十次测试,直到某一次时webhook被我搞挂了,它只显示bad request。所以我便产生了一个新的想法:把普通帐号当bot用。方法论还是类似,但这次肯定不能看discord的文档了,这次要自己动手。用firefox登录某个discord帐号,找到要发车的channel(没错,两种方法都只能固定发到某一个channel),按下F12调出开发者工具,选择“网络”选项卡,随便打几个字就能看到一个叫做typing的请求出现了。发一条消息,然后就会出现messages请求,对这个请求右键“复制”->“复制为cURL”。现在你随便找台VPS粘贴那段crul命令运行,那个帐号就会发出一毛一样的信息。然后把里面的信息内容换成$1,两个双引号换成反斜杠+双引号(因为前者好像不能用),再把它保存成函数,这样就能像用webhook一样随便用了。

当然它也有ratelimit问题,但好像比webhook稍微宽限一些,比如可以只用等2s。

现在我可以在我有管理员权限的聊天室之外的地方发车了,比如我有不知道多少个小号的某persona聊天室。我找了一辆短发老师的车,发了起来,然后不到50张图片就被某傻逼SJW发现是某知名futaba lover ddOs的小号,然后呼叫狗管理封号了。当然我发的图片好像还在,毕竟短发老师是18+(手动滑稽

接下来我就去完善我的shell脚本了,首先我需要能解析参数,这样我就能加入不同的参数让一个脚本能做两件事了。我翻出了三年前写的一个shell脚本,拖某H站压缩包的,可以说是我写过的最大的shell项目。我反正看了几个小时才搞懂当时是怎么用getopt的。但无论如何我总算搞定了,上传discord部分我抽象成了两个函数:nanako()用来处理webhook,futaba()用来处理普通帐号(因为futaba比较NB,手动滑稽)。除此之外我还加了一个--message参数来直接输出信息,某些时候可能会用得上。

然后我便反手把它传到了github上,但愿discord那边不会注意到:https://github.com/die-Deutsche-Orthopaedie/shitty-arse-discord-bots

IV Future

这可以说是非常有前景的项目。discord bot多得是,但我这个项目是用纯bash写的。一方面是所谓的我只会bash(其实py我也勉强会一些,但不常用),另外一方面纯bash脚本还有其他一些好处:

  • 在很多VPS上光初始化python等高级脚本语言就需要很长时间,尽管用regexp处理HTML是异端但绝大多数情况下比HTML解析器效率高很多,而且绝大多数情况下不会出错;bash也有HTML解析器用,但额外的好处并不能抵消加载HTML解析器的额外时间开销;
  • 使用高级脚本语言会带来各种莫名其妙的依赖包问题,而且很多VPS上或者能给你一个shell的地方并没有装什么高级脚本语言;但用纯shell写的脚本只会用到grep、sed和curl,绝对不会有什么shell环境没有这三项东西;
  • 我可以预见这个脚本有一个使用场景是raidin’,比如我就和某persona聊天室有个人恩怨,等时机成熟了我可能会考虑往里面的某个channel里一分钟灌几千张萝莉本子。此时能在任意shell环境里用的脚本将会非常有用;或者如果它没有执行.sh的权限,也可以生成所需的命令直接粘贴进去。有很多地方可以提供免费的ssh access,比如ovh的免费主机,还有其他一些玩意;它们也不需要太好的网络性能,毕竟discord本身就有ratelimit,我能做的就是往里面塞尽可能多的小号,如果一个小号两秒一张萝莉本子,20个小号一分钟就能灌600张进去,这将是件非常爽的事情,搞不好会逼得他们狗管理删掉那个糟糕channel也说不定。但这肯定是下下策了,更高级的方法当然是社工下狗管理的帐号,然后获得那个聊天室的部分管理权限,这样等到破坏的时候就会相当爽,搞不好还能将ddOs(还有一大堆trolls)从ban list里删掉,给那些狗管理造成更大的麻烦(手动滑稽
  • 我反正已经想好了,只要能解决多个bots之间的调度问题,我就是在那个糟糕channel里演奏贝多芬第⑨交响曲都没问题,当然,是用萝莉本子来演奏,我还没想好具体的形式,但我可以肯定,这将成为discord史上最大的行为艺术(手动滑稽*2

那个bot脚本本身也有改进的空间。除了discord相关的函数外,其实具体参数(webhook url和curl命令)也可以抽象出来,保存到某个配置文件里,使用比如-C/--config-file的方式调用;hentai站点部分也可以抽象成函数,某些站点可以用搜索用名称(tag)来命名bot,有的不能,但总体上来说可以合并,只需要让前者自动命名显示名称即可(当然也可以自定义参数)。总体而言就是这样,我要接着搞脚本去了。

UPDATE:终于写得差不多了,无论是配置文件、各种地方的函数抽象、下载再上传还是获取图片和发discord分离都写完了,可以说相当满意。现在就可以往里面添加新的站点了,比如pixiv.net。但p站有点特殊,因为它尽管也能搜集一堆链接,但直接扔discord里没法显示出来,我也很怀疑它能不能直接用链接来wget,搞不好连wget都没法用,只能上更操蛋的curl了。总之只能用下载再上传的方式搞。但上传后的返回信息可以利用起来,提取处discord图片外链保存成一个新的文件,下次raid的时候就可以直接用了(手动滑稽