#260 Python Redis Debug

2018-05-06

如果可以的话,使用 redis-cli monitor 命令来输出所有 Redis 命令也很方便。

有时,条件不允许,或者 Redis 需要处理其他的连接,我希望将自己代码调用的 Redis 命令输出到日志中,方便调试。

#259 邮件格式 (RFC 822)

2018-05-05

相关的文章:

邮件是由纯文本组成,其详细的格式有很多 RFC 规范需要遵守。我这里只能对我所了解的,也是基础的 —— 或者说最核心的 —— 格式做一个说明。

最核心的部分是 1982 年的 RFC 822 (STANDARD FOR THE FORMAT OF ARPA INTERNET TEXT MESSAGES),之后又做过一些更新, 比如 RFC 2822 和 RFC 5322 (Internet Message Format) 和一堆补丁更新。本文要讲的基本格式,从开始到现在并没有什么明显变化。

  1. 邮件是一种纯文本格式,最开始只包含 ASCII 字符,后来引入了 MIME 之后,可以制定别的编码,比如 UTF-8 等。

  2. 换行符是 \r\n,也就是 CR + LF。

  3. 整体来说,一封邮件由邮件头(Headers)和邮件体(Payload)组成。

  4. 邮件头包含若干个头字段

  5. 邮件头和邮件体之间用一个空行隔开

  6. RFC2882 和 RFC5322 都规定了电子邮件每一行的长度,排除行末 CRLF,不可以超过 998 个字符,建议不超过 78 个字符。

There are two limits that this specification places on the number of
characters in a line.  Each line of characters MUST be no more than
998 characters, and SHOULD be no more than 78 characters, excluding
the CRLF.

如果太长,应该拆分成多行,下一行行首加上至少一个空格或者制表符,表示是上一行的延续。

邮件示例

From: Bob <bob@markjour.com>
To: Mark <mark@markjour.com>
Subject: Hello

Hello, Bob,

Would you like to join me for dinner?

--
Mark

RFC 822 中关于字符的定义

                                            ; (  Octal, Decimal.)
# 字符
CHAR        =  <any ASCII character>        ; (  0-177,  0.-127.)
# 字母
ALPHA       =  <any ASCII alphabetic character>
                                            ; (101-132, 65.- 90.)
                                            ; (141-172, 97.-122.)
# 数字
DIGIT       =  <any ASCII decimal digit>    ; ( 60- 71, 48.- 57.)
# 控制字符
CTL         =  <any ASCII control           ; (  0- 37,  0.- 31.)
                character and DEL>          ; (    177,     127.)
# 回车
CR          =  <ASCII CR, carriage return>  ; (     15,      13.)
# 换行
LF          =  <ASCII LF, linefeed>         ; (     12,      10.)
# 空格
SPACE       =  <ASCII SP, space>            ; (     40,      32.)
# 制表符
HTAB        =  <ASCII HT, horizontal-tab>   ; (     11,       9.)
# 引号
<">         =  <ASCII quote mark>           ; (     42,      34.)
# 回车换行
CRLF        =  CR LF
# 空白
LWSP-char   =  SPACE / HTAB                 ; semantics = SPACE
# 连贯空白, 折行空白
linear-white-space =  1*([CRLF] LWSP-char)  ; semantics = SPACE
                                            ; CRLF => folding
# 特殊字符
specials    =  "(" / ")" / "<" / ">" / "@"  ; Must be in quoted-
            /  "," / ";" / ":" / "\" / <">  ;  string, to use
            /  "." / "[" / "]"              ;  within a word.
# 分隔符
delimiters  =  specials / linear-white-space / comment
# 文本
text        =  <any CHAR, including bare    ; => atoms, specials,
                CR & bare LF, but NOT       ;  comments and
                including CRLF>             ;  quoted-strings are
                                            ;  NOT recognized.
# 原子字符
atom        =  1*<any CHAR except specials, SPACE and CTLs>
quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or
                                            ;   quoted chars.
qtext       =  <any CHAR excepting <">,     ; => may be folded
                "\" & CR, and including
                linear-white-space>
domain-literal =  "[" *(dtext / quoted-pair) "]"
dtext       =  <any CHAR excluding "[",     ; => may be folded
                "]", "\" & CR, & including
                linear-white-space>
