标签 python 下的文章

最近在搞一个自动化运行的小项目,基于某些原因,运行过程中可能会时不时的报错什么的,开始是每天或隔几天登录机器查看运行情况,修复问题,重新运行程序,不过这样搞感觉就不能称之为自动化了,就想着能不能搞个什么消息通知,报错或者运行状态日志实时发送,有问题可以及时收到,然后进行修复,最初的想法是通过邮件发送,不过后来发现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)

最近使用Python WordPress XML-RPC模块把采集的文章发布到WordPress,不过发现偶尔会报 Fault -32700: 'parse error. not well formed' 的错误,大部分文章都能正常发布,只有极个别文章发布的时候会报这个错误。

因为WordPress XML-RPC模块是使用xml发布文章的,当文章内容中带有xml中不支持的字符时,也就是xml文件中含有非法字符时,是一个Bad XML,不符合xml规范的。

检查一下报错的文章,发现有字符\x08,而xml允许的字符范围#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF],因此在使用xml时需要把这个范围之外的字符过滤掉,也就是说

\x00-\x08
\x0b-\x0c
\x0e-\x1f

这三组字符是不允许出现在XML中的,解决方法很简单,用正则表达式替换

re.sub("[\x00-\x08\x0b-\x0c\x0e-\x1f]", "", text)

在VPS上用pip安装一个python包时,直接提示Killed退出了

#Output
Installing collected packages: pypinyin
Killed

无法安装,好奇怪,别的包都安装上了,就这个出问题,在stackoverflow搜到一个问答,https://stackoverflow.com/questions/43245196/pip-install-killed ,貌似是内存不足导致的。

按照问答里说的,查看Linux kernel日志

tail -f /var/log/kern.log
#Output
Out of memory: Kill process 18741 (pip) score 251 or sacrifice child
Killed process 18741 (pip) total-vm:178620kB, anon-rss:121956kB, file-rss:2996kB

看来确实是内存不足,Out of memory,然后直接被系统kill掉了。

内存不够,增加swap交换分区,正好vps用的是Ubuntu系统,直接参照这篇文章 https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-16-04

Ubuntu添加Swap分区

查看系统是否有Swap分区

sudo swapon --show

如果没有任何输出,表示没有swap分区,也可以通过free命令验证一下

free -h
#Output
              total        used        free      shared  buff/cache   available
Mem:            486         316          81           5          88         132
Swap:             0           0           0

可以看到Swap的大小是零,也就是没有swap分区。

创建Swap分区文件

创建一个1G大小的文件作为交换分区的文件

sudo fallocate -l 1G /swapfile

设置这个文件权限

sudo chmod 600 /swapfile

格式化为交换分区文件

sudo mkswap /swapfile

启用交换分区文件

sudo swapon /swapfile

验证一下swap分区文件是否创建成功

sudo swapon --show
#Output
NAME      TYPE  SIZE USED PRIO
/swapfile file 1024M   0B   -1

设置开机时自启用Swap分区

备份/etc/fstab文件,防止出错

sudo cp /etc/fstab /etc/fstab.bak

输入下面的命令在/etc/fstab文件添加一行swap文件的信息

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

优化Swap设置

调整Swappiness值

sudo sysctl vm.swappiness=10
#Output
vm.swappiness = 10

调整Cache Pressure值

sudo sysctl vm.vfs_cache_pressure=50
#Output
vm.vfs_cache_pressure = 50

设置开机时自动调整这两项设置值
/etc/sysctl.conf文件最后添加下面两行内容

vm.swappiness=10
vm.vfs_cache_pressure=50

OK,搞定,测试一下看pip是否能安装这个包了,哈哈,不再是Killed,变成了Successfully installed,没有问题,成功安装。

#Output
Installing collected packages: pypinyin
Successfully installed pypinyin-0.33.0

之前写的一个python小程序,在windows服务器上跑的还可以,偶尔出点小意外,打算换到linux服务器上去跑,本来想着应该没啥问题,结果把环境配好以后,一运行,出问题了,报错 FileNotFoundError: [Errno 2] No such file or directory ,这是什么鬼,找不到文件???而且还是在windows环境下能跑,linux环境下跑就出问题了?看错误信息,好像是subprocess模块报的错,难道是环境没配好?子程序缺文件?不能运行?直接在shell里运行一下子命令看看,没有问题啊,可以正常执行的,这还真是奇怪了。

google了一下,发现是一个参数惹的祸,shell=False,因为这个程序用了subprocess模块来执行一些子程序

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None)

shell参数默认是False,因为在windows和linux环境下subprocess模块执行子程序的方式不一样,官方文档是这样写的

Execute a child program in a new process. On POSIX, the class uses os.execvp()-like behavior to execute the child program. On Windows, the class uses the Windows CreateProcess() function. The arguments to Popen are as follows.
args should be a sequence of program arguments or else a single string. By default, the program to execute is the first item in args if args is a sequence. If args is a string, the interpretation is platform-dependent and described below. See the shell and executable arguments for additional differences from the default behavior. Unless otherwise stated, it is recommended to pass args as a sequence.
On POSIX, if args is a string, the string is interpreted as the name or path of the program to execute. However, this can only be done if not passing arguments to the program.
On Windows, if args is a sequence, it will be converted to a string in a manner described in Converting an argument sequence to a string on Windows. This is because the underlying CreateProcess() operates on strings.
The shell argument (which defaults to False) specifies whether to use the shell as the program to execute. If shell is True, it is recommended to pass args as a string rather than as a sequence.
On POSIX with shell=True, the shell defaults to /bin/sh. If args is a string, the string specifies the command to execute through the shell. This means that the string must be formatted exactly as it would be when typed at the shell prompt. This includes, for example, quoting or backslash escaping filenames with spaces in them. If args is a sequence, the first item specifies the command string, and any additional items will be treated as additional arguments to the shell itself.
On Windows with shell=True, the COMSPEC environment variable specifies the default shell. The only time you need to specify shell=True on Windows is when the command you wish to execute is built into the shell (e.g. dir or copy). You do not need shell=True to run a batch file or console-based executable.
args is required for all calls and should be a string, or a sequence of program arguments. Providing a sequence of arguments is generally preferred, as it allows the module to take care of any required escaping and quoting of arguments (e.g. to permit spaces in file names). If passing a single string, either shell must be True or else the string must simply name the program to be executed without specifying any arguments.

