0%

技术杂谈

有时候我们在逛博客、技术帖子的时候会发现有人是这么分享代码的?

这其实是一张图片,虽然说里面的内容不能复制,但是这张图片整体看看起来就很精致有没有?左上角的三个红黄绿的按钮,就是 Mac 中的窗口操作按钮,然后代码的高亮配色都和我们 IDE 中的配色是完全一致的,整体就是一个 Mac 的风格。

最近群里也有小伙伴在问这个是怎么做的,这里就来给大家介绍一下。

这个工具叫做 carbon,它有自己的网站、代码仓库,另外还有一个类似的VS Code 插件,这里统一给大家介绍下。

网站

carbon 自己维护了一个网站:https://carbon.now.sh/,我们可以打开它来看看:

可以看到 carbon 提供了多种选项,比如主题选择、编程语言、还有一些底色、间距的配置,下方就是代码的预览效果,同时我们还可以在这里编辑代码。

我们可以在下方任意贴上我们想要贴的代码,比如这里有一段 Python 代码:

1
2
3
4
def get_vowels(string):
return [vowel for vowel in string if vowel in 'aeiou']

print("Vowels are:", get_vowels('This is some random string'))

我们可以直接贴进去,然后我们可以选择喜欢的主题配色,语言,同时可以选择背景颜色,还可以点击 setting 的按钮配置更详细的内容:

这里的 Window controls 可以控制左上角的现实效果,比如是否带有圆角效果、按钮是否带有颜色,另外还可以控制内边距、阴影等等,还有一些自动调整宽度的配置。

完事之后直接点击复制,这里有几个选项,比如复制图片、URL 或者直接复制 iframe。

比如点击复制为 iframe 链接就会得到如下内容:

1
2
3
4
5
<iframe
src="https://carbon.now.sh/embed?bg=rgba%28255%2C255%2C255%2C1%29&t=material&wt=none&l=python&ds=true&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=0px&ph=0px&ln=false&fl=1&fm=Hack&fs=14px&lh=133%25&si=false&es=2x&wm=false&code=def%2520get_vowels%28string%29%253A%250A%2520%2520%2520%2520return%2520%255Bvowel%2520for%2520vowel%2520in%2520string%2520if%2520vowel%2520in%2520%27aeiou%27%255D%2520%250A%250Aprint%28%2522Vowels%2520are%253A%2522%252C%2520get_vowels%28%27This%2520is%2520some%2520random%2520string%27%29%29"
style="width: 561px; height: 146px; border:0; transform: scale(1); overflow:hidden;"
sandbox="allow-scripts allow-same-origin">
</iframe>

复制为 URL 就会直接得到一个可供使用的 URL:

1
https://carbon.now.sh/?bg=rgba%28255%2C255%2C255%2C1%29&t=material&wt=none&l=python&ds=true&dsyoff=20px&dsblur=68px&wc=true&wa=true&pv=0px&ph=0px&ln=false&fl=1&fm=Hack&fs=14px&lh=133%25&si=false&es=2x&wm=false&code=def%2520get_vowels%28string%29%253A%250A%2520%2520%2520%2520return%2520%255Bvowel%2520for%2520vowel%2520in%2520string%2520if%2520vowel%2520in%2520%27aeiou%27%255D%2520%250A%250Aprint%28%2522Vowels%2520are%253A%2522%252C%2520get_vowels%28%27This%2520is%2520some%2520random%2520string%27%29%29

有了 URL 之后我们可以到任意地址引用。

当然导出也是,可以选择导出矢量图 svg 还是 png,如图所示:

我的话一般都是点击复制,然后用我的 ipic 工具上传到 CDN 上面。

仓库

同时 carbon 还是开源的,GitHub 仓库地址是:https://github.com/carbon-app/carbon,其实这就是刚才我们看到的网站的源码,是基于 Node.js 开发的,扒一扒 package.json源码:

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
"dependencies": {
"@next/bundle-analyzer": "^10.2.2",
"@reach/visually-hidden": "^0.15.0",
"actionsack": "^0.0.14",
"axios": "^0.21.1",
"cm-show-invisibles": "^3.1.0",
"codemirror": "5.61.1",
"codemirror-graphql": "^1.0.1",
"codemirror-mode-elixir": "^1.1.2",
"codemirror-solidity": "^0.2.3",
"date-fns": "^2.21.3",
"dom-to-image": "^2.6.0",
"downshift": "^6.1.3",
"dropperx": "^1.0.1",
"eitherx": "^1.0.2",
"email-validator": "^2.0.4",
"escape-goat": "^3.0.0",
"firebase": "^8.6.2",
"graphql": "^15.5.0",
"highlight.js": "^10.7.2",
"lodash.debounce": "^4.0.8",
"lodash.omitby": "^4.6.0",
"match-sorter": "^6.3.0",
"morphmorph": "^0.1.3",
"ms": "^2.1.3",
"next": "^10.2.2",
"next-offline": "^5.0.5",
"prettier": "^2.3.0",
"react": "^17.0.2",
"react-click-outside": "^3.0.0",
"react-codemirror2": "^7.2.1",
"react-color": "^2.19.3",
"react-dom": "^17.0.2",
"react-image-crop": "^6.0.16",
"react-mailchimp-subscribe": "^2.1.3",
"tohash": "^1.0.2",
"use-climate-change-reminder": "^0.0.7"
},

可以看到它主要基于 Next、React、CodeMirror 开发的,如果大家想基于此进行二次开发也可以,比如对接云存储,实现一些自动化等等。

如果觉得刚才的网站能够满足需求的话,那大可继续使用刚才的网站。

VS Code 插件

另外还有一个类似的 VS Code 插件,也可以实现类似的功能,我觉得还挺好用的,插件叫做 CodeSnap,现在已经十七万多次下载了。

大家在 VS Code 里面搜索生成就好了。

那怎么生成代码图片呢?

其实很简单,我们先选中想要分享的代码:

然后右键菜单选择 CodeSnap 即可,接着在 VS Code 面板就可以生成对应的代码预览效果了:

这时候我们可以点击上方的这个按钮,就可以直接把代码对应的图片下载下来了,如图所示:

得到的效果就和你自己编辑器里面看到的一样,是不是感觉很不错?

CodeSnap 也可以支持在 VSCode 里面更改 settings,目前支持如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
codesnap.backgroundColor: The background color of the snippet's container. Can be any valid CSS color.

codesnap.boxShadow: The CSS box-shadow for the snippet. Can be any valid CSS box shadow.

codesnap.containerPadding: The padding for the snippet's container. Can be any valid CSS padding.

codesnap.roundedCorners: Boolean value to use rounded corners or square corners for the window.

codesnap.showWindowControls: Boolean value to show or hide OS X style window buttons.

codesnap.showWindowTitle: Boolean value to show or hide window title folder_name - file_name.

codesnap.showLineNumbers: Boolean value to show or hide line numbers.

codesnap.realLineNumbers: Boolean value to start from the real line number of the file instead of 1.

codesnap.transparentBackground: Boolean value to use a transparent background when taking the screenshot.

codesnap.target: Either container to take the screenshot with the container, or window to only take the window.

codesnap.shutterAction: Either save to save the screenshot into a file, or copy to copy the screenshot into the clipboard.

关于更多的内容大家可以直接到 CodeSnap 插件的主页查看:https://marketplace.visualstudio.com/items?itemName=adpyke.codesnap

另外这里也顺便提下我的 VS Code 主题,我用的是 Community Material Theme 这个插件:

然后使用它提供的第一个默认主题:

如果大家觉得不错的话也欢迎试试哈~

希望对大家有帮助。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

有时候我们可能为了测试各种各样的功能,需要用邮箱注册一些网站,然后过段时间就发现这个网站开始往我们的邮箱发送一些垃圾广告信息,比如如图所示:

image-20210725124859662

很多邮箱服务有自动垃圾邮件分类的功能,但免不了的还是有些不好区分,来了不感兴趣的邮件我们还是要手动设置下规则,有时候关还关不掉。

如果我们只是为了简单测试某些功能,同时又不想发生上面的事情,那我们其实可以用一些小号邮箱来注册,或者生成一些临时邮箱来注册。

这里来介绍一个工具,帮助快速生成临时邮箱,简单好用。

tmpmail

这个工具叫做 tmpmail,GitHub 地址是 https://github.com/sdushantha/tmpmail,它就是一个简单的命令行 Shell 脚本,安装完了之后就可以使用了,Windows、Linux、Mac 上都是可用的。

由于我使用的是 Mac,这里介绍下 Mac 的安装方式,首先安装依赖:

1
brew install w3m curl jq

然后根据 GitHub 对应的提示安装即可:

1
curl -L "https://git.io/tmpmail" > tmpmail && chmod +x tmpmail

我们也可以把它移动到对应的系统路径下,比如 /usr/bin,/usr/local/bin 等:

1
mv tmpmail /usr/local/bin/

这样就安装好了,tmpmail 就可以正常使用了。

使用

使用其实非常简单,首先我们使用 tmpmail 就可以生成一个临时邮箱:

1
tmpmail

运行结果类似如下:

1
2
3
[ Inbox for hl9dvc3wbub@yoggm.com ]

No new mail

这里其实就是生成了一个临时邮箱,叫做 hl9dvc3wbub@yoggm.com,然后下面提示了 No new mail,就是没有新邮件的意思。

有朋友就会说了,咋没有密码啊?其实我们无需关心密码的对不对,我们只关心它收到的邮件内容就好了,比如我们应该是拿着这个邮箱去别的网站注册账号,然后网站会往这个邮箱发送一封激活邮件,点击就激活了。所以对于这个临时邮箱,我们只要能知道邮箱里面收到的邮件就好了。

所以 tmpmail 其实相当于帮我们维护了密码,我们无需关心,它可以自动帮我们把收件箱里面的邮件列出来。

OK,如果我们要更换邮箱也可以,直接重新生成一个就好了:

1
tmpmail --generate

运行结果如下:

1
j2uabw3jmfn@wwjmp.com

这样就重新生成了一个新的邮箱。

这时候重新运行 tmpmail,它就会使用当前最新生成的邮箱,运行结果如下:

1
2
3
[ Inbox for j2uabw3jmfn@wwjmp.com ]

No new mail

OK,准备工作就绪。

注册测试

接下来我们就随便找个网站注册个账号试试吧。

比如 Zyte 这个平台,前身叫做 Scrapinghub,提供一些数据爬取的服务,网址是 https://www.zyte.com/,如图所示:

我们来注册下试试:

填上用户名之后,我们使用刚才 tmpmail 生成的临时邮箱来注册这个账号,输入密码之后点击注册。

这里 Zyte 就提示我们激活邮件就发送出去了,我们需要到对应邮箱里面查收激活链接并激活账号。

查收激活

OK,回到 tmpmail,看看邮件收到没。

还是输入:

1
tmpmail

这时候就可以看到如下运行结果了:

1
2
3
[ Inbox for j2uabw3jmfn@wwjmp.com ]

231112827 bounce+6ec610.a13e529-j2uabw3jmfn=wwjmp.com@mg.zyte.com Zyte Email confirmation

非常赞,这里就显示了是 bounce+6ec610.a13e529-j2uabw3jmfn=wwjmp.com@mg.zyte.com 发送的邮件,邮件标题是 Zyte Email confirmation,然后最前面有个 ID,是 231112827。

P.S.:其实看着 Zyte 也是一个临时邮箱发送的这个邮件。

那这么打开这个邮件呢?很简单,tmpmail 命令加这个 ID 就好了:

1
tmpmail 231112827

运行结果如下:

这里就把邮件的内容展示出来了,内容就是感谢您的注册,然后点击链接激活即可,tmpmail 还自动解析了可点击的内容,比如超链接变成了可点击的内容,我们可以直接鼠标点击中间蓝色的 Confirm mail address 就在命令行下触发了链接的访问。

然后接下来命令行还显示了点击链接之后的网页内容:

基本上就是感谢您的注册,您的账户已经成功激活了。

完事了!

到现在为止我们就轻松利用 tmpmail 利用临时邮箱注册好了一个测试账号。

重新回到 Zyte 里面,输入刚才的邮箱和密码就成功登录进来了:

这里有代理服务、自动内容提取、云爬虫、Splash 渲染服务,大家感兴趣的话也可以试试看~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

事情是这样的,最近组里新建了一个代码仓库来开发一个新的产品,再加上今天北京下大雨很多同事选择在家工作(包括我也是),于是我就选择用自己的个人电脑来工作。

但我的个人电脑里面的 Git 信息是用的我自己的个人邮箱:

1
2
git config --global user.name "Germey"
git config --global user.email "cqc@www.phunsics.com"

这两行命令大家用过 Git 的肯定都敲过对吧?

这个配置是全局生效的,所以如果我用 Git 的 commit 命令来提交代码的话,那么 commit 的名字和邮箱就会变成刚才我配置的个人信息。

然后如果把代码推送到公司的代码仓库里面,里面就会出现一个奇奇怪怪的用户名和头像,就像这样子:

图中上面两次 commit 就是我用个人电脑提交的,最后的那次 commit 是我上周在公司用公司电脑提交的。

这是不是很奇怪?

如果其他人也用的个人邮箱提交,那公司代码库里面就会出现各种怪怪的提交人的记录,无从知晓。

这肯定不能忍啊,以后要是有谁写了奇怪的代码都不好查是谁写的。

于是乎,我灵机一动,想:为何不在提交代码的时候做一个限制呢?

能做到吗?当然可以!

Git Hook

这里就介绍一个知识点 - Git Hook,它的意思就是在 Git 各种事件执行前和执行后执行一些自定义的逻辑,比如说,我们定义一个 pre-commit 的 Git Hook,那就能在 commit 之前执行一些操作,我们定义一个 post-push 的 Git Hook,那就能在 push 操作之后执行一些操作。

有关具体的内容可以参考官方文档:https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks

好,那这里我其实就是需要在 commit 之前做一下 Git 信息检查就好了,比如检查配置的邮箱不是工作邮箱,那就不允许执行 commit,所以就不会出现奇奇怪怪的 commit 记录了。

实操

说干就干。

配置 Git Hook 的工具有很多,Git 有原生支持,当然我们也可以用第三方库来做。

目前我们的代码仓库是基于 Node.js 开发的,所以 Node.js 的项目配置 Git Hook 比较流行的解决方案就是 husky,所以这里我也用 husky 来做了。

首先安装下 husky:

1
yarn add husky

然后配置一个 Node.js 的 prepare 命令,这个命令可以在装完包 Node.js 包之后自动执行,所以 prepare 命令就配置成 husky 初始化的脚本,package.json 里面增加如下配置:

1
2
3
4
5
6
{
"scripts": {
...
"prepare": "npx husky install"
},
}

OK,这样的话,其他人如果 clone 了这个仓库,装完所有 Node.js 包之后就会自动初始化 husky 的配置,然后在项目本地生成一个 .husky 的初始化目录,这样 Git Hook 就生效了。

Git Hook 生效之后,所有定义在 .husky 目录下的 Hook 脚本都会被应用,比如如果在 .husky 目录下添加一个 pre-commit 的脚本,那执行 commit 的之前,该脚本就会被预先执行做一些检查工作。

所以 .husky 目录下我就创建了一个 pre-commit 的脚本,写入了如下内容:

1
2
3
4
5
6
7
8
9
EMAIL=$(git config user.email)
if [[ ! $EMAIL =~ ^[.[:alnum:]]+@microsoft\.com$ ]];
then
echo "Your git information is not valid";
echo "Please run:"
echo ' git config --local user.name "<Your name in Microsoft>"'
echo ' git config --local user.email "<Your alias>@microsoft.com"'
exit 1;
fi;

这是一个 Linux Shell 脚本,完全遵循 Shell 语法。

这里其实就是获取了 git config user.email 的返回结果,然后用正则表达式匹配是否符合公司邮箱格式,比如我们公司邮箱后缀当然是 microsoft.com 后缀,所以这里就用了 ^[.[:alnum:]]+@microsoft\.com$ 来进行匹配了。这里值得注意的是,为什么这里没有用 \S 来代表非空白字符,而是用了一个 [:alnum] 呢?这是因为 Bash Shell 本身不支持 \S 这种匹配,所以这里得换成 [:alnum]

然后如果不匹配怎么办呢?

那就输出一些错误提示就好了,比如这里就提示请使用 git config —-local 命令来配置用户名和邮箱,之所以用-—local 是因为不想该配置影响全局的 Git 配置,所以这个配置只针对该仓库生效,然后 exit 1 就触发异常退出,程序运行终止,从而也不会触发 commit 命令了。

有了这个配置,我们来尝试下效果。

这会我没有做任何修改,Git 还是原来的配置,即我的全局个人邮箱配置。

这时候我执行下 commit 命令,就出现错误提示了:

1
2
3
4
Your git information is not valid
Please run:
git config --local user.name "<Your name in Microsoft>"
git config --local user.email "<alias>@microsoft.com"

很棒!检测出来了。

按照这个提示说的,然后我运行下配置命令:

1
2
git config --global user.name "Qingcai Cui"
git config --global user.email "xxxx@microsoft.com"

这里呢,我就配置了我的公司个人信息和公司邮箱。

然后重新再执行 commit 命令,就不会再出现如上的错误提示了!commit 成功!

大功告成!!!

有了它,我们就可以成功阻止一些奇奇怪怪的 commit 乱入公司的代码仓库了!

然后我把这个 PR 发出去了,有同事似乎也是深有感触,说道:

哈哈哈,有了这个,以后我们应该再也不会看到我们的代码仓库里面有 QQ 邮箱啦!