# 注释
comment     =  "(" *(ctext / quoted-pair / comment) ")"
ctext       =  <any CHAR excluding "(",     ; => may be folded
                ")", "\" & CR, & including
                linear-white-space>
quoted-pair =  "\" CHAR                     ; may quote any char
phrase      =  1*word                       ; Sequence of words
word        =  atom / quoted-string

对应上 ASCII:

  0 -  31 控制字符, 其中包括常用的:
          - HT ( 9)  水平制表符
          - LF (10) 换行
          - CR (13) 回车
       32 空格
 33 -  47 符号 !"#$%&'()*+,-./
 48 -  57 数字
 58 -  64 符号 :;<=>?@
 65 -  90 大写字母
 91 -  96 符号 [\]^_`
 97 - 122 小写字母
123 - 126 符号 {|}~
      127 控制字符(DEL)

CHAR 0-127
CTL  0-37 + 127

符号中:
()<>[]@,;:\".
    13 个被视作特殊字符,需要转义
!#$%&'*+-/=?^_`
    15 个就是普通符号

atom = 数字 + 字母 + 普通符号

邮件头格式

field       =  field-name ":" [ field-body ] CRLF
     field-name  =  1*<any CHAR, excluding CTLs, SPACE, and ":">
     field-body  =  field-body-contents
                    [CRLF LWSP-char field-body]
     field-body-contents =
                   <the ASCII characters making up the field-body, as
                    defined in the following sections, and consisting
                    of combinations of atom, quoted-string, and
                    specials tokens, or else consisting of texts>

字段名称允许使用的字符范围非常宽泛,而且大小写不敏感,

但是一般实践中:

  • 由字母和中划线组成
  • 首字母大写,或全部大写

常用字段

  • Received MTA 轨迹(传输过程中的相关信息)
  • Date 发信时间,格式:Fri, 21 Nov 1997 09:55:06 -0600
  • Sender Mail From 地址
  • From 发件人
  • Subject 邮件标题
  • To 收件人
  • Cc 抄送
  • Bcc 密送
  • Reply-To 回复地址
  • Message-ID 邮件标识
  • References 回复邮件标识,逗号隔开
  • In-Reply-To 回复邮件标识(会话发起的第一封)
  • Return-Path 发信任地址(2020/07/31,邮件的 Return-Path 头是什么)
  • Comments 说明
  • Keywords 关键字

一般采用 X- 开头的字段名称表示自定义字段,或者叫拓展字段:

常见的拓展字段:

  • X-Mailer

#258 Logrotate

2018-05-03

配置

# cat /etc/logrotate.conf
weekly
su root adm
rotate 4
create
#dateext
#compress
include /etc/logrotate.d

以 Nginx 配置为例:

