有一个没有流量的辣鸡采集站,源站的图床不支持https,所以原来是在有访问的时候把图片下载到服务器的,最近服务器到期了要换小鸡,发现图片占的空间不小,就想着直接用nginx反代源站图床好了,省点硬盘,后来发现cloudflare worker可以直接反代图片,这下图片连服务器资源都省了,直接白嫖了。
如果想白嫖一些设置有referer防盗链的图床,也可以用worker反代,然后修改请求的Referer头,cloudflare workers当然还可以搞很多有意思的玩法,免费版每天有10W次的额度,对于没啥流量的辣鸡站足够用了。

使用nginx配置图片反向代理,只需把图片请求发送到源站就可以了

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
            # 反向代理图片
            proxy_pass  http://图片源站域名;
            expires     30d;
        }

使用 cloudflare worker 配置图片反向代理

首先要一个cloudflare的账号,登录,在左侧菜单中找到workers,初次使用workers会生成一个随机的子域和一个随机名的worker项目,这个没关系,可以随时进行修改,点开新建的worker项目,快速编辑,把下边的代码复制粘贴,然后可以在右侧窗口直接测试代码,保存并部署。

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
    const imageUrl = new URL(request.url);
    imageUrl.hostname = "源站域名";  // 替换成源站域名
    imageUrl.protocol = "http";  // 源站的http协议

    // 构建新的请求对象
    const imageRequest = new Request(imageUrl, request)

    // 删除 Referer 请求头,也可以使用 imageRequest.headers.set('Referer', '') 修改Referer 
    imageRequest.headers.delete('Referer')

    // 从远程服务器获取响应
    const imageResponse = await fetch(imageRequest)

    // 将响应体作为字节数组读取
    const imageBuffer = await imageResponse.arrayBuffer()

    // 构建新的响应对象
    const response = new Response(imageBuffer, imageResponse)

    // 设置 Content-Type 标头
    response.headers.set('Content-Type', imageResponse.headers.get('Content-Type'))

    // 返回响应对象
    return response
}