希望对大家有帮助~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人随笔

想必大家都是从学生时代过来的,或者现在还处于学生时代。

在学生时代,大家有没有见过,有的同学非常非常努力,上课听得非常认真,笔记也记录得非常认真,同时各种颜色和标记把书上画得密密麻麻,但是似乎考试成绩并没有那么理想。然而有的同学,几乎也不记笔记,书上甚至几乎都是干干净净的,也不见有多么努力,但是考试成绩就是好。

这是为什么呢?最近看了《认知天性》这本书的一些内容,觉得说得还是很有道理的,在这里想把我的一些感悟写下来。

那些没有效果的努力

首先我们不能否认智力上确实有的同学就是聪明,这一点在数理化上体现得更为明显一些。但是在偏重一些记忆的科目上,上述的情况也有很多很多。比如同样记忆一些知识点,背英语单词等,有些同学非常努力地在大声反复背和念,但是效果并不理想,但有的同学其实就简单看几遍,然后后面就记得很牢。

说这个例子我想表明的一个观点是:耗费心血的学习才是深层次的,效果也会更持久,不花力气的学习就像在沙子上写字,今天写上,明天字就消失了。

之前老师经常教导我们,背单词要大声读出来,多读几遍,多拼几遍,手口耳并用。学习一个知识点的时候,我们要反复去看和阅读,并进行一些集中练习。但其实,这些并没有抓住本质,反而可能还是效率最低的学习方式。因为这个单词或知识点,我们只是在大声阅读、拼写而已,这种行为其实可以归为一种不花力气的学习,这个学习过程并没有给大脑带来什么挑战,这种记忆效率往往并不理想,而且就算当时记住了,那也会很快忘记了。

其实我之前其实也很多时候也用刚才说的模式来学习。比如背单词的时候我就反复拼好多遍、读好多遍,而很少主动去默写,去联想记忆,去做自测等等。所以,大部分时间,我的其实就是反复机械式地向大脑灌输一些强化的信息,最后的结果就是,很多单词和知识点,我当时从 0 到记忆住一个知识点的过程比较漫长,而且后来绝大多数知识点也都很快忘记了。

我印象深刻的知识是怎么来的

但我也回想起来,似乎之前学习过的某些知识点我到现在还记得,而且记得很牢。

比如我现在就记得几个地理知识点,亚洲和北美洲的分界线是白令海峡,北美洲和南美洲的分界线是巴拿马运河,等等。

为什么这几个知识点我记得这么牢呢?因为我当时在记忆这几个知识点的时候,我姐姐正好来了我家,我就让她给我提问这几个知识点。一开始提问的时候我其实并没有记录得多么牢,绞尽脑汁也没想出来,导致好几个问题答错了,错了之后我就去再记一遍,然后又让我姐姐给我提问。似乎经历了三四轮吧,最后我把所有的问题都答对了,于是后来我就对这些知识点的印象深刻很多。

正是因为这个过程,我的大脑经历了反复几次有挑战性的任务,在姐姐跟我提问的过程中,大脑触发了一些主动的思考和联想,然后没有记住的知识点再进行更有针对性的记忆。经过反复的几次提问,大脑的思考,知识的检索,这几个知识点就在我脑海中的印象变得更加深刻。

所以,耗费心血、对大脑有挑战性的学习才是深层次的,这个过程往往伴随着大脑对信息的主动检索,效果也会更好。

什么才是有效的学习

所以,在很多情况下,我们要进行一些有效的学习,就需要做一些对大脑有挑战性的事情,这里举几个例子方便大家理解:

  • 比如记忆一个知识点或者背单词的时候,反复阅读的效果往往是很差的,我们可以尝试让自己不看书本复述或默写出来,对大脑带来一些挑战,这样记忆一遍会更加深刻,另外让别人给自己提问或者默写也是不错的强化记忆的有效手段。

  • 比如读一本书,我们自以为是看完了,但其实本质上我们只是经历了一次泛读的过程,当时有个印象,但过段时间就几乎忘得差不多了。我们可以尝试拿出一张纸或者打卡一个空的思维导图,让大脑把整体的思维脉络、核心内容梳理出来。这当然是一个非常难的过程,我们可能很难一次性完成,但没关系,想不起来的就把书再打开看看,然后继续默写。多尝试两三次,我们会对整个书本的内容在大脑里面形成更深刻的印象。

  • 比如看了一篇讲座或者学习了一个课,最有效的学习和记忆手段往往是把读后感或者总结写出来,或者动手跟着做一遍,因为这个过程伴随着非常多的脉络梳理和大脑主动检索内容的过程。有人说,一个内容只看一遍,最后我们往往只能记住一小部分,但是如果我们能够把整个内容讲出来,那最后我们几乎能记住绝大部分。我也在做类似的尝试,比如今天我看了《认知天性》这本书的第一章内容,我就尝试把自己的读后感写出来,也就是你正看到的这篇文章,我相信我以后会对这部分内容的理解和记忆更加深刻。比如我学习了一个新的知识点,我就尝试把它梳理成自己的一篇文章,在梳理的过程中,我经历了思考、检索、记忆等各个过程,最后理解也会更加全面和深刻。

人天性是懒惰的,学习的过程中,大脑跟着一起懒惰,去机械式地学习往往是不可取的。所以,总的来说,我们在学习的时候不要死板地反复阅读,可以尽量给大脑一些有挑战性的工作,比如尝试默写、复述、思考和梳理等等,让大脑主动去检索和记忆,如果发现想不起来,那我们也知道哪部分内容需要重点学习的内容。只有这样,才是学习和记忆的正确方式。

共勉。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

之前我曾经写过一篇文章说 Google ReCAPTCHA 验证码的绕过方法,当时介绍的是用 2Captcha,然而有些朋友跟我反映说 2Captcha 价格比较贵,而且用起来比较复杂。

今天再给大家介绍另一个用于破解 Google ReCAPTCHA 的方法。

ReCAPTCHA 介绍

可能大家还没听说过什么是 ReCAPTCHA,可能由于某些原因,这个验证码在国内出现不多,不过想必大家应该多多少少见过或用过。它长这个样子:

这时候,只要我们点击最前面的复选框,验证码算法会首先利用其「风险分析引擎」做一次安全检测,如果直接检验通过的话,我们会直接得到如下的结果:

如果算法检测到当前系统存在风险,比如可能是陌生的网络环境,可能是模拟程序,会需要做二次校验。它会进一步弹出类似如下的内容:

比如上面这张图,验证码页面会出现九张图片,同时最上方出现文字「树木」,我们需要点选下方九张图中出现「树木」的图片,点选完成之后,可能还会出现几张新的图片,我们需要再次完成点选,最后点击「验证」按钮即可完成验证。 或者我们可以点击下方的「耳机」图标,这时候会切换到听写模式,验证码会变成这样:

这时候我们如果能填写对验证码读的音频内容,同样可以通过验证。 这两种方式都可以通过验证,验证完成之后,我们才能完成表单的提交,比如完成登录、注册等操作。 这种验证码叫什么名字? 这个验证码就是 Google 的 ReCAPTCHA V2 验证码,它就属于行为验证码的一种,这些行为包括点选复选框、选择对应图片、语音听写等内容,只有将这些行为校验通过,此验证码才能通过验证。相比于一般的图形验证码来说,此种验证码交互体验更好、安全性会更高、破解难度更大。

其实上文所介绍的验证码仅仅是 ReCAPTCHA 验证码的一种形式,是 V2 的显式版本,另外其 V2 版本还有隐式版本,隐式版本在校验的时候不会再显式地出现验证页面,它是通过 JavaScript 将验证码和提交按钮进行绑定,在提交表单的时候会自动完成校验。除了 V2 版本,Google 又推出了最新的 V3 版本,reCAPTCHA V3 验证码会为根据用户的行为来计算一个分数,这个分数代表了用户可能为机器人的概率,最后通过概率来判断校验是否可以通过。其安全性更高、体验更好。

体验

那哪里可以体验到 ReCAPTCHA 呢?我们可以打开这个网站:https://www.google.com/recaptcha/api2/demo,建议科学上网,同时用匿名窗口打开,这样的话测试不会受到历史 Cookies 的干扰,如图所示:

这时候,我们可以看到下方有个 ReCAPTCHA 的窗口,然后点击之后就出现了一个验证图块。

当然靠人工是能解的,但对于爬虫来说肯定不行啊,那怎么自动化解呢?

接下来我们就来介绍一个简单好用的平台。

解决方案

本次我们介绍的一个 ReCAPTCHA 破解服务叫做 YesCaptcha,主页是 http://yescaptcha.365world.com.cn/,它现在同时可以支持 V2 和 V3版本的破解。

我们这次就用它来尝试解一下刚才的 ReCAPTCHA 上的 V2 类型验证码:https://www.google.com/recaptcha/api2/demo

简单注册之后,可以找到首页有一个 Token。我们可以复制下来以备后面使用,如图所示:

它有两个关键的 API,一个是创建验证码服务任务,另一个是查询任务状态,API 如下:

API 文档可以参考这里:http://docs.yescaptcha.365world.com.cn/

经过 API 文档可以看到使用的时候可以配置如下参数:

参数名 是否必须 说明
token 请在个人中心获取 (Token)
siteKey ReCaptcha SiteKey (固定参数)
siteReferer ReCaptcha Referer (一般也为固定参数)
captchaType ReCaptchaV2(默认) / ReCaptchaV3
siteAction ReCaptchaV3 选填 Action动作 默认verify
minScore ReCaptchaV3 选填 最小分数(0.1-0.9)

这里就有三个关键信息了:

  • token:就是刚才我们在 YesCaptcha 上复制下来的参数

  • siteKey:这个是 ReCAPACHA 的标志字符串,稍后我们会演示怎么找。

  • siteReferer,一般是 ReCAPTCHA 的来源网站的 Referer,比如对于当前的案例,该值就是 https://www.google.com/recaptcha/api2/demo

那 siteKey 怎么找呢?其实很简单,我们看下当前 ReCAPTCHA 的 HTML 源码,从源码里面找一下就好了:

这里可以看到每个 ReCAPTCHA 都对应一个 div,div 有个属性叫做 date-sitekey,看这里的值就是:

1
6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-

好,万事俱备了,只差代码了!

开工

我们就用最简单 requests 来实现下吧,首先把常量定义一下:

1
2
3
4
TOKEN = '50a07xxxxxxxxxxxxxxxxxxxxxxxxxf78'  # 请替换成自己的TOKEN
REFERER = 'https://www.google.com/recaptcha/api2/demo'
BASE_URL = 'http://api.yescaptcha.365world.com.cn'
SITE_KEY = '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-' # 请替换成自己的SITE_KEY

这里我们定义了这么几个常量:

  • TOKEN:就是网站上复制来的 token

  • REFERER:就是 Demo 网站的链接

  • API_BASE_URL:就是 YesCaptcha 的 API 网址

  • SITE_KEY:就是刚才我们找到的 data-sitekey

然后我们定义一个创建任务的方法:

1
2
3
4
5
6
7
8
9
10
def create_task():
url = f"{BASE_URL}/v3/recaptcha/create?token={TOKEN}&siteKey={SITE_KEY}&siteReferer={REFERER}"
try:
response = requests.get(url)
if response.status_code == 200:
data = response.json()
print('response data:', data)
return data.get('data', {}).get('taskId')
except requests.RequestException as e:
print('create task failed', e)

这里就是调 API 来创建任务,没什么好说的。

如果创建成功之后会得到一个 task_id,接下来我们就需要用这个 task_id 来轮询查看任务的状态,定义如下的这么一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def polling_task(task_id):
url = f"{BASE_URL}/v3/recaptcha/status?token={TOKEN}&taskId={task_id}"
count = 0
while count < 120:
try:
response = requests.get(url)
if response.status_code == 200:
data = response.json()
print('polling result', data)
status = data.get('data', {}).get('status')
print('status of task', status)
if status == 'Success':
return data.get('data', {}).get('response')
except requests.RequestException as e:
print('polling task failed', e)
finally:
count += 1
time.sleep(1)

这里就是设置了最长轮询次数 120 次,请求的 API 就是查询任务状态的 API,会得到一个任务状态的结果,如果结果是 Success,那就证明任务成功了,解析其中的 response 结果就是验证码破解之后得到的 token。

两个方法调用一下:

1
2
3
4
5
if __name__ == '__main__':
task_id = create_task()
print('create task successfully', task_id)
response = polling_task(task_id)
print('get response:', response[0:40]+'...')

运行结果类似如下:

1
2
3
4
5
6
7
8
9
response data: {'status': 0, 'msg': 'ok', 'data': {'taskId': '1479436991'}}
create task successfully 1479436991
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Working'}}
status of task Working
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Working'}}
status of task Working
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Working'}}
status of task Working
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Success', 'response': '03AGdBq27-ABqvNmgq96iuprN8Mvzfq6_8noknIed5foLb15oWvWVksq9KesDkDd7dgMMr-UmqULZduXTWr87scJXl3djhl2btPO721eFAYsVzSk7ftr4uHBdJWonnEemr9dNaFB9qx5pnxr3P24AC7cCfKlOH_XARaN4pvbPNxx_UY5G5fzKUPFDOV14nNkCWl61jwwC0fuwetH1q99r4hBQxyI6XICD3PiHyHJMZ_-wolcO1R9C90iGQyjzrSMiNqErezO24ODCiKRyX2cVaMwM9plbxDSuyKUVaDHqccz8UrTNNdJ4m2WxKrD9wZDWaSK10Ti1LgsqOWKjKwqBbuyRS_BkSjG6OJdHqJN4bpk_jAcPMO13wXrnHBaXdK4FNDR9-dUvupHEnr7QZEuNoRxwl8FnO2Fgwzp2sJbGeQkMbSVYWdAalE6fzJ8NwsFJxCdDyeyO817buBtvTJ4C06C1uZ92fpPTeYGJwbbicOuqbGfHNTyiSJeRNmt-5RKz0OUiPJOPnmVKGlWBOqwbwCW1WZt-E-hH4FEg4En5TITmmPb_feS9dWKUxudn1U0hHk2vV9PerjZLtI7F67KtgmcqRrARPbwnc6KyAi3Hy1hthP92lv4MRIcO2jx0Llvsja-G2nhjZB0ZoJwkb9106pmqldiwlXxky4Dcg7VPStiCYJvhQpRYol7Iq1_ltU2tyhMqsu_Xa8Z6Mr5ykRCLnmlLb8DV8isndrdwp84wo_vPARGRj7Up9ov-ycb5lDKTf1XRaHiMCa8d2WLy0Pjco9UnsRAPw0FW3MsBJah6ryHUUDho7ffhUUgV1k86ryJym6xbWch1sVC4D5owzrCFn6L-rSLc5SS1pza2zU5LK4kAZCmbXNRffiFrhUY8nP4T1xaR2KMhIaN8HhJQpR8sQh1Azc-QkDy4rwbYmxUrysYGMrAOnmDx9z7tWQXbJE4IgCVMx5wihSiE-T8nbF5y1aJ0Ru9zqg1nZ3GSqsucSnvJA8HV5t9v0QSG5cBC1x5HIceA-2uEGSjwcmYOMw8D_65Dl-d6yVk1YN2FZCgMWY5ewzB1RAFN1BMqKoITQJ64jq3lKATpkc5i7aTA2bRGQyXrbDyMRIrVXKnYMHegfMbDn0l4O81a8vxmevLspKkacVPiqLsAe-73jAxMvsOqaG7cKxMQO9CY3qbtD55YgN0W4p2jyNSVz3aEpffHRqYyWMsRI5LddLgaZQDoHHgGUhV580PSIdZJ5eKd0gOjxIYxKlr0IgbMWRmsG_TgDNImy1c5oey8ojl-zWpOQW7bnfq5Z4tZ10_sCTfoOZVLqRuOsqB1OOO9pLRQojLBP0HUiGhRAr_As9EIDu6F9NIQfdAmCaVvavJbi1CZITFjcywP-tBrHsxpwkCXlwl996MK_XyEDuyWnJVGiVSthUMY306tIh1Xxj93W3KQJCzsfJQcjN-3lGLLeDFddypHyG4yrpRqRHHBNyiNJHgxSk5SaShEhXvByjkepvhrKX3kJssCU04biqqmkrQ49GqBV9OsWIy0nN3OJTx8v05MP8aU8YYkYBF01UbSff4mTfLAhin6iWk84Y074mRbe2MbgFAdU58KnCrwYVxcAR8voZsFxbxNwZXdVeexNx5HlIlSgaAHLWm2kFWmGPPW-ZA7R8Wst-mc7oIKft5iJl8Ea0YFz8oXyVgQk1rd9nDR3xGe5mWL1co0MiW1yvHg'}}

如果其返回的是如上格式的数据,就代表 ReCAPTCHA 验证码已经识别成功了,其返回的 response 字段的内容就是识别的 token,我们直接拿着这个 token 放到表单里面提交就成功了。

那这个 token 怎么来用呢? 其实如果我们用浏览器验证验证成功之后,点击表单提交的时候,在其表单里面会把一个 name 叫做 g-recaptcha-response 的 textarea 赋值,如果验证成功,它的 value 值就是验证之后得到的 token,这个会作为表单提交的一部分发送到服务器进行验证。如果这个字段校验成功了,那就没问题了。

所以,如上的过程相当于为我们模拟了点选验证码的过程,其最终得到的这个 token 其实就是我们应该赋值给 name 为 g-recaptcha-response 的内容。 那么怎么赋值呢? 很简单,用 JavaScript 就好了。我们可以用 JavaScript 选取到这个 textarea,然后直接赋值即可,代码如下:

1
document.getElementById("g-recaptcha-response").innerHTML="TOKEN_FROM_YESCAPTCHA";

注意这里的 TOKEN_FROM_YESCAPTCHA 需要换成刚才我们所得到的 token 值。我们做爬虫模拟登录的时候,假如是用 Selenium、Puppeteer 等软件,在模拟程序里面,只需要模拟执行这段 JavaScript 代码,就可以成功赋值了。 执行之后,直接提交表单,我们查看下 Network 请求:

可以看到其就是提交了一个表单,其中有一个字段就是 g-recaptcha-response,它会发送到服务端进行校验,校验通过,那就成功了。 所以,如果我们借助于 YesCaptcha 得到了这个 token,然后把它赋值到表单的 textarea 里面,表单就会提交,如果 token 有效,就能成功绕过登录,而不需要我们再去点选验证码了。 最后我们得到如下成功的页面:

当然我们也可以使用 requests 来模拟完成表单提交:

1
2
3
4
5
6
def verify(response):
url = "https://www.google.com/recaptcha/api2/demo"
data = {"g-recaptcha-response": response}
response = requests.post(url, data=data)
if response.status_code == 200:
return response.text

最后完善一下调用:

1
2
3
4
5
6
7
if __name__ == '__main__':
task_id = create_task()
print('create task successfully', task_id)
response = polling_task(task_id)
print('get response:', response[0:40]+'...')
result = verify(response)
print(result)

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
response data: {'status': 0, 'msg': 'ok', 'data': {'taskId': '1479436991'}}
create task successfully 1479436991
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Working'}}
status of task Working
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Working'}}
status of task Working
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Working'}}
status of task Working
polling result {'status': 0, 'msg': 'ok', 'data': {'status': 'Success', 'response': '03AGdBq27-ABqvNmgq96iuprN8Mvzfq6_8noknIed5foLb15oWvWVksq9KesDkDd7dgMMr-UmqULZduXTWr87scJXl3djhl2btPO721eFAYsVzSk7ftr4uHBdJWonnEemr9dNaFB9qx5pnxr3P24AC7cCfKlOH_XARaN4pvbPNxx_UY5G5fzKUPFDOV14nNkCWl61jwwC0fuwetH1q99r4hBQxyI6XICD3PiHyHJMZ_-wolcO1R9C90iGQyjzrSMiNqErezO24ODCiKRyX2cVaMwM9plbxDSuyKUVaDHqccz8UrTNNdJ4m2WxKrD9wZDWaSK10Ti1LgsqOWKjKwqBbuyRS_BkSjG6OJdHqJN4bpk_jAcPMO13wXrnHBaXdK4FNDR9-dUvupHEnr7QZEuNoRxwl8FnO2Fgwzp2sJbGeQkMbSVYWdAalE6fzJ8NwsFJxCdDyeyO817buBtvTJ4C06C1uZ92fpPTeYGJwbbicOuqbGfHNTyiSJeRNmt-5RKz0OUiPJOPnmVKGlWBOqwbwCW1WZt-E-hH4FEg4En5TITmmPb_feS9dWKUxudn1U0hHk2vV9PerjZLtI7F67KtgmcqRrARPbwnc6KyAi3Hy1hthP92lv4MRIcO2jx0Llvsja-G2nhjZB0ZoJwkb9106pmqldiwlXxky4Dcg7VPStiCYJvhQpRYol7Iq1_ltU2tyhMqsu_Xa8Z6Mr5ykRCLnmlLb8DV8isndrdwp84wo_vPARGRj7Up9ov-ycb5lDKTf1XRaHiMCa8d2WLy0Pjco9UnsRAPw0FW3MsBJah6ryHUUDho7ffhUUgV1k86ryJym6xbWch1sVC4D5owzrCFn6L-rSLc5SS1pza2zU5LK4kAZCmbXNRffiFrhUY8nP4T1xaR2KMhIaN8HhJQpR8sQh1Azc-QkDy4rwbYmxUrysYGMrAOnmDx9z7tWQXbJE4IgCVMx5wihSiE-T8nbF5y1aJ0Ru9zqg1nZ3GSqsucSnvJA8HV5t9v0QSG5cBC1x5HIceA-2uEGSjwcmYOMw8D_65Dl-d6yVk1YN2FZCgMWY5ewzB1RAFN1BMqKoITQJ64jq3lKATpkc5i7aTA2bRGQyXrbDyMRIrVXKnYMHegfMbDn0l4O81a8vxmevLspKkacVPiqLsAe-73jAxMvsOqaG7cKxMQO9CY3qbtD55YgN0W4p2jyNSVz3aEpffHRqYyWMsRI5LddLgaZQDoHHgGUhV580PSIdZJ5eKd0gOjxIYxKlr0IgbMWRmsG_TgDNImy1c5oey8ojl-zWpOQW7bnfq5Z4tZ10_sCTfoOZVLqRuOsqB1OOO9pLRQojLBP0HUiGhRAr_As9EIDu6F9NIQfdAmCaVvavJbi1CZITFjcywP-tBrHsxpwkCXlwl996MK_XyEDuyWnJVGiVSthUMY306tIh1Xxj93W3KQJCzsfJQcjN-3lGLLeDFddypHyG4yrpRqRHHBNyiNJHgxSk5SaShEhXvByjkepvhrKX3kJssCU04biqqmkrQ49GqBV9OsWIy0nN3OJTx8v05MP8aU8YYkYBF01UbSff4mTfLAhin6iWk84Y074mRbe2MbgFAdU58KnCrwYVxcAR8voZsFxbxNwZXdVeexNx5HlIlSgaAHLWm2kFWmGPPW-ZA7R8Wst-mc7oIKft5iJl8Ea0YFz8oXyVgQk1rd9nDR3xGe5mWL1co0MiW1yvHg'}}
status of task Success
get response: 03AGdBq27-ABqvNmgq96iuprN8Mvzfq6_8noknIe...
<!DOCTYPE HTML><html dir="ltr"><head><meta http-equiv="content-type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width, user-scalable=yes"><title>ReCAPTCHA demo</title><link rel="stylesheet" href="https://www.gstatic.com/recaptcha/releases/TbD3vPFlUWKZD-9L4ZxB0HJI/demo__ltr.css" type="text/css"></head><body><div class="recaptcha-success">Verification Success... Hooray!</div></body></html>

最后就可以发现,模拟提交之后,结果会有一个 Verification Success... Hooray! 的文字,就代表验证成功了!

至此,我们就成功完成了 ReCAPTCHA 的破解。

上面我们介绍的是 requests 的实现,当然使用 Selenium 等工具也可以实现,具体的 Demo 在文档也写好了,请大家参考文档的说明使用即可。

小福利

现在 YesCaptcha 的价格我觉得相比之前介绍过的 2Captcha 实惠很多了,它破解一次是花费 10 点数,10 块钱是 10000 点数,所以平均破解一次验证码一分钱,新用户是送 1000 点数,可以破解 100 次。我个人觉得很实惠了。

大家有需要的话可以试试看!

溜了溜了~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

今天逛 GitHub 的时候发现了 GitHub 出了一个新的 Feature,叫做 GitHub Copilot,说可以帮我们自动写代码!

网址是这个:

点进来之后就几个醒目的大字 - Your AI pair programmer,你的人工智能编程伙伴。

这里有几个示例是这样的:

怎么个人工智能法呢?

这里前 7 行都是人写的,后面的 17 行都是人工智能帮写的。

我们需要写啥呢?我们来详细看看这个例子。

首先创建了一个 sentiment.ts 文件,然后引入了一个 Node.js 的包叫做 fetch-h2,然后写了两行注释:

1
2
// Determine whether the sentiment of text is positive
// Use a web service

什么意思呢?就是用注释写了我要写个啥东西,翻译过来如下:

  • 判断一句话的包含的情感是正面的还是负面的。(比如说“我好开心”就包含了积极情绪,句子包含的情感就是正面的;比如“你太坏了”就包含了负面评价,句子的情感就是负面的。)

  • 使用 Web 服务来实现。

然后定义了一个方法的声明:

1
async function isPositive(text: string): Promise<boolean>

没了。

就导入了一个包,然后写了两句注释,定义了一个方法的参数和返回值,人做的事就这么多。

然后 GitHub Copilot 就能帮我们把代码写出来,它写的内容如下:

1
2
3
4
5
6
7
8
9
10
  const response = await fetch(`http://text-processing.com/api/sentiment/`, {
method: "POST",
body: `text=${text}`,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
const json = await response.json();
return json.label === "pos";
}

没错,它智能分析了我们人写的注释和方法的声明,然后就把代码写出来了。

这里就调用了一个 API,然后还自动构造了 POST 请求,获取返回结果,然后比对返回结果的 label 是不是 pos,如果是,那就代表句子包含了积极程序,返回 false 就不是积极情绪。

虽然说规范程度上一般,没有异常处理什么的,但是已经相当了不起了有没有!

  • 它居然能准确理解注释中我们描述的两个需求

  • 它居然知道调用哪个 API 来判断文本的情感信息

  • 它居然还能没有语法错误地把一个 JavaScript 方法写出来

牛逼了!

当然它不止能写 JavaScript,还能写很多其他的语言,我们再来看一个 Python的 例子。

这里我们新建了一个 parse_expenses 的 Python 文件,然后定义了一个 parse_expenses 方法,接收一个参数叫做 expenses_string,然后写明注释如下:

1
2
3
4
5
6
7
8
9
"""
Parse the list of expenses and return the list of triples (date, value, currency).
Ignore lines starting with #.
Parse the date using datetime.
Example expenses_string:
2016-01-02 -34.01 USD
2016-01-03 2.59 DKK
2016-01-03 -2.72 EUR
"""

这里就写了,解析下面三行消费数据,然后返回日期、数值、单位,同时要求忽略掉开头是 # 的行,时间要用 datetime 库来解析出来。

然后 AI 就帮我们写了如下代码:

1
2
3
4
5
6
7
8
9
expenses = []
for line in expenses_string.splitlines():
if line.startswith("#"):
continue
date, value, currency = line.split(" ")
expenses.append((datetime.datetime.strptime(date, "%Y-%m-%d"),
float(value),
currency))
return expenses

看完这个我惊呆了,它全都做到了!

  • 跟它说了忽略开头是 # 的行,它就添加了一个判断

  • 而且它智能分析了下面的三行数据是什么格式的,然后还知道用空格把它分开

  • 分开之后,针对日期,他还知道用 datetime 解析一下,而且还知道是什么格式,年月日中间用的是横线

  • 数值还自动转成了 float 类型

  • 最后组成了一个元组返回了

简直,我简直不相信这是 AI 写的,感觉这个作为面试题,人也不一定一次性完整写得很好,AI 全都做到了!

这。

当然除了 JavaScript、Python,它还会很多语言,比如 Go、Ruby、TypeScript 都会。

这是背后究竟是什么技术呢?

看了看官网的介绍,说是基于 OpenAI 做的,官方原话如下:

1
Trained on billions of lines of public code, GitHub Copilot puts the knowledge you need at your fingertips, saving you time and helping you stay focused.

翻译过来就是:

1
GitHub Copilot 接受了数十亿行公共代码的训练,让您所需的知识触手可及,从而节省您的时间并帮助您保持专注。

反正就是他们训练了一个模型,这个模型接受了数十亿行代码作为训练输入,最后就学会了人怎么写代码了。

这波可以。

然后官方还介绍说:

GitHub Copilot 尤其擅长写 Python、Go、Ruby、JavaScript、TypeScript,并且现在已经发布成了 VS Code 中的一个插件。在我们写 Code 的时候,这个插件就会跟 OpenAI 的模型通信,然后目前看到的内容帮助我们自动写出想要的代码,基本流程如下图所示:

好家伙,那我赶紧来下载看看。

到 VS Code 里面搜索下 Copilot,果然有,已经十万多下载量了。

装上之后,它让我登录 GitHub 授权,登录之后,它弹了一个令人悲伤的信息:

它说我现在还没有权限使用,请访问 https://copilot.github.com 申请假如白名单。

也就是还没完全开放使用,需要申请才能用。

于是乎,我就去申请了下,点下网站的 Sign Up 即可,现在我已经在等待名单中了,等通过了我应该就能用了,如图所示:

大家感兴趣的话也赶紧去申请试试吧!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

最近在实现一个功能,那就是显示图片的分辨率信息。

由于分辨率无非就是宽乘以高的格式嘛,比如 250x140 这样的。

然后我代码里面就实现成了这样子:

1
return `${dimension?.width}x${dimension?.height}`;

dimension 就是分辨率对象,它有宽高两个信息。

然而,一位大佬给我 Review 代码的时候发现了这个问题,他说你看看其他地方是怎么表示的,需不需要不同地区做 Localization(国际化 i18n 处理)?

于是我就找了下 Chrome 浏览器怎么显示的,随便打开了一张图片:

这不就是这么显示的吗?

我写的没错啊?到底问题出在了哪里?

不解之时又去求助大佬,大佬说:

1
Is Chrome using the letter x or are they using the × character?

我恍然大悟,原来是字符问题!

然后我就追踪了下 Chrome 这页的代码:

由于这个信息是在选项卡显示的,那么一定在 title 节点里面,我把这个字符复制了出来,跟字母 x 对比了下:

果然不是字母 x,而是字符 ×,有趣!

后来我改成了 × 就好了,修改如下:

1
return `${dimension?.width}×${dimension?.height}`;

然后告诉了大佬,大佬欣慰地笑了,说:

1
Nice, no localization necessary =)

妙极了!

哈哈,这里就简单记录下,非常有意思,不然我还一直以为是一个字母 x 呢。

以后大家表示分辨率的时候,更标准的形式应该是用字符 × 而不是字母 x,比如应该是:

1
250×160

而不是:

1
250x160

涨姿势了!

彩蛋:我的微信昵称其实也有类似的字符,比如「崔庆才丨静觅」中间的「丨」是一个汉字(发音为 gun),而不是竖线「|」,哈哈哈。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

VS Code 想必大家都听说过吧,VS Code 凭借其强大的插件生态简直把 IDE 玩出花来了,现在我身边越来越多的程序员朋友现在都转向使用 VS Code 来写代码了,我也不例外。

但大家知道 VS Code 本身是用什么写的吗?没错,其实是 JavaScript 写的,准确来说是用 TypeScript 写的。

这时候我就想问,既然是用 TypeScript 写的,那它有没有网页版呢?

有人就要问了,网页版的 VS Code 有啥用啊,其实它能解决很多痛点:

  • 如果我能在网页版的 VS Code 里面写代码,这样换了一个 PC 之后我只需要打卡这个网页就能接着写了,多么简单方便。

  • 另外我写了代码,想给别人复现现场看效果,直接甩给他一个网页链接就好了。

  • 别人遇到问题想让我帮调试,那他在里面写完了,然后直接给我链接就好了。

舒服吧!

那就来整一个吧!

于是我就找官网的支持,但是没找到官网有说 VS Code 网页版的任何事情。

但是找到了一个开源项目,叫做 code-server,运行之后就可以在浏览器里面打卡 VS Code 了,GitHub 地址是:https://github.com/cdr/code-server

它的官方介绍是:

Run VS Code on any machine anywhere and access it in the browser.

正式我想要的!它在浏览器里面的运行效果如图所示:

安装

接下来那就安装试试吧,它支持多个平台,只需要运行一条命令就能安装了:

1
curl -fsSL https://code-server.dev/install.sh | sh

这条命令运行之后会自动判断当前的平台,然后运行安装步骤。

安装完了之后会有一个可用的 code-server 命令,运行之后便可以在本地启动 code-server 服务了,然后就可以在浏览器中打开 VS Code 了,就像上图所示。

Docker

但这 code-server 仅仅是在本地运行起来了。

如果我想将其部署到公网供我随时访问呢?或者极端一点,如果我想为其他人也部署一个 code-server 怎么办呢?或者我想为一百个人部署自己专属的 code-server 怎么办呢?

所以,这时候一个很好的方案就是上 Docker + Kubernetes 了。

既然要上 Docker,那就顺便对 code-server 做一些基础化的配置吧,比如预装一些插件,比如设置一些主题,比如设置一些编辑器配置等等。

我本地的 VS Code 现在在用一个我个人觉得比较好看的主题,叫做 Material Ocean,效果是这样的:

这是通过安装一个插件实现的,叫做 Material Theme:

另外还有一些基础的插件,比如 Python的支持、自动提示等等。

另外既然要支持 Python 了,那也可以在 Docker 里面配置一些基础的 Python 库,以免使用的时候再安装。

其他的一些配置比如代码规范、缩进、换行等都可以通过 VS Code 的一些 settings.json 配置来实现。

等等还有一些其他的优化项可以自行发挥啦。

基本上就是这么多了,所以接下来就可以写 Dockerfile 了。

这里我直接基于 ubuntu 18.04 来开始搭建了,编写一个 Dockerfile 如下:

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
FROM ubuntu:18.04

RUN apt-get update && apt-get install -y \
openssl \
net-tools \
git \
zsh \
locales \
sudo \
dumb-init \
vim \
curl \
wget \
bash-completion \
python3 \
python3-pip \
python3-setuptools \
build-essential \
python3-dev \
libssl-dev \
libffi-dev \
libxml2 \
libxml2-dev \
libxslt1-dev \
zlib1g-dev

RUN chsh -s /bin/bash
ENV SHELL=/bin/bash