# cat /etc/logrotate.d/nginx
/var/log/nginx/*.log {
        daily       # 按日切割
        missingok   # 如果文件不存在,则不创建
        rotate 14   # 最多保留 14 个日志文件
        compress    # 压缩
        delaycompress   # 延迟压缩
        notifempty      # 如果文件为空,则不创建
        create 0640 www-data adm

        # 可能一次切割多个日志,
        # 但是后面遇到的每个脚本都只执行一次,
        # 在所有日志切割之前或之后
        sharedscripts

        prerotate
                if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
                        run-parts /etc/logrotate.d/httpd-prerotate; \
                fi \
        endscript

        postrotate
                invoke-rc.d nginx rotate >/dev/null 2>&1
        endscript
}

其他常用选项:

  • dateext 部分日志需要添加日期后缀
  • lastaction/endscript 最后执行的指令,很有用,比如最后将日志备份到某些地方

比如:

rsync -au *.gz 192.168.64.234:/backup/nginx-logs/`hostname -s`/

参考资料与拓展阅读

#257 关于 DNS(历史和未来)

2018-05-02

历史

从阿帕网(ARPANET)时代一直到互联网的早期,网络节点比较少,都是通过本地 hosts 文件来实现主机名到 IP 地址的映射。

根据维基百科的信息,斯坦福研究所(SRI-NIC,Stanford Research Institute Network Information Center)负责维护一个公共 HOSTS.TXT 文件(RFC 606、RFC 608),各个网络节点定期通过 FTP 下载最新版本进行同步。

PS:这个时候如果有主机名重复了谁来管?打电话过去让他们改名?

这套机制一直运行了十几年。随着互联网规模扩大,公共 HOSTS.TXT 文件变得越来越大,变化也越来越频繁。每当有新的主机加入网络、IP 地址发生变化,都需要重新同步整个文件。

这种模式存在几个明显问题:

  • 中央管理机构压力越来越大;
  • 全网频繁同步效率较低;
  • 主机数量增长后难以维护;
  • 名称冲突和管理问题越来越复杂。

后来人们开始设计域名和域名相关的公共设施。RFC 819《The Domain Naming Convention for Internet User Applications》和 RFC 881《The Domain Names Plan and Schedule》提出了域名系统的早期设计思路。

最终在 1983 年形成了下面两个 RFC 文档:

  • RFC 882, DOMAIN NAMES - CONCEPTS and FACILITIES
  • RFC 883, DOMAIN NAMES - IMPLEMENTATION and SPECIFICATION

几年后(1987),正式的 DNS 标准 RFC 1034 和 RFC 1035 推出。我们现在常见的 A / MX / NS / CNAME / PTR / SOA 等记录类型都是在这个标准中定义的。

  • RFC 1034, Domain Names Concepts and Facilities
  • RFC 1035, Domain Names Implementation and Specification

这套标准一直运行到现在。虽然几十年来不断增加新的记录类型、支持国际化域名(IDN)、引入 DNSSEC、安全加密传输等扩展能力,但其核心设计思想基本没有改变:

  • 分层命名;
  • 分布式管理;
  • 缓存加速;
  • 递归解析。

DNS 可以说是互联网历史上寿命最长、最成功的基础设施之一。并且,在可以预见的未来,DNS 仍然会是互联网最重要的基础设施之一。

DNS over TCP

最初 DNS 主要运行于 UDP 53 端口。RFC 1035 同时规定了 TCP 支持,最早 TCP 主要用于:

  • 区域传送(AXFR);
  • 超大响应数据返回。

今天绝大多数 DNS 服务仍同时支持 UDP 和 TCP。

DNS 的管理权问题

互联网本是美国国防部高级研究计划局(DARPA)的产物,早期域名分配几乎由南加州大学教授乔恩·波斯特尔一人代管。1998年,克林顿政府为避免让联合国下属的国际电信联盟(ITU)——一个由各国政府和官营电信主导的机构——接管互联网,刻意设计了一条"私有化"路线:成立非营利组织 ICANN,负责全球域名系统(DNS)和顶级域名分配,与美国商务部签合同但通过"私人身份"运作,以此保持互联网开放、不受单一政府直接操控。然而 ICANN 总部在加州、受美国法律管辖,全球13台根服务器多数也在美国手里,等于事实上的互联网域名最高仲裁者仍是美国。这让包括中国、俄罗斯在内的大量国家长期不满,伊拉克战争期间".iq"域名被暂停解析一事更被视为美国"悬剑在手"的象征。

2013年斯诺登曝光美国大规模监听后,国际社会对"美国垄断互联网命门"的反弹到达顶点,联合国层面甚至出现推动另建政府主导治理体系的呼声。为消解这一合法性危机、顺应互联网去中心化叙事,奥巴马政府于2014年宣布启动移交程序——将美国商务部对 ICANN 的"合同监管权"解除。2016年3月,ICANN 正式提交脱离美国政府联系的过渡计划。

真正的问题不在"美国是否善良",而在于:这根命门本来就不该由任何一个国家独握。移交的本质是把制度合法性从"美国庇护下的开放"转向"多方利益攸关体(multi‑stakeholder)自我维持的透明与问责"。但争议也正在于此——保守派担心脱离美国后,ICANN 会被威权国家通过政府咨询渠道逐步蚕食;支持者则认为根服务器的分布式结构和自下而上的共识机制本身就是防火墙。美国让出的不是善意,而是一张越来越护不住的垄断椅子。

新的发展

年份 技术/标准 RFC 主要创新
1983 DNS 概念与实现 RFC 882, RFC 883 首次提出 DNS,替代 HOSTS.TXT
1987 DNS 正式标准 RFC 1034, RFC 1035 建立现代 DNS 架构,至今仍是核心基础
1997 动态更新(DDNS) RFC 2136 DNS UPDATE 接口,允许客户端动态修改 DNS 记录
1998 NOTIFY RFC 1996 主 DNS 主动通知从 DNS 更新,不用定时轮询
1999 IXFR(增量同步) RFC 1995 只同步变更部分,降低 Zone Transfer 成本
1999 EDNS0 RFC 2671 -> 6891 打破 UDP 512 字节限制(拓展到 KB 级别)
2000 SRV 记录 RFC 2782 DNS 服务发现机制
2003 国际化域名(IDNA) RFC 3490 系列 支持中文、日文等 Unicode 域名
2005 DNSSEC RFC 4033/4034/4035 DNS 数据签名与验证
2008 Source Port Randomization RFC 5452 提高抗缓存投毒能力
2010 NSEC3 RFC 5155 防止 DNSSEC Zone Walking(枚举所有域名)
2011 CAA 记录 RFC 6844 -> 8659 允许域名所有者声明:仅承认某些 CA 给该域名颁发的证书
2013 DNS Cookies RFC 7873 防御 DNS 放大攻击
2016 DNS over TLS (DoT) RFC 7858 DNS 加密传输
2018 QNAME Minimization RFC 7816 降低 DNS 查询隐私泄露
2018 DNS over HTTPS (DoH) RFC 8484 DNS 走 HTTPS 通道
2020 SVCB / HTTPS RR RFC 9460 服务发现和 HTTPS 配置优化
2021 Oblivious DoH (ODoH) RFC 9230 隐藏客户端 IP
2022 DNS over QUIC (DoQ) RFC 9250 基于 QUIC 的 DNS
2023 DDR RFC 9462 自动发现 DoH/DoT 服务
  • DDNS

RFC 2136: Dynamic Updates in the Domain Name System (DNS UPDATE)

复用 DNS 报文格式,但把 Opcode 设为 5(UPDATE),直接在 UDP/TCP 53 端口上向权威 DNS 服务器发送增删改指令(四段式结构):

段 作用
Zone​ 声明你要改哪个 zone(如 home.arpa.或 example.com.)
Prerequisite​ 前置条件——"只有当 host A == 1.2.3.4时才更新"(原子 compare-and-swap,防竞态)
Update​ 实际操作——ADD / DELETE 某条 RR
Additional​ 认证载体——通常是 TSIG(RFC 2845)或 GSS-TSIG(RFC 3645, Kerberos)

注意:DDNS 本身可以说是一个需求,就是动态调整 DNS,虽然 RFC 2136 经常被称之为 DDNS,但实质是 DDNS 的一种技术方案。
有一些脚本或者路由器插件轮询 IP 然后通过云服务商 API 来动态修改 DNS 记录,在很多场景下也被称之为 DDNS,不要混淆。
云服务商 DNS 都不对外提供 UPDATE 操作,它们通过 HTTP API(IAM/Key 鉴权)来提供 DNS 记录更新。

RFC 2136 DDNS 的典型阵地是你自有/私有部署的受控权威服务器(BIND、PowerDNS、Windows AD DNS、Infoblox 等),其中最常见生产场景是:
客户端发起 DHCP 请求时会带上 hostname(Option 12),DHCP 服务器分配地址后,通过 TSIG/GSS-TSIG 向可写 zone 发 UPDATE,自动注册内部名 A 记录及对应 PTR(反向区也要可被管理)。

  • DNSSEC
    安全

  • DNSCrypt
    2011 年提出。
    DNSCrypt 在 DNS 请求和解析服务器之间建立加密通信,并验证服务器身份。
    它并非 IETF 标准,但曾经被 OpenDNS 等服务广泛采用。

  • DNS over TLS(DoT)
    2016 年成为标准。RFC 7858 Specification for DNS over Transport Layer Security (TLS)
    DNS 全程加密,默认使用 TCP 853 端口。

  • DNS over HTTPS(DoH)
    2018 年成为标准。RFC 8484 DNS Queries over HTTPS (DoH)
    特点:

  • 基于 HTTPS;
  • 默认使用 HTTPS 443 端口;
  • 更容易穿透网络限制;
  • 可以复用现有 Web 基础设施。

  • DNS over QUIC(DoQ)
    RFC 9250。
    基于 QUIC,兼具 UDP 的低延迟和 TLS 的安全性。

  • DNS over Tor
    通过 Tor 网络转发 DNS 查询,其主要目标是隐藏客户端来源地址,提高匿名性。
    它不是 DNS 标准协议,而是一种部署方案。

  • Oblivious DNS-over-HTTPS(ODoH)
    ODoH 在 DoH 基础上增加代理节点,确保代理服务器看不到查询内容,DNS 服务器看不到客户端 IP,从而进一步提升隐私保护能力。

参考资料与拓展阅读

#256 配置 Node 环境

2018-04-27
sudo apt install -y nodejs npm
# 已经不需要这句了:
# sudo ln -s `which nodejs` /usr/bin/node

# node -v
# npm -v

npm config set registry=https://registry.npm.taobao.org
sudo npm upgrade -g npm

# sudo npm install -g yarn --registry=https://registry.npm.taobao.org
curl --compressed -o- -L https://yarnpkg.com/install.sh | bash

yarn config set registry https://registry.npm.taobao.org

配置

echo registry=https://registry.npm.taobao.org > ~/.npmrc

npm config get registry
https://registry.npmjs.org/
npm config set registry=https://registry.npm.taobao.org

yarn config get registry
https://registry.yarnpkg.com
yarn config set registry https://registry.npm.taobao.org

#255 SMTP 拓展

2018-04-24

RFC#821 定义的 SMTP 协议非常简单(简陋)。
1993 年,RFC#1425 SMTP Service Extensions 定义了 SMTP 协议的拓展框架。
这个向前兼容的安全拓展框架是通过 EHLO 命令来实现。

#254 邮件发送中会遇到的各种地址

2018-04-16

注意:这边不是讨论 邮箱地址的格式。

格式

  • 邮箱地址
  • "名称" <邮箱地址>

含义

  • SMTP 会话(投递)
  • Mail From 真实投递的发信人
  • Rcpt To 真实投递的收信人
  • 邮件内容(显示)
  • From 发信人
    • 如果和 Mail From 地址不同,可能会显示:由 xxx 代发
  • To 收信人
  • Cc 抄送人
  • Bcc 密送人
  • Rely-To 回复地址
    • 客户端点击回复的时候用的
    • 如果没有这个字段,就会回复 From 地址
  • Sender 发信人
  • Return-Path / Reverse-Path / Envelope-From
    • 作用是在邮件投递出现问题的时候,邮件服务将邮件退回这个地址
    • 如果我们看到这几个名字
    • 可能是发信人自己在邮件中声明
    • 可能是收信方收到邮件之后添加的,单独字段,或放在 Received 头中

关于抄送和密送

碳式复写纸 carbon paper
副本,抄送 carbon copy => CC
密送 blind carbon copy => BCC

按照设计,密送地址不希望被其他收信人、抄送人察觉,只是密送地址才知道自己是密送。

CC, BCC in SMTP

SMTP 服务器不处理 CC、BCC,SMTP 客户端应该自行处理
TO 地址 + CC 地址 + BCC 地址一起放到 SMTP 会话的 RCPT TO 字段

所以,按照我的理解,邮件客户端:

在一次 SMTP 会话中,如果有 3 个 TO/CC 地址,2 个 BCC 地址,应该对那 3 个地址批量发送,然后对那 2 个 BCC 地址分别加上 BCC 头,分别发送。
更稳妥一点:如果是批量发送邮件,不要放 BCC 到邮件头!!!显示一个 密送:xxx 也没啥意义。

  • PS:MSN(Outlook),网易邮箱发出去的邮件,不会加 BCC 头
    甚至网易可能在显示邮件原文的时候会移除 BCC 头(给网易邮箱发的 BCC 头都不见了)
  • PS:Gmail,QQ 邮箱发出去的邮件,密送人会看到 BCC 头
from_addr = "from@markjour.com"
to_addrs = ["to@markjour.com"]
cc_addrs = ["cc1@markjour.com", "cc2@markjour.com"]
bcc_addrs = ["bcc@markjour.com"]

msg = f"""
From: {from_addr}
To: {", ".join(to_addrs)}
Cc: {", ".join(cc_addrs)}

Hello World
""".strip()

send_to = to_addrs + cc_addrs + bcc_addrs

server = smtplib.SMTP('smtp.126.com')
server.set_debuglevel(1)
server.login(api_user, api_key)
server.sendmail(from_addr, send_to, msg)
server.quit()

#253 Python SMTP

2018-04-13
import logging
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

logging.basicConfig(level=logging.DEBUG)

SMTP_HOST = 'smtp.example.com'
SMTP_PORT = 587
SMTP_USERNAME = 'your_username'
SMTP_PASSWORD = 'your_password'
SMTP_STARTTLS = True

sender = 'sender@example.com'
recipients = ['rcpt01@example.com', '中国 <rcpt02@example.com>']
subject = 'Test Email'
content_text = 'This is a test email.'
content_html = '<html><h1>Hello</h1></html>'


def encode_recipient(name, addr):
    pass


msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = ', '.join(recipients)
msg['Subject'] = subject
msg.attach(MIMEText(content_text))
msg.attach(MIMEText(content_html))
print(msg.as_string())


def smtp_debug(self, *args):
    msg = ' '.join(map(str, args))
    logging.debug(msg)


smtp = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
smtp._print_debug = smtp_debug
smtp.set_debuglevel(1)
smtp.ehlo()
if SMTP_STARTTLS:
    smtp.starttls()
    smtp.ehlo()
if SMTP_USERNAME and SMTP_USERNAME:
    smtp.login(SMTP_USERNAME, SMTP_PASSWORD)

smtp.sendmail(sender, recipients, msg.as_string())
smtp.quit()

#252 邮件中的时间格式

2018-04-10

比如:

Sun, 20 Jun 2018 00:47:04 -0700 (PDT)
Thu, 10 Jun 2021 16:10:03 -0700 (PDT)
Thu, 10 Jun 2021 08:06:31 -0700 (PDT)

定义

定义在 RFC 822 的 5. DATE AND TIME SPECIFICATION。

date-time   =  [ day "," ] date time        ; dd mm yy hh:mm:ss zzz
day         =  "Mon"  / "Tue" /  "Wed"  / "Thu" /  "Fri"  / "Sat" /  "Sun"
date        =  1*2DIGIT month 2DIGIT        ; day month year e.g. 20 Jun 82
month       =  "Jan"  /  "Feb" /  "Mar"  /  "Apr" /  "May"  /  "Jun" /  "Jul"  /  "Aug" /  "Sep"  /  "Oct" /  "Nov"  /  "Dec"
time        =  hour zone                    ; ANSI and Military
hour        =  2DIGIT ":" 2DIGIT [":" 2DIGIT]
                                            ; 00:00:00 - 23:59:59
zone        =  "UT"  / "GMT"                ; Universal Time
                                            ; North American : UT
            /  "EST" / "EDT"                ;  Eastern:  - 5/ - 4
            /  "CST" / "CDT"                ;  Central:  - 6/ - 5
            /  "MST" / "MDT"                ;  Mountain: - 7/ - 6
            /  "PST" / "PDT"                ;  Pacific:  - 8/ - 7
            /  1ALPHA                       ; Military: Z = UT;
                                            ;  A:-1; (J not used)
                                            ;  M:-12; N:+1; Y:+12
            / ( ("+" / "-") 4DIGIT )        ; Local differential
                                            ;  hours+min. (HHMM)

总结就是:

[day-of-week,] day month year hour:minute[:second] timezone
  1. 周几和秒是可选的,据我观察,没有邮件省略这两部分
  2. 周几和月份采用三字母英文缩写(首字母大写)
  3. 年份是 2 位数字,后来的规范更新中建议采用 4 位数字。出于兼容性考虑,一般都保留了对 RFC 822 两位数字年份的支持。
  4. 时区除了数字之外,可以使用 UT、GMT、EST、EDT、CST、CDT、MST、MDT、PST、PDT,
    还有 25 个字母(J 没有使用),Z 表示 UTC/GMT 时间,A - M 表示 -1 ~ -12 时区,N - Y 表示 1 到 12 时区。
  1 2 3 4 5 6 7 8 9 10 11 12
West A B C D E F G H I K L M
Eest N O P Q R S T U V W X Y

Python

生成符合要求的时间字符串比较简单:

import time
time.strftime('%a, %d %b %Y %H:%M:%S %z')
# 'Tue, 10 Apr 2018 09:10:05 +0800'

但是由于这个灵活度比较大,解析起来最好借助专业的库(email.utils)来做这个事。

import time
import datetime
import email.utils
import pytz

# 解析 ############################################

date_str = 'Sun, 20 Jun 2018 00:47:04 -0700 (PDT)'
email.utils.parsedate_to_datetime(date_str)
# datetime.datetime(2018, 6, 20, 0, 47, 4, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200)))
email.utils.parsedate_tz(date_str)
(2018, 6, 20, 0, 47, 4, 0, 1, -1, -25200)

# 生成 ############################################

# email.utils.formatdate(timeval=None, localtime=False, usegmt=False)
email.utils.formatdate()
# 'Tue, 10 Apr 2018 09:10:41 -0000'

# email.utils.format_datetime(dt, usegmt=False)
dt = datetime.datetime.now()
email.utils.format_datetime(dt)
# 'Tue, 10 Apr 2018 09:16:43 -0000'

tz = pytz.timezone('Asia/Shanghai') # <DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>
dt = datetime.datetime(2018, 4, 10, 9, 10, 0, tzinfo=tz)
# datetime.datetime(2018, 4, 10, 9, 10, tzinfo=<DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>)
email.utils.format_datetime(dt)
# 'Tue, 10 Apr 2018 09:10:00 +0806'

#251 邮箱地址的格式

2018-04-07

规范

规范定义比较复杂,甚至支持注释。

我简化一下(去掉注释,去掉双引号,去掉 [IPv4] / [IPv6] / 主机名 做域):

  1. 格式:域内部分@域
  2. 域内部分:

  3. 长度不超过 64

  4. 大小写字母 + 数字(62)
  5. ASCII 标点符号(19)

    !#$%&'*+-/=?^_`{|}~
    
  6. 可以加入点号(.)隔开,不放首尾,不连续出现

  7. 域名

  8. 每一级域名 1 - 63 个字符,总长度不超过 253
    这个限制和 DNS 报文设计有关
    国际化域名转换成 Punycode 之后也必须遵守这个约定

  9. 允许包含数字、字母(大小写不敏感)和短横线(-)
  10. 短横线不能出现在首尾位置

实践

实际上的邮件地址会更加简单:

  1. 长度限制
  2. QQ 邮箱 3 - 18
  3. 网易邮箱 6 - 18
  4. Gmail 6 - 30
  5. 新浪邮箱 4 - 16
  6. 字符限制:字母数字 + .-_
  7. 一般大小写不敏感
  8. 连字符(.-_)不可连续出现
    1. 网易免费邮箱只支持下划线,网易 VIP 邮箱支持点和下划线
    2. Gmail 只支持点和加号
      1. 在实际投递中,点和加号会被忽略
      2. 点可以用作单词风格
      3. 加号通常用做来信归类,比如注册淘宝时 +taobao,订阅开发者头条时 +toutiao,相关邮件就方便搜索归类。
  9. 部分邮箱不支持全数字(别有用途,或是避免 QQ 号冲突,或是避免手机号冲突)
  10. 开头结尾字符限制:
    1. 字母数字开头 + 字母数字结尾
    2. 字母开头 / 字母数字结尾

正则表达式

以规范为准,参考真实场景下的实践:

域内部分:

/[a-z0-9]+([.-_#][a-z0-9]+)+/;

域名部分:

/[a-z0-9]+(-[a-z0-9]+)?(\.[a-z0-9]+(-[a-z0-9]+)?)+/;

汇总在一起就是:

/^[a-z0-9]+([.-_#][a-z0-9]+)+@[a-z0-9]+(-[a-z0-9]+)?(\.[a-z0-9]+(-[a-z0-9]+)?)+$/;

参考资料与拓展阅读