所以在linux环境下,当shell=False(默认)时,subprocess.Popen使用os.execvp()来执行子程序,args参数需要是一个列表,如果args参数是个字符串的话,会被当做是命令本身或者可执行文件的路径,也就是说字符串只能是命令本身,而不能有额外的命令参数,如果字符串里带有命令参数的话,那参数也会被当作命令本身传递给os.execvp()来执行,比如说你要执行 cat test.txt, cat test.txt这整个字符串都会被当作可执行文件的路径来执行,但是又不存在cat test.txt这样的一个可执行的命令或者可执行文件的路径,所以就会报FileNotFoundError: [Errno 2] No such file or directory这样的错了。

shell=True时,会直接使用shell来执行子程序,如果args参数是字符串会直接传递给shell执行,如果args参数是个列表,args[0] 被视为命令交给shell执行,args[1:] 则会被忽略或者如果是可以改变shell行为的有效参数则对shell做相应调整,比如subprocess.run('cat test.txt', shell=True)正常执行查看test.txt文件,而subprocess.run(['cat', 'test.txt'], shell=True)则只会执行cat命令,而不会查看test.txt文件,而test.txt会被当作shell本身的参数,但是test.txt又不是shell的有效参数所以被忽略。

简单说就是当args参数是个字符串时,需要设置shell=True,当args参数是个列表的时候,shell保持默认的False。

写了个scrapy小爬虫准备放到闲置的那台windows服务器上跑一下,服务器环境 Windows Server 2012 R2 64位操作系统,先安装Python 3.6.5,直接官网下载64位安装包,安装完成,运行python时,结果弹出 api-ms-win-crt-runtime-l1-1-0.dll 丢失的错误,搜索了一下说是需要安装vc2015运行环境Visual C++ Redistributable Package,下载安装vc_redist.x64.exe 又报 0x80240017 的错误,尴尬了,说这个错误是需要安装 KB2999226 这个补丁程序,好吧,接着下载安装,结果又提示此更新不适用于您的计算机,好嘛,真是尴尬了,接着搜索,说是还需要按顺序安装几个更新,具体是哪些懒得研究了,直接打开系统更新,装了一堆更新重启了一回,然还不行,再手动检查系统更新,又出来3个更新,继续更新,不容易,终于可以了,接下来赶紧进入正题,安装scrapy。

Scrapy的安装

使用pip安装scrapy pip install scrapy

没有意外的报错了

building 'twisted.test.raiser' extension
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools

这是电脑上没有安装Microsoft Visual C++ 14.0的编译工具,所以无法对源码进行编译,解决的办法一种就是安装Microsoft Visual C++ Build Tools,当然更省事的是直接安装已经预编译好的Twisted包。

可以在 https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted 这里找到已经预编译好Twisted包的二进制安装包whl文件。

Twisted, an event-driven networking engine.
Twisted‑18.4.0‑cp27‑cp27m‑win32.whl
Twisted‑18.4.0‑cp27‑cp27m‑win_amd64.whl
Twisted‑18.4.0‑cp34‑cp34m‑win32.whl
Twisted‑18.4.0‑cp34‑cp34m‑win_amd64.whl
Twisted‑18.4.0‑cp35‑cp35m‑win32.whl
Twisted‑18.4.0‑cp35‑cp35m‑win_amd64.whl
Twisted‑18.4.0‑cp36‑cp36m‑win32.whl
Twisted‑18.4.0‑cp36‑cp36m‑win_amd64.whl
Twisted‑18.4.0‑cp37‑cp37m‑win32.whl
Twisted‑18.4.0‑cp37‑cp37m‑win_amd64.whl

选择我们需要的Twisted版本,cp后面是Python的版本号,cp36表示Python版本3.6,amd64表示64位。

下载完成之后,进入下载文件所在的文件夹运行以下命令安装Twisted,pip install Twisted-18.4.0-cp36-cp36m-win_amd64.whl,再运行pip install scrapy即可安装成功。

不过如果运行scrapy,一般还会出现如下的错误

    import win32api
ModuleNotFoundError: No module named 'win32api'

需要安装 pywin32,直接pip安装 pip install pywin32,当然如果pip安装失败的话,仍然是可以直接用预编译好的二进制安装包whl文件来安装的。

如果有下载图片的话,可能还会报如下错误

    import Image
ModuleNotFoundError: No module named 'PIL'

需要安装 pillow,直接pip安装 pip install pillow

windows环境下用pip安装一些带有c扩展的包时,可能会踩坑报错,这时直接下载预编译好的二进制安装包。

  1. https://www.lfd.uci.edu/~gohlke/pythonlibs/ 在这里下载预编译好的.whl文件,Ctrl + F 输入python包名查找,找到对应模块包,根据python版本选择下载。
  2. 进入下载.whl文件所在的文件夹,执行命令 pip install 带后缀的完整文件名 即可完成安装。