RUN ARCH=amd64 && \
curl -sSL "https://github.com/boxboat/fixuid/releases/download/v0.4.1/fixuid-0.4.1-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - && \
chown root:root /usr/local/bin/fixuid && \
chmod 4755 /usr/local/bin/fixuid && \
mkdir -p /etc/fixuid && \
printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml

RUN CODE_SERVER_VERSION=3.10.2 && \
curl -sSOL https://github.com/cdr/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server_${CODE_SERVER_VERSION}_amd64.deb && \
sudo dpkg -i code-server_${CODE_SERVER_VERSION}_amd64.deb

RUN locale-gen en_US.UTF-8

ENV LC_ALL=en_US.UTF-8

RUN adduser --disabled-password --gecos '' coder && \
adduser coder sudo && \
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers;

RUN chmod g+rw /home && \
mkdir -p /home/coder/workspace && \
mkdir -p /home/coder/.local && \
chown -R coder:coder /home/coder && \
chown -R coder:coder /home/coder/.local && \
chown -R coder:coder /home/coder/workspace;

USER coder

RUN git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf && \
~/.fzf/install

ENV PASSWORD=${PASSWORD:-P@ssw0rd}

COPY ./extensions /home/coder/.local/extensions

RUN /usr/bin/code-server --install-extension ms-python.python && \
/usr/bin/code-server --install-extension esbenp.prettier-vscode && \
/usr/bin/code-server --install-extension equinusocio.vsc-material-theme && \
/usr/bin/code-server --install-extension codezombiech.gitignore && \
/usr/bin/code-server --install-extension piotrpalarz.vscode-gitignore-generator && \
/usr/bin/code-server --install-extension aeschli.vscode-css-formatter && \
/usr/bin/code-server --install-extension donjayamanne.githistory && \
/usr/bin/code-server --install-extension ecmel.vscode-html-css && \
/usr/bin/code-server --install-extension pkief.material-icon-theme && \
/usr/bin/code-server --install-extension equinusocio.vsc-material-theme-icons && \
/usr/bin/code-server --install-extension eg2.vscode-npm-script && \
/usr/bin/code-server --install-extension ms-ceintl.vscode-language-pack-zh-hans && \
/usr/bin/code-server --install-extension /home/coder/.local/extensions/tkrkt.linenote-1.2.1.vsix && \
/usr/bin/code-server --install-extension dbaeumer.vscode-eslint

RUN /usr/bin/python3 -m pip install -U pip setuptools

RUN /usr/bin/python3 -m pip install requests httpx scrapy aiohttp pyquery beautifulsoup4 \
selenium pyppeteer pylint flask django tornado numpy pandas scipy autopep8

COPY settings.json /home/coder/.local/share/code-server/User/settings.json

RUN sudo chown coder /home/coder/.local/share/code-server/User/settings.json

COPY entrypoint.sh /home/coder/.local/entrypoint.sh

RUN sudo chmod +x /home/coder/.local/entrypoint.sh

WORKDIR /home/coder/workspace

EXPOSE 8080

ENTRYPOINT ["/bin/sh", "/home/coder/.local/entrypoint.sh"]

这里就直接把 Dockerfile 列出来了,主要分这么几步:

  • 装一些基本的环境依赖库

  • 设置 shell

  • 安装 code-server

  • 设置工作目录

  • 安装 code-server 插件

  • 安装 Python 常用的库

  • 设置 VS Code 的 settings

  • 设置运行目录

  • 设置运行脚本入口

比如 VS Code 插件我就提前装好了 Material Theme 插件,然后在 settings.json 里面启用对应的主题即可:

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
{
"workbench.colorTheme": "Material Theme",
"workbench.iconTheme": "material-icon-theme",
"git.enableSmartCommit": true,
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.fontSize": 16,
"editor.suggestSelection": "first",
"files.autoGuessEncoding": true,
"files.autoSave": "afterDelay",
"terminal.integrated.inheritEnv": false,
"vetur.experimental.templateInterpolationService": true,
"[typescript]": {
"editor.tabSize": 2
},
"[javascript]": {
"editor.tabSize": 2
},
"[python]": {
"editor.tabSize": 4,
"editor.defaultFormatter": "ms-python.python"
}
}

这里配置文件主要配置了主题、字体大小、缩进等内容,当然这个如果你要自己配置的话就按照自己的喜好来就好了。

最后通过 docker-compose.json 文件配置镜像信息:

1
2
3
4
5
6
7
8
version: "3"
services:
code-server:
container_name: "code-server"
build: .
image: "germey/code-server"
ports:
- "8080:8080"

OK,基本就是这样,运行:

1
docker-compose build

就可以成功构建一个镜像了,然后运行:

1
docker-compose push

即可把镜像 push 到我的 Docker Hub 上面,等待部署即可。

Kubernetes 部署

对于部署 Kubernetes 来说,如果要做到方便部署且灵活管理的话,那就不得不用到 Helm 了,我可以写一个 Helm Chart,定义好一些模板文件和占位符,同时设置默认的配置选项,这样我们就可以通过一条简单的命令来部署这个 Docker 镜像了。

如果你没有用过 Helm 的话可以搜索相关资料了解下。

这里 Chart 的具体实现我就不再赘述了,主要就包括了几个部分:

  • Deployment

  • Service

  • Ingress

  • PersistentVolumeClaim

  • ServiceAccount

  • Secret

具体的配置我都放在 GitHub 了:https://github.com/Python3WebSpider/CodeServer/tree/master/chart,需要的话自取即可。

有了 Chart 之后,我只需要一条命令即可部署一个在线的 VS Code,命令如下:

1
helm install code-server-<username> . --namespace <namespace> --set user=<username> --set password=<password>

注意运行目录在 chart 路径下才可以。

这里我们传入了 usrname、namespace、password。

这里我配置了解析域名 code-.scrape.center,比如我要配置一个 code-germey.scrape.center,密码是 1234,那就只需要运行该命令即可:

1
helm install code-server-germey . --namespace scrape --set user=germey --set password=1234

这里用户名我替换成了 germey,命名空间我用了 scrape,密码用了 1234。

运行这条命令之后,我就能得到一个 https://code-germey.scrape.center/ 网站了。

没错,就是一条命令部署一个 VS Code,而且有专属域名。

打开之后效果如下:

输入对应的密码之后,就可以进入对应的 VS Code 编辑器页面了,如图所示:

这里我可以新建 Python 文件,然后在线运行:

另外还可以在命令行下像在 Linux 下一样操作,比如安装一个新的 Python 库:

1
pip3 install pillow

非常方便。

另外插件页面也可以看到我安装的一些插件:

也可以在此继续添加想要的插件。

全屏之后活脱脱就是一个桌面版本的 VS Code!

唯一不是很方便的就是在里面跑一些 Web 服务,因为 Web 服务相当于在 Docker 里面运行的,不过不要紧,我们只需要在 Chart 里面增加几个端口映射就好了。

有了它,我在里面写了代码,切换了不同 PC,我不用再关心代码的同步问题了。另外我就可以一键给别人分配一个 Online 版本的 VS Code,别人写了代码之后也可以方便拿给我看问题,也方便直接给别人分享我写的代码和运行效果,简直不要太爽了!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

Python

之前我们了解了一些验证码的处理流程,比如图形验证码、滑块验证码、点选验证码等等,但是这些验证码都有一种共同的特点,那就是这些验证码的处理流程通常只需要在 PC 上完成即可,比如图形验证码如果在 PC 上出现,那么在 PC 上直接验证通过就好了,所有的识别、验证输入的流程都是在 PC 上完成的。

但还有一种验证码和此种情况不同,那就是手机验证码,比如 PC 上需要输入手机号,然后短信验证码需要发到手机上,然后再在 PC 上把收到的验证码输入即可通过验证。

那遇到这种情况,我们如何才能将这个流程给自动化呢?

验证码收发

通常来说,我们的自动化脚本会运行在 PC 上,比如打开一个网页,然后模拟输入手机号,然后点击获取验证码,接下来就需要输入验证码了。打开页面,输入手机号、点击获取验证码等流程我们可以非常容易地实现自动化,但是验证码被发送到手机上了,我们怎么能把它转到 PC 上呢?

为了自动化整个验证码收发的流程,这时候我们想要完成的就是——当手机收到一条短信的时候,它能够自动将短信转发到某处,比如一台远程服务器上或者直接发到 PC 上,在 PC 上我们可以通过一些方法再把短信获取下来并提取验证码的内容,然后自动化填充验证码即可。

那这里关键的部分其实就是怎样完成这两个步骤:

  • 如何监听手机收到了短信

  • 如何将手机短信转发到想要的位置

这两个步骤缺一不可,而且都需要在手机上完成。

解决思路自然很简单了,我们以 Android 手机为例,如果有 Android 开发经验的话,其实这两个功能实现起来还是蛮简单的。

注意:这里我们仅仅简单介绍基本的思路,不会完全详细展开介绍具体的代码实现,感兴趣的话可以自行尝试。

首先如何监听手机收到了短信呢?

在 Android 开发中,整体就分为三个必要环节:

  • 注册读取短信的权限:在一个 Android App 中,读取短信是需要特定的权限的,所以我们需要在 Andriod App 的 AndroidManifest.xml 中将读取短信的权限配置好,比如接收短信的权限配置如下:
1
<uses-permission android:name="android.permission.RECEIVE_SMS"></uses-permission>
  • 注册广播事件:Android 有一个基本组件叫做 BroadcastReceiver,也就是广播接收者的意思,我们可以用它来监听来自系统的各种事件广播,比如系统电量不足的广播、系统来电的广播,当然系统收到短信的广播也就不在话下了。所以这就类似我们注册一个监听器,用来监听系统收到短信的事件。
    比如这里我们可以同样在 AndroidManifest.xml 里面注册一个 BroadcastReceiver,叫做 SmsReciver:
1
2
3
4
5
<receiver android:name=".receive.SmsReciver">
<intent-filter android:priority="999">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
  • 实现短信广播接收:这里就需要我们真正实现短信接收的逻辑了,这里只需要实现一个 SmsReceiver 类来继承一个 BroadcastReceiver 然后实现其 onReceive 方法即可,其中 intent 参数里面便包含了我们想要的短信息内容,实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SmsReciver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
SmsMessage msg = null;
if (null != bundle) {
Object[] smsObj = (Object[]) bundle.get("pdus");
for (Object object : smsObj) {
msg = SmsMessage.createFromPdu((byte[]) object);
Log.e("短信号码", "" + msg.getOriginatingAddress());
Log.e("短信内容", "" + msg.getDisplayMessageBody());
Log.e("短信时间", "" + msg.getTimestampMillis());
}
}
}

如此一来,我们便实现了短信的接收。

短信收到之后,发送自然也就很简单了,比如服务器提供一个 API,我们通过请求该 API 即可实现数据的发送,这个通过 Android 的一些 HTTP 请求库就可以实现,比如 OkHttp 等构造一个 HTTP 请求即可,这里就不再赘述了。

不过总的来说,整个流程下来其实还需要花费一些开发成本的,对于如此常用的功能,有没有现成的解决方案呢?自然是有的。我们可以借助于于一些开源实现,我们就没必要重复造轮子了。

这里我们就介绍一个开源软件,叫做 SmsForwarder,中文翻译过来叫做短信转发器,其 GitHub 仓库地址为:https://github.com/pppscn/SmsForwarder。

它的基本流程架构图如下:

架构图非常清晰,SmsForwarder 可以监听监听收到短信的事件,获取到短信的来源号码、接受卡槽、短信内容、接收时间等内容,然后将其通过一定的规则转发出去,支持转发到邮箱、微信群机器人、企业微信、Telegram 机器人、Webhook 等。

比如我们可以配置类似这样的规则,如图所示:

转发规则

比如当手机号符合一定的规则就转发到 QQ 邮箱,比如内容包含“报警”就转发到阿里企业邮箱,比如内容开头是“测试”就发动给叫做 TSMS 的 Webhook。

其中QQ邮箱、阿里企业邮箱都是我们已经配置好的发送方,都属于邮箱类型,TSMS 也是一种发送方,属于 Webhook 类型,如图所示:

发送方

我们也可以点击添加发送方按钮来添加对应的发送方,比如添加邮箱的发送方,我们可以设置 SMTP 配置下发件邮箱、SMTP 服务器、SMTP 端口、授权密码等内容:

添加/编辑发送方邮箱

设置 Webhook 我们可以选择是 GET 还是 POST 请求,然后填入对应的 URL、密钥等内容:

添加/编辑发送方网页通知

设置转发规则页面如图所示:

支持正则匹配规则 & 支持卡槽匹配规则

比如这里我们可以选择匹配卡槽、匹配的字段、匹配的模式,还可以配置正则来设置匹配的值,这里就配置了尾号是 4566 的手机号来执行一定的发送操作,收到的短信会发送到钉钉这个发送方。

实战演示

比如这里我们来尝试下,这里我们用 Flask 写一个 API,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from flask import Flask, request, jsonify
from loguru import logger

app = Flask(__name__)


@app.route('/sms', methods=['POST'])
def receive():
sms_content = request.form.get('content')
logger.debug(f'received {sms_content}')
# parse content and save to db or mq
return jsonify(status='success')


if __name__ == '__main__':
app.run(debug=True)

代码很简单,这里设置了一个路由,接收 POST 请求,然后读取了 Request 表单的内容,其中 content 就是短信的详情内容,然后将其打印出来。

我们将代码保存为 server.py,然后将其运行起来:

1
python3 server.py

运行结果输出如下:

1
2
3
4
5
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 269-657-055

为了方便测试,我们可以用 Ngrok 将该服务暴露到公网:

1
ngrok http 5000

注意:Ngrok 可以方便地将任何非公网的服务暴露到公网访问,并配置特定的临时二级域名,但一个域名有时长限制,所以通常仅供测试使用。试用前请先安装 Ngrok,具体可以参考 https://ngrok.com/。

运行之后,可以看到输入结果如下:

1
2
3
4
5
6
7
8
9
10
11
Session Status                online                                                                                                   
Session Expires 1 hour, 59 minutes
Update update available (version 2.3.40, Ctrl-U to update)
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://1259539cb974.ngrok.io -> http://localhost:5000
Forwarding https://1259539cb974.ngrok.io -> http://localhost:5000

Connections ttl opn rt1 rt5 p50 p90
9 0 0.00 0.00 0.00 0.00

这里我们可以看到 Ngrok 为我们配置了一个公网地址,比如访问 https://1259539cb974.ngrok.io 即相当于访问了我们本地的 http://localhost:5000 服务,这样手机上只需要配置这个地址即可将数据发送到 PC 了。

接下来我们手机上打开 SmsForder,添加一个 Webhook 类型的发送方,配置如下:

这里 Server 的地址我们就直接设置了刚才 Ngrok 提供的公网地址了,记得 URL 路径后面加上 sms。

接着我们添加一个转发规则:

这里我们设置了内容匹配规则,比如匹配到内容开头为测试的时候,那就将短信内容转发到 Webhook 这个发送方,即发送到我们刚刚搭建的 Flask 服务器上。

OK,配置完成之后,然后我们给该手机尝试发送一个验证码,内容如下:

1
测试验证码593722,一分钟有效。

这时候就可以发现刚才的 Flask 服务器接收结果是这样的:

1
2
3
4
5
received +8617xxxxxxxx
测试验证码593722,一分钟有效。
SIM2_China Unicom_
2021-03-27 18:47:54
SM-G9860

可以看到刚才验证码的内容就成功由手机发送到 PC 了,接着我们便可以对此消息进行解析和处理,然后存入数据库或者消息队列即可。爬虫一端监听消息队列或者数据库改动即可将其填写并进行一些模拟登录操作了,该步骤就不再赘述了。

批量收发

当然以上只针对于一部手机的情况,如果我们有大量的手机和手机卡,我们可以实现手机的群控处理,比如统一安装短信接收软件,统一配置相同的转发规则,从而实现大量手机号验证码的接收和处理。

比如一个群控系统就是这样的:

卡池

当然还有更专业的解决方案,比如有专业的手机卡池,配合以专业的软件设备实现短信的监听。

比如如下的设备支持插 128 张 SIM 卡,就可以实现同时监听 128 个手机号的验证码,如图所示:

具体的技术这里不再阐述,详细可以自行查询相关的设备供应商。

接码平台

当然如上的方案成本还是比较高的,而且这些方案其实已经不限于简单接收短信验证码了,比如手机群控系统一般都会做手机群控爬虫,而卡池也可以用来做 4G/5G 蜂窝代理,如果仅仅做短信收发是可以的,但未免有些浪费了。

如果我们不想耗费过多成本想实现短信验证码的自动化,还有一种方案就是接码平台,其基本思路是这样的:

  • 平台会维护大量的手机号,并可能开放一些 API 或者提供网页供我们调用来获取手机号和查看短信的内容。

  • 我们调用 API 或者爬取网页获取手机号,然后在对应的站点输入该手机号来获取验证码。

  • 通过调用 API 或者爬取网页获取对应手机号短信的内容,并交由爬虫处理。

具体的操作步骤这里就不再详细阐述了,这里简单列几个接码平台:

由于接码平台管控比较严格,所以可能随时不可用,请自行搜集对应的平台进行使用。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

VS Code 之大,简直是无奇不有啊!

这不,前几天我就发现了一个插件,用这个插件我们甚至可以在 VS Code 里面交友!就像一些交友软件一样,喜欢的右滑、不喜欢的左滑,互相喜欢的就匹配成功,然后就可以聊天!进而???

简直是太骚了啊!

