feffery-antd-components(简称fac),是国内大佬费弗里(Feffery)老师基于著名的React UI组件库ant design进行二次开发,将ant design中的诸多实用组件及特性引入Dash,帮助开发者使用极低的纯Python代码量,即可快速开发出现代化的交互式web应用,将你有关web应用的美好憧憬✨高效地实现。

作为一个开源项目,任何人都可以以任何形式,免费使用它,来打造你心中理想的web应用。
目前fac处于快速迭代期,更多更新的功能请移步官网:
我(岳涛@心馨电脑)在CSDN发布的fac系列学习笔记得到了费弗里(Feffery)老师本人的许可。为了方便更多的fac初学者入门,我把官网的示例代码片段扩充成一个个完整的小项目,适当添加了注释或补充,拿来即用。通过学习fac,我的个人体会是:如果你不是前端开发工程师,暂时不想或没有时间深入前端技术的学习,fac比Vue更适合你。

import dash
import re
from dash import html, dcc
import feffery_antd_components as fac
from dash.dependencies import Input, Output, State
app = dash.Dash(__name__)
app.layout = html.Div(
[
# 注册窗口
html.Div(
[
# 标题
html.Div(
fac.AntdTitle('欢迎注册XX平台', level=2),
style={
'textAlign': 'center',
'marginBottom': '3rem'
}
),
# 表体
html.Div(
[
fac.AntdForm(
[
fac.AntdFormItem(
fac.AntdInput(
id='username',
placeholder='用户名可以是英文字母、数字或汉字及其组合。'
),
id='username-item',
label='用户名',
required=True,
tooltip='用户名可以是英文字母、数字或汉字及其组合。',
style={
'marginBottom': '2rem'
}
),
fac.AntdFormItem(
fac.AntdInput(
id='email',
placeholder='请输入您的邮箱。'
),
id='email-item',
label='邮箱',
required=True,
style={
'marginBottom': '2rem'
}
),
fac.AntdFormItem(
fac.AntdInput(
id='password',
placeholder='请输入密码。',
mode='password'
),
id='password-item',
label='密码',
required=True,
style={
'marginBottom': '2rem'
}
),
fac.AntdFormItem(
fac.AntdInput(
id='confirm-password',
placeholder='请再次输入密码。',
mode='password'
),
id='confirm-password-item',
label='确认密码',
required=True,
style={
'marginBottom': '2rem'
}
),
fac.AntdFormItem(
fac.AntdDatePicker(
id='birthday',
placeholder='请输入您的出生日期。'
),
id='birthday-item',
label='出生日期',
style={
'marginBottom': '2rem'
}
),
fac.AntdFormItem(
fac.AntdButton(
'提交注册',
id='submit',
type='primary',
size='large'
),
wrapperCol={
'offset': 4
}
)
],
wrapperCol={
'span': 20
},
labelCol={
'span': 4
},
)
],
style={
'padding': '3rem',
}
)
],
style={
'width': '50rem',
'height': '40rem',
'position': 'fixed',
'top': '10rem',
'left': '20rem',
'borderRadius': '2rem',
'boxShadow': '0 0.5rem 1rem rgb(150,150,150)',
'padding': '3rem'
}
)
],
style={
'padding': '20px',
'margin': '20px',
}
)
# 点击提交注册按钮后利用正则表达式验证邮箱
@app.callback(
[Output('email-item', 'help'),
Output('email-item', 'validateStatus')],
[Input('email', 'value'),
Input('submit', 'nClicks')],
# prevent_initial_call=True
)
def verify_username(email, nClicks):
if nClicks:
if not email:
return [
'邮箱不可为空!',
'error'
]
else:
# 定义邮箱验证规则
re_mail = r'^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+){0,4}$'
if not re.match(re_mail, email):
return [
'请输入合法邮箱!',
'error'
]
else:
return [
None,
None
]
return dash.no_update
# 点击提交注册按钮后利用正则表达式验证用户名
@app.callback(
[Output('username-item', 'help'),
Output('username-item', 'validateStatus')],
[Input('username', 'value'),
Input('submit', 'nClicks')],
# prevent_initial_call=True
)
def verify_username(username, nClicks):
if nClicks:
if not username:
return [
'用户名不可为空!',
'error'
]
else:
# 在用户名中提取字母数字和汉字
re_username = re.sub(u"([^\u4e00-\u9fa5\u0030-\u0039\u0041-\u005a\u0061-\u007a])", "", username)
if not username == re_username:
return [
'用户名中只能包含:字母、数字和汉字,不能有其他字符!',
'error'
]
else:
return [
None,
None
]
return dash.no_update
# 密码验证
@app.callback(
[Output('password-item', 'help'),
Output('password-item', 'validateStatus'),
Output('confirm-password-item', 'help'),
Output('confirm-password-item', 'validateStatus')],
[Input('password', 'value'),
Input('confirm-password', 'value'),
Input('submit', 'nClicks')],
# prevent_initial_call=True
)
def verify_password(password, confirm_password, nClicks):
# 用户没有点击提交注册按钮,仅验证密码和确认密码是否一致
if password and confirm_password:
if password == confirm_password:
return [
None,
None,
'密码一致!',
'success'
]
else:
return [
None,
None,
'两次输入的密码不一致,请重新输入!',
'error'
]
# 用户点击了提交注册按钮
if nClicks:
if not password:
return [
'密码不可为空!',
'error',
None,
None
]
elif not confirm_password:
return [
None,
None,
'确认密码不可为空!',
'error',
]
elif not password == confirm_password:
return [
None,
None,
'两次输入的密码不一致,请重新输入!',
'error'
]
elif password == confirm_password:
return [
None,
None,
'密码一致!',
'success'
]
return dash.no_update
if __name__ == '__main__':
app.run_server(debug=True)
| 元字符 | 匹配内容 |
|---|---|
| . | 匹配任意字符(不包括换行符) |
| ^ | 匹配开始位置,多行模式下匹配每一行的开始 |
| $ | 匹配结束位置,多行模式下匹配每一行的结束 |
| 匹配前一个元字符0到多次
| 匹配前一个元字符1到多次
? | 匹配前一个元字符0到1次
{m,n} | 匹配前一个元字符m到n次
\ | 转义字符,跟在其后的字符将失去作为特殊元字符的含义,例如\.只能匹配.,不能再匹配任意字符
[] | 字符集,一个字符的集合,可匹配其中任意一个字符
| | 逻辑表达式 或 ,比如 a|b 代表可匹配 a 或者 b
(…) | 分组,默认为捕获,即被分组的内容可以被单独取出,默认每个分组有个索引,从 1 开始,按照"("的顺序决定索引值
(?iLmsux) | 分组中可以设置模式,iLmsux之中的每个字符代表一个模式,用法参见 模式 I
(?:…) | 分组的不捕获模式,计算索引时会跳过这个分组
(?P…) | 分组的命名模式,取此分组中的内容时可以使用索引也可以使用name
(?P=name) | 分组的引用模式,可在同一个正则表达式用引用前面命名过的正则
(?#…) | 注释,不影响正则表达式其它部分,用法参见 模式 I
(?=…) | 顺序肯定环视,表示所在位置右侧能够匹配括号内正则
(?!..) | 顺序否定环视,表示所在位置右侧不能匹配括号内正则
(?<=…) | 逆序肯定环视,表示所在位置左侧能够匹配括号内正则
(?<!..) | 逆序否定环视,表示所在位置左侧不能匹配括号内正则
(?(id/name)yes|no) | 若前面指定id或name的分区匹配成功则执行yes处的正则,否则执行no处的正则
\number | 匹配和前面索引为number的分组捕获到的内容一样的字符串
\A | 匹配字符串开始位置,忽略多行模式
\Z | 匹配字符串结束位置,忽略多行模式
\b | 匹配位于单词开始或结束位置的空字符串
\B | 匹配不位于单词开始或结束位置的空字符串
\d | 匹配一个数字, 相当于 [0-9]
\D | 匹配非数字,相当于 [^0-9]
\s | 匹配任意空白字符, 相当于 [ \t\n\r\f\v]
\S | 匹配非空白字符,相当于 [^ \t\n\r\f\v]
\w | 匹配数字、字母、下划线中任意一个字符, 相当于 [a-zA-Z0-9_]
\W | 匹配非数字、字母、下划线中的任意字符,相当于 [^a-zA-Z0-9_]
^[0-9]*$^\d{n}$^\d{n,}$^\d{m,n}$^(0|[1-9][0-9]*)$^([1-9][0-9]*)+(.[0-9]{1,2})?$^(\-)?\d+(\.\d{1,2})?$^(\-|\+)?\d+(\.\d+)?$^[0-9]+(.[0-9]{2})?$^[0-9]+(.[0-9]{1,3})?$^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$^\-[1-9][]0-9″*$ 或 ^-[1-9]\d*$^\d+$ 或 ^[1-9]\d*|0$^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$^[\u4e00-\u9fa5]{0,}$^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$^.{3,20}$^[A-Za-z]+$^[A-Z]+$^[a-z]+$^[A-Za-z0-9]+$^\w+$ 或 ^\w{3,20}$^[\u4E00-\u9FA5A-Za-z0-9_]+$^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$^%&’,;=?$\”等字符:[^%&',;=?$\x22]+[^~\x22]+^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$^($$\d{3,4}-)|\d{3.4}-)?\d{7,8}$\d{3}-\d{8}|\d{4}-\d{7}^\d{15}|\d{18}$^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$^[a-zA-Z][a-zA-Z0-9_]{4,15}$^[a-zA-Z]\w{5,17}$^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$^\d{4}-\d{1,2}-\d{1,2}^(0?[1-9]|1[0-2])$^((0?[1-9])|((1|2)[0-9])|30|31)$^[1-9][0-9]*$^(0|[1-9][0-9]*)$^(0|-?[1-9][0-9]*)$^[0-9]+(.[0-9]+)?$^[0-9]+(.[0-9]{2})?$^[0-9]+(.[0-9]{1,2})?$^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$[\u4e00-\u9fa5][^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))\n\s*\r (可以用来删除空白行)<(\S*?)[^>]*>.*?</\1>|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)[1-9][0-9]{4,} (腾讯QQ号从10000开始)[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)\d+\.\d+\.\d+\.\d+ (提取IP地址时有用)((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))import re
text = 'a今b天c周d末e'
pattern = r'A今b天C周d末E'
print(f'默认匹配:{re.findall(pattern, text)}')
print(f're.I模式:{re.findall(pattern, text, re.I)}')
# 输出结果:
默认匹配:[]
re.I模式:['a今b天c周d末e']
s = 'hello World!'
regex = re.compile("hello world!", re.I)
print regex.match(s).group()
#output> 'hello World!'
#在正则表达式中指定模式以及注释
regex = re.compile("(?#注释)(?i)hello world!")
print regex.match(s).group()
#output> 'hello World!'
这个功能是为了支持多语言版本的字符集使用环境的,比如在转义符\w,在英文环境下,它代表[a-zA-Z0-9_],即所以英文字符和数字。如果在一个法语环境下使用,缺省设置下,不能匹配"é" 或 “ç”。加上这L选项和就可以匹配了。不过这个对于中文环境似乎没有什么用,它仍然不能匹配中文字符。
import re
text = '今天\n周末!'
pattern = r'^周末'
print(f'默认匹配:{re.findall(pattern, text)}')
print(f'多行模式:{re.findall(pattern, text, re.M)}')
# 输出结果
默认匹配:[]
多行模式:['周末']
s = '''first line
second line
third line'''
# ^
regex_start = re.compile("^\w+")
print regex_start.findall(s)
# output> ['first']
regex_start_m = re.compile("^\w+", re.M)
print regex_start_m.findall(s)
# output> ['first', 'second', 'third']
#$
regex_end = re.compile("\w+$")
print regex_end.findall(s)
# output> ['line']
regex_end_m = re.compile("\w+$", re.M)
print regex_end_m.findall(s)
# output> ['line', 'line', 'line']
DOT表示.,ALL表示所有,连起来就是.匹配所有,包括换行符\n。默认模式下.是不能匹配行符\n的。
import re
text = 'a今\nb天\nc周\nd末\ne!'
pattern = r'.+'
print(f'默认匹配:{re.findall(pattern, text)}')
print(f're.S模式:{re.findall(pattern, text, re.S)}')
# 输出结果
默认匹配:['a今', 'b天', 'c周', 'd末', 'e!']
re.S模式:['a今\nb天\nc周\nd末\ne!']
s = '''first line
second line
third line'''
#
regex = re.compile(".+")
print regex.findall(s)
# output> ['first line', 'second line', 'third line']
# re.S
regex_dotall = re.compile(".+", re.S)
print regex_dotall.findall(s)
# output> ['first line\nsecond line\nthird line']
例如写一个匹配邮箱的正则表达式
email_regex = re.compile("[\w+\.]+@[a-zA-Z\d]+\.(com|cn)")
email_regex = re.compile("""[\w+\.]+ # 匹配@符前的部分
@ # @符
[a-zA-Z\d]+ # 邮箱类别
\.(com|cn) # 邮箱后缀 """, re.X)
顾名思义,ASCII表示ASCII码的意思,让 \w, \W, \b, \B, \d, \D, \s 和 \S 只匹配ASCII,而不是Unicode。
import re
text = 'a今b天c周d末e'
pattern = r'\w+'
print(f'默认匹配:{re.findall(pattern, text)}')
print(f'ASCII模式:{re.findall(pattern, text, re.A)}')
# 输出结果:
默认匹配:['a今b天c周d末e']
ASCII模式:['a', 'b', 'c', 'd', 'e']
在默认匹配模式下\w+匹配到了所有字符串,而在ASCII模式下,只匹配到了a、b、c(ASCII编码支持的字符)。
注意:这只对字符串匹配模式有效,对字节匹配模式无效。
显示编译时的debug信息。
import re
text = '今天\n周末!'
pattern = r'^今天'
print(f'默认匹配:{re.findall(pattern, text)}')
print('----------------')
print(f'多行模式:{re.findall(pattern, text, re.DEBUG)}')
# 输出结果
默认匹配:['今天']
----------------
AT AT_BEGINNING
LITERAL 20170
LITERAL 22825
0. INFO 4 0b0 2 2 (to 5)
5: AT BEGINNING
7. LITERAL 0x4eca ('今')
9. LITERAL 0x5929 ('天')
11. SUCCESS
多行模式:['今天']
正则表达式的模式是可以同时使用多个的,在 python 里面使用按位或运算符 | 同时添加多个模式
如 re.compile('', re.I|re.M|re.S)
print re.I
# output> 2
print re.L
# output> 4
print re.M
# output> 8
print re.S
# output> 16
print re.X
# output> 64
print re.U
# output> 32
re模块有12个函数,分为4类
查找并返回一个匹配项的函数有3个:search、match、fullmatch,他们的区别分别是:
import re
target = '周末快乐,a今天\n周末b!'
pattern = r'周末b'
print(f'search-查找任意位置的匹配项:{re.search(pattern, target).group()}')
print(f'match-必须从字符串开头匹配:{re.match(pattern, target)}')
print(f'fullmatch-整个字符串与正则完全匹配:{re.fullmatch(pattern, target)}')
# 输出结果:
search-查找任意位置的匹配项:周末b
match-必须从字符串开头匹配:None
fullmatch-整个字符串与正则完全匹配:None
查找多项函数主要有:findall函数 与 finditer函数:
import re
target = '周末快乐,a今天\n周末b!'
pattern = r'周末b'
print(f'findall-从字符串任意位置查找,返回一个列表:{re.findall(pattern, target)}')
print(f'finditer-从字符串任意位置查找,返回一个迭代器:{re.finditer(pattern, target)}')
# 输出结果
findall-从字符串任意位置查找,返回一个列表:['周末b']
finditer-从字符串任意位置查找,返回一个迭代器:<callable_iterator object at 0x00000210F54168C8>
re.split(pattern, string, maxsplit=0, flags=0)
用 pattern 分开 string , maxsplit表示最多进行分割次数, flags表示模式,就是上面我们讲解的常量!
import re
target = '周末快乐,a今天\n周末b!'
pattern = r','
print(f'split-分割:{re.split(pattern, target)}')
# 输出结果
split-分割:['周末快乐', 'a今天\n周末b!']
str模块也有一个 split函数,在不需要正则支持且数据量和数次不多的情况下使用str.split函数更合适,反之则使用re.split函数。
替换主要有sub函数 与 subn函数,他们功能类似!
re.sub(pattern, repl, string, count=0, flags=0)
repl替换掉string中被pattern匹配的字符, count表示最大替换次数,flags表示正则表达式的常量。
值得注意的是:sub函数中的入参:repl替换内容既可以是字符串,也可以是一个函数哦! 如果repl为函数时,只能有一个入参:Match匹配对象。
re.subn(pattern, repl, string, count=0, flags=0)
re.subn函数函数与 re.sub函数 功能一致,只不过返回一个元组 (字符串, 替换次数)。
import re
target = '周末快乐,a今天\n周末b!'
pattern = r','
repl = r'!'
print(f'sub-替换:{re.sub(pattern, repl, target, count=2, flags=re.IGNORECASE)}')
print('--------------')
print(f'subn-替换:{re.subn(pattern, repl, target, count=2, flags=re.IGNORECASE)}')
# 输出结果
sub-替换:周末快乐!a今天
周末b!
--------------
subn-替换:('周末快乐!a今天\n周末b!', 1)
compile函数 与 template函数 将正则表达式的样式编译为一个 正则表达式对象 (正则对象Pattern),这个对象与re模块有同样的正则函数(后面我们会讲解Pattern正则对象)。
而template函数 与 compile函数 类似,只不过是增加了我们之前说的re.TEMPLATE 模式。
group和groups是两个不同的函数。
group()和group(0)效果一样,用于获取正则表达式匹配到的全部结果,参数默认0;
group(num),num为正整数,获取正则表达式匹配结果中相应的第n个括号内的元组;
groups(),用于获取正则表达式全部括号内的元组。m.groups() == (m.group(1), m.group(2), …)
或者m.groups()[0] == m.group(1),m.groups()[1] == m.group(2)