如果域名在cloudflare,还要配置路由触发器,并添加对应dns,直接指向服务器ip即可,worker只有匹配路由规则的请求才会触发,其他的请求还是直接发送到服务器的。
添加路由,img.xxxxx.com/uploads/*,具体路由匹配规则可以查看文档 https://developers.cloudflare.com/workers/platform/triggers/routes/#matching-behavior

域名不在cloudflare的,应该直接添加cname解析指向workers.dev项目域名就可以了,不过这个没测试。

当然也可以直接使用cloudflare workers提供的workers.dev项目域名,不过好像据说workers.dev已经在墙外了。

最近在搞一个自动化运行的小项目,基于某些原因,运行过程中可能会时不时的报错什么的,开始是每天或隔几天登录机器查看运行情况,修复问题,重新运行程序,不过这样搞感觉就不能称之为自动化了,就想着能不能搞个什么消息通知,报错或者运行状态日志实时发送,有问题可以及时收到,然后进行修复,最初的想法是通过邮件发送,不过后来发现telegram机器人更简单好用一些,就搞了一个telegram机器人来接收python logging日志消息,跑了几天,效果貌似还不错。

准备一个telegram账号

可以使用google voice来注册,注册的时候要用telegram手机app,telegram desktop版本无法发送SMS短信验证码,当然也可以搞一个虚拟机,使用 android-x86,建议虚拟机内存搞的大一点,不然真是卡卡卡,安装的时候,不能安装最新版的tg app,不然还是无法发送SMS短信验证码,也不知道是不是google voice虚拟号的问题,不过最终用telegram andriod app 8.9.0版本成功注册,是在 apkapure 下载的,然后就可以安装 telegram desktop版本,用app接收验证码登录了。

创建一个telegram bot机器人

  1. 首先搜索联系 @BotFather注意,官方 Telegram 机器人的名称旁边有一个蓝色复选标记,单击Start激活 BotFather 机器人。
  2. 选择或输入/newbot命令并发送。
  3. 给机器人起一个闪亮霸气的名字,并且设置机器人的username,username需要以bot结尾。
  4. 机器人创建完成后,会收到机器人链接t.me/<bot_username>token,这个token要保存好。
  5. 点击收到的机器人链接,打开和机器人的聊天窗口,随便发送点什么,比如 hello bot
  6. 浏览器访问 https://api.telegram.org/bot<token>/getUpdates,把<token>替换成第4步收到的token,注意一定先给机器人发个消息,才能通过这个链接获取chat_id
  7. 把机器人token和chat_id保存好。

python安装telegram-logging包

通过Telegram bot机器人发消息,可以用requests包,直接使用telegram的http api发送,https://core.telegram.org/bots/api#sendmessage

不过为了省事,还是用现成的轮子,telegram-logging,可以直接通过pip安装

pip install telegram-logging

可以参考telegram-logging包给的示例代码,把之前保存的bot token和chat_id替换到对应的位置。

但是有一个点要注意,日志消息中不能有 <>& 这几个html的特殊符号,不然tg消息会发送失败,会报错400 BAD_REQUEST

telegram-logging包中没有对<>&进行处理,所以直接使用这个包的话,日志消息中就不要有这几个字符。

不过因为这个包比较简单,可以直接把包中的telegram.py文件复制出来,进行改造一下,把<>&分别替换成对应的html实体字符。

在TelegramFormatter.format中增加代码

        if record.msg:
            # telegram 消息机器人,telegram的消息中不能包含<>,不然会报错
            # https://core.telegram.org/bots/api#html-style
            record.msg = record.msg.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')

可以新建一个日志消息的包目录 log,新建一个文件 __init__py,把改造过的 telegram.py也复制到这个目录,在程序运行开始时,先导入一下 log,然后使用logging记录日志消息时,就会把设置的对应级别消息通过bot机器人直接发送到telegram,这样就可以在telegram里一边愉快的看小姐姐,一边处理程序问题,两不耽误,哈哈。

具体代码如下:

# __init__.py


import logging

from .telegram import TelegramFormatter, TelegramHandler


logger = logging.getLogger()
logger.handlers.clear()  # 每次被调用后,清空已经存在handler,防止多次调用该函数时重复生成日志
# 设置为DEBUG级别
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s: %(message)s')


# 标准流处理器,设置的级别为WARAING
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARAING)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)


# 文件处理器,设置的级别为DEBUG
file_handler = logging.FileHandler(filename=filename, encoding='utf-8')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# telegram bot处理器,设置的级别为INFO
telegram_formatter = TelegramFormatter(
    fmt="[%(asctime)s %(name)s] %(levelname)8s\n\n%(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    use_emoji=True,
    # (Optional) If you want to use custom emojis:
    emoji_map={
        logging.DEBUG: "\U0001F41B",
        logging.INFO: "\U0001F4A1",
        logging.WARNING: "\U0001F6A8",
        logging.ERROR: "\U0000274C",
        logging.CRITICAL: "\U0001F4A5",
    })

telegram_handler = TelegramHandler(bot_token="<Your Telegram Bot Token>", chat_id="<Your Telegram Chat ID>")
telegram_handler.setLevel(logging.INFO)
telegram_handler.setFormatter(telegram_formatter)
telegram_handler.addFilter(tg_filter)
logger.addHandler(telegram_handler)

# telegram.py


"""A simple Telegram logging module with Handler and Formatter.
因为有一些hmlt字符“<>&”需要预处理,所以对原包进行修改
https://core.telegram.org/bots/api#html-style
"""

import logging
from urllib import error, parse, request


class TelegramFormatter(logging.Formatter):
    """TelegramFormatter.
    """

    EMOJI_MAP = {
        logging.DEBUG: "\u26aa",
        logging.INFO: "\U0001f535",
        logging.WARNING: "\U0001F7E0",
        logging.ERROR: "\U0001F534",
        logging.CRITICAL: "\U0001f525",
    }

    def __init__(self,
                 fmt: str = '%(asctime)s - %(levelname)s - %(message)s',
                 datefmt: str = None,
                 use_emoji: bool = True,
                 emoji_map: dict = None):
        """:fmt: str, default: '%(asctime)s - %(levelname)s - %(message)s'\n
        :datefmt: str, default: None\n
        :use_emoji: bool, default: True\n
        :emoji_map: dict, default: None\n
        """
        super().__init__(fmt, datefmt)
        self.use_emoji = use_emoji
        self.emojis = self.EMOJI_MAP
        if emoji_map:
            self.emojis.update(emoji_map)

    def format(self, record):
        if self.use_emoji and record.levelno in self.emojis:
            record.levelname = self.emojis[record.levelno]

        if record.msg:
            # telegram 消息机器人,telegram的消息中不能包含<>,不然会报错
            # https://core.telegram.org/bots/api#html-style
            record.msg = record.msg.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
        return super().format(record)


class TelegramHandler(logging.Handler):
    """Send log messages to Telegram.
    https://core.telegram.org/bots/api#sendmessage
    """
    def __init__(self,
                 bot_token: str,
                 chat_id: str,
                 timeout: int = 5,
                 **params):
        """:bot_token: Telegram bot_token\n
        :chat_id: Telegram chat_id\n
        :params: https://core.telegram.org/bots/api#sendmessage
        """
        logging.Handler.__init__(self)
        self.bot_token = bot_token
        self.chat_id = chat_id
        self.timeout = timeout
        self.kwargs = params
        self.kwargs["parse_mode"] = "HTML"

    def emit(self, record):
        try:
            url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage"
            params = {
                "chat_id": self.chat_id,
                "text": self.format(record),
            }
            params.update(self.kwargs)
            data = parse.urlencode(params).encode()
            req = request.Request(url, data=data)
            with request.urlopen(req, timeout=self.timeout):
                pass

        except error.URLError:
            self.handleError(record)

本地测试用的apache服务器,发现URL不带扩展名,只有文件名时,apache服务器也会自动访问对应的文件,搜索了一下,是apache服务器配置有一个MultiViews选项。

MultiViews的作用是,如果apache服务器收到一个URL请求比如 /some/dir/foo,然后 /some/dir/foo 这个对象不存在,那么apache会自动查找 /some/dir 目录下所有名为 foo.* 的文件,无论是什么后缀名,如果正好这个目录下有 foo.jpg,那就返回 foo.jpg ,这就容易导致在设置伪静态的时候出现一些奇奇怪怪的问题了。

所以,如果不想要apache自动加隐性后缀名的话,那找到对应虚拟主机的目录配置

<Directory  "/wwwroot/example">
        Options +Indexes +Includes +FollowSymLinks +MultiViews
        AllowOverride All
        Require local
</Directory>

把 +MultiViews 去掉,重启apache即可。

打算把之前采集今日头条那个小站的更新搞起来,上次更新是去年还是前年来,总之,原先那个用scrapy写的爬虫代码很久没跑过了。大概流程可以看之前写的那篇文章 Scrapy 抓取今日头条文章小记,博客也差不多断更许久了,正好就借着这篇文章同时更新一下代码跟博客吧。

测试了一下之前的代码,发现之前的代码采集文章列表没问题,不过文章详情页抓不到内容了,抓到的页面只有一段js代码,没有文章的任何内容,看来头条调整详情页了,用浏览器打开看看,发现初次访问文章详情页,跟爬虫采集到的一样,是一段js代码,然后自动刷新了一下页面,文章内容才显示出来。

之前头条文章详情页没有反爬机制,直接采集就可以了,现在加上详情页也加上反爬了,大概流程是,初次访问详情页链接,会返回一个cookie值__ac_nonce,用__ac_nonce计算出一个__ac_signature签名值,同时带着__ac_nonce__ac_signature的cookie值再次刷新页面,就正常返回文章详情页了。不需要每次访问详情页都重新计算__ac_signature值,只需要计算一次,后续访问详情页直接带着__ac_nonce__ac_signature的cookie值就可以了,有一定的时效。

具体如何破解__ac_signature参数值,就不深入研究了,直接搜索window.byted_acrawler等关键词就有很多相关的js逆向分析文章,还是奔着有没有现成可用的整理过的js代码的思路找了一下,在github上找到了,感谢Bindian9710分享的代码,
https://github.com/Bindian9710/Spider-Crack_Js/blob/master/今日头条/头条.js,这个代码可以直接用pyExecJS模块解析,返回_signature计算值,不过要获取详情页的__ac_signature还需要对这个代码简单改动一下,原代码应该是采集文章列表时用url计算来获取_signature参数值的,详情页的__ac_signature计算函数同列表的计算函数是相同的,只是函数传的参数稍有不同。

头条.js 14行,设置User-Agent,改成跟爬虫UA一致

window.navigator = {
    userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0",
}

头条.js 672行,_signature函数需要改动一下,改成详情页的通过__ac_nonce值来计算__ac_signature

function _signature(e, t) {
    var a, r;
    a = window.byted_acrawler
    r = a.sign
    var c = r.call(a, '', e)
    return c
}

function get_signature(cookie_ac_nonce) {
    return _signature(cookie_ac_nonce)
}

改好后,然后另存为sign.js,写一个测试这个js计算的__ac_signature值是否可用的python代码

import execjs
import requests

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:86.0) Gecko/20100101 Firefox/86.0'
}
_r = requests.get('https://www.toutiao.com/a6936101864131953166/', headers=headers)
_ac_nonce = _r.cookies["__ac_nonce"]  # 初次访问详情页,获取__ac_nonce的值
print(f'__ac_nonce: {_ac_nonce}')

with open('./sign.js', 'r', encoding='utf-8') as f:
    toutiao_js = execjs.compile(f.read())  # 加载js

_ac_signature = toutiao_js.call('get_signature', f'{_ac_nonce}')  # 计算__ac_signature
print(f'__ac_signature: {_ac_signature}')

# 同时带着`__ac_nonce`和`__ac_signature`的cookie值再次访问详情页
headers.update({
    'Cookie': f'__ac_nonce={_ac_nonce}; __ac_signature={_ac_signature};',
})
r = requests.get('https://www.toutiao.com/a6936101864131953166/', headers=headers) 

print(r.status_code)
print(r.content.decode('utf-8'))

运行一下,正常获取文章详情页的内容了
20210308230808.png

算出来的__ac_signature_02B4Z6wo00f01QMQkFQAAIBC.O9vqmPdwIkDEpTAACCEde 要比在浏览器里的看到的值短上许多,貌似长度是需要初始化一下参数控制的,window.byted_acrawler.init({ aid: 24, dfp: true }),不过尝试在js里加上对应的长度控制代码,运行会报错 execjs._exceptions.ProgramError: TypeError: Cannot read property 'x' of undefined

不研究长度的问题了,虽短却也够用了,可以正常获取文章详情页的内容了,再把原先的scrapy代码改造一下,列表获取还用之前的方法,只需要在采集详情页前,先获取一下__ac_nonce__ac_signature即可,又可以愉快的跑起来了。

2021.3.9更新

lnmp环境开启IMAP模块,如果已经安装好了lnmp,但是没有启用imap模块,其实是不需要重新编译php,直接编译安装imap模块就可以了。

1、进入lnmp源码目录(使用lnmp一键安装包,注意lnmp的版本,一般在当前用户主目录可以找到)

ubuntu@VM:~$ cd lnmp1.6/src

2、解压php源码(注意自己安装的PHP版本,解压对应的压缩包)

ubuntu@VM:~/lnmp1.6/src$ tar -xjvf php-7.3.5.tar.bz2 > /dev/null

3、编译安装IMAP模块

ubuntu@VM:~/lnmp1.6/src$ cd php-7.3.5/ext/imap
ubuntu@VM:~/lnmp1.6/src/php-7.3.5/ext/imap$ /usr/local/php/bin/phpize
Configuring for:
PHP Api Version:         20180731
Zend Module Api No:      20180731
Zend Extension Api No:   320180731

ubuntu@VM:~/lnmp1.6/src/php-7.3.5/ext/imap$ ./configure --with-php-config=/usr/local/php/bin/php-config --with-kerberos --with-imap-ssl

ubuntu@VM:~/lnmp1.6/src/php-7.3.5/ext/imap$ sudo make && sudo make install
Build complete.
Don't forget to run 'make test'.

Installing shared extensions:     /usr/local/php/lib/php/extensions/no-debug-non-zts-20180731/

4、修改php.ini,在最后一行加上 extension=imap.so

ubuntu@VM:~/lnmp1.6/src/php-7.3.5/ext/imap$ sudo vim /usr/local/php/etc/php.ini

5、重启php-fpm

ubuntu@VM:~/lnmp1.6/src/php-7.3.5/ext/imap$ sudo lnmp php-fpm restart
+-------------------------------------------+
|    Manager for LNMP, Written by Licess    |
+-------------------------------------------+
|              https://lnmp.org             |
+-------------------------------------------+
Gracefully shutting down php-fpm . done
Starting php-fpm  done

通过phpinfo查看imap是否开启。

======================================
2019.6.11分割线

写了一个小功能,自动读取邮件,部署到服务器上发现执行报错IMAP extension must be enabled,然后才注意到lnmp一键安装包默认是不开启imap模块的。

好吧,所以如果要使用imap模块的话,需要自己把相应的php编译参数加上,我用的是lnmp1.6 php7.3.5,要修改include/php.sh文件的778行

./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-config-file-scan-dir=/usr/local/php/conf.d --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-iconv-dir --with-freetype-dir=/usr/local/freetype --with-jpeg-dir --with-png-dir --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization ${with_curl} --enable-mbregex --enable-mbstring --enable-intl --enable-pcntl --enable-ftp --with-gd ${with_openssl} --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-zip --without-libzip --enable-soap --with-gettext ${with_fileinfo} --enable-opcache --with-xsl ${PHP_Modules_Options}

在编译配置参数的最后加上 --with-imap --with-imap-ssl --with-kerberos

./configure --prefix=/usr/local/php --with-config-file-path=/usr/local/php/etc --with-config-file-scan-dir=/usr/local/php/conf.d --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-iconv-dir --with-freetype-dir=/usr/local/freetype --with-jpeg-dir --with-png-dir --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization ${with_curl} --enable-mbregex --enable-mbstring --enable-intl --enable-pcntl --enable-ftp --with-gd ${with_openssl} --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-zip --without-libzip --enable-soap --with-gettext ${with_fileinfo} --enable-opcache --with-xsl ${PHP_Modules_Options} --with-imap --with-imap-ssl --with-kerberos

根据自己php版本,在对应的位置加上相应编译参数,然后再安装lnmp,就是开启imap模块的了。

当然,可能已经安装好了lnmp,但是没有启用imap模块,那就要重新编译php了,发现lnmp没有提供单独编译php的指令,自己写了一个单独编译php的小脚本,适用lnmp1.6 php7.3.5,其他版本根据需要自行修改脚本

#!/bin/bash
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH

cur_dir=$(pwd)
LNMP_Ver='1.6'
Stack="lnmp"
Php_Ver="php-7.3.5"

. lnmp.conf
. include/main.sh
. include/init.sh
. include/php.sh

clear
echo "+-----------------------------------------------------------------------+"
echo "|                           Re-build PHP for LNMP                       |"
echo "+-----------------------------------------------------------------------+"

Press_Install
cd ${cur_dir}/src
Download_Files ${Download_Mirror}/web/php/${Php_Ver}.tar.bz2 ${Php_Ver}.tar.bz2
Check_PHP_Option
Install_PHP_73

lnmp restart

切换到lnmp1.6一键安装包目录

把以上代码写到文件rebuild_php.sh
添加可执行权限 chmod +x rebuild_php.sh
执行脚本重新编译php sudo ./rebuild_php.sh

重新编译前,不要忘了把php启用imap的编译参数--with-imap --with-imap-ssl --with-kerberos加上。