究竟是何方神圣呢?

这个 VS Code 插件叫做 vsinder,什么意思?就是 VS Code + Tinder (一个国外交友软件)的简写,Logo 是这样的:

这爱心不能太明显了。

抱着好奇心,我在 VS Code 里面搜了一下,还真有,而且现在已经 3w+ 次安装了:

那就索性来试试吧,点击 install,完了之后在 VS Code 的左侧就会出现一个爱心💗的入口,如图所示:

点击一下,一上来就要求 Sign-in,这里是用的 GitHub 来登录的,点击之后会跳转到 vsinder 的 SSO 登录页面,GitHub 登录成功之后就会提示授权成功:

回到 VS Code,这里就显示 GitHub 的个人信息了,它获取了我的 GitHub 头像、昵称、年龄等信息:

当然我们也可以点击 edit profile 来修改自己的个人信息,点击之后如图所示:

这里可以填写昵称、个人简介、擅长的编程语言、生日等信息,生日是用来计算年龄的。

然后下面还有一个关键的信息,就是你要在这里找什么人?一个是 love 一个是 friendship,如果选了 love,还会提示选择男的还是女的,还有年龄等等:

比如这里我就果断选了 —— friendship!(逃

点击保存之后,还有另外一个入口就是 edit code pics,就是把自己觉得最牛逼的代码贴上,这样才会更多的人右滑对不对!?

我是写 Python 的,那我选什么好呢?

对了,那就选 Python 之禅了!大道至简,浑然天成!

Python 命令行输入:

1
import this

运行结果如下:

这里我就复制一下,粘贴进去了,最多 600 字符,那这里我就直接截断了。

虽然这不是完完全全的 Python 代码,但把 Python 最核心的设计思想说出来了,是不是 Python 里面最牛逼的?(是!

我相信别人看到之后一定会疯狂 like 我的。

接下来保存下,然后我就点击 start swiping 开始滑动了!如图所示:

一上来就滑到一个:

这个人名字叫 Leon,23 岁,写了个代码是用来 walkDir 的,也就是遍历文件目录的,看简介似乎还是个俄语?俄罗斯人啊。

但是,这个一看就不太行,他不用 Python,如果用 Python 的话,直接用 glob 包不就解决的事吗?不行不行,果断点击 x。

这里还有快捷键,如果不喜欢可以点击键盘的左箭头,如果喜欢可以点击右箭头。

这里果断左箭头了!

又来了一个,这人写 Go 的:

但是似乎就会个 Hello World,不行不行,果断还是左箭头了。

接下来一个 19 岁的孩子写了段 Java:

还可以,写了一个 random 选择,随机抽取一句话返回,还行吧,那就 like 一下吧。

然而这时候,我发现了什么?一个聊天按钮?还有个 1?

我跟这个 19 岁的孩子匹配上了??

点击头像还能聊天?

我发了一个:Your code is really cool! (其实也就是那样,小孩子嘛,稍微夸一夸啦

过了一会,也没理我。

估计可能时区不一样,还在睡觉呢吧?

算了我也不给他机会了,点击 ummatch 可以直接取消匹配,这样他就没法给我发消息了:

不一会我又匹配了一个写 Python的,但这会我忘记了他写的代码是什么样子的了怎么办呢?

不用担心,只需要点击他的昵称就又能看到他写的代码了:

他写的是一个 numpy 的调用,看起来还不错的样子。

于是,后面的事情,我就不说了,我们当然是欢乐地聊起了 Python。(手动滑稽

有人说?这个真能找到对象吗?还真能!

这个插件的作者 Ben Awad 做完这个插件之后,录了个视频上传了 Youtube,现场演示了自己是怎么在这里面找对象的。

不仅仅是匹配到了妹子,而且更牛逼的是他仅仅通过几句聊天就轻松拿到了电话号码???

大师!

不愧是大师啊!

相信你也能行的,来试试吧~

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人随笔

今天早上很早就醒了,激动得睡不着觉,一直期待着篮网和雄鹿的抢七大战。

总体上看,其实雄鹿整体纸面实力还是占优一些,但是篮网有我杜,我始终相信有我杜什么奇迹都会发生。再加上篮网主场,还有一些历史数据,比如雄鹿客场抢七没赢过,还有裁判吹罚对主场有利,还有腾讯女主播不是美娜…反正种种的一切似乎还是偏向篮网的。

比赛开始,第一节我其实就求篮网不挖坑就行了,因为之前几场都是开局先挖一个大坑,然后后面就很难填了,不过好在第一节僵持住了。

第二节和第三节就不说了,反正挺焦灼的,我和我的好朋友,也是阿杜球迷,一直线上交流着,进球我们就庆祝,不进就互相安慰下。

一直到第四节吧,还剩三四分钟那会还是领先的,但是后来一两分钟居然被反超了,最后不到一分钟,落后四分,那会我俩都觉得要凉凉了。

但是阿杜站出来了,最后雄鹿罚球完就剩两分分差。

那会是 109:107,篮网落后 2 分,绝杀 or 加时 or 回家,生死就在一瞬间!!!

那会我真的是感觉心脏都要跳出来了,疯狂刷着文字直播和实时比分,因为文字直播和实时比分比视频快一些。

我简直那几秒激动得快不行了,真的很难想象阿杜在场上是什么心情。

但!我那会刷到了文字直播,突然比分变成了 109:110,我知道进三分了!!!

我那会真的太激动了,绝杀了!!!真的很难以想象那会我是多么欣喜!!!

然后马上视频直播就来了,看着阿杜最后在最后几秒,运球,转身,三分!进了!!!

我那会看到就直接超大声喊出来了!!!太激动了!!!

太牛逼了,阿杜就是永远的神!

然而,看了看回放,阿杜踩线了,是 2 分,最后变成了 109 平,最后进了加时。

也可以吧,还有机会的。

加时一上来篮网就领先了 2 分,后来好几分钟双方都没得分,最后就剩一分钟多一点了吧,篮网还领先 2 分,我觉得是不是稳了。

后来雄鹿字母哥打进追平。

我记得就在这时候,下一个回合让我特别失望,阿杜面临严防没进,但是有前场板,给了空位的哈里斯。我这时候心想,体现你价值的时候到了,哈里斯,你得进啊!!!然而,伴随着打铁声,还是没进…

哈里斯你就不能争口气吗???

113:111 了,篮网最后时刻又落后了 2 分,我的心一下子又揪了起来。

最后几秒钟,最后的时刻,篮网落后两分,哈登把球给阿杜,霍勒迪防得挺好的,但阿杜最后没有再完成绝杀了。球挺正的,但是短了,是真的累了。

我记得阿杜投出的那一刻,眼中充满了期待,但可惜没进,他的眼神又转为了失落和无力,太可惜了。

就这么结束了吗?这么关键的时刻就以这种方式告终了吗?

我就心想,最后还有暂停的吧,教练你都不叫个暂停跑个战术的吗?就算你没啥战术,最后叫个暂停让阿杜休息休息也行啊,搞不懂纳什什么蜜汁操作。

但也没办法吧,球员毕竟也是可以叫暂停的,阿杜应该就是想自己干,最后一个球没有进就是没有进了。

输了,认了。

关于阿杜,毋庸置疑,联盟第一人无需辩驳。阿杜打满全场 53 分钟,48 分,全场一秒钟都没有歇,可谓是倾尽所有,无数次挽救球队于危难之中,阿杜真的是尽力了。尤其第四节最后的绝平,真的太牛逼了!yyds!

关于哈登,哈登也是尽力了吧,我对哈登没有什么怨言,他也是有伤在身,打满全场,虽然说表现不佳,因为没有恢复最佳状态,全场三分也不准也没有多少突破杀伤,但哈登G5 坚持带伤复出,已经连续带伤打了三场了,还是致敬。

关于纳什,我不想多说了,太多太多让我吐槽的了。就说今天最后时刻,即使球员没叫暂停,你叫个暂停布置下战术不行吗?叫个暂停让阿杜休息下不行吗?另外不怪你季后赛全场死用主力,因为毕竟球员也想上场拼尽全力,但是关键时刻,我真的没看出来纳什你有啥战术啊?另外季后赛死用主力没啥,常规赛你还让阿杜每场打那么久,甚至都能上好多次 40 分钟,至于吗?还有,格林今天就打了几分钟,哈里斯这么拉垮你换格林试试不行吗,天王山不记得是谁打出来的了吗?还有我一直很迷惑的,阿利泽约翰逊,常规赛也是拿 20 + 20 的,就这么被你彻底雪藏了?

关于哈里斯,算了,没得说了,关键时刻就是挺不起来的,萎了一个系列赛了,交易走吧。

关于格里芬,很硬,非常硬,防字母防得很好了,进攻也很不错,打出远超薪资的身价,我估计下赛季肯定不会这么低工资了,蔡老板高薪也得拿下啊。

关于布朗,我觉得还挺不错的,很有活力,尤其本场比赛的几个前场板,非常给力。

关于小乔丹,哦,这么久不见我已经不认识你了。

关于字母哥,那一垫脚欧文一直让我耿耿于怀,小脚一挪?阿德托昆博?就冲你这一脚,太脏了!我不会祝福你的,最后总冠军只要不是雄鹿,都好。希望 76 人晋级,让大帝好好教训下字母哥。

最后再说回阿杜吧,我支持阿杜十多年了,每次有阿杜的球我也是密切关注,赢了开心一整天,输了可能难过一整天,今天就这么遗憾输了,挺难过的,挺惋惜的,最后功亏一篑。但不管怎样,阿杜整个赛季带给我的惊喜已经远超我预期了,赛季开始前我很担心阿杜跟腱大伤之后状态大不如前,但到如今,一次次的超神表现带给了我太多的惊喜,身体条件看着也很不错,似乎跟腱伤势的隐患已经非常小了。最后这个系列赛,天王山之战、这场比赛,真的你已经完全倾尽全力,不能要求你更多了,真的,太强了。你用自己的实力让各路的黑子闭上了嘴,用自己一次次投篮证明了自己就是妥妥的联盟第一人。虽然输了,但是你这个赛季带给大家的惊喜,你收获的尊重和认可,我想也是你非常想得到的东西,也为此为你感到高兴。篮网输了,但你没输。

哦对了,再补一句,篮球的比赛,还是得看直播而不是回放,因为只有直播,一切结果是未知的,才能有无限的期待,才能有无限的代入感,才能在这个过程中感受进球霎那间的极致情绪的绽放!就像本场第四节阿杜的绝平球,进球一瞬间肾上腺素飙升到极致的感觉,这或许是篮球带给我的澎湃激情!如果是录播,一切都知道的结果,那是不会有这种感觉的。另外也很感谢和我一起看球的朋友,一同分享喜悦,一起分享感动,真的难能可贵。

阿杜的赛季结束了,我这个赛季也不看球了,腾讯体育也卸载了。

下赛季卷土重来,哥三人健健康康地,继续向前进。

还是那五个字——篮网总冠军!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人随笔

运营公众号已经两年多时间了,之前时间还好,原创不少,大家也都挺愿意看的。但最近我观察发现,我的公众号「进击的 Coder」呈现出几个不健康的状态:

  • 总体阅读下滑

  • 粉丝粘性下滑

  • 粉丝数量下滑

其实根本原因还是原创变少了,当然还有其他的影响因素一起导致了这样的情况。

原创是公众号的至关重要的一部分,是公众号的血液,没有原创内容的文章是很难脱颖而出的,粉丝关注关注的其实就是能从中获取一些独到的价值。

但由于最近一段时间原创文章确实少,都在转载一些文章,和其他技术公众号大同小异,都是转来转去的,久而久之,读者就觉得没什么意思,取关什么的那就是自然而然的了。

那为什么原创变少了呢?总结下来有这么几点原因:

  • 我工作比较忙,现在处于项目的关键阶段,项目周期比较赶,确实写原创文章的时间变少了。

  • 由于最近半年和一年写的文章都是爬虫书的一部分,所以不好提前发出来,所以这就导致了公众号的原创文章变得很少。

  • 最近我也开了一个新的公众号「崔庆才丨静觅」,当时就是想在这个小号上简单记录自己的一些想法的,所以在这个小号上写了一些原创,但由于我当时太基于追求小号的质量和增长了,所以不论是技术文章还是个人感悟都发到小号上,「进击的 Coder」就直接转载这个小号上的文章,所以一些原创被分流了。

当然除了原创变少,还有几个导致目前不健康状态的原因:

  • 由于最近我大多为转载的文章,所以读者的一些留言询问一些问题我可能不了解细节,那就可能回答比较浅或者干脆放出来不予回答,跟读者的互动越来越少。

  • 公众号生态的变化,公众号生态早已一片红海,越来越多的创作者都加入公众号,都在竞争流量,转载和广告大量出现,导致读者现在对公众号的打开率越来越低,不论是单个还是总体。

  • 微信公号本身的推荐算法应该也有一定的作用,比如原创文章的权重相比之前我感觉更高了,反而转载过多的文章权重比例进一步下滑。

嗯,基本上就是这样的现状,导致了公号出现了不健康的状态。

经过一些思考和跟小伙伴的讨论,我们得到了一些思考,决定后面转变一下运营思路,大致总结下:

  • 依然坚持原创是根本,唯有原创才是公众号经久不衰的秘诀,所以以后会继续坚持原创,当然由于本人时间原因,每周每个号 1-2 篇应该算是一个小目标,也就是两个号「进击的Coder」和「崔庆才丨静觅」两个号每周会发 2-4 篇原创文章。

  • 关于原创我们也在联系一些渠道,比如如果大家有不错的原创文章,也欢迎前来本公众号投稿。

  • 之前为了每天都发文章,可能着急忙慌地找几篇就转了,质量并不高,最后导致阅读也不好,给读者提供的价值也不够,所以调整了一下策略——非必要不发文。也就是说,如果今天确实没有什么好发的,那就不要制造信息垃圾了。要发就发一些有价值的,高质量的,所以会更加严格控制公号的文章质量。

  • 区分好「进击的 Coder」和「崔庆才丨静觅」两个号的定位,之前我对两个公号的定位并不清晰,甚至有一段时间不论技术文章还是个人感悟都发到「崔庆才丨静觅」上面,「进击的 Coder」一直是转载文章,这样会导致「进击的 Coder」出现上述我说的不健康的状态。所以后面,「进击的 Coder」会侧重于发技术文章,也就是说我平时写的有关技术的所有文章都会放到这个号上面来,包括网络爬虫、技术总结、工具推荐、Web、AI、小知识点等等。「崔庆才丨静觅」更加私人一点,就会发我日常的一些感悟,比如我的工作感想、我的生活状态、我的碎碎念、我的读书笔记等等。如果大家感兴趣的话可以关注下。

  • 增加和读者的互动,之前的一些留言,跟读者的互动性不好,后面会注意多增加一些互动和交流,增加读者粘性。

  • 不定期搞一点小互动,比如送一些小礼品、书等等。

PS:当然毫不避讳地说,由于我也需要公号来获取一些收入,所以有时候公号也会接一些广告的,但是我也会控制好广告的频率和质量,广告也都是一些技术课程类的,大家如果感兴趣希望如果也可以支持下,感激不尽!

就是这样啦,如果大家还想到什么好的点子也欢迎告诉我哈。我还是非常希望我能够通过两个公众号为大家提供更好的内容和价值的。

Fighting!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人记录

这件事是前几天发生的,端午假期我出去参加同学婚礼,假期最后一天我坐高铁回北京。

高铁上,一个小男孩子在说话比较大声,而且看起来比较调皮,敲了小桌板几下,发出了一些声响。然后他爸爸就很生气,应该是呵斥了他几句,然后那个孩子就哭了起来。

其实哭得还挺大声的,一直哭了好几分钟,如果一般情况下,听到孩子哭其实是很让人心烦的,但是这次发生的事情让我唯一的感觉却是替孩子感到惋惜和同情。

那个孩子中间说了好几次:“爸爸,是我做的不对吗?”

爸爸一开始没搭理,孩子问了好几次之后,他爸爸就回答:“别说话,你看别人都不说话,你也不要说话啊。”

孩子应该是听到并不是自己想要的答案,然后就接着问:“爸爸,是我做的让你感到失望了吗?”

爸爸还是那句话:“别说话,安静一点。”

然后孩子还在继续问:“爸爸,我不想让你生气和失望,爸爸是我那里做的不对呢?”

爸爸依然还在说:“别说话,你看别人都不说话,你也不要说话。”

卧槽,听到这里我真的感觉这爸爸怎么这样啊!

后面反复了几句,爸爸依然就是那个回复,后来孩子也不再问了,也就不说话了,火车上又安静了起来。

但我内心还是久久不能平静,直到现在想起来真的想说孩子爸爸几句,真的我感觉这件事让我真的很同情这个孩子,而且对爸爸这种态度感到非常的愤怒,怎么能这么培养和教育孩子呢?

孩子在成长的过程中,尤其在这个阶段,他对整个世界没有很好的认知,不知道自己做的事情是不是对的,而且对世界充满了好奇,所以他遇到自己不明白的事情的时候,非常迫切需要得到一个答案和反馈,如果这时候家长可以给予好的引导,对孩子的成长简直太重要了。

而且更让我感到惋惜的是,这个孩子真的算很懂事了,他已经可以说出“爸爸,是我做的让你感到失望了吗?我不想让你生气和失望。”这样的话了,孩子都能表达自己的感受和想法,这真的已经非常好了。

但是他爸爸的回答简直太让人失望和生气了,他非但没有正面回答孩子的所有问题,而且一直在命令孩子不要讲话,完全没有一点正向的反馈,跟孩子好好说说不好吗?

比如就说,宝贝我没有生气和失望,就是刚才的你声音比较大,会影响到其他的乘客,我们要做一个好的孩子,在火车上不要大声讲话以免影响他人,以后我们火车上安静一些,这样就更好了。首先能够正面回答孩子的疑问,然后通过一些正向的激励来帮助孩子了解一些知识和让他认识到应该做的事情,这样不好吗?这孩子听了之后,一方面就知道自己做的是不是对的,应该怎么做才能更好,因为这个孩子本身已经很懂事了,他也想成为一个好的孩子,知道了哪些是对的,我相信这个孩子后面会注意的。而且经过这些事,孩子也能明白一些道理,这是从生活中学到的,对孩子特别重要。

虽然我没有孩子啊,但站一个旁观者的角度,真的觉得家长应该提供一个更好的引导和教育,这样孩子才能更好地成长。

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

个人记录

今天比较忙,等到写这篇文章的时候已经第二天了,但是还是不得不写写,因为杜兰特这场天王山之战的表现是在是太牛逼了!

我平时是看 NBA 的,是杜兰特的死忠粉,喜欢杜兰特有十年了,从高中的时候经常听到同学议论 NBA,说雷霆队有个叫杜兰特的很厉害,跟着看了几场球,后来就成了杜兰特的球迷。

我也习惯称杜兰特为阿杜、KD,所以后面文章我就叫阿杜了,感觉比较亲切。

当然现在我也是毫无疑问的的篮网球迷,因为阿杜去哪个球队我就支持哪个球队。从雷霆到勇士到篮网,他也经历过低谷、经历过高光,勇士的两连冠真的发自内心为他感到高兴,但前年他遭遇了跟腱断裂大伤,我也一直在关注各种消息期盼他能王者归来。

这个赛季,终于盼来了阿杜的复出,我特别希望他能回到当时巅峰的状态,希望跟腱的伤势对他的影响尽量是最小的,但也免不了的每场比赛都会担心他的跟腱伤势会不会复发。尤其是到了现在的季后赛,对抗强度上升,更是每场都在祈祷阿杜不要受伤。

幸运的是,这个赛季没有再看到阿杜的跟腱伤势有任何复发迹象,当然他也在尽力保护自己,比如落地的时候会用摔倒的方式缓冲一下力量。每多看一场,我对阿杜跟腱的恢复情况就会越乐观一些,应该算是彻底好了吧,谢天谢地。

扯远了,今天我来就是专门来说说今天阿杜的超神表现的。

可以说,今天的表现,任何华丽的词藻都不足以表达阿杜的牛逼了,简直就是天神下凡!打满全场 48 分钟,一分钟都没有休息,23 投 16 中,砍下 49 分 + 17 篮板 + 10 助攻的超级三双,外加 3 抢断 2 盖帽,率领篮网在一度落后 17 分的情况下、在两大巨星队友都遭遇伤病的情况下、在众多队友都失准的情况下,带领球队完成惊天大翻盘赢下雄鹿,最后一节真的是把我都看哭了,太悲壮了!

关注这场比赛的朋友都知道这场比赛对阿杜来说意味着什么,本来篮网的形势一片大好,但是和雄鹿的系列赛第一场哈登就因为腿筋伤势退场,第四场欧文又被字母歌垫脚无法出战。虽然说哈登今天出战了,但是状态远不及正常水准。上半场一开场就大比分落后,这时候靠的就只有阿杜了。最后的结果大家也都知道了,阿杜天神下凡般的表现把这场比赛硬生生拿下了!把字母哥彻底打服了,把众多对阿杜一直持怀疑态度的群众打服了,把国内第一杜黑徐静雨打服了。我愿称这场比赛就是阿杜彻底的封神之战,是迄今为止阿杜职业生涯最牛逼的一场比赛,没有之一。

先说说徐静雨吧,他是网上一个比较知名的说球的。客观来讲,他确实对一些比赛或者球员的分析有自己的独到见解,这方面我是认可他的。他是库里球迷,一说到库里那就可劲吹,但经过观察,我发现他是阿杜的黑粉,可能是因为阿杜抢了库里两个 FMVP 的原因吧,一直耿耿于怀,然后一个劲说 MVP 比 FMVP 含金量大多了,反正就是库里有的就可劲吹,没有的就可劲贬。一听到他说阿杜不好的我就来气,甚至连 CJ 单换阿杜的话都说出来,就觉得他真是一个无脑黑子,中间我还把他拉黑了一段时间。但最近发现,徐静雨的口风发生了变化,一方面的原因可能是库里没有进季后赛,另一方面却是也是阿杜打出的表现的确让人心服口服,身体条件也观察到愈发硬朗,攻防两端的表现的确是非常超群。今天我专门去看了徐静雨怎么说,一进去就看到几个大字「全力杜已超全力詹」,好家伙,这是彻底被阿杜打服了,视频中他也对阿杜各种赞誉,被阿杜的表现深深折服,甚至都已经说全力杜已经超过 12 年全力詹这样的话了,全网第一杜黑被打服了。

然后说说我心里的感受吧,其实徐静雨说的一番话我也是认同的,就是阿杜需要一个契机来证明自己的超群价值。这时候有人可能就说了,阿杜的能力还需要证明吗?其实在我看来,需要的。

  • 首先在雷霆时期,阿杜的表现足够牛逼了,尤其 13-14 赛季已经证明了自己单核带队的能力,并勇夺得常规赛 MVP,但这毕竟是常规赛。

  • 在雷霆最后一个赛季,阿杜在跟勇士的鏖战中最后被翻盘,最后阿杜就加入了勇士,这也成了阿杜一生中的一个巨大的黑点,网上一众黑子各种骂。当然我不认为这是黑点哈,我觉得阿杜去到更好的地方,先实现自己的冠军梦就好,他做的决定我是完全支持的。

  • 阿杜在勇士夺得了两个冠军,但是在很多人看来,这两个冠军似乎都太简单了,毕竟勇士这个球队本身就太强大了,银河战舰级别的配置,所以外界都觉得这两个冠军没有什么含金量,也在质疑阿杜要是单核带队是不行的。

  • 阿杜的内心是很敏感的,他也很在意外界对自己的看法,想要赢得外界的尊重。所以他离开勇士的一个原因就有想再证明自己的价值。

  • 篮网今年阵容经过一番操作机会变得非常豪华了,阿杜、哈登、欧文,篮网整个阵容可以说都能比得上 17-19 赛季的勇士了,阿杜伤停一段时间,哈登又带队打出逆天表现,于是又有人开始质疑阿杜的带队能力,说阿杜单核带队不行,只能作为一个强力终结点,不能很好地带动队友。

综合以上几点,加上现在特殊的情况,三巨头除了阿杜其他两个都伤了,而且篮网刚刚经历了从 2-0 到 2-2 点局面,被雄鹿连扳两盘,而且上一场欧文刚刚伤了,不论是阵容上还是士气上,篮网都处于很大的劣势。而天王山之战的重要性不言而喻,此时篮网能依靠的就只有阿杜了。

所以如果这一场,阿杜能够爆发并带领球队获胜,就是对自己非常非常好的证明,这是天时造就的机会,证明自己的时候到了!

不负众望,阿杜天神下凡般的表现大家也看到了,不管是进攻终结还是防守盖帽,攻防两端打出来了统治级表现。另外阿杜不仅仅作为一个终结点,还送出了 10 次助攻,不光得分还能串联球队,就是以往的阿杜和哈登的超强结合体,攻防、串联,所有的所有都做到淋漓尽致。另外阿杜对胜利的渴望也是到了极致,打满了全场 48 分钟,一个个无解中投,一次次突破上篮,还有最后的压哨三分,死神镰刀挥舞起来,彻彻底底的统治级表现,这就是一场封神之战,打破所有的质疑,赢得了足够的尊重。所以这也是我愿意把他称之为阿杜生涯最牛逼的比赛,没有之一。

最后,希望篮网全员能够恢复健康,继续向前征战。

篮网总冠军!阿杜 FMVP!

更多精彩内容,请关注我的公众号「进击的 Coder」和「崔庆才丨静觅」。

技术杂谈

大家知道我平时会写一些文章,有的朋友也会经常问我:你是用的什么软件写作的?用什么软件管理这些文章的?

我之前其实有介绍过我正在使用的支持跨平台写作的解决方案,是基于苹果的 iCloud Drive 来实现的。

首先我是非常喜欢用 Markdown 来写内容的。在电脑上,我可以用文本编辑器 Typora,工作空间都是在 iCloud Drive 上,写完之后就会自动同步,然后我在手机上是用了另外一款软件叫做 Blockquote,也能直接打开 iCloud Drive 的文件,手机上也可以编辑 Markdown 文件。

手机上的软件类似这样子:

但是呢,说实话手机上的这款软件我觉得并没有那么好用,但主要因为他能直接打开原生的md文件,而且没有那么花里胡哨的功能,所以当时就选它了。

但这几天由于我出门了,不方便带电脑,所以有时候就想用手机来写,平时我大多数都是用电脑比较多,这次改成手机直接用 Blockquote 还觉得有些不习惯,逐渐感到有些乏力。

于是就想着看看有没有更好用的替代品。

基本需求

我先说一下我的基本的需求:

  • 第一点呢就是我必须要做到跨平台的同步,因为我有好多台设备,在不同的时间在不同的设备上工作,比如上班时间在公司的 iMac 上,下班了在家里的 iMac 上,周末又会带着 Macbook Pro 出去,路上又在用 iPhone,偶尔还会用用 iPad。在这个过程中我想要随时随地把一些东西记录下来,所以我想要实现无缝的协作切换和高效率的实时同步。所以,我的所有的文本和笔记都必须要是跨平台协同同步的,这个是一个最基本的要求。由于我使用的是苹果全家桶,所以说我之前基于 iCloud Drive 就可以很轻松的实现这所有的功能,然后不同平台上用不同的编辑器来编辑就行了。

  • 第二点我想要保留原生态,即文本的平台无关性。我想编辑的内容是实实在在存在的一个 md 后缀的纯文本文件,好多 md 文件就放在一个文件夹下,也有对应的放图片的文件夹,以便于我随时随地能够把一个 md 文件分享出去或者切到另外的软件打开,甚至加到 Git 中保存起来。比如说我之前调研过「熊掌记」,它对 Markdown 支持挺好的,但是当我想获取原始的 Markdown 文件树的时候,发现「熊掌记」已经把它做到平台里面了,我是很难获取整个的 md 文件夹的,所以我就没法用 Git 来管理我所有的 md 文件了。

基于这两点,之前的方案是能实现的,但是手机上的 Markdown 编辑软件我实在觉得乏力。

所以我也在反思,为什么我一定要执着于原生的 md 文件的编辑呢?我思考了一下,我真正想要的就是第一点——跨平台的同步,是不是我在编辑一个真实的 md 文件不重要,如果我用的平台能够支持导入 md,支持导出 md,那不就好了吗?甚至版本控制我也不用做了,如果一个笔记软件能支持版本控制那我直接用不就好了吗?想到这里,觉得之前似乎走到坑里去了,现在其实很多的厂商都已经提供了这样的解决方案,比如说印象笔记、有道云笔记、还有 Notion 等等。各种各样的笔记软件,其实都已经做到全平台的支持和跨平台的同步了。

Notion

经过一番勘查,我找到了 Notion。经过这么几年的发展,Notion 的功能也变得越来越强大了。而且它对 Markdown 也有一定的支持,虽然说它的语法并不是完完全全的 Markdown,但一些特色功能的确很能打动人心。

首先 Notion 的设计思路就挺好的 —— 万物皆是 Block,即一个块。比如一段文本、一张图片、一个视频、一段代码,只不过 Block 有自己的类型,每个 Block 就像一个个小组件,最终组合成了一篇文章。有了 Block 的设计,一些实现上的灵活性就更高了。要添加新的功能只需要加新的 Block 就好了。

另外 Notion 的功能还是很强的,因为 Block 的加持,所以文档不仅仅可以是文档,又可以是一个项目管理图、一个课程表、一个思维导图、一个计划表等等,All in One!

但不能光说好啊,对于不好的地方,我也进行了一番查阅,Notion 有什么不好的呢?其中很多人反馈的就是它的服务器在国外,所以说一些加载延迟比较高,网络同步效率一般。另外一个反馈就是 Notion 对于中文的支持不好,现在我看到 Notion 只支持了韩文还有英文,然后据小道消息,中文的支持。还在计划和测试中,但是不确定什么时候正式上线。还有就是 Notion 因为是一款国际化软件,所以对国内生态支持不好,比如微信登录、国内手机号登录等等。

反正这些问题还是蛮受诟病的。

遇见 wolai

然后找着找着,我就又发现了另外一款软件。叫做我来 wolai,就基本上是一个国内版的 Notion。

wolai 这名字的确有点奇怪啊,但这影响不大,重要的还是功能。

我一开始对这个软件其实还是持有怀疑态度的,因为我总觉得国内的一些公司可能作为刚初创的公司,它的这个质量的把控可能并不是很好,或者是说服务不稳定。另外可能用着用着这个公司就跑路了,倒闭了,这也是有可能的。

但怎么说呢?我个人试用了一下这一款软件,整个我觉得还是挺符合我的需求的。虽然说它是对标了国外的 Notion 所有的一些功能键。和 Notion 非常的相似。但我觉得只要做到好这个软件好用。能够满足我的需求。那他不妨成为我的一个候选。

我还专门去查了一下这个公司的创始人以及他们做事情的一些态度理念。

看了之后,发现 wolai 其实才上线了一年的时间,但功能已经相对完善了,我觉得现在的功能点做的还算不错了。

wolai 的创始人叫马锐拉,知乎主页是:https://www.zhihu.com/people/marila

他们做 wolai 的初衷是这样说的:

最初开始做 wolai ,出于一个非常简单的原因——作为一个管理者,面对当时公司中几十个互相割裂的内部系统、平台,我往往需要通过拼凑信息碎片并手动构建关联,才能完成信息的追溯与重组。循环往复、极为低效地搜寻信息需要耗费巨大的心力,更别说一群人通过这样的方式来进行协作,可想而知是多么痛苦。而一些相对简单的部门内部协作流程,和协作本身,也没有很好地系统可以承载、实现,最后都变成一个个微信群。每个人都要在忍受无数“信息噪音”的同时,随时留意与自己真正相关的聊天内容。我们都被绑架在工作群聊中。凡此种种,让我觉得信息必然有更好的组织方式,大量日常协作流程也不应该依赖于在群聊中共享信息流。我开始意识到,我们缺少一个真正解决这些问题的工具。

他们对 wolai 的定位是这样的:

  1. 可以更好地组织信息,不论是组织方式还是交互体验;

  2. 可以让每个人都能自由构建流程与应用,满足团队日常工作所需;

  3. 可以进行更好的分享与协作;

  4. 不能是一个呆板严肃的被老板逼着用的办公软件;

  5. 团队或企业可以完成日常 80% 以上的协作,让 Office 回归专业软件,让微信回归即时通讯……

到今天为止,已经累计发布了将近 900 个版本,平均一天发布近 3 次,优化、迭代、完善了无数产品功能;也有了 Windows 、Mac 、Linux 、iOS 、Android 全平台客户端;建立了云服务的空间计划体系,夯实了云端协作平台的基础……终于也算变成了一个有模有样的产品。

卧槽,牛逼,能一年搞得这样,真的很佩服了,尤其还支持 Linux!

而且这个界面设计感,我觉得很不错!

其实我本来想好好介绍一下这个软件的,但是我感觉我自己写了之后,和原来官方写的文章其实也大差不差。而且我感觉人家的这篇文章写的也挺好的。

所以大家可以直接来看看原介绍文章吧:https://zhuanlan.zhihu.com/p/379723832

功能

我就简单说几个让我觉得眼前一亮的功能吧。

界面

大家可以发现,我每次介绍软件都要先评论一下界面,就简单放个官方的图吧:

文章支持 Markdown 绝大多数语法,而且文章上面可以放头图,而且还有专门的 icon,五颜六色的 tag 支持,整个风格是 Material 风格设计,一些边距、图标让人感觉很舒服。

他们自己也说了:

在调性上,区别于千篇一律使用蓝色为主色调的企业应用,我们反其道而行,以暖色为主色调。去除矩形的锋利棱角,用圆润的边角体现 wolai “专业却不失温度”的企业文化和产品理念。

一些文章可以灵活分类组织,每个分类还有单独的图标,井然有序,给单调的列表增加了几分活力有木有!

我爱了。

看看我这篇文章就用 wolai 写的,就看这个图标设计吧,左上角可以选一个图标,支持 Emoji,各种颜色的扁平图标,甚至是动态日历,比如这就是个动态日历:

同时设置完了之后还能显示在文章标题前面,这细节到位了!

全平台同步

另外很重要的当然就是这个全平台的同步了,我看了一下,它支持 Mac、Windows、Android、iPad、iPhone 各种各样的平台,尤其还支持 Linux!这个一上来就把这个好感度拉满了。

而且 Mac 也专门为 M1 芯片做了适配。

很妙!

导入导出

另外我测试了一下,它支持 Markdown 的导入和导出功能,也就是说,只要我有一个 md 的文件,那我可以立马将它转到这个平台里面去,同时他还支持 pdf、md 文件的导出。

这不算什么,我比较欣赏的是 wolai 的一个批量导入的功能,也就是说我可以把我之前所有的 md 文件,然后直接压缩成一个 zip 包。一键上传,wolai 可以解析然后转换,然后所有的文章就导入进来了,牛逼!

编辑历史

还有一个比较特色的功能是它可以保留每一个 Block 的修改历史,那也就是说如果我们想要回滚一些版本的话,是可以非常方便的查看的。这就类似于他比较好地集成了 Git 版本控制工具,但这个功能得高级版才能支持。

情怀

还有个加分项,就是整个团队他对一些细节的把控,甚至可以称作一种情怀。

比如吧,说他们为了呈现团队成员列表,自己还开发了自己的头像小程序,而且甚至还开发了一个头像创作系统,自己捏自己的脸,放一个对比图吧:

卧槽,右边那个我感触太深了,我实在是看不下去哪些乱七八糟的图像混杂排布的效果。wolai 这真的牛逼了!我看到之后太舒服了!

而且这些头像不是死的!这是他们的头像制作小程序,自己可以给自己捏脸,如图所示:

这个我觉得做的确实很有情怀了。

还有一个,为了将传统文化瑰宝融入 wolai ,他们还与故宫博物院取得联系,并获得文物图片的使用授权。只有在 wolai ,我和你才能….使用故宫文物作为页面题头图。

卧槽,情怀拉满了!

细节

另外再说个细节吧,就是这个中英文的混合排版。

我其实对于排版是非常严格的,就是我如果看到一个中文和一个英文单词之间没有一定的间距的话,会觉得很难受。

所以说在公众号排版的时候,中英文中间我都会留一个间距,比如说就中英文之间加一个空格。

不同于 Notion,wolai 它对中英文的排版支持的这个细节把控就做得很到位。比如说我这里敲一个中文和英文,那它会自动的在这两个之间加一个空白,如图所示:

这个真的是很细节了!感动。

有一点需要说明,wolai 在中英文之间插入的间隔并非实际空格,只是样式上的限定。一旦将文字复制或者导出为本地文件,却是没有空格的。

最后

现在我已经用起来了,它可以支持多个工作空间,比如我这里就创建了三个,「进击的Coder」可以作为团队型的空间,可以支持多人协作。另外还有两个空间就专门留给我,一个用来写技术,一个用来写生活,如图所示:

再告诉大家个好消息吧,我在写这篇文章的时候,使用的是 wolai 的个人免费版,它的 Block 数量是有限制的,最多是 5000 个,也就是说,按照一篇文章 100 个 Block 来算,那写 50 篇文章就没 Block 数量了,那就得收费了。

但是!!!从 2021 年 6 月 15 日开始!也就是 wolai 一周年到来之际,个人免费版开始不再限制块儿的数量了,那也就是说我们不管写多少文章,那都不会有限制了!

我当时看到这个消息的时候,看了下日期,6 月 14 日,哦?幸福来得也太突然了吧!

这次我真的不是为这个软件专门打广告的,确实是真心的推荐。

我现在也不能完全承诺,以后我就一定会在这个平台上来管理我的所有的文档。因为我刚开始用,可能还得适应一段时间,要是中途遇到什么 bug 也会看看开发团队的解决问题的态度。

我也不能承诺这软件一定是你中意的,但至少我看到的这个软件提供的一些功能点是我想要的。各个方面是很契合我的需求,而且里面的一些细节确实让我感动满满。

我也不能承诺这个团队以后会不会因为各种原因停止运营和开发这个软件。但我相信,能做出这个产品的团队,能把细节把控到这个地位的团队,也一定不会差了。

最后大家如果想试用看看,可以扫个码注册下:

希望对大家有帮助~

更多精彩内容,请关注我的公众号「进击的Coder」和「崔庆才丨静觅」。

个人随笔

昨天(准确说是前天了),鸿蒙 2.0 发布了,当时没来得及看,睡觉之前看了看回放,说实话这次的更新发布的确让我眼前一亮,甚至自己还有点小心动。因为我现在用的苹果全家桶,所以之前一直对安卓并不太关注,但发布会看到鸿蒙现在建立起的生态还有一些互联的体验,着实让我惊叹一把。甚至我心里都有一点想把自己现有的小米之家的一套以后换成华为全屋智能。

别说,还真有可能哈哈。

扯远了,说回正题。

发布会上宣布了一个消息,那就是现有的很多华为手机都可以开始升级鸿蒙系统,所以一个明显的感觉就是花粉们都开始纷纷尝鲜,从朋友圈就能看得出来大家的期待和热情。

然而,还有一些声音又开始热闹起来了,一些人又开始问了,甚至喷起来了——鸿蒙系统 2.0 到底是不是基于安卓?鸿蒙真无耻,明明基于安卓,还把安卓的名字抹掉叫做鸿蒙。看了这些问题和评论,我心里说有点嗤之以鼻吧又觉得太极端,我就单纯觉得争论这个真的挺无聊的,争论这个有什么意义呢?

看完这篇文章,你可能就更了解我为什么这么说了。

一些渊源

因为众所周知的一些原因,美丽国开始打压华为,断供芯片等关键元器件,而且华为不能再使用谷歌的移动服务。

断供芯片就不说了,说说后者。

谷歌移动服务有一个简称,就是 GMS,英文是 Google Mobile Service,比如 Gmail、Google Play 等等都属于其中。华为不能使用 GMS,这对华为的海外市场可谓是一个巨大的打击。我们国内没法用谷歌的服务自然大家也习以为常了,所以这个变化对国内市场影响不大。然而华为主要消费群体不会局限于国内,国外也是一个非常重要的市场。GMS 之于国外用户就像微信之于国内用户,华为手机没法用 GMS,也就没法用 Google Play,没了 Google Play,那就连应用都没法好好地装了。有人会说,那我把 Google Play 等 App 的 apk 下载下来安装不就好了吗?不行的,没有 GMS,那就相当于就得不到 Google 的认证,所以即使是把 Google Play 的 apk 下载下来也是没法用的。所以,所有 GMS 的生态都没法用了,尤其对于那些已经重度依赖 GMS 的国外用户来说,对买不买华为手机是有非常大的顾虑的。

在去年的第一季度,华为的手机销量在全球是位居第一的,然而今年第一季度,华为的全球手机销量已经跌至第六,被三星、苹果、小米、OPPO、VIVO 赶超了,第五的 VIVO 是 10%,而华为今年第一季度已经跌到 4% 了。

嗯当然对于这种情形,华为也有自己的应对措施了。比如华为打造了 Huawei Mobile Service,即 HMS,但显然这个相比 GMS 还是有很多不成熟的地方,或者让用户从 GMS 迁移到 HMS,也一定是有很多顾虑和成本的。

万般艰难之下,一个重磅消息就诞生了,那就是鸿蒙操作系统的宣布!还记不记得华为宣布鸿蒙的那一天,朋友圈、微博炒的是那么火热,连国家的各个媒体号都在疯狂宣发。为什么?因为当时宣称,这是我们中国人的操作系统!

哇,你就想象吧,那种大国自豪感,虽然说网友们平时天天在网上喷来喷去的,但是鸿蒙宣布的那一天,每个人都怀着一种大国自豪感,显得是那么团结。

后来一段时间,鸿蒙宣布开放源代码,当时记得有一段时间华为在挤牙膏,放出来了一些源码,当时一些热心网友研究之后就炸锅了,这开发工具怎么和 Android Studio 这么像啊?然后开始从 SDK 或者各处去扒安卓的影子,争先恐后去当第一个扒出安卓蛛丝马迹的人。然后一些套壳安卓的声音就不绝于耳了,骂声变得此起彼伏。我当时其实没有太关注这些,也没去深究这些,但当时给我的感觉就是,某些人就开始揪着套壳安卓这个事不放下,喷来喷去。

当时我的态度其实比较坦然,因为我也算懂些安卓,深知从 0 开发出一套安卓系统是一件多么难的事,安卓这发展了十几年才发展成这样子。如果从 0 开始造一个操作系统,即使抛开开发成本,其生态的建立也是极其漫长的过程,如果真有了,而市面上大多数软件都不兼容,微信都没法跑,大批大批的软件运行不了,那谁还会去用呢?几乎是必死的一条路了。所以当时鸿蒙发布会宣布兼容安卓应用的那个时候,我就觉得底层肯定和安卓脱不了干系了,内核肯定基于 Linux,至于多么像,那就得看把安卓改到哪个地步了。

是否基于安卓

好,那下面就来说说,鸿蒙到底是不是基于安卓。

要回答这个问题,我们得首先知道,安卓究竟是什么,要说安卓,就不得不先说一个名字,叫做 AOSP。

AOSP,就是 Android Open Source Project 的简称,翻译过来就是安卓开源项目,官网是 https://source.android.com/。

官网长这样:

AOSP 是开源的,主要是谷歌贡献的,当然其他的厂商也贡献了,比如华为、三星,甚至千千万万普通开发者。

那么 AOSP 和我们常说的安卓有什么关系呢?简单来说,安卓是谷歌在 AOSP 的基础上增加了 GMS(前文提到了),AOSP 是开源的,GMS 是闭源的,二者结合起来就成了现在的安卓系统,最纯正最原生的安卓系统大家可以到谷歌官方下载个 Android Studio 开个虚拟机体验下,现在已经发布到 11 了,网址是:https://developer.android.com/。

OK,但是我们平常用的一些手机的系统和原生安卓也还是有些许不同的,比如小米的 MIUI、三星 OneUI、OPPO 的 ColorOS,锤子的 Smartisan OS,这些都是基于 AOSP 开发的 ROM,而且用也上了 GMS,虽然国内因为某些原因没法用,但它也是有的,况且它们也没有像华为一样被禁。另外小米等公司也大大方方承认这是基于 Android 的,所以你可以看到小米手机的系统里面就写着,MIUI 什么版本,安卓内核什么版本。

OK,那鸿蒙呢?因为安卓 = AOSP + GMS,而 GMS 已经被禁了啊,那何必还称自己基于安卓呢?是吧,那就叫鸿蒙。

是的,就是这样。

所以,AOSP,它是开源的,谷歌放弃了它的所有权。AOSP 人人都能用,人人都能基于它开发。可以认为,AOSP 是安卓和鸿蒙的妈,安卓和鸿蒙是 AOSP 的孩子,只不过不同一个时间段,安卓在 AOSP 基础上多了个 GMS,而鸿蒙没有。

很多人就是像通过 AOSP、安卓、鸿蒙之间的关系想努力证明安卓和鸿蒙是一样的,鸿蒙是套壳安卓的,何必呢?

而且,我们必须要承认,华为的鸿蒙基于 AOSP 做了很多修改,基于鸿蒙的一些设计战略,分布式互联系统的出发点,少不了底层的一些修改和优化。至于难度怎么样?小米、锤子人家都能做成这样,对于华为这么大的公司来说,还会难吗?

而且我相信,将来鸿蒙虽然基于 AOSP 改,但未来也会离 AOSP 越来越远,甚至慢慢面目全非都有可能。

鸿蒙的核心优势

但其实我想说的重点不是上面的内容,不是为鸿蒙和安卓的区分做一个辩驳,因为我认为这是没有意义的。

因为真正有意义的点在于,我们应该关注华为借着鸿蒙,怎样发挥了公司的本身优势,建立了怎样一个基于鸿蒙的巨大生态。

华为不会纠结于到底是不是真正自己从 0 开始实现一个系统,这真的没有意义。华为是站在 AOSP 的基础上,想着怎么把自己的优势发挥出来,怎么将无法使用 GMS 的损失降低,怎么建立一套新的生态体系,怎样重新让华为的市场振作起来,甚至还可以证明,我们中国做的系统也一样可以引起一些可以称之为颠覆性的浪潮。

OK,大家相比也看到了发布会的一些演示了,我觉得华为有一个点做得非常好,那就是充分发挥了自己公司的优势。

华为是什么公司?是一家通信公司,通信技术在世界都是名列前茅,而且华为也一直在着眼于打造物联网全屋智能。

所以,这些碰撞在一起,将鸿蒙定位成一个分布式智能终端操作系统,这就一下子把鸿蒙的局限性从安卓手机这个层面抬高了,格局就不一样了。

华为很聪明,鸿蒙一下子被抬升到这个高度,那自然把名字也扩展到其他的设备上吧。

  • 比如说,华为家的智能手表,其实本来说智能也智能,但没个很正式的名字,那就叫鸿蒙!

  • 比如说,智能家电里面的芯片吧,其实就是个嵌入式的设备,很多厂商都能做啊,智能饮水机、智能燃气灶,智能加湿器,小米家不也做得挺好的吗,也能通过 App 控制,但你说小米家的智能加湿器里面运行的系统叫啥名?说不上来。但华为就不一样了,鸿蒙的定位上来了,格局上来了,那我们就叫它鸿蒙系统啊,名字嘛,就是个叫法而已。

好家伙,这名字一改,那我岂不是就可以对外宣称说,我们鸿蒙系统可以运行在手表,运行在所有的智能 IoT 设备上了,鸿蒙一下子逼格又提升了一个档次,不但格局上来了,技术也显得更牛逼了。鸿蒙还能兼容这么多平台,一听就牛逼吧。

所以,我还是很佩服华为的这个思路的,高!实在是高!

但光有战略不够啊,那得实打实做出点东西才行对吧。

有,这个必须有!而且正好撞枪口上了,通信和物联网,就是华为的优势点所在。

在鸿蒙基础上,华为配合上自家的通信技术的加持,将设备联动做得尽善尽美,基于超级终端这个定义,将所有的设备联系在一起。

  • 比如说,手机和大屏 Pad 的联动,华为把手机在 Pad 的镜像和操作做到了极低的延迟和极高的流畅度,同时加上软件的一些优化,实现数据的共享。

  • 比如说,手机和耳机和电视的联动,通过手机作为中枢,将视频流、音频流的传输和同步做到了极致,实现了无缝的转接体验。

  • 比如说,华为手机、无人机组成的多机位拍摄,将硬件和软件之间的关系解耦,每个终端都能自由选择连接的硬件,这其中也是需要非常强的通信技术。

这就是把自己最牛逼的技术正好最大化地发挥出来了啊!

鸿蒙发布会上提到了两个核心技术:

  • 软时钟同步
  • 抗干扰算法

软时钟同步是通信行业非常重要的技术指标,另外基于 5G 的时钟同步更为重要,该项技术能极大地影响通话体验、音视频的同步效果。华为通信出身,又是 5G 引领者,搞这个自然不在话下了。之于抗干扰算法,不必多说,这属于无线通信的范畴,将自己的技术应用到鸿蒙也是顺理成章了。

所以,最后大家可以看到,华为借着鸿蒙,把自家最牛逼的技术实现了完美的落地,打造了物联网生态,又带动了自家全屋智能家居的发展,拉高了鸿蒙的布局,又证明了鸿蒙的确能行,还能顺便给国家注入一剂强心剂!一举多得。

这才是鸿蒙真正的优势所在。

这才是鸿蒙真正的战略所在。

这才是鸿蒙对消费者、对技术发展、对国家、乃至对世界的意义。

Oh,这时候你再跟我说,鸿蒙是套壳安卓,你看看我还理你嘛?

个人记录

今天看到一个消息,说微信 PC 版最新版本可以刷朋友圈了,还加了许多新功能,简直喜大普奔,我赶紧装来试试。

不过可能让大家觉得稍有遗憾的是,现在发布的只支持 Mac 版,而且处在内测阶段,Windows 版的后续才会发布。

别问我怎么搞到的,看完这篇文章你也能立马获取。

装完之后,打开微信就提示了最新版本的更新界面:

这是 Mac 上的重大更新:

  • 可以刷朋友圈了
  • 看视频号的视频和直播了
  • 可以支持深色模式了

就是主要这么几大更新,我们来一起看看。

首先就是深色模式,这个简直太舒服了,现在很多 Mac 软件都已经适配了深色模式,唯独微信迟迟没有更新,这下终于有了!

我把 Mac 调整到深色主题,然后微信就会跟着变化了。

深色模式是这个样子的:

很完美,以后深夜再也不用面对那明晃晃的白色聊天窗口了。

接下来就是朋友圈,在侧栏出现了这样的小图标:

一点就会弹出一个新的窗口,而不是呈现在右侧,窗口是张这个样子的:

在右上角有刷新按钮和消息按钮,消息按钮点击之后同样以浮窗的形式呈现:

另外朋友圈点赞和评论的操作基本上和手机上是一样的:

小视频播放也不在话下:

嗯,整体感觉还是不错的,以后朋友圈就可以在电脑上看了。

接下来就是视频号,测试了一下转发了一个视频号的内容,显示是这样的:

然后还是以弹窗的形式呈现:

不过只有转发的操作,没法点赞和评论。

然后就是直播,转发的直播在聊天中是这样的:

点开之后显示是这样的:

这个是我的视频号哈,就简单开了一下直播,照的是家里的房顶,没啥实质性的东西,就仅做测试哈。

整体体验下来感觉这个更新还是很给力的,尤其对我有用的就是看朋友圈的功能了。

但是稍微有点遗憾的是,还不支持发朋友圈的功能,希望微信能快快支持。

最后,如果你也想体验一下,请在本公号回复“微信最新版”获取下载地址,大家晚安!

技术杂谈

背景

有时候我会碰到快速搭建测试服务的需求,比如像这样:

搭建一个 HTTP Service,这个服务器可以 run 在本地,也需要公网可以访问,请求该服务可以得到一组自定义的 JSON 数据。不为别的,就为临时快速做点测试用。

这时候我想要以最短的速度完成,比如一分钟就写出来,这时候可以怎么做?

比如大家可能想到了,跑个 Flask 或者 FastAPI,把示例代码改改,然后 Python 一个命令就跑起来了。

比如代码像这样:

1
2
3
4
5
6
7
8
from typing import Optional
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
return {"Hello": "World"}

然后我用命令跑起来:

1
uvicorn main:app --reload

OK,说好的自定义 JSON 就已经完成了。

可是我要加需求了,我要支持跨域访问,怎么做?这时候我可能又要去搜 FastAPI cors 关键字,然后找到 https://fastapi.tiangolo.com/tutorial/cors/ 文档,然后加上类似这样的一些配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
...

也还行对吧。

那现在我又改需求了,我要返回一张图片怎么办?我要返回一个文件怎么办?我要 HTTPS 访问怎么办?

甚至说,我代码写的不熟怎么办?为了搞这个 API Service 我得花大半个小时,太得不偿失了吧。

毕竟大家都挺忙的。

所以我会想,这些简单的事,为啥要写代码解决啊?难道没有工具通过一些可视化配置来完成吗?

如果你也有这个痛点,请继续向下看。

如果你没有,既然来都来了,客官继续看看嘛…

解决方案

所以现在我的需求是:我想通过一个便捷的工具快速搭建一个 API Server,能配置返回 JSON 或者图片或者文件等等,甚至说动态路由、动态转发等等功能,如果这些步骤还能通过可视化图形界面来搞定就更好了。

来了,今天就给大家推荐一个工具,叫做 Mockoon。

Mockoon 是一个可以通过图形化界面帮我们快速搭建 API 服务的工具,支持数据模拟、路由解析、跨域访问、HTTPS、自定义延时、Docker 等等各种你想要的功能,同时支持支持 Windows、Mac、Linux,页面整体是这样子的:

这布局,和 PostMan 有异曲同工之妙啊。

比如左侧我们可以配置一个个请求列表,点进去可以在右侧配置详情,比如配置是 GET 还是 POST 请求,path 是什么,Response Body 是什么,Response Headers 是什么,另外还有一些规则和基础设置。

另外在最上面我可以配置运行的 host 和 port,然后左上角还有一个运行按钮,一点就相当于启动了 Server 了,启动之后按钮就会变成红色,再按一下就会停止,比如这里我就配置了运行在本地 3894 端口:

然后我修改下 Body:

1
2
3
4
5
6
7
{
"data": [
{"id": 1, "name": "Picture3", "url": "https://qiniu.www.phunsics.com/l4ol8.jpg"},
{"id": 2, "name": "Picture2", "url": "https://qiniu.www.phunsics.com/zy2w3.jpg"},
{"id": 3, "name": "Picture1", "url": "https://qiniu.www.phunsics.com/v10oo.jpg"}
]
}

这里我返回一个 JSON 格式的列表,包含了三个字段。

然后接下来我要配置跨域访问,就加一个 Response Header:

1
Access-Control-Allow-Origin: '*'

然后点击左上角的运行按钮就成了。

Mockoon 还提供了快捷访问的功能,接着点右上角的打开按钮:

浏览器就打开了,然后数据就看到了:

咔咔咔,就这样,我们通过非常简单的可视化配置就完成了 API Server 的搭建,熟练的话一分钟就完成了。

另外还有太多功能,比如 HTTPS、多请求处理、日志、路由、模板配置这里就不再一一叙述了,用到的时候查文档就好啦:

另外 Mockoon 还支持命令行,比如通过 mockoon-cli 就可以快速创建一个 API Server,如图所示:

img

命令行的使用和安装可以参考:https://github.com/mockoon/cli#installation

以上便是这个工具的简单介绍,更多功能等待你的探索!

技术杂谈

在这里介绍一个工具,使用它我们可以非常方便地使用 Python 下载 Youtube 的视频,叫做 pytube。

利用它我们可以实现如下功能:

  • 支持 progresive 和 DASH 视频流的下载
  • 支持下载完整的播放列表
  • 可以处理下载进行中和下载完成的回调
  • 提供命令行直接执行下载
  • 支持下载字幕
  • 支持将字幕输出为 srt 格式
  • 获取视频缩略图

下面我们就来详细了解一下它的使用方法。

安装

首先看下安装过程,安装非常简单,只需要使用 pip3 安装即可,命令如下:

1
pip3 install pytube

或者直接源码安装也行:

1
pip3 install git+https://github.com/pytube/pytube

安装完成之后就可以使用 pytube 命令了。

使用

这里先介绍两个最常见的用法,那就是直接使用 pytube 命令,它可以用来下载单个 Yotube 视频或者视频列表。

比如这是一个视频 https://youtube.com/watch?v=2lAe1cqCOXo,截图如下:

我们可以直接使用命令下载:

1
pytube https://www.youtube.com/watch?v=2lAe1cqCOXo

很快视频就能被下载下来了:

1
2
3
Loading video...
YouTube Rewind 2019 For the Record YouTubeRewind.mp4 | 83 MB
↳ |█████████ | 21.4%

同样地,pytube 还能下载播放列表,比如这是一个播放列表 https://www.youtube.com/playlist?list=PLS1QulWo1RIaJECMeUT4LFwJ-ghgoSH6n,截图如下:

使用 pytube 同样可以轻松下载:

1
pytube https://www.youtube.com/playlist?list=PLS1QulWo1RIaJECMeUT4LFwJ-ghgoSH6n

它可以解析播放列表,然后一个个下载下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
Loading playlist...
Python Tutorial for Beginners 1 - Getting Started and Installing Python (For Absolute Beginners).mp4 | 63 MB
↳ |████████████████████████████████████████████| 100.0%
Python Tutorial for Beginners 2 - Numbers and Math in Python.mp4 | 11 MB
↳ |████████████████████████████████████████████| 100.0%
Python Tutorial for Beginners 3 - Variables and Inputs.mp4 | 15 MB
↳ |████████████████████████████████████████████| 100.0%
Python Tutorial for Beginners 4 - Built-in Modules and Functions.mp4 | 16 MB
↳ |████████████████████████████████████████████| 100.0%
Python Tutorial for Beginners 5 - Save and Run Python files py.mp4 | 37 MB
↳ |████████████████████████████████████████████| 100.0%
Python Tutorial for Beginners 6 - Strings.mp4 | 78 MB
↳ |███████████████████████████████████ | 79.6%

当然除了默认的命令配置,还可以支持查看 list,查看字幕,筛选语言等等,具体的命令如下:

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
usage: pytube [-h] [--version] [--itag ITAG] [-r RESOLUTION] [-l] [-v]
[--logfile LOGFILE] [--build-playback-report] [-c CAPTION_CODE]
[-lc] [-t TARGET] [-a [AUDIO]] [-f [FFMPEG]]
[url]

Command line application to download youtube videos.

positional arguments:
url The YouTube /watch or /playlist url

optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
--itag ITAG The itag for the desired stream
-r RESOLUTION, --resolution RESOLUTION
The resolution for the desired stream
-l, --list The list option causes pytube cli to return a list of
streams available to download
-v, --verbose Verbosity level, use up to 4 to increase logging -vvvv
--logfile LOGFILE logging debug and error messages into a log file
--build-playback-report
Save the html and js to disk
-c CAPTION_CODE, --caption-code CAPTION_CODE
Download srt captions for given language code. Prints
available language codes if no argument given
-lc, --list-captions List available caption codes for a video
-t TARGET, --target TARGET
The output directory for the downloaded stream.
Default is current working directory
-a [AUDIO], --audio [AUDIO]
Download the audio for a given URL at the highest
bitrate availableDefaults to mp4 format if none is
specified
-f [FFMPEG], --ffmpeg [FFMPEG]
Downloads the audio and video stream for resolution
providedIf no resolution is provided, downloads the
best resolutionRuns the command line program ffmpeg to
combine the audio and video

更多详细的说明可以参考官方文档:https://python-pytube.readthedocs.io/en/latest/user/cli.html

代码使用

当然除了这些,pytube 还支持以 Python 编程的方式来进行下载,同时提供了便捷的链式操作,比如这段代码:

1
2
3
4
5
6
7
8
9
>>> from pytube import YouTube
>>> YouTube('https://youtu.be/2lAe1cqCOXo').streams.first().download()
>>> yt = YouTube('http://youtube.com/watch?v=2lAe1cqCOXo')
>>> yt.streams
... .filter(progressive=True, file_extension='mp4')
... .order_by('resolution')
... .desc()
... .first()
... .download()

这里大家可以看到,要使用 pytube,只需要导入其中的 Youtube 这个类,然后传入 URL 声明 Youtube 对象就好了。接着我们可以直接调用其 streams 方法获取所有的视频源,然后可以通过 first 或者 filter 或者 order 等进行排序或筛选等处理,然后最后调用 download 方法就可以执行下载了。

下面我们来剖析一下具体是怎么回事。

首先我们来声明一下 YouTube 对象:

1
2
3
4
>>> from pytube import YouTube
>>> yt = YouTube('https://youtu.be/2lAe1cqCOXo')
>>> yt
<pytube.__main__.YouTube object at 0x7f88901e9890>

然后看看 streams 是什么:

1
2
3
4
>>> yt.streams
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="24fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">, <Stream: itag="22" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">, <Stream: itag="137" mime_type="video/mp4" res="1080p" fps="24fps" vcodec="avc1.640028" progressive="False" type="video">, <Stream: itag="248" mime_type="video/webm" res="1080p" fps="24fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="399" mime_type="video/mp4" res="1080p" fps="24fps" vcodec="av01.0.08M.08" progressive="False" type="video">, <Stream: itag="136" mime_type="video/mp4" res="720p" fps="24fps" vcodec="avc1.4d401f" progressive="False" type="video">, <Stream: itag="247" mime_type="video/webm" res="720p" fps="24fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="398" mime_type="video/mp4" res="720p" fps="24fps" vcodec="av01.0.05M.08" progressive="False" type="video">, <Stream: itag="135" mime_type="video/mp4" res="480p" fps="24fps" vcodec="avc1.4d401e" progressive="False" type="video">, <Stream: itag="244" mime_type="video/webm" res="480p" fps="24fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="397" mime_type="video/mp4" res="480p" fps="24fps" vcodec="av01.0.04M.08" progressive="False" type="video">, <Stream: itag="134" mime_type="video/mp4" res="360p" fps="24fps" vcodec="avc1.4d401e" progressive="False" type="video">, <Stream: itag="243" mime_type="video/webm" res="360p" fps="24fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="396" mime_type="video/mp4" res="360p" fps="24fps" vcodec="av01.0.01M.08" progressive="False" type="video">, <Stream: itag="133" mime_type="video/mp4" res="240p" fps="24fps" vcodec="avc1.4d4015" progressive="False" type="video">, <Stream: itag="242" mime_type="video/webm" res="240p" fps="24fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="395" mime_type="video/mp4" res="240p" fps="24fps" vcodec="av01.0.00M.08" progressive="False" type="video">, <Stream: itag="160" mime_type="video/mp4" res="144p" fps="24fps" vcodec="avc1.4d400c" progressive="False" type="video">, <Stream: itag="278" mime_type="video/webm" res="144p" fps="24fps" vcodec="vp9" progressive="False" type="video">, <Stream: itag="394" mime_type="video/mp4" res="144p" fps="24fps" vcodec="av01.0.00M.08" progressive="False" type="video">, <Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">, <Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus" progressive="False" type="audio">, <Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus" progressive="False" type="audio">, <Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">]
>> type(yt.streams)
<class 'pytube.query.StreamQuery'>

可以看到这里 streams 是一个 StreamQuery 对象,然后输出出来看起来像是一个列表,其中包含了一个个 stream 对象。

所以,StreamQuery 就是我们需要重点关注的对象了。

比如接下来我们使用 filter 或者 order_by 方法进行处理:

1
2
>>> type(yt.streams.filter(file_extension='mp4'))
<class 'pytube.query.StreamQuery'>

可以看到它依然还是一个 StreamQuery 对象。

然后根据分辨率进行排序:

1
2
>>> type(yt.streams.filter(file_extension='mp4').order_by('resolution'))
<class 'pytube.query.StreamQuery'>

还是一样,返回的还是 StreamQuery 对象。

这下明白为什么它可以进行链式操作了吧,因为每次 filter 或者 order_by 对象返回的依然还是 StreamQuery 对象,依然还是可以调用对应的方法的。

不过也不是每一个都是支持链式操作的,比如接下来我们对 StreamQuery 对象调用 first 方法:

1
2
3
4
>>> yt.streams.first()
<Stream: itag="18" mime_type="video/mp4" res="360p" fps="24fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">
>>> type(yt.streams.first())
<class 'pytube.streams.Stream'>

看到这里返回的就是单个 Stream 了。

对于 Stream 对象,我们最常用的就是 download 方法了。

1
yt.streams.first().download()

调用完毕之后,视频就会下载在运行目录中。

以上我们就介绍了利用命令行和代码进行视频下载的过程。

更多

当然这个库还有很多强大的功能,都在文档 https://python-pytube.readthedocs.io/en/latest/ 写得很清楚了,这里带大家稍微看下。

比如获取视频的属性:

1
2
>>> yt.title
YouTube Rewind 2019: For the Record | #YouTubeRewind

获取视频的缩略图:

1
2
>>> yt.thumbnail_url
'https://i.ytimg.com/vi/2lAe1cqCOXo/maxresdefault.jpg'

在初始化的时候设置处理方法或者设置代理:

1
2
3
4
5
6
>>> yt = YouTube(
'http://youtube.com/watch?v=2lAe1cqCOXo',
on_progress_callback=progress_func,
on_complete_callback=complete_func,
proxies=my_proxies
)

关于 filter 的一些用法,可以参考 https://python-pytube.readthedocs.io/en/latest/user/streams.html#filtering-streams,比如说过滤只保留有音频的流媒体:

1
2
3
4
5
>>> yt.streams.filter(only_audio=True)
[<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">,
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus" progressive="False" type="audio">,
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus" progressive="False" type="audio">,
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus" progressive="False" type="audio">]

保留 mp4 后缀的视频:

1
2
3
4
5
6
7
>>> yt.streams.filter(file_extension='mp4')
[<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2" progressive="True" type="video">,
<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2" progressive="True" type="video">,
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028" progressive="False" type="video">,
...
<Stream: itag="394" mime_type="video/mp4" res="None" fps="30fps" vcodec="av01.0.00M.08" progressive="False" type="video">,
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2" progressive="False" type="audio">]

比如获取字幕:

1
2
3
>>> yt = YouTube('http://youtube.com/watch?v=2lAe1cqCOXo')
>>> yt.captions
{'ar': <Caption lang="Arabic" code="ar">, 'zh-HK': <Caption lang="Chinese (Hong Kong)" code="zh-HK">, 'zh-TW': <Caption lang="Chinese (Taiwan)" code="zh-TW">, ...

这里各国语言的字幕几乎都有。

另外还可以把字幕打印出来,比如输出 srt 格式:

1
2
3
4
5
6
7
8
9
10
>>> caption = yt.captions.get_by_language_code('en')
>>> print(caption.generate_srt_captions())
1
00:00:10,200 --> 00:00:11,140
K-pop!

2
00:00:13,400 --> 00:00:16,200
That is so awkward to watch.
...

对于播放列表的处理,比如新建 Playlist 对象,然后取出每一个视频的第一个视频流并下载:

1
2
3
4
>>> from pytube import Playlist
>>> p = Playlist('https://www.youtube.com/playlist?list=PLS1QulWo1RIaJECMeUT4LFwJ-ghgoSH6n')
>>> for video in p.videos:
>>> video.streams.first().download()

另外还有一些异常处理机制:

1
2
3
4
5
6
7
8
9
10
11
>>> from pytube import Playlist, YouTube
>>> playlist_url = 'https://youtube.com/playlist?list=special_playlist_id'
>>> p = Playlist(playlist_url)
>>> for url in p.video_urls:
... try:
... yt = YouTube(url)
... except VideoUnavailable:
... print(f'Video {url} is unavaialable, skipping.')
... else:
... print(f'Downloading video: {url}')
... yt.streams.first().download()

总之,使用这个库,我们不仅可以使用命令行方便地下载 Youtube 视频和播放列表,还可以使用代码灵活地控制,一举两得!

个人记录

今天还是照常上班,和以往没有太多不同。

然而,办公室里面有位同事一句话惊动了周围所有同事——我们今年又多了五天假期?!

听罢,同事们像是刚中了彩票一样,一个个高兴得像个孩子,但还不知道具体咋回事。

:哪啊?哪说的啊?

:快看看邮件啊

:哪封邮件啊?

:就今天上午刚发的公司全员邮件啊!标题叫 Announcing Wellbeing Days

:好家伙,你不说我还真没注意

其实我也没注意,然后开始翻邮件,果不其然,找到了!

标题很明显了:

Announcing Wellbeing Days

具体的邮件内容不详细说了,总体意思就是说,现在全球新冠疫情严重,大家都面临非常多的压力,其中有一句关键的信息:

Thus, I am excited to announce five new paid WellbeingDays to support you.

是的,为此公司决定 2021 年给每个员工都加五天全薪“幸福关怀假”。

巨硬牛逼!

然后我又详细看了看,这个针对哪些员工呢?

2021 年 9 月 30 之前入职的员工都可以获得这五天假期,2021 年 9 月 30 到 12 月 1 日入职的员工,可以获得三天假期。

今年我的年假已经变成:

去年留到今年的还有 13 天,今年 15 天还没用,再加上 5 天,33 天年假了。

简直是良心了,这下我终于也体验到了一会“别人公司”的幸福感。

想来的,简历可以砸过来了。