From 29be3ca20a23b2b12093c1e5ecfe74a5f254a09d Mon Sep 17 00:00:00 2001 From: cyanineeee <> Date: Thu, 15 Aug 2024 00:22:23 +0800 Subject: [PATCH] update from gridea: 2024-08-15 00:22:23 --- 404.html | 89 + archives/index.html | 657 ++++++ atom.xml | 1881 +++++++++++++++++ favicon.ico | Bin 0 -> 36050 bytes images/avatar.png | Bin 0 -> 36050 bytes index.html | 291 +++ page/2/index.html | 293 +++ page/3/index.html | 293 +++ page/4/index.html | 293 +++ page/5/index.html | 343 +++ post/ajax-cors-trouble/index.html | 176 ++ post/an-zhuang-nb_conda-bao-cuo/index.html | 183 ++ .../index.html | 174 ++ .../index.html | 203 ++ .../index.html | 186 ++ .../index.html | 172 ++ .../index.html | 174 ++ post/cc-jie-gou-ti-qian-tao/index.html | 249 +++ post/cczhi-zhen/index.html | 462 ++++ post/conda-he-pip-de-qu-bie/index.html | 172 ++ .../index.html | 172 ++ .../index.html | 572 +++++ .../index.html | 210 ++ .../index.html | 218 ++ post/fei-yi-kai-fa-ri-zhi-3/index.html | 204 ++ .../index.html | 189 ++ post/guan-yu-kua-yu/index.html | 176 ++ post/hui-diao-han-shu/index.html | 172 ++ .../index.html | 246 +++ .../index.html | 239 +++ .../index.html | 215 ++ post/js-promise/index.html | 179 ++ post/js-yi-bu-he-dan-xian-cheng/index.html | 201 ++ .../index.html | 178 ++ .../index.html | 172 ++ .../index.html | 172 ++ post/lin-shi-ji-lu/index.html | 615 ++++++ post/mysql-null-he-kong-de-qu-bie/index.html | 181 ++ post/nginx-xue-xi/index.html | 260 +++ post/part2/index.html | 535 +++++ .../index.html | 179 ++ .../index.html | 299 +++ post/python-wang-luo-bian-cheng/index.html | 453 ++++ .../index.html | 236 +++ post/robottxt/index.html | 258 +++ .../index.html | 291 +++ .../index.html | 289 +++ .../index.html | 179 ++ .../index.html | 208 ++ .../index.html | 192 ++ .../index.html | 174 ++ post/the-import-system-of-python/index.html | 209 ++ .../index.html | 176 ++ post/tong-bu-he-yi-bu/index.html | 172 ++ .../index.html | 193 ++ post/upload-files-by-django/index.html | 364 ++++ .../index.html | 366 ++++ .../index.html | 248 +++ post/uwsgi-django-bu-shu/index.html | 298 +++ post/wen-jian-chu-cun-shu-ju-ku/index.html | 183 ++ styles/main.css | 1002 +++++++++ tag/-uPahkw58/index.html | 170 ++ tag/M8-hjmU6C/index.html | 209 ++ tag/axGgidGI4/index.html | 232 ++ tag/p8kCxqhZ6/index.html | 124 ++ tags/index.html | 100 + 66 files changed, 18201 insertions(+) create mode 100644 404.html create mode 100644 archives/index.html create mode 100644 atom.xml create mode 100644 favicon.ico create mode 100644 images/avatar.png create mode 100644 index.html create mode 100644 page/2/index.html create mode 100644 page/3/index.html create mode 100644 page/4/index.html create mode 100644 page/5/index.html create mode 100644 post/ajax-cors-trouble/index.html create mode 100644 post/an-zhuang-nb_conda-bao-cuo/index.html create mode 100644 post/anaconda-an-zhuang-bao-shi-chu-xian-de-channels-zhao-bu-dao-wen-ti/index.html create mode 100644 post/anaconda-da-bu-kai-anaconda-navigator/index.html create mode 100644 post/anaconda-pei-zhi-xia-zai-jing-xiang-yuan/index.html create mode 100644 post/bing-fa-bing-xing-chuan-xing-tong-bu-yi-bu/index.html create mode 100644 post/catch-the-enter-key-while-submit-a-form/index.html create mode 100644 post/cc-jie-gou-ti-qian-tao/index.html create mode 100644 post/cczhi-zhen/index.html create mode 100644 post/conda-he-pip-de-qu-bie/index.html create mode 100644 post/da-jian-jing-tai-wen-jian-fu-wu-qi-lai-gui-bi-ben-di-kua-yu-fang-wen-wen-jian/index.html create mode 100644 post/django-xue-xi-qian-hou-duan-bu-fen-chi-de-part2/index.html create mode 100644 post/djangos-logic-for-finding-templates-folders/index.html create mode 100644 post/extra-details-of-python-for-long-term-part2/index.html create mode 100644 post/fei-yi-kai-fa-ri-zhi-3/index.html create mode 100644 post/fei-yi-xiang-mu-kai-fa-ri-zhi-1/index.html create mode 100644 post/guan-yu-kua-yu/index.html create mode 100644 post/hui-diao-han-shu/index.html create mode 100644 post/install-centos-in-virtualbox-and-configure-the-network-part1/index.html create mode 100644 post/install-centos-in-virtualbox-and-configure-the-network-part2/index.html create mode 100644 post/jquery-dong-hua-fang-zhi-duo-ci-hong-fa-chong-fu/index.html create mode 100644 post/js-promise/index.html create mode 100644 post/js-yi-bu-he-dan-xian-cheng/index.html create mode 100644 post/jupyter-notebook-qi-dong-500-wen-ti/index.html create mode 100644 post/li-jie-js-de-then-catch-reject-promise-deng/index.html create mode 100644 post/liao-xue-feng-wang-zhan-nodejs-dui-yu-jing-tai-wen-jian-de-chu-li/index.html create mode 100644 post/lin-shi-ji-lu/index.html create mode 100644 post/mysql-null-he-kong-de-qu-bie/index.html create mode 100644 post/nginx-xue-xi/index.html create mode 100644 post/part2/index.html create mode 100644 post/preview-pictures-in-front-end-by-using-urlcreateobjecturl-and-some-details-in-this-means/index.html create mode 100644 post/python-da-kai-zui-jin-guan-bi-de-word-wen-jian-xu-qiu-jie-jue/index.html create mode 100644 post/python-wang-luo-bian-cheng/index.html create mode 100644 post/requestspost-he-requestssessionpost-fang-fa-de-qu-bie/index.html create mode 100644 post/robottxt/index.html create mode 100644 post/scrapy-item-fu-zhi-tian-chong-xi-jie-zhu-yi/index.html create mode 100644 post/scrapy-pa-chong-kuang-jia-geng-shen-ru-li-jie-yi-ji-shi-yong/index.html create mode 100644 post/shi-yong-gridea-jian-li-bo-ke-shi-yu-dao-de-wen-ti/index.html create mode 100644 post/the-attribute-of-form-nameforvalue/index.html create mode 100644 post/the-effect-of-the-mask-attribution-in-css/index.html create mode 100644 post/the-error-in-documentwrite-method-when-its-inner-is-lessscirptgreater/index.html create mode 100644 post/the-import-system-of-python/index.html create mode 100644 post/the-solution-of-insert-images-into-markdown/index.html create mode 100644 post/tong-bu-he-yi-bu/index.html create mode 100644 post/traverse-a-jquery-object-and-the-differencebetween-this-and-dollarthis/index.html create mode 100644 post/upload-files-by-django/index.html create mode 100644 post/upload-files-by-djangopart2-without-modelform/index.html create mode 100644 post/using-django-upload-images-and-display/index.html create mode 100644 post/uwsgi-django-bu-shu/index.html create mode 100644 post/wen-jian-chu-cun-shu-ju-ku/index.html create mode 100644 styles/main.css create mode 100644 tag/-uPahkw58/index.html create mode 100644 tag/M8-hjmU6C/index.html create mode 100644 tag/axGgidGI4/index.html create mode 100644 tag/p8kCxqhZ6/index.html create mode 100644 tags/index.html diff --git a/404.html b/404.html new file mode 100644 index 0000000..58bcadf --- /dev/null +++ b/404.html @@ -0,0 +1,89 @@ + + +
+ + + +E: Package 'python3.10-venv' has no installation candidate
说明你应该更新一下系统的可安装软件列表apt-get update
[uwsgi]
+http=0.0.0.0:8000 #设置为0.0.0.0表示外网可以访问 127.0.0.1表示本机访问
+chdir=/my/mysite #项目根目录
+wsgi-file=mysite/wsgi.py #项目根目录里面的同名文件内的wsgi.py文件
+process=2 #启动进程数 跟服务器cpu有关
+threads=2 #线程数
+pidfile=uwsgi.pid #储存进程的pid 以便于后面停止运行
+daemonize=uwsgi.log #日志文件
+master=true #主进程启动
+
+root 84204 1.0 3.8 47520 36724 ? S 11:07 0:00 uwsgi --ini uwsgi.ini
+root 84206 0.0 3.0 121252 29816 ? Sl 11:07 0:00 uwsgi --ini uwsgi.ini
+root 84207 0.0 2.7 47520 26280 ? S 11:07 0:00 uwsgi --ini uwsgi.ini
+root 84250 0.0 0.0 6432 720 pts/0 S+ 11:08 0:00 grep --color=auto uwsgi
+
+如果没有设置停止的pid或者启动失败导致pid生成失败,这样就会导致有uwgsi进程挂在后台,因此需要彻底关闭:pkill -f uwsgi -9
PS.
+uwsgi invalid request block size: 4937 (max 4096)...skip
buffer-size = 65536
即可。1.启动失败: 端口被占用
+解决方法: 更换端口/停止占用端口的进程
+sudo lsof -i:端口号
- 查看端口 kill -9 端口号
- 停止端口进程
2.停止失败 stop无法关闭uwsgi
+原因: 重复启动uwsgi导致uwsgi.pid中进程号失效
+解决方法 : ps 出uwsgi进程,手动kill
wsgi.py
文件的相对于项目根目录的路径安装uwsgi报错ERROR: Failed building wheel for uwsgi
+搜索解决方法:更新pip即可 - 更新系统依赖 - 安装psycopg2-binary -
+都没有用
+最后:apt-get install build-essential python3-dev
然后pip即可
+stackoverflow
raise ImproperlyConfigured(
+django.core.exceptions.ImproperlyConfigured: The app module <module 'cyanine' (namespace)> has multiple filesystem locations (['/my/imsys/./cyanine', '/my/imsys/cyanine']); you must configure this app with an AppConfig subclass with a 'path' class attribute.
+
+原因:没有正确设置app,因为想要添加一个展示页面作为项目的index,所以就直接将这个html文件以及附带的静态文件一股脑地放在一个文件夹里面然后添加到项目中,还在setting中注册了app名字,但是实际上这个app里面缺少对应的配置文件,这就开发模式下能够凭借文件路径运行正确,但生产模式部署的时候就导致uwgis运行的时候找不到文件路径报错。
+因此,对于django的app来说,需要有两个文件:__init.py__
和apps.py
文件,其中apps.py
中内容为
from django.apps import AppConfig
+class PollsConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "你注册在setting中的app名字"
+
+
+]]>sudo apt install nginx
+安装成功之后输入nginx -v
查看版本。
+PS. nginx安装之后会默认占用80端口
/etc/nginx/site-enabled/default
是nginx的配置文件
+核心参数
/etc/nginx/uwsgi_params
sudo /etc/init.d/nginx start|stop|restart|status
分别代表启动|停止|重启|查看状态
+PS. nginx只要修改就需要重启,否则配置不生效
具体如下:
+#default文件
+ # try_files $uri $uri/ =404; #注释掉这一句
+ uwsgi_pass 127.0.0.1:8000; #添加这两句
+ include /etc/nginx/uwsgi_params;
+
+#uwsgi.ini
+#去掉http 修改为 socket
+socket = 127.0.0.1:8000 #这个端口号要和nginx的一致
+
+
+PS. sudo nginx -t
可以快速方便的告诉你配置文件是否有语法错误
/var/log/nginx/error.log
错误日志:/var/log/nginx/access.log
对应uwsgi日志 - 和uwsgi.ini
同级下的uwsgi.log
buffer-size = 65536
- 主要还是得看日志try_files
/home/mysite_static/
STATIC_ROOT
代表静态文件路径 -- 例如/home/mysite_static/static/
python3 mange.py collectstatic
- 收集项目所有的静态文件location /static {
+root /静态文件夹;
+}
+
+特别要注意路径,这个‘静态文件夹’下面还有一个static的文件夹,里面才是真正存放了所有的静态文件
+404.html
文件,当视图触发http404的时候自动显示该页面(在debug=false的时候彩起作用) -- 添加完之后记得重启nginx、uwsgi等服务大概是因为项目中的静态文件路径和nginx的有所区别,
+需要注意alias和root的区别,
+nginx会根据请求的路径,去请求静态文件夹中可能存在的,文件夹中的静态文件。简而言之,nginx是可以嵌套的,但是前提是请求的路径需要对应的上。
download_link
+office_link
+usage
$('#id_presettime').datetimepicker({
+ format: 'yyyy-mm-dd hh:ii'
+});
+
+class orderForm(ModelForm):
+ class Meta():
+ model = Orderform
+ fields = "__all__"
+
+ #方法一 原生的属性
+ widgets = {
+ #为html表单中name类型的input标签添加"class=form-control"
+ 'name' : forms.TextInput(attrs = {"class" : "form-control"})
+ #为html表单中password类型的input标签添加"class=form-control"
+ 'password' : formsPasswordInput( attrs = {"class" : "form-control"})
+ ...
+ }
+
+ #方法二 扩写父类函数的属性
+ def __init__(self,*arg,**kwargs):
+ super().__init__(*arg,**kwargs)
+ for name, field in self.fields.items():
+ # print(name,field) #这个是展示 self.fields.items()可以直接拿到定义在Meta中的fields字段
+ field.widget.attrs = {'class' : 'form-control'} #为生成的表单input赋予css属性
+
+input
的type
类型为datetime-local
function changeDatetimeInput(id){
+ var time = $(`#${id}`).val().replaceAll('/','-');
+ $(`#${id}`).attr('type','datetime-local');
+ $(`#${id}`).val(time)
+}
+changeDatetimeInput('id_presettime');
+changeDatetimeInput('id_endtime');
+
+需要注意的是,后端django传来的之间类型是yyyy/mm/dd hh:ii
,但是html内置的时间选择input框的时间字符串格式要求是yyyy-mm-ddThh:ii
,因此需要将原来的斜杠(/)替换成短横(-),中间·的T
是一个时间字符表示,可以用大写字符T表示,但最终解析的时候空格也可以,具体参考
DateTimeInput
from django import forms
+class DateTimeLocalInput(forms.DateTimeInput):
+ input_type = "datetime-local"
+
+class DateTimeLocalField(forms.DateTimeField):
+ # Set DATETIME_INPUT_FORMATS here because, if USE_L10N
+ # is True, the locale-dictated format will be applied
+ # instead of settings.DATETIME_INPUT_FORMATS.
+ # See also:
+ # https://developer.mozilla.org/en-US/docs/Web/HTML/Date_and_time_formats
+ input_formats = [
+ "%Y-%m-%dT%H:%M:%S",
+ "%Y-%m-%dT%H:%M:%S.%f",
+ "%Y-%m-%dT%H:%M"
+ ]
+ widget = DateTimeLocalInput(format="%Y-%m-%dT%H:%M")
+
+然后再自己的modelform中将想要修改input的typ类型为datetime-local的字段设置为DateTimeLocalField
即可:
#用法
+class orderEditForm(ModelForm):
+ presettime = DateTimeLocalField() #在这里将想要的字段设置即可
+ class Meta():
+ model = Orderform
+ fields = "__all__"
+
+<!-- {{field}}生成的具体样式 -->
+<input type="text" name="endtime" value="1899/12/31 00:00" class="form-control" id="id_endtime">
+
+raise ValidationError
from django.conf import settings
+import hashlib
+
+def md5(data_string):
+ obj = hashlib.md5(settings.SECRET_KEY.encode('utf-8'))
+ obj.update(data_string.encode('utf-8'))
+ return obj.hexdigest()
+
+搜索不到就返回None,可以用来判断是否是合法请求
+所有验证通过的信息都储存在对应的cleaned_data
中
'Manager' object is not callable
+原因是objects
是一个属性而不是一个方法,写成objects()
会报这个错
业务过程:收到用户的提交 -> 校验(在数据库比较) -> 成功 生成随机字符串写入到用户浏览器的cookies以及数据库中(这里只需要调用request.session()
方法即可 django会自动处理两个事件),并且会将传入的字符经过处理之后,以{session_key:session_data}
键值对的形式保存在数据库中,其中session_key就是传递给浏览器的cookies,而session_data就是在下面代码中定义的字典(经过字符转换,需要使用的时候django会解码)
else: #登陆成功
+ request.session['info'] = { #写入session
+ 'id':login_object.id,
+ 'name' : login_object.name,
+ }
+
+PS. 特别需要注意的是,在后面的调用request.session中的属性的名字都是来自于此!
+因此需要在用户访问的时候判断用户是否登录 -> 已登录继续访问 未登录跳转回登陆页面:
+用户请求-> 拿到用户cookies并判断:
#第一种方法 在每个请求前都加上对于cookies的判断
+ if not request.session.get('info'):
+ return redirect('/login/')
+
+太low😎
+应该使用django的中间件高效的处理
process_request
方法对传入的请求进行处理,通过process_response
对视图函数的return进行包装。如果某个请求不能通过一个中间件,那么就会直接返回给用户,而不会到达视图函数。process_request
方法如果返回为None,那么继续向后执行;如果需要返回,那么返回的类型与视图函数的返回类似(例如render,redirect,HttpResponse等),并且不在向后执行。#middleware.py文件
+from django.utils.deprecation import MiddlewareMixin
+from django.shortcuts import render,redirect
+from django.http import HttpResponse
+
+class SessionChectMiddleware(MiddlewareMixin):
+ def process_request(self,request):
+ #排除不需要登陆的页面
+ allow_url = ['/login/','/register/']
+ if request.path_info in allow_url:
+ return
+ #读取session
+ info = request.session.get('info')
+ if info:
+ return
+ else:
+ return redirect('/login/')
+
+#在setting.py文件中注册
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+...... ,
+ "cake.middleware.SessionChectMiddleware",
+]
+
+PS. request.path_info可以获取到请求的url路径。
+也就是将浏览器的cookies和session删除
+同样使用request.session中的方法即可
每次视图函数的中传递的request中都包含session参数,因此可以直接在模板中调用{{request.session.info.属性}}
来获取对应的信息(因为request是默认传递的),不用刻意通过别的模板参数来渲染。
<button class="btn btn-secondary dropdown-toggle btn-sm" type="button" id="dropdownMenuButton2" data-bs-toggle="dropdown" aria-expanded="false">
+ {{ request.session.info.name}}
+</button>
+
+-> python中如何动态生成图片并写入值 (不做过多了解 仅仅当成黑盒使用)
+参考文献
import random
+from PIL import Image,ImageFont,ImageDraw,ImageFilter
+#需要注意!! 这里的字体文件font_file路径组合是基于运行的根目录决定的 需要根据相对位置的不同进行调整!这里我将他放在根目录下面的static/文件夹内
+def check_code(width=120, height=30, char_length=5, font_file='./static/kumo.ttf', font_size=28):
+ # font = ImageFont.load_default() #使用默认字体
+ code = []
+ img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
+ draw = ImageDraw.Draw(img, mode='RGB')
+ def rndChar():
+ """
+ 生成随机字母
+ :return:
+ """
+ return chr(random.randint(65, 90))
+ def rndColor():
+ """
+ 生成随机颜色
+ :return:
+ """
+ return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255))
+ # 写文字
+ font = ImageFont.truetype(font_file, font_size)
+ for i in range(char_length):
+ char = rndChar()
+ code.append(char)
+ h = random.randint(0, 4)
+ draw.text([i * width / char_length, h], char, font=font, fill=rndColor())
+ # 写干扰点
+ for i in range(40):
+ draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
+ # 写干扰圆圈
+ for i in range(40):
+ draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor())
+ x = random.randint(0, width)
+ y = random.randint(0, height)
+ draw.arc((x, y, x + 4, y + 4), 0, 90, fill=rndColor())
+ # 画干扰线
+ for i in range(5):
+ x1 = random.randint(0, width)
+ y1 = random.randint(0, height)
+ x2 = random.randint(0, width)
+ y2 = random.randint(0, height)
+
+ draw.line((x1, y1, x2, y2), fill=rndColor())
+
+ img = img.filter(ImageFilter.EDGE_ENHANCE_MORE)
+ return img,''.join(code)
+
+if __name__ == '__main__':
+ # 1. 直接打开
+ # img,code = check_code()
+ # img.show()
+
+ # 2. 写入文件
+ img,code = check_code()
+ with open('code.png','wb') as f:
+ img.save(f,format='png')
+
+ # 3. 写入内存(Python3)
+ # from io import BytesIO
+ # stream = BytesIO()
+ # img.save(stream, 'png')
+ # stream.getvalue()
+
+ # 4. 写入内存(Python2)
+ # import StringIO
+ # stream = StringIO.StringIO()
+ # img.save(stream, 'png')
+ # stream.getvalue()
+
+ pass
+
+需要注意!!`check_code()`的字体文件`font_file`路径组合是基于运行的根目录决定的 需要根据相对位置的不同进行调整!这里我将他放在根目录下面的`static/`文件夹内
++ 然后就是部署到djang中:主要是将前端请求图片的url替换为动态的,然后生成验证码的视图函数返回对应的验证码图片。 +```python +from .function.checkcode import check_code #这个就是刚才的生成验证码图片的函数 +from io import BytesIO #将图片储存到内存中需要用的库 +def image_code(request):
+'''生成验证码'''
+img,str = check_code()
+print(str)
+stream = BytesIO()
+img.save(stream, 'png')
return HttpResponse(stream.getvalue()) #从内存中读取到图片然后以http response的形式传回给前端 前端的img标签解析之后就是一个图片
+
+```html
+<img src="/image/code/">
+<!-- 只要调用对应的url就可以 -->
+
+
+PS. 需要注意的是:同一个文件中不要出现名字相同的函数,不管是引用的还是本文件的,会导致调用出错
+通过session! - 这样每个用户在登陆的时候对于验证码的验证就不会受到干扰,并且重复刷新也会更新session中对应的验证码。
+这里有很多需要注意的点!
add_errors()
可以向forms中指定字段添加错误提示信息,然后能在前端的{{fields.属性字段.errors.0}}
中取到。request.POST
作为data参数会报错。因此应该将验证码字段单独剔除。from .models import UserInfo #model中的类 是最原本的用户信息类 包括账户名和密码以及自动生成的id
+from io import BytesIO #生成图片储存在内存
+from .encrypt import md5 #加密
+
+class loginForm(forms.Form):
+ name = forms.CharField(label='用户名',widget=forms.TextInput(
+ attrs = {
+ 'class' :"my-input-item"
+ }
+ ))
+ password = forms.CharField(label='密码',widget=forms.PasswordInput(
+ attrs = {
+ 'class' :"my-input-item"
+ }
+ ))
+ code = forms.CharField(label='验证码',widget=forms.TextInput(
+ attrs={
+ 'class':"my-input-item" ,
+ 'style':"margin-top:0;" ,
+ 'placeholder':"验证码"
+ }
+ ),required=True)
+
+ def cleaned_password(self):
+ return md5(self.changed_data.get('password')) #md5加密
+
+#登录页面
+def login(request):
+ if request.method == 'GET':
+ form = loginForm()
+ return render(request,'login.html',{
+ 'form':form
+ })
+
+ login_form = loginForm(data=request.POST)
+ if login_form.is_valid():
+ user_input_code = login_form.cleaned_data.pop('code')
+ #验证码的校验
+ code = request.session.get('image_code','') #这个是在生成验证码的时候存入session中的,如果过时或者获取不到默认为空
+ if code.upper() != user_input_code.upper(): #不考虑大小写
+ #如果验证码不相等
+ login_form.add_error('code','验证码错误')
+ return render(request,'login.html',{
+ 'form' : login_form,
+ })
+
+ login_object = UserInfo.objects.filter(**login_form.cleaned_data).first() #校验用户密码
+ if not login_object:
+ login_form.add_error('name','用户名或密码错误')
+ return render(request,'login.html',{
+ 'form' : login_form,
+ })
+ else: #登陆成功
+ request.session['info'] = { #写入session
+ 'id':login_object.id,
+ 'name' : login_object.name,
+ }
+ request.session.set_expiry(60 * 60 * 24 * 7) #再重新设置session 保存7天
+ return redirect('/login/manage/')
+ else: #表单验证未通过
+ return render(request,'login.html',{
+ 'form' : login_form,
+ })
+
+#生成验证码
+def image_code(request):
+ '''生成验证码'''
+ img,str = check_code()
+ request.session['image_code'] = str
+ request.session.set_expiry(60) #设置验证码session 60s过时
+ stream = BytesIO()
+ img.save(stream, 'png')
+ return HttpResponse(stream.getvalue())
+
+PS. UserInfo是在model.py文件中的ORM类,对应着储存登录用户帐号密码的数据库表。
+]]>空值时不占用空间的; 2、null其实是占用空间的; 打个比方来说,你有一个杯子,空值代表杯子是真空的,NULL代表杯子中装满了空气,虽然杯子看起来都是空的,但是区别是很大的。 NULL 其实并不是空值,而是要占用空间,所以mysql在进行比较的时候,NULL 会参与字段比较,所以对效率有一部分影响
+ +那什么时候用null什么时候用空值呢?
+++]]>更推荐使用空值。两者的含义不一样,前者代表不清楚,后者代表缺失值。但实际使用上null会在数据库操作方面造成很多的麻烦,因此更推荐设置为空值,并且字段设置为
+NOT NULL
class UserInfo(models.Model):
+ name = models.CharField(max_length=32)
+ password = models.CharField(max_length=64)
+
+default=
字段,代表给原来没有的缺失值赋值。例如:age = models.CharField(defalut=18)
age = models.CharField(null=True,blank=True)
UserInfo.objects.create(name='cyanine',password="123456")
插入数据 需要类中的属性对应
+a = UserInfo.objects.all()
获取所有数据 以列表返回,其中每一个元素都是一个对象 通过.属性
获取具体的数据a.name
,结合.filter()
可以进行筛选查询
+.first()
获取获取到的第一个对象
删除数据
+.delete()
+可以添加filter
进行筛选 例如UserInfo.objects.filter(name='cyanine').delete()
删除所有name为cyanine的数据
+使用.all()
选择全部UserInfo.objects.all().delete()
删除这张表中的所有数据
更新数据
+UserInfo.objects.all().updata(password=123456)
更新所有的password为123456
+还可以结合.filter()
进行筛选
{% url 'url_name' other_param%}
这个url_name 就是在urls.py文件中的path('url/example', views.function,name = url_name)
的name为url从的重命名,这样修改path对应的url,就不用在维护前端的页面了。ForeignKry
方法中的to
和to_field
参数在django中设置。用途是可以方便对输入数据的合法性判断,例如员工不能属于一个不存在的部门,就可以很方便的进行判断。department = models.ForeignKey(to = 'Department', to_field = 'id')
to
表示关联的表名,to_field
表明关联的表的一个列名字段。要注意的是,django在储存这样的字段时默认命名方式是关联表名_id
(例如这个就是depaetment_id
)。obj.联表名_id
,获取到的时储存在这张表中的对应关联表的数字,而不是我们想要的具体指代,因此django规定了如果直接.联表名
就可以获取到关联表对应的那一行数据(封装为一个queryobj对象),然后就可以继续通过关联表内的属性进行查找。例如,部门表中一共有两个部门,字段为(id,部门名),内容为(1,主管部)(2,运行部),员工表关联department = models.ForeignKey(to = 'Department', to_field = 'id',on_delete=models.CASCADE)
级联删除department = models.ForeignKey(to = 'Department', to_field = 'id',null=True,blank=True,on_delete=models.SET_NULL)
置空 需要注意的是 置空的前提就是这个字段可以为空null = True
这些都是django中的操作。choice
参数。gender_choice = (
+ (1,'男'),
+ (2,'女'),
+) #注意是元组套元组
+gender = models.SmallIntegerField(verbose_name='性别',choices = gender_choice) #verbose_name就是对列名(属性)的注解 可写可不写 写上为好
+#SmallIntegerField是小整数字段
+
+obj.gender
获取的就是数据库中对应的数字,但是django封装了一个判断方法,可以很方便的帮我们判断,然后返回的是我们希望现实的字段:obj.get_gender_display()
。其中函数名字组成方式为get_属性名字/字段名字_display()
#views.py
+class MyForm(Form):
+ user = forms.CharField(widget=forms.Input)
+ password = forms.CharField(widget=forms.Input)
+ email = forms.CharField(widget=forms.Input)
+def user_add(request):
+ form = MyForm() #实力化一个form表单类
+ return render(request,'user_add.html',{'form':form}
+
+<!-- user_add.html -->
+<form method='post'>
+{% for i in form %}
+{{ i }}
+{% endfor %}
+<!-- 或者也可以一个一个自己写出来 比如{{form.user}} {{form.password}} -->
+</form>
+
+__str__
方法实现对连接表属性的直接展示{{ obj.label }}
可以遍历出在modelform定义字段中的label
参数#views.py
+#根据staff表单生成modelform类
+class staffMdoelForm(ModelForm):
+ class Meta:
+ model = Staff
+ fields = ['name','age','gender','phonenumber','create_time']
+
+#添加员工
+def staff_add(request):
+ form = staffMdoelForm() #实例化modelform类
+ return render(request,'staff_add.html',{
+ 'form':form
+ })
+
+<div style="width:50%;margin-left: 25%;margin-top: 25px;">
+ <div class="container" >
+ <form method="post">
+ <h2>添加新的员工信息</h2>
+ {% csrf_token %}
+ {% for field in form %}
+ <div class="mb-3">
+ <label class="form-label">{{field.label}}</label>
+ {{ field }}
+ </div>
+ {% endfor %}
+ <button type="submit" class="btn btn-primary">确认添加</button>
+ </form>
+ </div>
+</div>
+
+#连接表modelform处理(html不用做太多改变)
+#在models.py中的从表添加__str__方法 让调用这个类的时候返回的不再是一个queryobject而是__str__定义好的返回值
+class Staff(models.Model):
+ name = models.CharField(verbose_name='员工姓名',max_length=32)
+......
+ def __str__(self):
+ return self.name
+#在views.py中定义modelform类的时候 字段选择直接写定义的而不是在数据库表单中的字段
+class userModelForm(ModelForm):
+ class Meta:
+ model = UserInfo #注 Userinfo表单中的identify是引用了Staff表单中的id作为外键
+ fields = ['name','password','identify']
+
+from django.shortcuts import redirect
或者from django.http import HttpResponseRedirect
/?id=
信息传递,代码如下:<a href="/login/manage/delete/?id={{user.0}}">删除</a>
#这其中的{{id.0}}通过django的模板语法动态赋予目标的id信息def delete_user_info(request):
+ id = request.GET.get('id')
+ UserInfo.objects.filter(id = id).delete()
+ return HttpResponseRedirect('/login/manage/')
+
+{% extends 'nav.html'%}
必须处于子页面的第一个加载位置,意味着如果前面有哪怕{% load static %}
也会报错。#html中
+<a href="{% url 'edit' user.0 %}">操作...</a>
+#url中
+path('login/manage/<int:id>/edit/',views.edit_user_info,name = 'edit')
+#views中
+def edit_user_info(request,id):
+ default_userinfo = UserInfo.objects.filter(id = id).first()
+ ....
+
+可以看到<int:id>
是在/edit/
之前的,但是在html的url中这个顺序不影响。同时在views中对应的函数可以直接接收到url中正则匹配到的参数,而不用再通过解析get请求获取。
obj.get_联表名_display
获取到和后端get_联表名_display()
的效果,直接返回对照过后的结果而不是对应的关联id。{{number : add 1}}
就是python的+=1
操作,{{datetime | date :'Y-m-d}}
就是python中的strftime()
函数(要注意的是里面的格式不需要带%
)is_valid()
检验提交post请求中的form表单中的字段是否为空,具体的字段就是在modelform
中fields
列表定义的那些.class staffMdoelForm(ModelForm):
+ phonenumber = forms.CharField(label='电话号码',max_length=11,min_length=11)#进行的字段校验
+ #规定电话号码的长度为11位
+ #还可以通过validators参数进行正则筛选
+ class Meta:
+ model = Staff
+ fields = ['name','age','gender','phonenumber','create_time']
+ phonenumber = forms.CharField(label='电话号码',max_length=11,min_length=11)
+ #规定电话号码的长度为11位
+ #还可以通过validators参数进行正则筛选
+
+#setting.py
+# LANGUAGE_CODE = "en-us" 注释掉
+LANGUAGE_CODE = "zh-hans" #添加
+
+instance
参数#views.py中的一个方法
+default_userinfo = UserInfo.objects.filter(id = id).first() #根据id在数据库中获取对应的一条数据
+userform = userModelForm(instance=default_userinfo) # 通过instance参数设定一个实例化的表单 让他在前端渲染的时候自动在value上显示从数据库中拿到的默认值
+
+instance
参数和data
参数可以实现数据库数据的更新#在views.py中的某个函数
+default_userinfo = UserInfo.objects.filter(id = id).first()
+userform = userModelForm(data=request.POST,instance=default_userinfo)
+if userform.is_valid():
+ userform.save()
+ return redirect('/login/manage/')
+else:
+ return render(request,'edit_info.html',{
+ 'userform': userform,
+ })
+
+instacne
属性可以修改:userform = userModelForm(data=request.POST,instance=default_userinfo)
+# useform.instance.字段 = 值
+
+obj.objects.all().order_by('属性名')
"django.core.exceptions.ValidationError" error
migrations
__init__.py
文件以外所有的文件,然后重新执行数据库迁移命令:python manage.py makemigrations
, python manage.py migrate
'orderForm' object has no attribute 'get'
input
标签的样式的时候重写的父类方法出错,参数多写了一个self
#错误源码
+ def __init__(self,*arg,**kwargs):
+ super().__init__(self,*arg,**kwargs)
+ for name, field in self.fields.items():
+ # print(name,field) #这个是展示 self.fields.items()可以直接拿到定义在Meta中的fields字段
+ field.widget.attrs = {'class' : 'form-control'} #为生成的表单input赋予css属性
+
+super().__init__(self,*arg,**kwargs)
这一行中的self即可,正确写法: super().__init__(*arg,**kwargs)
super
不需要self
作为方法的第一个参数:super()
是一个类 reference -- 感觉好多...暂时搁置一下...😖在form表单添加novalidate
<form method="post" novalidate >
+
+default=None
而不是null
。blank=True
但是不开启null=True
,那么在插入的时候,如果传回来的数据为空,插入会报错不能为null字段...总之还是很迷惑的报错....但是解决方法还是最好设置两个都为True并且设置默认字段为None
。如果为null
,那么在数据传入到前端的时候会默认输出null或者none,而不是将那片区域置空,对于前端的用户输入逻辑不太友好。__all__
一次性获取全部,exclude = [ 排除的字段 ]
排除掉不要的字段。class orderForm(ModelForm):
+ class Meta():
+ model = Orderform
+ fields = "__all__" #注意要引号
+ # exclude = ['字段1','字段二', .... ]
+
+from django.core.validators import RegexValidator #需要导入正则类
+class exampleModelForms(forms.ModelForm):
+ moblie = forms.CharField(
+ label = '手机号码',
+ validactors = [RegexValidator(r'^1[3-9]\d{9}$','手机号格式错误')]
+ #校验失败返回后面的提示信息而不是默认的了~
+ )
+
+class exampleModelForms(forms.ModelForm):
+ def clean_字段名(self): #这个需要在一个modelform类中 这个函数名modelform会自动生成对应的方法
+ pass
+ name = self.cleaned_data.get('name')
+ if name=='admin':
+ raise ValidationError('admin是超级管理员,不能注册!')#这个错误会直接扔进该字段的错误类别中:name.errors
+ return self.cleaned_data.get('name')
+
+在modelform中字段中设置参数disabled= True
即可,这样在前端页面渲染出来的这一条字段是不可以更改的。
order_edit_form = orderEditForm().filter(id=你要查询的).exist()
返回True/Falseexclude
排除自己后判断:order_edit_form = orderEditForm().exclude(id='自己的id' ).filter(id=你要查询的).exist()
返回True/False总结 编辑和添加的不同
+filter()
还支持传入字典:order_edit_form = orderEditForm().filter(id=你要查询的,other=你要查询的)
+#等于
+query_dict = { 'id' : 要查询的, 'other' : 要查询的 }
+order_edit_form = orderEditForm().filter(**query_dict)
+ #需要注意传入字典的时候需要两个**
+
+PS. 通常我们再变量前加一个星号(*)表示这个变量是元组/列表,加两个星号表示这个参数是字典
+update
😳。 或者采用dict的模式,处理不变的字段和更新后的字段 组合成为一个更新后的字典然后通过modelform保存.. row_object = Orderform.objects.filter(id = id).first() #注意这里实例化的是一个form类 之前错误的原因是这里也写成了一个modelform类 这就导致下面instance的时候会报错 而且默认不更新的前端数据实际上是会传递回来的 作为表单的默认值
+ order_edit_form = orderEditForm(data=request.POST,instance=row_object)
+ #这里实例化的是一个modelforms类
+
+除了固定的等于(=
)查询外,还可以通过给字段/属性添加下划线的方式规定范围查询
list = BookInfo.objects.filter(id__gt=3)
代表id大于3的数据exclude
过滤list = BookInfo.objects.filter(btitle__endswith='部')
查询是否数据中btitle字段以‘部’这个字结尾的一行数据。list = BookInfo.objects.filter(bpub_date__year=1980)
查询1980年发表的图书list = BookInfo.objects.filter(bpub_date__gt=date(1990,1,1))
查询1980年1月1日后发表的图书。PS. 1. filter
可以连续使用 :list=BookInfo.objects.filter(bread__gt=20).filter(id__lt=3)
2. filter
可以传入字典
request.GET.get('page', '1')
获取get请求,如果get没有附带page参数,那么默认是1.reference
+在python工程中,当python在一个目录下检测到__init__.py
文件时,就会把它当成一个module。__init__.py
可以是空的,也可以有内容。
--未整理完
+getattr
方法可以获取一个模块其中的方法和属性,包括class名、直接定义的方法以及属性。
+代码示例:
#在example.py文件中
+class magical_method():
+ def __init__(self,num1,num2,num3):
+ self.num1 = num1
+ self.num2 = num2
+ self.num3 = num3
+ def __contains__(self,tar):
+ if tar == self.num1 or tar == self.num2 or tar == self.num3:
+ return False
+ else:
+ return True
+ def name(self):
+ print('qwe')
+
+def another_example():
+ print('asdasd')
+
+ppp = 2
+
+#在同目录下的test.py文件中
+import example
+attr = dir(example)
+print(attr)
+
+#输出
+['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'another_example', 'magical_method', 'ppp']
+
+需要注意的几个点:一只会输出直接定义在模块中的类名、方法、属性(变量),如果属性/方法/类是在其他类/方法之下,那么不会遍历出来;二可以通过isinstance(attr, type)
方法判断是否是类、callable(func_name)
判断是否是函数(是否可以被调用)、hasattr(obj, attr)
用来判断属性是否在指定类中;三如果没有__name__ == "__main__"
,那么在dir的时候会执行该模块。
HTTP基础响应
+# 1.首先需要socket进行协议、端口设置
+# 2. 然后因为性能原因 需要设置多进程响应处理客户端请求
+# 3. 设置处理客户端发送信息
+# 4. 返回响应的html以及http头信息
+import socket
+import multiprocessing
+class HttpServer:
+ def __init__(self,port): #进行socket初始设置 绑定端口开启监听
+ self.port = port
+ self.server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #实例socket对象
+ self.server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #进行socket选项设置
+ self.server_socket.bind(('0.0.0.0',port)) #绑定监听端口
+ self.server_socket.listen() #开启监听 在构造函数内开启监听代表每一个实例都是一个http服务器程序
+
+ def start(self): #接收服务端信息 进行处理
+ while True:
+ client_scoket, client_addr = self.server_socket.accept()
+ print(f'【新的客户端连接】客户端ip{client_addr[0]},访问端口{client_addr[1]}')
+ handle_socket = multiprocessing.Process(target=self.handle_response,args=(client_scoket,))#启动一个进程处理这个客户端请求
+ handle_socket.start()
+
+ def handle_response(self,client_socket): #处理客户端发送的请求信息
+ request_headers = client_socket.recv(1024)
+ print(f'【客户端清请求头信息】{request_headers}') #处理客户端请求头信息
+
+ #开始处理响应给客户端的信息
+ response_start_line = 'HTTP/1.1 200 OK\r\n' #本次相应成功
+ #手动写http响应头 之后会发送给客户端会被浏览器解析
+ response_header = 'Server:Cyanine Hrrp Server\r\nContent-Type:text/html\r\n'
+ #返回的html代码 也就是页面主题 最基本的开发就是需要在代码中硬嵌入html页面代码
+ response_body = "<html>"\
+ "<head>"\
+ "<meta charset=utf-8>"\
+ "<title>Cyanine Http Server Response</title>"\
+ "</head>"\
+ "<body>"\
+ "<h1>"\
+ "Cyanine's Http server response..."\
+ "</h1>"\
+ "</body>"\
+ "</html>"
+ #要注意在响应头和响应body之间添加换行 否则浏览器不能解析出响应内容
+ response = response_start_line + response_header + '\r\n'+response_body
+ client_socket.send(bytes(response,'UTF-8')) #服务器响应
+ client_socket.close() #https是无状态协议 因此相应完成一次之后就会关闭连接
+
+def main():
+ #80端口大部分服务的默认端口 因此如果使用的话 就可以直接属于域名/主机地址访问服务
+ #如果不是的话 需要指定端口 :**
+ http_server = HttpServer(9090)
+ http_server.start()
+if __name__ == '__main__':
+ main()
+
+一些代码更详细的讲解
+self.server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #实例socket对象
+ocket(family,type[,protocol])函数中,family 指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数为AF_INET;type 是要创建套接字的类型,socket.SOCK_STREAM表示流式socket,使用TCP协议的时候选择此参数,SOCK_DGRAM数据报式socket,使用UDP协议的时候选择此参数;protocol 指明所要接收的协议类型,通常为0或者不填。
+self.server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #进行socket选项设置 level: 设置选项所在的协议层编号,有四个可用配置项,其中socket.SOL_SOCKET表示基本嵌套字接口....剩下的好复杂,看不太懂,暂且放下。看到的[参考](https://blog.csdn.net/c_base_jin/article/details/94353956)
client_socket.send(bytes(response,'UTF-8')) #服务器响应
+使用bytes
的原因是socket只能发送字节数据流,因此需要将response转换为bytes类型,再发送。
http服务器建立相应目录
+之前的http服务只能进行一次固定的响应,并且html代码出现在python代码中不方便修改,没有进行前后端分离,较为落后。因此我们可以建立一个目录,目录下有不同的HTML页面,根据请求的不同,相应响应目录下对应的html文件。
+具体实现:
# 添加相应目录 原因在于代码维护以及更加高效的响应
+# 需要注意 读取html或者其他文件都是以bytes的格式
+# os的注意点 os.getcwd() os.sep os.path.normpath()
+# 正则的写法 暂不做详细了解
+
+import socket
+import os #os处理响应文件目录
+import re #正则匹配请求中的文件地址
+import multiprocessing
+HTML_ROOT_DIR = os.getcwd() + os.sep + "template"
+
+class HttpServer:
+ def __init__(self,port): #进行socket初始设置 绑定端口开启监听
+ self.port = port
+ self.server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #实例socket对象
+ self.server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #进行socket选项设置
+ self.server_socket.bind(('0.0.0.0',port)) #绑定监听端口
+ self.server_socket.listen() #开启监听 在构造函数内开启监听代表每一个实例都是一个http服务器程序
+
+ def start(self): #接收服务端信息 进行处理
+ while True:
+ client_scoket, client_addr = self.server_socket.accept()
+ print(f'【新的客户端连接】客户端ip{client_addr[0]},访问端口{client_addr[1]}')
+ handle_socket = multiprocessing.Process(target=self.handle_response,args=(client_scoket,))#启动一个进程处理这个客户端请求
+ handle_socket.start()
+
+ #读取对应文件数据
+ def read_file(self,file_name):
+ file_path = os.path.normpath(HTML_ROOT_DIR + file_name)
+ print('【请求文件路径】'+ file_path)
+ f = open(file_path,'rb') #二进制读取
+ file_data = f.read()
+ f.close()
+ print('【请求文件路径】该文件请求结束!')
+ return file_data
+
+ #获取二进制文件
+ def get_binary_data(self,file_name):
+ response_body = self.read_file(file_name)
+ return response_body
+
+ #读取html文件 返回相应的信息
+ def get_html_file(self,file_name):
+ response_start_line = 'HTTP/1.1 200 OK\r\n' #本次相应成功
+ #手动写http响应头 之后会发送给客户端会被浏览器解析
+ response_header = 'Server:Cyanine Hrrp Server\r\nContent-Type:text/html\r\n'
+ response_body = self.read_file(file_name)
+ return response_start_line + response_header + '\r\n' + response_body.decode('utf-8')
+ def handle_response(self,client_socket): #处理客户端发送的请求信息
+ request_headers = client_socket.recv(1024)
+ print(f'【客户端清请求头信息】{request_headers}') #处理客户端请求头信息
+ file_name = re.match(r"\w+ +(/[^ ]*)", request_headers.decode('utf-8').split('\r\n')[0]).group(1)
+ if file_name == "/": #如果请求的是根目录 那么实际访问的是index.html页面
+ file_name = '/index.html'
+ if file_name.endswith(".html") or file_name.endswith(".htm"):
+ client_socket.send(bytes(self.get_html_file(file_name),'utf-8'))
+ else:
+ response_start_line = 'HTTP/1.1 200 OK\r\n' #本次响应成功
+ response_header ='Server:Cyanine Hrrp Server\r\nContent-Type:image/x-icon\r\n'
+ client_socket.send((response_start_line + response_header + '\r\n').encode('utf-8'))
+ client_socket.send(self.get_binary_data(file_name))
+ client_socket.close() #https是无状态协议 因此相应完成一次之后就会关闭连接
+
+def main():
+ #80端口大部分服务的默认端口 因此如果使用的话 就可以直接属于域名/主机地址访问服务
+ #如果不是的话 需要指定端口 :**
+ http_server = HttpServer(70)
+ http_server.start()
+
+if __name__ == '__main__':
+ main()
+
+代码细节解析
+一这是一个简单的http服务器相应目录,只是简单处理index.html、hello.html以及favicon.ico文件;二为了结构清晰,尽管获取get_binary_data()
方法只是给read_file
换了个名字,但是能够将功能更加清晰的分割开来;三不管什么响应,都要记得添加对应的http响应头;四响应头和响应内容和分开发送;五要注意手写响应头的时候各个部分之间的\r\n
;
+同时这里还遇到了一个难以理解的问题,如果是以80端口启动服务,那么就算favicon的请求没有响应头,浏览器也会正确解析出来并显示图标,但是如果换成其他的端口,那么就不能正常相应,必须要添加响应头。猜测可能是因为80端口是默认,而请求favicon也是默认的一个请求,因此在这个活动中,浏览器会自动解析把,但是非默认端口就不可以。和一位相同问题的大佬的讨论以及他的代码。这个问题怎么也找不到答案,因此暂且搁置吧😭。总是还是要记得在每一个响应前添加响应头。不过当然针对简单的,复杂的话有很多web框架会帮我们滴~~~
动态请求处理
+web有两个处理阶段:静态处理阶段、动态处理阶段。
+之前的响应目录实际上是静态处理,而动态web是可以根据动态的判断决定最终返回的数据内容。
+python动态处理实现:(只是简单的原理了解,不涉及复杂的动态相应框架)
# 处理动态请求
+import socket
+import os #os处理响应文件目录
+import re #正则匹配请求中的文件地址
+import multiprocessing
+HTML_ROOT_DIR = os.getcwd() + os.sep + "template"
+import sys
+sys.path.append('packages')
+
+class HttpServer:
+ def __init__(self,port): #进行socket初始设置 绑定端口开启监听
+ self.port = port
+ self.server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #实例socket对象
+ self.server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #进行socket选项设置
+ self.server_socket.bind(('0.0.0.0',port)) #绑定监听端口
+ self.server_socket.listen() #开启监听 在构造函数内开启监听代表每一个实例都是一个http服务器程序
+
+ def start(self): #接收服务端信息 进行处理
+ while True:
+ client_scoket, client_addr = self.server_socket.accept()
+ print(f'【新的客户端连接】客户端ip{client_addr[0]},访问端口{client_addr[1]}')
+ handle_socket = multiprocessing.Process(target=self.handle_response,args=(client_scoket,))#启动一个进程处理这个客户端请求
+ handle_socket.start()
+
+ def handle_response(self,client_socket): #处理客户端发送的请求信息
+ request_headers = client_socket.recv(1024)
+ print(f'【客户端清请求头信息】{request_headers}') #处理客户端请求头信息
+ file_name = re.match(r"\w+ +(/[^ ]*)", request_headers.decode('utf-8').split('\r\n')[0]).group(1)
+ if file_name.startswith('/packages'): #访问动态页面
+ #获取动态参数
+ request_name = file_name[file_name.index('/',1)+1:]#访问路径
+ # print("访问路径:"+request_name)
+ param_value = '' #请求参数
+ if request_name.__contains__('?') :#?是url中的参数分隔符号
+ request_value = request_name[request_name.index('?') + 1 :]
+ param_value = request_value.split('=')[1]
+ request_name = request_name[0:request_name.index('?')]
+ # print(request_name)
+ model_name = request_name.split('/')[0]
+ method_name = request_name.split('/')[1]
+ model = __import__(model_name)
+ method = getattr(model,method_name)
+ response_body = method(param_value)
+ print('【响应数据是】:'+response_body)
+
+ response_start_line = 'HTTP/1.1 200 OK\r\n'
+ #手动写http响应头 之后会发送给客户端会被浏览器解析
+ response_header = 'Server:Cyanine Hrrp Server\r\nContent-Type:text/html\r\n'
+ response = response_start_line+response_header+'\r\n'+response_body
+ print(response)
+ client_socket.send(bytes(response,'UTF-8'))
+ client_socket.close()
+
+
+def main():
+ #80端口大部分服务的默认端口 因此如果使用的话 就可以直接属于域名/主机地址访问服务
+ #如果不是的话 需要指定端口 :**
+ http_server = HttpServer(80)
+ http_server.start()
+
+if __name__ == '__main__':
+ #http://localhost/packages/echo/service?param=canshu
+ main()
+
+同时还需要一个在同目录下的packages文件夹,文件路径如下
+├─packages
+│ │ echo.py
+│ │ __init__.py
+│ │
+│ └─__pycache__
+│ echo.cpython-310.pyc
+│ echo.cpython-311.pyc
+│
+├─template
+│ favicon.ico
+│ hello.html
+│ index.html
+│
+├─网络编程
+│ 01-server.py
+│ 02-client.py
+│ 03-echo-server.py
+│ 04-echo-client.py
+│ 05-UDP-server.py
+│ 06-UDP-client.py
+│ 07-broadcast-client.py
+│ 08-broadcast-server.py
+│ 09-http-server.py
+│ 10-http-lib-server.py
+│ 11-dynamic-request.py #这个是本文件
+
+其中的/packages/echo.py文件内容如下:
+def service(text):
+ if text:
+ response = f'<head><title>Cyanine\'s Http Server</title><meta charset="utf-8"></head><body><h1>参数信息:{text}</h1></body>'
+ return response
+ else:
+ return '<h1>没有参数信息</h1>'
+
+需要注意的:
+一在packages
文件夹下一定要有__init.py__
文件,这样才能在__import__
的时候正确识别到模块,同时也需要提前设定默认的模块路径(sys.path.append(path/to/module)
);二响应的时候除了需要确定响应头,在响应的html代码中需要规定编码方式,否则会出现乱码(<meta charset=utf-8>
);三动态处理我刚听起来高大上,但实际上操作一遍,感受就是对url的解析,加上一些程序处理参数,就是动态处理;四动态处理url需要用到很多对字符串的操作。
+PS. 另外一个方便的小tips,在写项目结构的时候,命令行里使用tree
会生成目录结构,tree > txtname.txt
会将目录输出到这个txt文件中,参数/f
会显示所有的文件层级,不加参数只会显示到所有的目录层级。
urllib3模块
+用这个模块可以实现浏览器的模拟访问,是urllib的升级版,两者功能类似,只有细微差别。
Twisted模块 (类似java中nio)
+是python中专门实现异步处理的io概念,主要是提升服务端的数据处理能力。理解twisted的设计思想,那么需要对比传统的服务器程序开发。早期没有多核CPU概念,单线程处理的效率低下,多线程并发编程有可能产生死锁问题(不同进程以及线程之间的等待和唤醒机制)(因为都是一个一个进程去执行)。所以后来,如果不使用并发编程,就不会产生种种问题(资源切换、系统调度、同步与等待等),
++阻塞设计
+
+服务端与客户端的recv
+会浪费大量服务器资源 -> 这就是阻塞IO
多线程是不能解决阻塞IO的,因此最好的方法是非阻塞IO(分为同步非阻塞IO和异步非阻塞IO),因此实现下来就是在一个进程中不断地进行循环处理
+-> twisted是一个事件驱动型的网络引擎,最大的特点就是提供有一个事件循环处理,当外部事件发生时,使用回调机制来触发相应的操作处理,多个任务在一个线程中执行的时候,这种方式可以使程序尽可能地减少对其它线程的以来,也使得程序开发人员不再关注线程安全问题。
+-> twisted中的所有处理事件全部交给reactor进行统一管理。
+-> reactor 进行所有输出输出有关的事件注册。在整个程序的运行中,reactor循环会以单线程的形式持续运行,当需要执行回调处理的时候会停止循环,当回调操作执行完毕之后将继续采用循环的形式进行其他任务处理。
使用twisted开发TCP程序
+会使服务端的资源利用带来极大便利。
import twisted
+import twisted.internet.protocol
+import twisted.internet.reactor
+
+SERVER_PORT = 8080
+
+class Server(twisted.internet.protocol.Protocol): #继承父类
+ def connectionMade(self): #复写服务端连接方法
+ print(f'客户端{self.transport.getPeer().host}连接成功...')
+ return super().connectionMade()
+ def dataReceived(self, data: bytes): #复写服务端数据接收方法
+ print('【服务端收到数据】' + data.decode('utf-8')) #处理操作
+ self.transport.write(('【ECHO】' + data.decode('utf-8')).encode('UTF-8')) #进行回应 类比socket的send
+ return super().dataReceived(data)
+
+#注册reactor
+#reactor根据工厂获得相应事件回调处理类
+class DefaultServerFactory(twisted.internet.protocol.Factory):
+ protocol = Server
+
+def main():
+ #服务监听
+ twisted.internet.reactor.listenTCP(SERVER_PORT,DefaultServerFactory())
+ print('服务器启动完毕,等待客户端连接...')
+ twisted.internet.reactor.run()
+
+if __name__ == '__main__':
+ main()
+
+import twisted
+import twisted.internet.protocol
+import twisted.internet.reactor
+
+SERVER_PORT = 8080
+SERVER_HOST = 'localhost'
+
+class Client(twisted.internet.protocol.Protocol):
+ def connectionMade(self):
+ print('服务器连接成功...')
+ self.send() #建立连接之后就发送数据
+ return super().connectionMade()
+
+ def send(self): #自定义发送的方式
+ input_data = input('请输入发送的数据:')
+ if input_data:
+ self.transport.write(input_data.encode('utf-8'))
+ else:
+ self.transport.loseConnection() #如果没有数据发送就关闭连接
+ def dataReceived(self, data: bytes): #接收服务端的数据
+ print(data.decode('utf-8'))
+ self.send() #进行下一次数据发送
+ return super().dataReceived(data)
+
+class DefaultClientfactory(twisted.internet.protocol.ClientFactory):
+ protocol = Client
+ #如果连接断开 就停止reactor的循环
+ clientConnectLost = clientCOnnectionFailed = lambda self, connector,reason : twisted.internet.reactor.stop()
+
+def main():
+ twisted.internet.reactor.connectTCP(SERVER_HOST,SERVER_PORT,DefaultClientfactory()) #服务监听
+ twisted.internet.reactor.run() #启动reactor循环
+
+if __name__ == '__main__':
+ main()
+
+整个框架还是处于一个模糊状态,但是对twisted的事件轮询机制还是有了一点清楚的认知。
+我的理解:
++将数据的处理和数据的接收发送、服务器的连接这两个部分剥离开。在reactor中如果接收到一个信息,那么就会调用到twisted循环中的某个处理程序,然后处理完成之后将数据返回给reactor进行发送,然后twisted事件就会继续循环。相当于将一个socket进程中的accept()阻塞和实际的处理剥离开,让处理程序不受到阻塞程序的影响,因此可以在一个进程中高效的处理多个客户端的连接,节省了服务器的资源。
+
+(有点像两个圈,reactor一个圈,twisted一个圈,当遇到数据需要处理的时候两个圈就会连一条线,处理完之后就把线擦去)
暂时先到这里,后面还有twisted的UDP客户端开发以及deferred的概念。 -- 2023-06-26
+deferred
+]]>B/S和C/S架构、 后者使用TCP协议,前者使用HTTP协议(HTTP是对TCP的扩充)。
+所有的网络通讯都必须经过OSI(七层架构) -- 七层架构详解 => TCP/IP 四层架构 、五层架构
+但是为了方便程序开发,socket便出现了,他包装了七层架构中对数据的处理 ,让开发只专注于上层,而不用去为了数据传输和接收为每一个架构的数据处理费心。socket是对各种网络协议的抽象实现。不同语言为了方便开发,都会对网络协议进行包装,因此socket是一个通用的概念。
socket是不同进程之间的通讯,这意味着不仅能进行客户机和服务器之间,同一台主机之间的不同进程也可以通过socket进行交流。
+socket主要是对TCP/IP协议和UDP协议的包装:
+TCP/IP : 有状态、三次握手、四次挥手、性能较低资源占用大;
+UDP : 无状态、没有握手与挥手、不保存单个结点连接信息、适合广播操作
+总 -> 不管是TCP还是UDP,都是对传输层的保证,数据都会通过七层架构进行处理,最后经过物理层发出。socket的存在包装了传输层,因此现在程序员开发的时候就只需要关注核心带吧,不需要在注意具体的操作协议。
TCP通讯,C/S架构
+import socket
+SERVER_HOST = 'localhost' #服务器端
+SERVER_PORT = 7000 #本程序端口
+
+def main():
+ with socket.socket() as server_socket:
+ server_socket.bind((SERVER_HOST,SERVER_PORT)) #绑定本机端口
+ server_socket.listen() #开启监听
+ print(f'服务器启动完毕,在{SERVER_PORT}端口监听,等待客户端链接...')
+ #进入阻塞 直到客户端进行链接后解除
+ client_conn,address = server_socket.accept() #进入阻塞状态、
+ with client_conn:
+ print(f'客户端已连接到服务端,主机地址是{address[0]},端口是{address[1]}')
+ client_conn.send("请求已经收到,测试成功!".encode('UTF-8'))
+
+if __name__ == '__main__':
+ main()
+
+telnet localhost 7000
import socket
+SERVER_HOST = '127.0.0.1' #服务器主机名称/ip地址
+SERVER_PORT = 7000 #服务器链接端口
+
+def main():
+ with socket.socket() as client_socket: #建立客户端socket
+ client_socket.connect((SERVER_HOST,SERVER_PORT))
+ print(f'服务器返回数据 -- {client_socket.recv(40).decode("UTF-8")}')
+
+if __name__ == '__main__':
+ main()
+
+echo程序模型
+import socket
+SERVER_HOST = 'localhost'
+SERVER_PORT = 7070
+coding = ['utf-8','gbk']
+def echo_server():
+ with socket.socket() as server_socket:
+ server_socket.bind((SERVER_HOST,SERVER_PORT)) #bind()函数传入元组
+ server_socket.listen() #监听端口
+ print('服务端已启动,等待客户端链接...')
+ socket_conn,addr = server_socket.accept() #等待接收 进入阻塞
+ with socket_conn: #在接收到的信息前添加【Echo】然后返回
+ #连接上了之后才while循环 不断进行通讯
+ #第一次连接成功发送一次提示
+ socket_conn.send('【Echo】连接成功,输入字符发送请求,输入byebye结束链接。'.encode('utf-8'))
+ while True:
+ response = socket_conn.recv(100).decode(coding[0])
+ if response.upper() == "BYEBYE":
+ print('客户端发送终止指令,连接结束...')
+ socket_conn.send('byebye'.encode(coding[0]))
+ break
+ else:
+ socket_conn.send(f'【Echo】{response}'.encode(coding[0]))
+if __name__ == '__main__':
+ echo_server()
+
+++这里需要注意的有两点(我犯的错😣),一是
+with as
的时候需要注意命名不要起冲突;二是再注意while的位置,连接成功后再while才能实现不断地通讯,而不是在连接之前就不断地while。
+这里省略telnet测试。
import socket
+SERVER_HOST = 'localhost'
+SERVER_PORT = 7070
+def echo_client():
+ with socket.socket() as client:
+ client.connect((SERVER_HOST,SERVER_PORT))
+ #连接成功之后进行通讯
+ while True:
+ response = client.recv(100).decode('utf-8')
+ if response.upper() == 'BYEBYE':
+ print('链接结束...')
+ break
+ else:
+ print(response)
+ text = input()
+ client.send(text.encode('utf-8'))
+if __name__ == '__main__':
+ echo_client()
+
+++需要注意,一个服务只能绑定一个端,如果程序端口被占用,那么就无法正常启动。
+
此时的程序已经实现了一个socket通讯,并且是基于TCP协议的,但是有一个重大问题:采用的是单进程的处理模型完成的通讯。这就意味着,如果当前服务端已经有一个客户端进行链接,那么另一个客户端链接的话就会因为主进程被占用而导致无法操作。因此提高性能就需要进行多进程优化。同时,当前服务端程序还会在客户端断开连接之后停止运行,这还意味着后一个服务端并不会像队列一样依次接收客户端,而是在第一个客户端完成连接之后关闭服务,导致后面的客户端失去连接。
+3. 修改服务端程序,采用多进程 (多并发编程)
import socket,multiprocessing #引入多进程处理
+SERVER_HOST = 'localhost'
+SERVER_PORT = 7070
+coding = ['utf-8','gbk']
+def echo_handle(socket_conn,addr):
+ with socket_conn: #在接收到的信息前添加【Echo】然后返回
+ #连接上了之后才while循环 不断进行通讯
+ #第一次连接成功发送一次提示
+ socket_conn.send('【Echo】连接成功,输入字符发送请求,输入byebye结束链接。'.encode('utf-8'))
+ while True:
+ response = socket_conn.recv(100).decode(coding[0])
+ if response.upper() == "BYEBYE":
+ print(f'客户端-{addr[1]}发送终止指令,连接结束...')
+ socket_conn.send('byebye'.encode(coding[0]))
+ break
+ else:
+ socket_conn.send(f'【Echo】{response}'.encode(coding[0]))
+def echo_server():
+ with socket.socket() as server_socket:
+ server_socket.bind((SERVER_HOST,SERVER_PORT)) #bind()函数传入元组
+ server_socket.listen() #监听端口
+ print('服务端已启动,等待客户端链接...')
+ while True:
+ socket_conn,addr = server_socket.accept() #等待接收 进入阻塞 因此在没有接收到客户端请求的时候while会停止在这里 接收到一个循环一次
+ process = multiprocessing.Process(target=echo_handle,args=(socket_conn,addr),name=f'客户端进程-{addr[1]}')
+ process.start() #启动进程
+
+if __name__ == '__main__':
+ echo_server()
+
+PS.高并发 -> 处理好服务端的处理效率。
+这里需要注意的是:一需要导入multiprocessing
模块处理多进程;二需要将处理函数单独剥离出去,然后根据每一个请求创建一个进程响应;三multiprocessing.Process
实例化参数中target
表示目标函数,args
表示传入进函数的参数,name
表示进程的名字。四还需要注意的是,在进入accept
的时候,主进程会进入阻塞,这意味着外边的while循环会暂停在accept()
这里,直到接受到后才进行下一个循环,也就是进入下一个阻塞等待,这也解决了修改之前服务端会在一个客户端终止连接之后结束服务,他会一直运行。
#=>
+UDP通讯
+相对于TCP是一种不安全连接,但是想能相对较好。在python使用中差别不大,只需要在引用socket中指定对应参数,同时也不需要监听、接收修改为recvfrom()、发送修改为sendto()。
import socket
+SERVER_HOST = 'localhost'
+SERVER_PORT = 7070
+
+def main():
+ #socket.AF_INET表示使用ipv4网络协议进行服务端创建
+ #socket.SOCK_DGRAM 创建一个数据报(UDP) 协议的网络端
+ with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as udp_server:
+ udp_server.bind((SERVER_HOST,SERVER_PORT)) #bind()函数传入元组
+ print(f'服务器启动完成,监听端口{SERVER_PORT}')
+ while True: #服务端响应就是在收到的信息前面添加【Echo】
+ data,addr = udp_server.recvfrom(100) #不断接收客户端信息
+ print(f'【服务器】客户端{addr[0]}:{addr[1]}成功连接!')
+ echo_data = f'【Echo】{data.decode("utf-8")}'.encode('utf-8')
+ udp_server.sendto(echo_data,addr) #将内容相应给接收到信息的对应的客户端
+if __name__ =='__main__':
+ main()
+
+import socket
+SERVER_HOST = 'localhost'
+SERVER_PORT = 7070
+
+#UDP客户端与TCP客户端的不同就在于 一是协议不同 二是不需要进行连接 只需要不断地发送接收即可
+def echo_udp_client():
+ with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as client:
+ while True:
+ send_data = input('请输入想发送的数据...\n')
+ if send_data:
+ client.sendto(send_data.encode('utf-8'),(SERVER_HOST,SERVER_PORT))
+ data,addr = client.recvfrom(100) #recvfrom会接受一个元组 包含两个元素data和主机地址 而主机地址又是一个元组 包含主机地址和端口两个元素
+ print(data.decode('utf-8'))
+ else: #如果输入内容为空 那么程序结束
+ break
+
+if __name__ == '__main__':
+ echo_udp_client()
+
+telnet
命令进行连接测试。UDP广播
+import socket
+BROASTCAT_CLIENT_ADDR = ('0.0.0.0',21567) #客户端绑定地址
+
+def mian():
+ with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as client: #这里不变
+ #设置广播模式
+ client.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
+ client.bind(BROASTCAT_CLIENT_ADDR) #绑定广播客户端的地址
+ while True:
+ message,addr = client.recvfrom(100)
+ print(f'接收到的广播客户端数据:【{message.decode("utf-8")}】,广播来源为{addr[0]}:{addr[1]}')
+
+if __name__ == '__main__':
+ mian()
+
+import socket
+BROASTCAT_SERVER_ADDR = ('<broadcast>',21567) #广播地址
+
+def main():
+ with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as server_socket: #这里不变 设置UDP编码
+ #设置广播模式
+ server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
+ server_socket.sendto(f'这是一条来自服务端的广播...'.encode('utf-8'),BROASTCAT_SERVER_ADDR)
+
+if __name__ == '__main__':
+ main()
+
+++setsockopt(self,level:int,optname:int.value:Union[int,bytes])
+
+level: 设置选项所在的协议层编号,有四个可用配置项
+socket.SOL_SOCKET 基本嵌套字接口
+socket.IPPROTO_IP ipv4嵌套字接口
+socket.IPPROTO_IPV6 ipv6嵌套字接口
+socket.IPPRPTP_TCP TCP嵌套字接口
+optname : 设置选项名称,例如,如果要进行广播则可以使用“socket.BROADCA“;
+value: 设置选项的具体内容
为什么要设置广播的端口呢?
+++"广播有一个广播组,即只有一个广播组内的节点才能收到发往这个广播组的信息。什么决定了一个广播组呢,就是端口号,局域网内一个节点,如果设置了广播属性并监听了端口号A后,那么他就加入了A组广播,这个局域网内所有发往广播端口A的信息他都收的到。在广播的实现中,如果一个节点想接受A组广播信息,那么就要先将他绑定给地址和端口A,然后设置这个socket的属性为广播属性。"
+
+reference
可以理解为,服务端向同一个局域网内的所有设备的这个端口号发送消息,然后只有接收端设置为广播模式并绑定这个端口之后,才能接收到客户端向着个端口发送的消息;而所有的局域网内的这个端口的设备就组成了一个广播组。具体设置来说,服务端需要设置这个广播组的端口和广播模式:
+BROASTCAT_SERVER_ADDR = ('<broadcast>',21567) #广播地址
+......
+server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) #设置广播模式
+server_socket.sendto(f'这是一条来自服务端的广播...'.encode('utf-8'),BROASTCAT_SERVER_ADDR) #发送到广播组的这个端口
+
+而客户端需要设置广播来源的地址和广播发送的端口即可:
+BROASTCAT_CLIENT_ADDR = ('0.0.0.0',21567) #客户端绑定地址
+......
+client.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) #
+client.bind(BROASTCAT_CLIENT_ADDR) #绑定广播客户端的地址
+
+广播接收端不一定能接收到广播,但是只要打开接收端就可以接收到广播;客户端执行之后就会一直等待服务端发送的消息。
+HTTP协议/HTTP服务器
+传统socket需要提供两个程序端(服务端、客户端),因此每一次服务端升级都需要进行客户端强制更新。因此在传统网络编程的基础上就实现了HTTP协议(HTTP是对TCP协议的一种更高级的包装),但是并没有完全脱离传统的TCP协议,是在TCP协议前面添加的头部信息。
+HTTP - 超文本传输协议 是对B/S架构的标准协议
+B/S相对于C/S的好处就是不用在开发一套客户端
+在整个http开发流程中,最重要的设计就是html代码的开发。但对于web服务器开发而言,最重要的是清楚http服务器的开发。
+在整个http请求和响应的处理过程中,关键问题就在于请求和响应的头部信息有哪些、响应状态码。
+HTTP协议中,设计了多种请求模式,主要有get、post。
+http头部信息:
+http状态码:1**、2**、3**、4**、
+常见响应头信息
spider中,setting是一个继承scrapy.Spider的一个实例属性,可以通过self.setting访问对应在setting.py中的环境变量。
+#在setting.py文件中
+MONGO_URI=' 对应的URI'
+
+#在spider中调用
+class ExampleSpider(scrapy.Spider):
+ ...#省略
+ def parse(self, response):
+ mongodb = self.settings['MONGO_URI']
+ logging.log(logging.INFO,mongodb)
+ #会在日志中输出
+ #2023-06-17 16:44:04 [root] INFO: 对应的URI
+
+scrapy同样支持为每一个Spider设置不同的环境变量,以避免所有环境变量都挤在一个setting.py文件中造成臃肿。在每一个spider实例中都有一个继承来的custom_settings属性可以修改,它是一个字典类型,所有单独的环境变量都可以在这里声明,包括cookies, header, 以及其他可以写在setting.py文件中的变量。声明方法跟Spider的name属性相同。调用则通过self.custom_settings使用在方法中。
+使用类方法from_crawler()可以访问到
+官方文档参考
++“Settings can be accessed through the scrapy.crawler.Crawler.settings attribute of the Crawler that is passed to from_crawler method in extensions, middlewares and item pipelines”
+
示例代码:
+class MyExtension:
+ def __init__(self, log_is_enabled=False):
+ if log_is_enabled:
+ print("log is enabled!")
+
+ @classmethod
+ def from_crawler(cls, crawler):
+ settings = crawler.settings
+ return cls(settings.getbool("LOG_ENABLED"))
+
+在from_crawler()
方法中的setting对象是一个类字典对象,因此可以用字典的方式访问环境变量(例如:e.g., settings['LOG_ENABLED']
),但是为了避免可能的类型错误(比如调用需要int类型,但是环境变量中是str类型,直接使用key访问就可能返回int类型造成错误),更推荐使用setting对象的api。
++官方文档中setting对象的api
+
+包括get()
,getbool()
,getdict()
,getdictorlist()
,getfloat()
,getint()
,getlist()
等,用途也能从方法名显而易见出,具体请参考官方链接。
https://www.cnblogs.com/twelfthing/articles/4709287.html
+https://www.cnblogs.com/fengf233/p/11298623.html#2.field()%E7%B1%BB
https://www.ziji.work/python/scrapy-many-spider-pipeline.html#SCRAPYSPIDER-4
+虽然现在基本看不懂(×
+http://kaito-kidd.com/2016/11/09/scrapy-code-analyze-entrance/
提点链接
+最后这篇博客的最后一句话简直一下子给我点透了,虽然之前专门去查了@classmethod
、factory method这些东西,但为什么这么做还是很混乱。
++大概就是检测spider类有没有from_crawler,有的话就return一个cls()的实例化对象,产生实例化对象后会自动调__init__方法。
+
结合官方文档的Crawler API、pipeline中from_crawler()方法、Spider中from_crawler()方法,以及大佬对scrapy源码的解析才有一种突然醒悟的感觉。
+++PS. 这里我一直犯了一个概念上的错误,我一直以为示例代码中
+from_crawler(cls, cralwer)
的crawler是类的属性参数(我误认为是有点像构造方法中的参数,被我搞混了😣),是一个定义在当前类里面的一个继承过来的属性,因此花了很多时间去查这个crawler
在scrapy中的作用。但是实际上它只是一个函数的形参,它本身不带有任何意义(就像是定义函数的时候括号里的a,b,c,d等参数一样)。然后又因为crawler在scrapy又是一个关键对象,所以查了很久都没有结果。
+直到我看到了上面那篇提点链接的博客,这才想通了。
从我的理解来说 :
+第一步,在运行这个爬虫之前,scrapy会为运行做一些准备,其中就包括判断pipeline, spider, 还有其他一些组件中的from_crawler()
方法是否被重写,这个判断就需要在类没有实例化之前调用,所以from_crawler()
被定义为类方法。
+第二步,之后根据具体的类,在从Crawler这个对象中实例化一个对应的from_crawler方法。
+上面这句话有两点需要着重理解。
from_crawler()
是一个“factory method”,scrapy会根据不同的对象为类赋予不同的from_crawler()
方法。在我的理解中,就是有一个类,专门负责为其他不同的类赋予不同的from_crawler()
方法,也就是类方法的类。创建from_crawler()
的含义就是为这个方法传入一个scrapy.crawler.Crawler
对象,而这个对象又是由scrapy通过一个Spider子类
和scrapy.settings.Settings
对象实例化而来。++官方文档
+
+classscrapy.crawler.Crawler(spidercls, settings)
+The Crawler object must be instantiated with a scrapy.Spider subclass and a scrapy.settings.Settings object.
因此,在Crawler对象中就有能过获取setting的API,这个API和2.1.2中的setting对象的一模一样(因为本来就是一个东西👏,都是setting对象,这里就有串起来的感觉了)
+知乎参考但是没有讲原理
+添加twisted定时器 以及 输出调用日志
+https://www.jianshu.com/p/5a5cdd7f2bfb
https://blog.csdn.net/python36/article/details/82683528
+]]>我在爬取到微博热搜的json数据之后,发无论如何都会报错数据库字段方面的问题,具体报错如下:
+Traceback (most recent call last):
+ File "D:\anaconda\envs\DjangoEnv\Lib\site-packages\twisted\internet\defer.py", line 892, in _runCallbacks
+ current.result = callback( # type: ignore[misc]
+ File "D:\anaconda\envs\DjangoEnv\Lib\site-packages\scrapy\utils\defer.py", line 307, in f
+ return deferred_from_coro(coro_f(*coro_args, **coro_kwargs))
+ File "D:\Aproject\django-project\project5-scrapy-tutorial\project_2\tutorial\tutorial\pipelines.py", line 46, in process_item
+ self.db[collection_name].insert_one(dict(hot))
+ValueError: dictionary update sequence element #0 has length 1; 2 is required
+
+具体的原因一直不清楚,只能模糊的猜测实在向数据库导入数据的时候,因为格式的原因出错了,但具体是什么原因,我把代码看了一遍又一遍怎么也找不出来哪里错了。猜测可能是爬取的json数据解析错误,所以将json数据下载到文件中,然后在jupyter中不断调试不断找,看看我是把那个括号给漏了😡。
+最后实在是找不到了,想着是不是可以通过调试一步一步的判断哪里出错了,但是我没有在scrapy的文档中找到关于pipeline的调试方法(只有关于spider的),最后在知乎上一篇文章找到了方法,参考链接,但是作者的方法在我(windows11+vsc)运行之后会报错,之后参考了评论区的方法,在和scrapy.cfg
同一层(项目根目录中)中建立run.py
文件,输入以下代码,再在项目中设置断点,然后debug文件run.py
,就可以实现调试的功能。
++具体代码:
+
import os
+from scrapy.cmdline import execute
+os.chdir(os.path.dirname(os.path.realpath(__file__)))
+try:
+ execute(
+ [
+ 'scrapy',
+ 'crawl',
+ 'weibo', #这里换成对应的spider名字
+ '-o',
+ 'out.json',
+ ]
+ )
+except SystemExit:
+ pass
+
+在调试之后,我发现pipeline.py
文件中对应class类中的process_item()
方法中的item
变量并不是我想象中的是一个由字典元素组成的列表,而是一个字典,并且key是在item.py
文件中设定的,value是在parse()
方法中赋值的、我想要的字典元素列表。终于确定的原因,因此修改也很简单。
将原本的process_item()
方法修改即可
def process_item(self,item,spider):
+ now = datetime.datetime.now() #以当前事件作为collection的名字
+ collection_name = datetime.datetime.strftime(now,'%Y-%m-%d:%H:%M:%S')
+
+ #原来错误的:
+ #for hot in item: ->修改为下面的部分
+ for hot in item['realtime']:
+ self.db[collection_name].insert_one(dict(hot))
+ return item
+
+官方文档
+首先,开发者为了方便在python中处理爬到的web数据,因此将item类设计为类字典结构,并且完全copy了python中dict的api(cv大法好😋)
++Item objects replicate the standard dict API, including its __init__ method.
+
因此实际上,item类就是一个在scrapy中的spider, pipeline之间进行数据交换的字典类。他的流程就是:
+第一步,在item.py
文件中设定item的key值;
+第二部,在spider中的parse
方法中被解析好的网页数据填充value;
+第三步,通过pipeline保存成为文件/保存到数据库等
详细的例子如下:
+4.1 在items.py中规定一个类,如下:
class Example(scrapy.Item): #必须继承scrapy.Item 才能使用对应的api
+ realtime = scrapy.Field()
+
+++这里的Field()的作用
+
+PS.官方文档
+" The Field class is just an alias to the built-in dict class and doesn’t provide any extra functionality or attributes." 表明Field类之际上只是python内置字典的别名,没有其他任何别的作用(Field()源码解析),当然复杂的而是item如何(说实话没看懂😵item源码解析)。
+当然也可以重写Overriding the serialize_field() method
方法,去规定具体的数据类型(具体参考官方文档)
+整个item类的使用非常类似与Django的Form类,不过Field()规定的字段类型是远远简单与Django的。
4.2 然后再spider中我爬取到的是一个json数据,例如
+{
+"ok": 1,
+ "data": {
+ "realtime": [
+ {
+ "star_name": {},
+ "word_scheme": "#新闻标题#",
+ "emoticon": "",
+ }
+ ]}
+}
+
+4.3 然后我在对item.py文件中的类进行填充,如下:
+class ExampleSpider(scrapy.Spider):
+ ...#略
+ def parse(self, response):
+ jsondata = json.loads(response.text) #使用scrapy中response属性text将爬取到的网页解析为str,然后使用json.loads方法转化为字典格式
+ realtime = jsondata['data']['realtime'] #提取热搜数据列表
+ item = Example() #实例化item
+ item['realtime'] = realtime
+ return item
+
+这里需要重点注意 item['realtime'] = realtime
,虽然在之前已经将realtime列表提取出来,但是在填充的时候,传入pipeline中进行储存的实际上是一个item字典,字典key是在item.py文件中定义的属性,字典value是在parse方法中填充的对象。所以实际上传入到pipeline模块中的item结构是如下:
{
+ 'realtime': [{
+ "star_name": {},
+ "word_scheme": "#新闻标题#",
+ "emoticon": "",
+ },
+ ]
+}
+
+4.4 也就是意味着,如果你在pipeline.py文件的对应类中的process_item方法中,如果需要用到原本的列表,首先从item字典中提取出来,具体如下:
+ def process_item(self,item,spider):
+ #....
+ for hot in item['realtime']:
+ #....
+ return item
+
+]]>Yhalcf1@b?vtAQeIt3P0cs z^QCwbJSLEL4^$L8P>BE#mTHLFsxCO8J!9yI7GMfa6sM@Ms?Enx;4+H4mmZSMK~S@? zoP%W)nFKpTXY>H#J#Ad22;Y8VV+}1o@!xA}0A6ZL@bkkZI-+*UtO9w7j#R*Qb!u2e z!VwHGQOx2a7u-Id0Ixb=$N~QjpuwvI0ZeWeOr$sqi|K<1>%qIRR3WuWa2dV{CCL&f zp~n$dMI;3-8%Tx0t$vMtg^6*{M)z(YM*{SO#veGYP%IjBu2@`i>%aHrm(? z7%oEWHh?m2eI{THt}@&&p>Lj8$3Lz5M%0wN*>Z<@ih3-ZxS_^H`~` tqa{XY- zOk2Wbu>5Q`akPx1=CG|EVBcLGbHT-8b?H?P$28=Vjk% 5{Tk#?48=WPB&UV(s`mVar>@Z$8n5 z18KlMMc@+GY~gccw*!S`g5&s&sUlBaN jx8ofEK&&tERoMp1n#~9%wVa}D#bPY;(7NP(hV|Tj>1FcbCTzo06&Fy7X34S|ezxcQA!et6}Y7jPu?WsqxZOX*@U!LlD zapK;20CQJS=vHmof}mDv`=NG`@<9!oeUq$n)HLlwW^A|Qo;7FlEdP};TE2FuvoV?0 z0=@TJJza7iA_1;m<9uXa%sfl%xf$uwo8*0a=F90=qN fI7t8k%60eLx6d{hSa%&3ap>8plM(U#%DR#GVGOQe9B?&n z7>0WiYg`AmZaK(l4@EKkD~izVOU%hCyu?YU#8|I^&sIGyQqi6N7zGkJmQb?2yjF^q zO5nxr->Y(w%MGVwKKS8J62>~ );~0 nLWK!=A(` zI*f~`L&IE9jlGZQ0Zfh>__MCWv{g;Nv!x2X5ILVt?G2=cCsN&gz2irxYW97$YvW~B zR{_Sl9`clP @an(CJ)XCgG8om(|uEdwxuKZB@eBSsYs{fBk>y}l8hg7$e9jmdbK z{iMMOM5!P(%KROOSr62`Boeym{(O}sOs0z5ds&d!v*Or=>N)0So?N1XRm@2)p)BPO z*}GWSSz|V>FIR=2e86O92zH<>C?T_o5wLg@SSUMP@}|y}o0x~vvIQsh@?v5XfOlmV zs5fjQxM*wjlj_L|$~LF%f0Z *1cA*^V>33hih~ls34@S*v{Jl zOTkwo@G*W7_J>e152-mC7OOUXl_CYhf(eqk^z2d9^6K?%#FGD-^P6`FEntG-1YNp- z>ZY)U6IO -?TAkAy#^$|`sw+9%YPbQL06Lvge 8idwvSW(y4KNb60{3$~RjxaiOvRRv5>O-tpHT+Qb28yImK zov<$`7WQTZmxu;q!|PFas*Bnc_Sb75U#4!7%_aR3qJ$2`J)z^+*tp*Y?^dc;eRhVQ z+f9|XI^6(4j+(Ex^W-4|ZE(`jLeQCJ!r7=Snhr3N@r3r4J_ACbsN`Y9G(0rI!+s0Q z9}*%tUQgvyD~#-f+ xbW`7k4Y3D)Jxg;ym z6PVz*cJaZj8nnZpR(@|!^En1K>wP~-WB_@XaWX*K2mTIq=x1)tds|`kSyMaJOvH1s z_w1W8!5=oza}GNx+LGPdteC{>L*v!6An5m++=IC8zhFs;P1+}wSK+1-j}%ic_66a% zp^ 64^}`>T zMpP=VTsPA6n9rOUi+BOXJ4oL-G5feVAd88N*k<_?M3o=%vPcH-Bhy_1 Bc zQOp&|5?Hf8io55e7(%21D#4(z>NItl4yqD5W=pbJaVVGUW(x8$V)6NLM9BjN=Hpa; zngiEb5KX+TYUUFxh-*|y=OO!GbKX3^jTa$v4E9R!^SY~)Ax7LPP*Ie`zrw{z;ujl^ ze^&>SW9B(Q*JsKrp6O4-0O@d6s&);NJeeg57GM+*neJtWY( r+WVwbZ+cwLW~a_YqIT~ ze}WlipSk~%_q9eo%xO^* =J@S;k>Z{nEymr?Kb8`!NoH>`Z;3*fp=jiGot)U! z$8PRCh@OF=P4{q`dc!8@$%8EV)mKWoB7?F4T0bpTfHskto14Z*!Z4>Ey2 zN%uZbv~!-m^A+)u@XjI8H~#@ M> z@bK_=T1;Kd$G}5mj%H{6aCmv )dpt1@dXnpafJn-xY`K#--&~Jb{&H=L84ko7YxWKW3AC~ zY;$CF(nz0;z6KKs|KiX*CR7i|v5jGZVsYylOo2#+Emj;6u)|7RLyZZbEo!(8W_B%n z%-(O$k}Be@FNn~kz4wb}3M%W^AlK|g!HYgfR+wxY`x(CjZ7z#qH`zx+HyyUiKw)gX zdB3q#LByN|tWisC1ud|G0&jz<_JD0xAdfD9;RxCXOOv9Z5fd4a;5jL{3C`*wxSY%@ z5e0)BXnm0z92mc|fb~?&A*9BeBWN%kmx=stxI&af(aYMWAGTPj+pMrAubz^vgEfL` zMX%CS=%bxw74OgQy>s=hqzdXwiB531u=O%@E&@H@_~de!@X6;NvX9{F6WBuWSLBwI z4XynBGF3xeTqwA|zMA;a87T^W`h48t5`P?H4HdX9= 0c+ zi!)^K_SXA|A8s$($+^x}cih5|#xMd6)~@+Xx2 >jWa*$R zR!?b(-R?2o+lCxu23bCp64p( NRnZzL=;_29pvnd1F6h`~jLwk|y#l$0cd71r1- z3|ryw2_4irdM*Vl;4+O;=Ey6jU`yz~0}_z)bg8hA$H|RSMT}^{E;XnhK7kpYD-V{o zMq9vboQDK3?#ds9K!oK)t3Z1IOMs)cL=W073#eJ3Qnsr3+{9K{h%Xt F%mA++m9< z)J$3P(i4@eTzG|tBfps$2Ns>v2ERZiej5#IBPYFiQ7viYcux7K+mjGK13OFfJc zuMykg2j)%OpP9aQ3P3F|GN)|Uig-0wR82zKDPkRgBhL0U(nI5&kcebzIJd-MDK9Qj z{bZXjo_gC=Aa&j8<_%Q?_8kG!{piIiQ01}l+R7GGR=*9bv`Ff4Gtl~WI0cUm@@oJ+ zZFTW(ks U 7sQ_mHyzsfw1h)>G<#MahU z0BdE3-xe9IBIHfnffziVosp)z>?}T )&ja6=GgX6(=-@?naw&5&c%z4Zkr>0Eq*Q(+qPF%g{KY&k(@<5-dEa`Ma zn_!ETT8>*$nArzh+z__0pKfc!(ki #C7FvkL zuL~63H6O-4c={YAtHSI+S{wzL?E7pJ5&zg9^u*ddv(IowtmX`ICKhiR7DgF28M5zO zUW^X4LCju@T{8AoL+7AdzeP@6Q`{doIWCTtpQ_r!1(&H5Hsl#Ds&imMpzM1sFdsA4 zb7&O`L {NawD1Ek7c7Pf7A;?c^yqyiCjHSdkf6jhoa?*lmYdGwrp}^6< zb#(N3a!$6uR8b|h&{i7pUV-`CSOg_}Od6dx#L@wnRTf^&$N;&TGw85&j2LC~d0NFl z_ppGOI&->>8qP*^q~$2Gz+c-}sL?)Vh|V}?U3!H9yb?<0RSEJ+ zzOPJok2xt(G!-TWU^J P=Q_&!K7IKHm`=5?r~ zH^;C@AJeRxWo4X*p4$c%d|DL+UhfdA)PZkcRBK0r_dKO}^|5q^?PQ+>Yu%lbov%D# zN73V!?`d-t^R6HL88c-Rr?aMlH1=MZf<2Q6EkB<05I*Cqo{`dTp>wmn{jFD8cZ0o` zqs-x-Zn&fU8Ng&l)$ZQ@o)``7&%5923|)@=`4sRVZ0-T~8*M>W+~+6YbLh3BksTo0 zcT+;zw|4r8+U+w;*OK$a#H!ohMdtqImkZ<+05%-ycVj==A!6ShxJhb~?9}-JS)l1O zHz5vp++kdrny$%%d}oC|eb+&nmp&pP+F~LeA?NkIin?oLz|&XxLtI*Zex|OpLH=c= zb?E0uji^-}CCBd5-;*0mR!ss^d%hdH&z&Wd@=8yfDY&B964F*C@TX31YI9K2G?YHg zwmUIV2R3)$Q(lf20k6+kKZjB#l8pWo<$P~?+Y#cQU8IN7YW=i6J!t-67wbR64zFsp z%=dH*d$GCS^Zk`g1>d%-?x(^-;|X(@E1u@{rfKBQNzvbIg#Cz{we)@<6PdO2$HvIY zE1}p1&}^fyZcs@0qa$`fxknq=Qnc<00ggA>S0+WX?;j?ev$^0p_vam{^{aZ#)OMtZ zbvyZm)2mZ%N61zlvYj;Xez?_GFVz6n5orzJ^TuzB0A~CS2``%lFsBmmy*riQ&vJ_L zP0Z;P7j6SE+sn_}^?D6>n@b5hP9JQ7I9;g>VcUO#n=pu}+1-_hvIrMof<=vN+I)d> z>M{KWxQ|)cJ`FA TsgH>S%Nsk=D)ZS$7^>$M{;K2tMPZ!$?(9m_JPzs91YqfMuu_Mm)ku zI-Tm#gDh7IeOhWbs8y0oYmtZob? W!+TnrlUL;9B XiN23AnM-ttogMVmHB^R>x#x8O#?dY_R@ ze=QXpPi!=PU4RzD3PsLmM~Fr6=z1bH1|?F?YFDMqU7=40-ZnGSq|DtTkRQc_TzR1o zpJq+t1HTF{%lg>lk^3zbbB$KbK2XMrfQCQt=-#fO9 j@RM~ao1-T{>ZeBH7tY_ z5POyz%q5LJUe$jdUB6a0BPZT(&balVQ`UXWO{ kOh}&n)Nq<^q45iYun8cpiY5L&$+x$;*E)ig&wBlUQ zD?7)!Qw}qcyPlY~jptGv%Pb1>aynf2@%o 2yOcZbKi`vhD_BAyuaXT0N zNN2}20PJcNpJzKV3#gE2l4vzAHV|D{tdJ5oKWnTla7Ywf_f=H z0)@~fw*+jR>~Pl 3YGQ(%RnOr77Wh+7WFaQML>nLEU=0$LO z^?~LNL_5?KP!_d!Kd*q{+QAYovo>5oi$cEOgsY}kQ1!+he9gmZfeG^lWU$wr;S;Rs zH8kK0YOfIDYw;0WDpP#Trr%cB%#LX L&vLRB$ z>FA32mgn3Si-&HlO*i_*mhRFaONo$ms-EuY0MoF&3pMphW4f*L4!fQwG|dY{nV!FN zc1B>B?g$LDO{p<9_Enwzz+yq-M4QK`z#>UJ9hH#HRVHdtKRU>c6BRSNGXhu+$r;oo zCegH*`qX_|p{oC-*zxvi=gjPPrlC6S2ik!T@{4JPgU0bjJtzd>5PI)6vh1!11|CH& z8JBn>dsLi$cjI&DR=H98mTU+&k&DJc5QNB&egmo_7naRdaMJ_W`)j;=vnlRsiJh_q zf^xws*JT}MLH&idueJunyB;}pf7ZP9<2N@lro?1=Br*$49kDs{PM;z;DCax|UjE{K zK8AT~Su(r0Hh3}|J?Dmf0YSQwtzhFbwMO;>HE%EHKy>wGJ?t6&H+qIW^BjxaQu9J7 zFLPxl58iLSjRb5m^4A|dc7Nd1Hg?df5Ruw60_)SVR;9ZZ?sJH59c Wg< zsWi&0^DG~4tPIb?o_=Hd$!M0iq?u&v;oR+d+d
pl DWJc^z4DIJ-;7SFQfGYE+9)vuowy44%EGj$`Psf(~npI&^C! z?!zbJF2r+MA}>=h+WbjMzeZd3lwUMC08FA02fEQOjbTS$PN<`}?^)xE>dGU*%}>Ma!E?wR1~_q>#@J zt~IU}t0}9a9X%^}D^P|hI=ed;=%_;UiqtvuA!9$zig!iTJLBm-r$}~sb~_k&vLbXH zt!3o`aD!bJV)$FH+4SfZ!vw)|$ckHRLP`p^fe*Lg-DJ$7_Z*yz+Tp n6a`*u5h33?C!^O;qPqVrem8(e(zI%Vo&sni};#jYx;G# z3SZ4uu02!1mn?uOpN``WcVz-C(RVJ9T&2m@QSxx<0;gNbeL`f91dETl9L+v@_2Gc& z-mIZY6Hv^F)_WHgz;j{>V1gMMey(}*=XmwuE{gjjC_eJmvC-$CgXaj `9KDY|;UwploF`FBJh7FX*sY zPqbq39yFAlkMet#|HAvNE*G6SC(!|hOXn-(T7!%CK-Q>tTlaJt(&ARXCP(ax;EKu` zrjxFA*_fx^cz>o+u*JPI=ex-S#COHR%_zwzrsiCNQTB=7Fw9hHr^4u46R#^Tm^S2# zx{GhxEgnoYAKse3mv2StJB9uf6=qS}kzGF$8tViT3wODMzBK_a6Ac-#$h^d51>S`Z z2k<2J0cl$iN$F-RYH%DUx*AeUGx_U2o-Wpu_RNnme)Akipw*Z;lAK?3xpkzWfBjuP zT0OkfBud5XZpCT0;P-sp@jB*usJ&$|bb!FpS~6Dh3Wqm6l1W;pnS0`PFKhZ;{QbcF z?8}9`s&n_e&9Y$+N=orA52+;9^gB`I^R>s7GW1Z(D-QnvV!PVvN6oT-VoXj|XYUp0 z*{OMoGkyDF;Y>Zl6+0wl!7dGQhu(G8ogtZ+$&h{wEmN~k&6v*O&uC8y%*6ZS4tMIj zcl+x`I23;GTzKU{XLa40Dd#Kif{y!Oq9_411mfK{feNarx=C61z1;p2Y>n336Fqgy zF1PK5o}@ZLkCd5ZT{NflpPLL;PCMRQ%|jjG&XHDr*)Gtofr3hjJ)la4p04ciq0_v2 zPPKB!g;J*NVf{N5jQh8yQUV2>b<1nA2hX)cS8J8Ff3R>0c$l{s35s31YZIuhrge5H zy}YWwYux^LB)82wwMER%X~MuZkakUxVICQnp*mD*>VGg0MS{Ki_IzQ@ac6i&<_Cys zu94BEn&~22CJMktn(yO~J8L)AJSxqvt2Wq4-ltW)s&alLFvW=L?XEQ+O8~dCgy)O9 z`8)@KGS H}~EF=t@tz%NaEFc79-mzigK z#X@5OF2N8pjR6SoXI>fEMVW7O0j6Jv71v-#oGlf~Kzb&Hg-mfqJiLcUum&b}vjiCK z5EhL{h=j=VirXfNL=olD{8W1&(FQ5-uEET+3kiDxzRUG5C-%h57m0Skz&;C&!;dQp zE_K-p!}yv)j>^=3t^{zU^3O`_8<~sD={5XCur%3*Gi381!*%brF^o%bm6UG`&k 6>?8!r^d>gq=#ieql!{IJpQ*4$l^| zb3HoxL2!ZvEdt@l6Z}*Gny#RM@hKz`fUJRn!m#?Lok4ic1)uRWgIM>>BHqQGc^Y5u zd9FdM8GdYQD&r|D9f2`C#~}1B!VcHcVt?`O_|MjUyk64$*rfs5UE%u6K-tnev!QK& zuHi>r*OO1YvA}DW|4R^tQ!}uEP?B*B;JEi=yOz)x-ANF3??>2@l$+n@&JTr3;N *|L2~D<`K&nCFutHhvBM^ivGGkNTk6H?ToJvofQ{SK7jwhxNxhs zGCe*%|3NeE6S$-Br<3Lb`sQMdD#0!M^o-!QiiOr#&l8Ku6N#X0L`NTeiB@d>`-RT( z%frVHA5mQsH`w*GSI3SLPo6Jv!!P>}1?Ux!`U*GDQ^t2^G_RIYrYGC`MUK;x49vhC z*8{aIbPhV{*&G-R4BUL6_+|>g OxGCm#Ep{Y`USvir_2rE;AJBYp z-%ERkey<1*H;K!&i@ilUdHAQN9^L~Jtb2K>pNQRo$u1ysUi+51_!?#k{@zA!DoW8) zp>3)N9LNY$IEHWCshG=A8AS`*^L01f%nAcO#HZxD25J>IM)F01>nijzGPS^u&nBY@ z9GRMVx&)a@*S~;E;mG@)6m3ABgRoK-sIK(8eDhSAM$0ky7uzZQC38YZC~ }NMkG!oOgJ>!mqSTQj4mbSp1&jiYr|ZwdnI4<>ov^V?WCt? zChs~+M?m;~q1vg7i;v6C%Ffd34nIoP>HGFu`gPZ7amE|-RkSr~hXry?E6yV6nM Z$#pbINu7vhL2btl`J|LQS^ssLl;d-7<(w2)^|lv$^rZ3 zZM}x=f=+j`{o(TIUQjMlRvLhNDCO#|Fi%f>%l!7E`lrv M)%)?>@J1$au~Bp+R_yu@^AD`^&1{@AH}!-SX0-l2@%FZ4=*bD*y>EAsr-@L^3N z|EU|8{b0zSm~z9K{GUwr)A;5RUnrh`#?c?+Z% 1DOHt6Snu^GZP%}e^Z z`FDhlr|;LE!Iv^G*c^UTS87PY307-d-nsj9L>JOcFopOD``%AOy?Ye+a~$8N?H(Ll z`z>az2Tx@-DnrRN$~x` ~~+84Yq zhcE7BND(Jl0)tG!slrdjo`UMd{88uATU6ICshhzE(R3-Z|5OkGl!Q1V%-1^-9wK0} z! c z0R-pw^NEP;kZ(P!I(+%v-DXcBBqNe3Lmqar5C%)ii2djf`PL=rh!#GOBgT$|hLO&6 zv8^^7nnm>&GNVHjbsbeC$63!3RDHjw`kC!+_$VWIGb7(-?0G}WhdzaSm3n`9Z0KC< z^LDoH>aF*cTZ0ju8{M+YLq~WWq`|>kxlO^-GL=Ub!ni6eh=PHS-D!h!&~sV+wHFVM zZ&fE44qL?mxejx_`v=QNr27Nc9#mg@%T}2F%ge}6^(B2*5+AtKfQu!{&WDMqEpPVR zV~E|e`*Eh>W{YMM %YZ-cUzQwQ_ pVI8v3=aNjQ?6@%<&!?Qw7gLdkYK+ zH?Zrd@8CKm{L>(nkR>T>bF%WPjg=kZv8#nMv=@8wJZ=1mciy1NR*AD-@bQyI1uHHx zxEuV-cO>V?Y*LTl#BQ$dx~gA7n >xzY3Wl4gOHCaZ3Z=fd3^kLSO=-Rpb*p Mo@IUb(5eKP6Sk;svGHBv8jr)-fpam9q=T=(6!a z&C?!y9dQM#?Zn8I*(#Ta2EXSOLU5l2%gaOs*Ij%{mI{?7N^_>t6Id27(TP-kNyGB^ z9fq9~Rg{-)834Dw_P~bgzo5J_{+t(qC)a#5S0g( zVpR5fePaz!#WCkmx& zDjVdLZwi}J6Fy+u7Z`eShu90?uhF&CVZ5#>#Z%~J0n`3y1Xec^v4tR9txC_}jS+{U z`SXT=qnfQ-E%W%3ql~Yv0}zFL#IpR`^S!SAzL-nl-#FxbV28cCMc<^cr7Z8Sb!&EG z+q!jTUJ7d2{p6V>Tf2$J1wPUinbFdjTaL3zrx}h*r-a9v+3JF6!dY#i(~SO#RYahk zNnn?i4H5|+lx9&lk~g?X!dFzK*QZXeXtm{3w5KH9wGgw2yML78sn^sgQQNc&+LBJ4 zpq%X*^CdCpt{8}>#}}>PH=p0EZM{%lEi3+v*;VN|tKmqL$c1}emgwFc|4~gVMMrMm zO{ZrI&zCmhL %qU*pc!sBJ~jjkC61UDqdgkDd6%r8~GCf(t31TLwstbf0f)Z zl-0dwe8hFxMo_TDR@xseCfG%RSB@-xauPj;Z9B%eq1tdv6&+>0r~+8QK;5pHL?b$o z+sx-6Qx&Baq=}tY#US4w$15;I3zgBv7k6s9x53*Z2Ji@Q*)fSNL?yDjN@e`3r~^Mn?VoA{@@O&U*g=4Qdj)d@yZ3p@wtiWSbzlCyE#1wz1bt0FF zAH2k$Kx_vbrFsga9kI*j8pF(1;gpwhkfsd|Qtv1z+ixsktIGYxgaM_r0aGiA4#AO5 zD%Xhb;nxUjj5w}_t5yBygpAHEgVJDch&Fti-`gw91et>jF@ik44 (w* z?GM^-IJBQdE_y`~wk}+5xDdqzY#Se**>~)G&l739TkGl0pPIiA<*hpJ?R)(5#`AiM zC>vw ERlZyeHmt z_*mwFe}Ke&*Babmb+flU9(qe>oP3r_?o{~QrIR{+-Br|Lb*TU8-%r1vZR$U^?7ywx zR;-ZpXRg~|U~ZQ}*`+?Dt)0N8pDy__XD*(uD(=PH;ynxdc<_?(O_)+e#%6-ykEMrd z6^+MK=N&J}c;%KJoDr~j>eWB7E&A{uU^nx!2BaR}#H+lF=;s^Eovw~2QT2@X9WJbd zC}@?~uTV2ePuOsW5De7YYC6qq#d*qwM9f54_5B*Z^P~SWJ?)6|n->TI38-K>RjqK{ z)fw%NGw`^xb5^5t*(wAq_IPZSC_Ks)sWw6PH2`~J4O!2x4;bp5KE@~{>@wtCe2$0G zHv%lX&>u;KY~<%zQZGRZdEwQy#$Tg58h;k>VXk@&y(&jAn*vZ3o$R(NP{ruXZsujn zVDEiWWS4!?-c@p#cEq-WG}==!Nl&xgf^_x{wXC+U!TwIF?)3v#f|VsLpNmqNx$u)E z&1bG32$OM cSt(=KH5z7 z8aVc3nP7Qi+ZnSO@L*ulr*Qr*qUc7k`~Io*B^tJHR_P;g$qnC9d~KiZa_TE%CC{IF zXFa$9Ha;$UNk-M0?cpxrlvjUFeK+#TpWIp5z9SwPbZg1jf7-Y>_MYv{r9-2dc8@=| z%$pYE9Y0ld;a5qE?=M@A z97`=XzIn0pLfoeJp`*6Gr*ftQFc6NpKjOWz<`7nc1C!Mn3d=~eGZ^>kDZ @v9AY=N2+$b3#5NV~`cNl!5MV8qv~F5JpJ z@MaAInh9V5aH-19ig}k&V1_FT>YiOe*IzzyY@lA29r=XRENl*0aU$8*wfuqixKNtf z_=uZE1TBW8746-rQ?e1Tg_jd C+B_apnF1z{|B4Dn?XVGN?GrHfL-Quo@H14#u>|J(t3Qqo8+Wv z{AHLP`?cPGyrst*7JOk#RMmT;gvK@YeC~bo^x$-6` z1}u^0w|n#%%Z z)!YTCp1|ycC{{z`XPwnK3BI3lQ49ae=zEpi*DUnIfPn!-(s)#$O~2b-&PPi{7ro;N zphvBhor>6Vs$tIZiuYkZu@s51TS9+7HV8GRt43CxzAX5ex1{|1%Ry0+$x~g&PlGg% ztfs!%Yu;u_OOcbExMWp~?n!TyWePsEUUl>Nete|v?XwkJ!7Y~BjcxDoZyKMH5+wpF z^cT^e^r+%c(F#U%(xI 3#b?QJmYcZbvo0xL;c=+g2hDDSW~2>G5&n6!!u7pi0xF- zeU8JY$nYN}L`cEy8@n4naol@soTA +io=3=Abow8L3LcI3iT>&I2V)XFbr!4%(-jc zTn?@X@V~x#)cthA%YiV8+muSfMgK^jZzJXd)cnb1$P4n9Q*H7BvqN#bXqBhq4cs`< z!EpP&fVyMFWT(T7Pzz^YYx5hv =D0xL!KtAQ^vOkQ^pduSwZrFR)65#xKzZPh|UQU zH6UFegO|uR{}4qMP@myLFhOdxGCMWpzjZX=yK({;zF+N8Fg>tEHwkq#0xhg3!e6Xm zXQ0`i__nAP-tAi1qJ3b}T{%<#zf}_;0Kd4O6km62rAz%DONI6|>Leg5Lzv3!x&IKr z%Wh#ax8Rb>2|5|Q(AZscnWd8v4xcXo-xUiU{RoewO_t9F;zm%eM@!N7q$-K#7t@wZ zE}=pknyS6HV7CZDA29TmoBQ^#k@?yw5!{l-7KySWeS7mCfW|8LyKnvUI>^JNS@qY4 zuqSD=EtzN4rVpsR>fG4F^M$P5WfEc@$sSLTdgl~(zBC!=B(?1tzmYP*ko(;qIJm&e ze^Ge+im3A6V)o-XzF}kFXUzOV?}ui2Q99W!f@KzUlaFQ22d|D?)Oz1@Yw}Xc@)x@z znXCZ2xwyIkfo~&@f E^K1+%- TU-*lzr#{>9yxkJl78@?WuD z=nenK!`syj80Ife@}gsbceBnqEQHd;c9yC=gn~SCv~S+aH|c8ud>L^*^6Q*?yXD5V z{+vB~N20~!mkPICU=YIGZ+mp$$@iD{p!#f#WenN$_ `y}Pqe2;k)Qkn+{0ImK3ojK9j|yqHMhID^9-4JV})V!@Y~J)vgj>s{$Eu3mOsZT z-d`W|Pkd*rbmsIu$zy@5+Y7I!7MfLX1JA+Rg9^Kw!)J?q=KhRvL$@ZB@5kLgt+KMS z=4ZJuZ#k0p%W7QWPT@bmm)Z@;_ML7q&wl_eQE~O#rTIYW@q0^_=i{%pw$=r7g;xGv zm4AKt acs{uUEay?*Ftm0{XvXFNM)yOvx{bA`a5e)Tzw45y-%cCe z$~gfIdX+eT0($Cvd!6N3z2v1cn125yJ%ekC%PpBl9kPEqH+4mL$;a!bUYvbA!@2I{ zd6+!^rl^s~yt1Bq-pjRQ3Ev?0(f8HXo^->G@qpRMbH)R+A?r%Tyv&X@YKzI^Q @&)C4@%5)cyYPB~nJjAmUEk3c<`lKs&K$SBfaOeyK3rT# E%43L#G=H (dt#nm;oP*rHXEv17!KxxYF@A;0YQL zX{z@R;89(6@JQzo+sX+0Le+J=@b cyXXt}leipPIMYyF%~&10=aM<4oDM z=8T~ek cPUxYu*m;P9De20gGCm5q?ltPNm4LS{OzAu4Sw#Rt;x+No<|0bG?v0ZQUkF#S&(3 zv19oWqBE(niQdw8r}%uw?Ca0t-;DBX8@Cak1ihr^9o-4*m;6gwZbvOdFC1z-z1()# z8K3uN-%`$G5v_=45~*C}Svt6IzJDG$@7rf=yUZ~ilTbkJ%r{_jj=xgw8ZL@bnmS!Q zb+pDfvHtYwm7G26t!#_Qh0-;``-#u*?vl*zEw>Y#$_rW}ynE@lK46FqAk&9T&UnYU zfG7B@feDo-*NkfWwStsP^?L2hR>LT ROkOA^mQ(jYM2A_v8hh0MHo>s@1Bq+ORqSj$mo^sy3XMB z&x1Z@mVY0jyT0qMZk)jgYXl)VgqB Uu z?nl3A5oh*V748NwX|G*xw}Rh{9Gz#5T`@mtqaV|D$;t29(O0b-a>b8+=6(P5{912Q zUct!=Me=y%-$!CEe|Vse{Nf;=6e@m|2UGQ4wY!O^g1&ufk?Q_vgO&%%z27|iF?n}_ z Bg#@#;_OPewX<&aN4KpO@U4IiYCP@yY0# z_q}u8Cu6-sK3l$r@)i^f?x{2{jUfs>4@1$ST`?(t505j2jGz65s17{{p12fjY0=j< zPnuASJ7sx_I=Ox7?iMWe0Qb8ssk!9fX!$(yC-j{EJzdkh#-CM}Mk+T6B@VkwZ2tle zN6zsZTOC*ZLro7E=*BLnGSB(`sEtv(LNg|00FQ9OD(tk`tg8D`8P|$KYHaqc4OVda z%flAi&UQr%2Rm(Aer>b#?pi)>#$9w&Fnb1_sIw_g#+-2qe~^2^ NL!~i?NwMRd8xtXf`J`g!k@=`MFVkv@;en3XOff1pNR$!4nU)ZE#u8Z z7@i6u6}qoQ^0@)s*(!Ugf91UHI$7Jth+pbI(#;Ih41u5y4t>%VEM1wXYgEp(ESsla z?M+XzcE5A1L Ika`3r5N8)9LrgGs2eIm90)O#LH*1lyNVEjKgI?r%6-?xv)OpVgk-iq3_S8Q5) zZ$;H6)ZVcvZ6jh+YD?_BRkcbbMy-fZyJFO?U5ek6|MT*GaUJ)4U&l4i^ZWgruz>Fw zN;~|I|5=WrtqaK(dw|`ZPy?sXwGP4Vu&-U#5&0?Ky{AHM1-nfYkGjjf>c@zDES0b7 zKl;)GRANYv{Jb*A^UzYbrXVxDpA~PJTeM=R-?z Z+_09O94UvEv{|By?<$> zWBYD+;bq`?wc^zLw)7UqV&~`VEce?fi=@`b7Rlu5XY5_RCjwC~Quj>uAulf)=}HF5 zn=j~||4kuVpVg1)wT}|1_Ln@&0kidv`&&B9ZX`+6l{Q;89dKGBf-j@RIy&rFLfps6 zlWslJj{AM&+7zM|ujVL1gXSdLGQ@_J1f?8{?t3&H>zR=`RYp;(C8N9_Z_Nw-W3G%? zFYSi;F;U-2Io~7(kjYG{R=&@Ib=r8sbgsWINV^@=-5~aZ-$y-+sottd(fm{7o;eq< zVSzuAnI?bu6KKQV@IA2OQi>aU7(1$aaQ^x~fIN#y!THmh-S<6={UtZyRXk~3!@kHT zv{r3VqeuQ {3&C(8q+l6LPWMjdMSFF RUJ!vj1_gW9eCJ&HP8b5i@l6KZXg9k4VeeTheARDd ymB{s@9Kb7yaBtg7B3j?bMVrW;u+AL*vE_1c$%a(wTnL z{YCXLBGM%THNLya4d_mykLsS_BZ?a|S%Xfs&a~fGay5{--W>-uZ}HHykfKz9z+$z6 z%eoVQ;%v&Ut2c%G^8>G{XA;e(a0x9|c9ryoottM7%&+0UX0({b!bi@Jpu21A-USj5 zqFqVbk)uxs$N2X>Vtii#=DTyZ&Uwq^Q@;N|=^$TUfAMV0QoyG7zl`VKTj 6|jG%x@^T!5{ z#D5zY0@z5j>rsGLBXEZeSH*%;UCD`d>({5#C$*4}kAYRXTy1ZLHsas~_{_Lr$dZ48 zo12kO)}OGgiX4Wd7hKu8LvOuAzu?`DB4OqEIAiYnd%7%`BFlm*@gsGIGVzwrvJhGJ zc!Z#B!nnY5_g qPCCx6dhJXlQGIPKdt)XkGf>>;X5&)1 ztyJx|{IqEZmQ}R*!;WMT0rDSD*T(Fp=lfwcc~7527M7nP$8}2s@7Ku<2X*4@#Px@{;S811 zD%S62soWoK`}b8o8VAu~Q^-l5ZVuIVe!V()k%6Y`cI-A3%yxbHJ}i{&hx2P|R`SGp z1V{gBwl`W);fqhvdAPKfoH !5z9sf-Kkrx6nr%W zmfHVhbSNfXUmGKSO2perKvgF$1GOhVv3mQCbR77at{$SkELp4`Xj^X{;P~cEw_G*z z5B(#jx~5|%p6RtY*9WmjCDp>$3tnw*m_8t3eVCvh!m{N*C|VHVmJ-V@@Xd-%pu!-| zO~uAY5`^;CRz1*Es$x7?KGno&6a3HbB|^_ih1Uy5OwwMIJQV|+C8pYg7Y!cswUfOg zegm2Qj_!RPN-$|m W;~YgAqeRaQSxZSr7MA zCreJp6jfNUE|mVNW~)%u_liG`;aNUs6(mYy{PsU96jCYuzm-cZSHyN7dJt+Twd!NT zrz_cGIfgJbxN1ZXdfuJ*xCuMzhz=|=mY;sIkw^rR8&jfYL`+QBM>PcVxk%%}{#xQN z4Wq%77vwyh!Cy52BlRcR#Rmf0p@y0SY?|TH?}zzj^Wu%9KgJtwqRKsg?Dzy1Dqr?r zFnfX17GM4BNsnT9k)68i2Pt~ zBcit*S9XUH4N9Jmm{&PG9&R>xxpQg&X`WJ?# zGQSZYE?=G1uM+Jtuv23Xj_MFvT~OnjMoJ)yUqr @)qik+G}Q&=Y7^gfr1 zOpce_WUo>B`;GkrZwq#9PU9Ge5?#Lq2Ls=z>U$ mTl=E`GVjtQyUC4Q0p4nTC8`pOOTnTGIR~+EGt=50`u!} zLlyL@c?YI(=`gGVUh-6XY8FtIw${y9O;33)RrPnnG>=Rr<(b U8w$ad0MMRqJo%U(;0B7d1;cM+1GW=Akp%hP)4 zY;elbt!TO_VBXx~qahsnQIm)qb*dD2Wz3`0dS$x#TiX0Ljahc|Ucz~0jF&!b^@7@7 ze!r%3-?%5%^0fO(Q|*$D>Y+yU@V x-{V~yk`guD)(JlPjZckPy-wRc-Oj z(Mt@Vc0Vu^+@WJ;0Nrb|&g;m2uhnBQbR$jifHLhacwXc^Lep^V^B+mRlWK0vnGclf zY)f}=E>jxL&P!66Ak1So988|-Y-1cIskwM+L=*9rE?UokYld-nql$6d(EqO)Pe*Gy z0eqd-Y{@x)Vqq++>Ki*TsRAbTvbua)%4M!6aEty;0@KE%>VZD-C1T0r8Lxd-Ios|7 zL-|R`UGUOF+0m6O+NvnL-PBIlpgT&S2Mux9(_nbWpB92R6^Mmk;tmYN;ipO@vY`Dy zT}fTC9d)OsVp)jAgpHqaSuT(92Wy|DGvrBY5m<7^kkUUMY=>x&(ggCHA?K;3%Jc20 zZSX#gv9gaR*XFU1d^>)rL!l7vp xZ@>f&xs6l5&f69kW6z! `XJ;te;fB$@@0=uo^5TKvcCP z26h;~Y4l%R`TVa?W-{{a{kD)JZD6A=7;g+-gPC{wCDT0)w-(lY@nMbpcfGeko!&jO zu9K)Sx8Gx@JybQ~Hqp+cLq1*=u-6R&3cnSTh!eiiIuqqTlF5)w6TgDsVD{#^LYgy`eWtJDrz6xW4kZ^ !e(NmaPa7?tOkoZkDrL#N8z3$pWTHaPKYmY zbsHxeF0UKb%x~AclviT-bI4hmiKpGV!FCE<$8hCr*-*i|9XB24SL^JkQdw~ve8yl{ zSO>*&R?$+Bo}O;Q<2pWCwkxuMNv=ZqsWAWGOh_;yb_SfPKR=ax{!$AL+%&Y!LWSf{ zz(GtevaM@Q3?<`q-{P?vK{lD1?|Mx7Y 3t#Oa)-nVBR7aRG%7;EdD_6#uihtfYaplrwxnbn*hn1oZ~^0|9oNB*qoyu2SA zDz&{S0g9wH4K!@-lr=-BbtEiJMQENdNcv1Y@#=Ka=BSr$WFcm6FRk6-*6&OT+d5s( ziV0 &cv^Zg+7U7unkxC-;&h)0C9k*)|7k>Vx*?U;w-wccL}(Qy^nM$r2p z&Xv!z+p&v ged`7JiKuAx z#Z*SpmeAlqWueN%8JwK!(QJ0EV@<~4YqfesJ+ZJ^&EN%!7_tcy3o0a^9c(r{*WYQ` zi0`;6*|;q6z;$WH+tPXF?ViCMChI={ovRDc9P+WRE|ONOEn{|5wLh!fTLk*{&Q^r~ zu3z6@+UtPJ`8@nX9-CS@SNo!d2J2GEH8$oa*${&c`X6t8)tleiV^1~l`^#jM#1qyP zK6X^uKXNMN8HP;*1DbsVwDcA-^>r4Jv24Dk&H9hrJrfPM(<~NO%m840D|wy=YjMw; z$I%8QR-cloopUXtX??blI$6Li*m_z^kh#kHQt^cW3CL6zHeNq!2949kxYsMhPNGZ3 zJ%*1AbzVpScb@4Kwb`jt;<#cB2!?2Te(RH-CTNY#=NsQv%bNHD_-5n3A46qLw|f}~ z%9XPy&nw+vMOiOn38Q`wAE_e82%47i2^5d@R )n&B*7MlN-&!Snrz*Fe tpPh>n3ta)QG zFW!g7omadWjlH+G3LP%DXlkTEcx>Pg*4veTqXcTZA&ov~xbE=R-iq)jrD_-9jXcRB zftT=p&Lz3??f(FCmcJYZC~Im9#L-5PW?Y4{i$ff{$)t|ET1*IgLoLQC@2qz(RRe&| zR?gFIGA)Kx W%v ziH<}Pt*0g#rFiXG^F=l0kyAK@U}7H{1UlOXe|Cw3g^WoJdzG3OfoNw`WYo`VeQNNw z2Gw$3#xa}pVu2SZL*4UQ=BSeHF1W<*$9JRb1(y|HRG5RjJFksb64iuugYyfF|3;5{ zvFKPH9T+EtlRH$8i=Q7A>*8+eyA=E?>X_I*KIv7!FVx)5r9{P%v;KBf7(`alcdv)e ziNvRt2d~#N#=|!XO`oH_qgN)*s)%BdQT`K{Y^y4!p{ lb9BckuPrLR(B$mW*A0NWF zsKczZm0;$(Md}jvOU|X4Isj|zf-7N2Q=o~}kD*Qd>I=t$Eo9O>eV_8o piV)ZFKlGvMJ3hBn%}tF2vz|biyJ?_N-u|;b2yb zApvb-k@M5~90{fd?S^k9W>xjo8q7~DbMR2K!nRj2W&h+uC+f9bhPJ=bX3}u1uz47y zsujEG2Pyu;MCB{5-35^uf_C&J3-alPCqLLegG0;=+MbnE{SQXBnLgC#oLX 5pi ziZ0S%*SAkLzw8jRo0gS{KCVbhi3?^hyQyB^mAJR$1()BbJna9PS2<9hJ(Q2n#Nqp2 z#Dg$vYFcqh;Fqjv{d$mi++Xs+Hf&iJWw?5IA)3y4ET#Je?VWU8_=}GPDU#s?d-O5n zzKc)M+A+H0B!;SvSC}+;m*`--X1;}?N!cmN>{Jd_!M6OkyGJpC^(SjL1E&ab`DvX; zyE>UHv(BTkC&+*9)xO_noXXpIrz~8w6yJ~aMb`+*cntNK=znA5uP~VRex>VZQR#AR z>aTAJNT7w5_sVhD7%B-Y2`hL84YF7<4a)X%=vo2DsR>hR_jIZFx4+uZJXmgfjf474 zsNToNX|K$>tntPIgLcmQKW$H>KH8mG{s;JCAe8uFQh{910;&qq`rk8uyy29}hKIhe zI*=CN+1L58O*9n=^7a9Jmx$IZL_0N1>%lBa!Tt-tn-u=0Zl5%jO+CXh?J80av~+mT zV)K6hVfMI+W!5dy2_)Hkh^r`)urbEfK4yaEY8sUu26?V>R0A&knU&cfBx~V+(ewoh z+%gFA-(d(JhWX#kIC%LfGQ-kGV}DsG?F#Ct4Kf7?+)p=5Jq$Ed3a@5i0Ug-+$bYQ% zuvqRYz$T}>mBPq0bN7NYoTjGPjBAvHtPBg#iGe3Ypo13)n1%zPM{zqgrrzQfHY^c_ zv)%)#b2|nU_{;y8;E?^imf(+?)!^2ma(Il o#j?3z!EXjzWG zN1kaO9ebn8ol0W8|FBt^Zv44S#zw!)hPgg*A1yy#OX=!k1L2*y)ACOnBt)$LIn!=n z>T{3u$KVNcGW*g&&V#YvJ12^MqSs@XjkiOnr%&qRkc%-BI%42q#G*UP#?6j3nDIng zUc`b_p|}EEHBx^{MO}Hd_mbE~sa%-{j8BcqspuN*f7B0n(sz(=WyzvR;r*m(bZ>$q zTk|`~(9un8B-qk!Lq-4mR$xG{RVco3?jI`Oh4tt8K6v6Jk<*#eN;hA+mg?c(om2DL za+@M%?!LWz5vhg%+kfWJ!FQ%%LpMUzcY`el^fAn&neMO;*l6D5#JnF%&mU)cYV!aK zQt^G_4wS=P&sj?K=s$X>B5C>`z|E#+W3geibes0A&s%O5R(IZ_)HeT@eobwQaXwvV zgU?^rTOr6}$UoYFt+$xnf=9(2lx{sGW2wYnz7CN&_||#Tyiz;r{7903aE%#k>ktYu zHQc|bJnRGQy?ol6CDAIR`97|o*ksLWvxU&cN?Fs$8TxRjl5 I~^cC|n*5YN~kxy=X9AZR&s)o&W(CyL>df~n!fm6082(jVY zS$wvD#a0o2T2U=mB)zHW*7y+)O&k;XaU!5fRIlKN5`v--Pr?RSqVDUUJ?*jN-=8Uc zxC?Uqbm;qI`tKQ@njo-C9EpoxEwrC*yc_K@V9~8yMOvEdX P)-MX#UCD?y=WxLl9S_bym5sJNZ(1KA#mn^K0Fcv&~BId#)cC-Wa zdT^b&Z}K}{$7tF5>rtA(V1w$F`u1<3#FsWVgkQmdMgIYQsm&ZMa5Hs#e?b?Iw^?Ty zwYUe>2aTxB12;{SrZp?0zH2MZldq-BjEXR?n|?`5(U{A;KGoY}e|Di2aJ~H5CFwJ) z_#*ty;@&Y^Go{9W|J`tdC@rNkSLNc+SC_hw;dP1-CRx`QLczfLMdBr2<5d0AtKbF- zQoPB9J74K@;(q#^!Frf+vG6G0PVso5_j*M5!~Ce@hc}9atJ(e=5ytWkn2QtUiu5Jw zhx3e`dDgO*_dqI`l2>%LOuB78wW+;xD)s(fZS+qEMSVFhJodSc&8g-+UWTIri 7l zHeK4<<&J6R3a8miy$2+&Pn 7$4*Qs~S4ZYrgJKpUEuW|3qoaMVIk41NV9Bf zbgJxdGkmk0h}7VX`_6%GKudR-)#P55O)W{r%kDMzufGgW*6#NoUUGf@yz}Y%L~hdN zhah2{ogG^Pj(0EBzxi_U8-qBv*$lnzVSR&M)`gUE@)0U-!%4W?JN(j%gg# GP^U)^2TCDB#?0rsZTo%jTM0(bo_xSQU^ zyDk}3;W7^}2>P6wk7|*b@XAiyAfJDQt%PcA!rUIzS pd*v7rhb`mko zJMGN^vb0p{J4dA1s#|rp+1IHYm3T@%`LgsMfJ$XtAi45|h~f6~PU>B|jF+wJ%hFeD z;nF%md?is8OF_OjVbedRocSuN6)l3>-_#`ZB%D>!CVPqExdDkyxKJNNY^hOm)!p0! z#jaaVaZ2a3$`dJJe^Sl{s~y835&O2@_Zt?f1&|Gk#RGM@x}QAv#;xgoj*G9fM0Q=j z`xz0atxk~6;@lAUqH8&)FpW(WIMTcbb4cCOkLY-fV@K0&2v%|}=@cKuB#O^!n!F3a zbxC0!IY-Y)h*+TY{RC;k{ZsQMZcoif(g?Lx-c>YQm=y${NfEqs`>+uoWklP$V)xj> z;zH4_g8bVjc%c9XQCv|k82a5lsMnSw+vw>)9l5jn3M5S)ADu7~7CaftJh+pdlucav z8? qDHnA?ilk{Zg1}Zk9BA;=jN*cF}Zz=77Lt{tJIMiW)HiuEEut9nmDWY9KO*x zx?`Ena3~@4@2$N*yg11CQWEsdH+SAt_<3dy?h4ZY9MMtEvd+=Ec>l=-lUEE{!nW`) z_U9WX_%dA^=kZ9`DX=oJaM+|&rV(0 %#k#GUU?M{(#9Carsj(nMS=&*O%e3*yIvzL&^~)f(8pR=2LD? z?Lfm*Qc!){pP##@&VMRX@5gJ?9G~H}SD>khqcbE`?^ux6uPl`Q_DN+-za&-6oRYMm z)3%o679 Cnrb>AU{p&MYx;hcy*&pUM zX0*HnbTS}^B^0uXWx{Q;bc%#NzoMEC{LpnhJ^ic+Ma<2WUpBD7J#l%#n}Tj-J{nJS zEVyT-T$Jq?q(}EEhj_c1E7fUmJXLN~g+S+;JTl~T$e&eYHLlSJI=&+H-M_JO6mPvH zukiy`uUDI#QU#7?8uUcQ5gi|x@xG5 2m1ByVPJ-2HQNTLTLs{0Ubg z(YpNzp}OBz5YuYkkPc%Fru9Yz!lw3(10_dtwXlr-o`0`RE0U{Z{9mt{*;fGruQ2y= zpEQBW=S#NT^Z4Gj^Rn9BwM&G(GIJeHvD)>*Oi56IEg!=T*inyvOh_A%?>=c#hn;N| zMLwyb>QU}))LtMs(6;jXHZfb={4P*(Jm%NKFNb>*6`tmQf`9(Q)XbIsvXIY_Iv=3+ zMM~>(epEaQlZaXGdNqQ0MKXM21#rh&g|}<{$z0_{Xn%X%AtpY^KK?kw@A&VNz|$r1 z8$q?T4-lq>9t*q7csZmiW%z;mn~3|8$*JEgR|h4TyBQMB_BT6QZoR4m#bH4R7MheL zyhc5KpI)r|PeoQa>8gsUSMsp|LPoxA_M*0fv41U^Zf7&0gEXvB?l?!KQ&jwJT;(X; z0O=SkCUy_0 7H8E<~$L|NkiLh!J@ok2h0NDD(VY>xevNaWS)%_o#(L)*u!(DPay%iUR~tYfaE zi8;J(&t9En``g~j;)Gx_#XR9l>Z~LE>(JYnLdc)Up%c?hZ?^CudkDE> onpwI&_8%m!$$21GmkeNfhMVRKvtCTVmveg>s1zYm~+)<11os zwiI_Vi7TeQ9V6kLVb*_BK7F<;`+nM$K8{0NRl~w5eE_PPB99lW4N=Oy)%PX#9)-OC zZt7ERJQ$n6)Fg$lonDy{+DSULcFtp-31|F>HkGT|Sg6rErybN sh=9 zG-X4!KGcDH5+0Liu||1{wrQh@C{jK19EB5njr|ME=VWr}zdb+BUg@^6QmZy`$<9q; zdFvp4vlIDKW}~Rs&{KOobM)$q6BxhouvVL#m$-u2HGy6nsQfbhlMUx4Y4GDEKSl@B zt0#sMh$#o{I>tCWcumnLu0?|rR$x)Jt{GLy(%WhjHWNU2x?^gR&y^`BY}DY1duiWZ z)oNQwzbxYp4Yv^bc8#@Wdw=86{{5pZZ9Y+fB>i@EiP{8ZIGu+mA8&!V&*s~|c*-`S z*-@KAwwHq8_#w2rMWS282vaPn{($6u8fd%vqU?KCd?q%fYyCrL66f|wt*7&A7%Tit z$T@b2!$#HO8=H}hmtz2Jaf9F_AhU}a^ W9HqyG)G%)qSG2&r{Rm#-6h`n!-PZXB63Xvmqxn{bgeJG z58vZv+KsuMDavOr?Fs)x&{~kg`^m-6mOQ$v`kURU U1; zzS`sxm92j~74OIGdSWQZCE+BXFk%;fBa?P9YmWNI7JN;FvD&J#!R$BYQyZeX8o@;O z3_!Mm=9BcS%125&>M*}HPO`c5o!J7`s3leo6R4Uap-T?MS*7 $j*&Nj z($mXNlUj<+%2-Z|U$&_Y;Gf$7*py@3G8}N@0l;NmnLc1VLC--|Ye2#QN;{+j+1*!= z>85!l5W+0G?_fO85FFOW4HahK7AbLUhWeo-CzP=>0oL(alhZ%nafslJj)eN8pz(uV zv9L<6)0qzVF;+A3ToVdJ(W$#dxy#Nnk-?Ivur|7U5PKRrW}xZiEGc)R7R6kD(+7 zmh_}*jQ^S~>gLYoB*1)d5%i;-{PFjS@l+c~Nt0QCTS@QQz+3zBc^C)8g_VB0O{lIG zN=bdo(cBdOJ690a+gi6=8_Z&L46rIEYE`(54&Gva>@U+8IXdntcSEIu6nH({o0laY zVU{WEC-D5~Jj{GVMufzn%=<_3LppM+X^y*3?Otyp;bLKR`_b)S*TqyxC3&d9EA>(# zg`&}4uh@3}fW>Whg{Owe6Ys;eIc96arq`cjEl3Ymq_wn7Hw k!>7Qtnkr6zjSY;Qe4ce-7aCKZp}9tPqeDy9v|??yMp;w{wiZPu3TmT0 z8#!HgS|h)lyCNn=nHzr1rX6rZ6x(>I*{bJ1xDtef4obzc0K2O<(yio{0?~vYpbhBP zzCANp)9Ovo9%_|B&2Jxh&PnTgKi9IKvh1_ dg0y&iV?7eCuEmW!Gv z4?oK9VPB8SV*xgC{L*U=>0rozNH0Lwp87B7!WDhyj`-nxMPpa+3JB4D=m^=Zd zEiO5xK^ YmS+7U8R(BA`an&p$+!L_qOz60MEG#Q4H;OopOn;|cd8@f;>J zrlRO@g>g71Nc;L^MkhE*He|LZxj1Ie?8m3K0z#2pBhy5L+U7T&`}%N+o|lk#Y8zv& z5Qi6`)W62V%XSi>uUXu5r^T#}_-i+f3+oq~*7J``O|l&LzdAIJbE~L-hU90tP#I`i z$B+%P=}7%Cl+doHRF{Z;{V?;uh~=6d6~%9_HQg&G=J*Y`qYo4gKQdg1SkEh>vV{gk zsLDCv<2Y&fOEMr8$q!^ALQ1jsv6^*CoDq4HJVWazZl&w&hDKX3eb|n>o`%6xQiXQ% zg-PBx1diQLS;9eMN9x36j_2}WD`kMv7d?)a$yA%og3VXftPy6jKH4rfDHa)f4 9sSUs7OffUY eejX3I7~^W_$Kyx~W>_Ld<9rnVny?0c8;_CC z2Qkg@DrOE14-y27pBB)Z5f+aI5Z-ad$k(-sIe+4@^PiJS+SSMe!(LwyPcl6H{*hFc z$_UB7`LU6xK@bsY?;MFZUIcTDSL8TVDVqkWTTx`Z7C#(qX7lqOR1;o=mdUI{ly2oe z$qXIojRK=I15cfI48H|9)KuolIyZV66GiqpM)&%gFYC&j5%EC-bR?Trx`^88DROZ7 zkA}YHl}Cgc7M(vD=4xIVZT;=>NHRlfxvVdbzo1Ch`$j_<1UUg6nCE?oWb64-JR2A9 z2jED=x82B)a_-Jtyg9E(9yA|*4a3FVdY0D->Nrj}JVL69k@cA!%*J-BEHJ`(h>mH) zck9SLM8s+eJ~5p*d9%2>XAou;y-8MFPp=lDeL2!#PLO$Uv#Zf1Ea!`<)yg3jSyB4! zE|$iwOdT1gPve>(>o#Fx?+*FCvP3gEvUJDW%gdDkdpAIEWbf_QonaRf`5!>igojtN z{^WR!_;4*$?q=*^fLMgtD!Dnl{Myn$IZ?oR;otoNPT`Nsf5-1zVV}+_hPwx(DR q1#Wd8Vn9%7jU5oh(tj+>Xf~IXb~@UpnOb z GV1qc~R1r$q~1(f0mA|jgiMM7k$IULcQ77 ztTHvawfVz=-0bBAbBG;Cb#KcH W4qSI92*q9JY0^UDJCXDDHP?0zrJVndJeGRPS9P;2Q+(*AN#o`>4(0VMg{_ zJP-J}lk6QR+IevnadPrihy#=Ug7J`%!zPVw0=3$AQ9&3GW(xH5uxln3_ccLpnzj&Y z3iioj@{vut4W~z1st>lL72aP=NR54uTmzY*2xlS+HD^&W|6cYFJig=bb)4<~H^Hp+ zz_m58_%2_s@8+#ay^(}!&{+2~*ZCk$@5-8_cRsI57wyAElY<4U54y}b_Brp|^WhwH zyCx$POHxqkC09oDuQ8)GRfrH; 0wPK}oYOaekat;I@ZCMw<9`yYO z5Fa#>eb+W$Y9U)=9b!^^;)%_qX>R1orbt}7BrFU22xhrGoo(Vhb}Op=u-TpcmPOYY zpNrf$uIAXSyE+fuyE5IPD1@LECHxQ0uT8uz-t&lSk4XLvI?DgGeVn}keQvD5P|1V; zNvrRVtm`*Yq%3_F>CIC;X?A+LFUF|Knw|!0l}|vDUr*&FZLvgO$g#RWW^HJH#5I_7 z0q(Eiuly%F$P6FX^n!nxx1OBhtH;$a3{9w#27=R(7dM$EQ1seEj|Iud==eF%-o^S{ zI7VI{es2Gh#tjnI+eWVUQdRtJzM`YC5`C(KmAl(5>S~k_Q80^K3RZ*`dy;vCbl!=) z*Rj vAr^W9sDCI`e-1^4NQQ9Gy()9E z{+0dqn2!5;y_f^mSgs-^mcDam;NZiCFB7np2zqS&+j46yq1}o@S0jIKkwdowT6?Lf zwSA3ARR8I^GY8x1*$>*~@vN{|0ivRPC7INBsj&HO`I}XW9&n5ezEhRHd~hf5MlE z7~8=@)x70~P77}DdvIklkt0mMzZ_m}^*H0esjE3 Ui#Zn||cD=>p8V9_1LpLdHf?$<7WiPx|$}Z$vAx?CHYJv8&l4gRF3t zl@b#XihG8*yV*c CJ(9lig>Fgh&jPoT6L76a7=J)apU5AiNs7l80 (PIJS)`_dPPT>G*~XL~V~yn7%zz8f zdXX`e`CVHT4Om>Tvm#=$1}D7#WsRy>7Y|EtwLt9v(67xlGB=l?W5s*g8AK=X=23z{ zS6}u>(Y*@skAHk5d)UbqsH^R9%r$%!EOp5@M*b@#HD*LNzHwj-E|O+vChT zS`$~ofumSao?DXpi+(x`AF^OA2Wu{wzm>B$r>lhrjm(N041yR|gu#v(a^C8-l&(t| zk|;m9Tdxf8=1y0%MOMw@lZ*z7Ch#lJx0-K@e?QIiDl_>uVD6bSam01qDS6UPGYAR4 zP(K}i^dvO8%cQ7irkOpiY7DbfR+OYgaUEQ5ME*P1lA&cRn9 A(>x>Nt;*5667 zI4$idD=32I{X+9eD9 _K=ZjUL*G=H%#l_Isz!qhe)`bgcl@8ldg; z14sO dfZT(U0c!d^uMsz;aTP`{1FvC?!~k)t1&pAb-$O Ppi-g&$7_7+pIw<8-)^)W_EpBwHiK9r>r{|>Z&?}9b1EBiaN$8J z6KJuqC)NhJA6dUKz;RH#>tXh+doQi_wZd`n_SG~``eREln+ZdP4w`ODV7<^#dhsH5 z;1)@T_teepMIod71mK5V)x&PvzXvbDUD9{oJ^%rjq3}W&UVFnk8&@mB{tu_SbQt z3Hei9`qph{{j+cD^)l 9&QV;gNY3;1x*4=?W2%ZS6Ge>GK zM 6bV`s825KNY5x=o6FbmJ-&3{w&0v$FHA)n-YW}OfWQQVVRlP};Dv$PH zDPX3vJ&Lm1V4$Hv%+dGXVO)bq04Luai8V>@&NB$J{jAzZ5b!&Yk$zj{X!`4~$2{Bn zH`EvZ0j_7pbI)F5e9ok1G+8U3uW;hFI`-~X@%vf6^bJw6$wck{MlDfHJ G8n*E uW0F>$ zO5v_!7tQq|ZAmWHl=^;bR=qyvUwrjDroa2;?YByyM^r|-pbZ7?8eay`hBf-{P)vV` zGGAm4y$9ZN*AbJB@8n()Rn{VKO$L>Du=w9ku6EKjvNzQDelwMYxjLq{h-euV|F0L} zhkY2I^HOuCP5GH)J1AVE=7<|`G{N^p+zxj710$&{OOu%z4PN}m9-kX TJU-~@k`iZ-6ybde z)f4&cW5l4gp35u!FEMF#qel;R8K5SWKRJR^H*Y4)u4Z||OE@6Yc)svpUbmAGcA6*8 zta`hY6+iT9^QIXH)@RXZ*K)T6u4p)ft+-RGT6o z)F_;w6g#NtoDFA6cJ7vTr+}BG=C;#221s`*ggc1u7@h!jSCJ}0dz!!Y3?AQq;9MS^ z6|NY7U0+E`6GJ~W>|<9wM5Wm_Ru3xtU`xg6e0FB3Y9i-k{5Ow0A(itqz;*9v{`%Qf zVnwsRtvD1Ts5~)0Wc16BN~KquG1PZFZpny~dR-2&$Pognq~};c3afk;uoltU6i^W= z9w}&w4F}x_M4Q~Gr{J2`-_!}c&F~+%%PKNadWA1z5bI7mIRJb*(Oi-1hkZu2zltQJ z!FKNGeTDgJ?gvTjlfC0SwA H<;0PUaVD^_(B7gOJcbXkk17ADs6^mf%KfVTsieWA+~&c+%z5xB&9?Z6 zdZdU9C8 DJ;N>o8RngUuzy8Mh(pD~wX zy!=F %wA0!c*M23VrP(!i;8UwP_t*Z_c?y07|Ek$??j0+dn}ds$rHHzYe2@Zr3i zHNq60#Y_zFALQdmVLGeWZUEZ>p}OE ^Xv&@!D{PiWcvb;V5y~z_1tz&@>lh2@+PYH{GsA{ z8Va#y&)nlCP0vX49&TU=4YB0>q{)N~ol4ccZ~+5Zr4%B%Kp1=57i_UEKS6YsiO%$n zs`V=e2*$a{<9Z4W38kPoLmX&134qD_lzvOO1R?kcW1Gq|#~2Z2`8JLY<+Qy5ZFa)6 z)Vc!x8vXsw`6fCGu>-6a3hDy`Tsx D#=$hD{x-Uw#K8aP=+ z>9&*|&@JIW4KcwwNnkKECP#TzF2`1-PkfE9k*TwgMXg&x1^#MdV*~IwqkVI2RxN7` zSYi$}$=CYVjY!4jXYJFoiZ3J-n7U9yWCIO *y4+;_>Jj*nm+Dz424pIJv|9Z-=ska(#RIk9@U3H3w zw}ujEN=OKFog>ZS2DR%H)2F3Ac7ZC+k!yjf)&c30W?eU)O6Yb0J~|y(ut=0A=RA-r zi(R>`fhfVD2!ct-P^%ENZ_8VN`~feKX(}+2h|7(M{8~wl?b_I`L_T(q4=qWD^Y vvH_ zZSQG3zy=`~EqT0hwnTYek;D^-DWFD@!9q_FN&;k{a=$SEe?=}E38zxvN<0Ie%1pw( zRRE4dfuu@XF>m@p 0~}}r0}YuJ31$;wL8XAH3ECh$pX+d5N{ptHc3^0miS}mAe<3Yp z0JN2sbby0<*uRwuc(b0?wJp`4kh+J($N(z%%rXZRBY&RJX+I<2uGAGT()osThLiBC zE@f&9Q(T5`m|Y#{Gf|{MLc%JF{hCb9kvLknLbRh8h%}8d6v=p`D{q`nmqd!AEyt9n z*6TpCP;qc7fx;b3)UG?@CDMVleiyu$>IpMFU2Io~ VK2*-+jyl^6#Y%k6iwqn2-4)(o?dV$WLk}F z?6GoICV>uOqMgHrpMXrR @)doAo>NY z%62`~=FC*K{xAw{6LPqo>2ajF`b-1GyM~ut0jqaZ&q_pQ5RCi$tW^AK zms_!2;&^!PY%yF;s(=a)iTM!guePJ6=&!9bK}#l%fSA@k#2~~sGKzz!0!`NADn#(Z zjD_|`v{cu*)FDwaBwzJ={Qbx5ZA%anKOQq(BWHbJFZ-Dt&^5yN9&pbDPEEV!Kl`RH zGt`sXFl%WW$(k@5`|!5P)PG`IZFudhqG|s=Av=`@OYHiG?!F1nu$Kc|ZwR_f(ic+7 zVn6%q1Drk#6nmz@1wICZb7d!eXUG1^-d0Hj_1TjO PPvL#Wjj3W?qj~@fzTrOD9~Ci`ii#x^Q1HW-Ht5^a ziS0GA0f1Tk4J~l>qQw3S*{VdDV{Ff5eOXQL#Xy#Ty0YX1_f7DI+ciwWpyZC~Ak!gO z=SrJ5BeRh-mX;lde0+&q0EcWC6KUZUrpj$yecScYy;T(OrF(+__sUIe03NkLF3XxB zZ%oe-w^}cvxkO-+bnUOp1lKOHWLu2WNk~u+;wj2O z*S;efloBEppTjRahG2BM!Q$ZKQc`g!18aiey@=^pyN!IHV?j` $|CT7l)Rark_X-l^lWJ8=C^$TW-T4|hyTv}p1?pQ&$MRXnB3{` z7!d 5(casT|Tc3QT|_ghv Z28T* zneglB<{iVuBb|6TV7{=J+`a%At_WM=KU@h=I4iMzc+0N{BhU4uQZF9Qya!30EsWoI zDJMl4Qhk9BZNyf?H55pOl>AjmD;J8gVmS}A`0d^$=!{ukiC==n-d2_McLRuFm; yNbk1aObAPSjId37V-wkp|Mzm5*OlAwzn5F< 3wKmQNeWp**(I_tIkocR1y^lw7i1eWZ;)nVC64=xB>MfAf1hSA{AZ zo5pc~O!Tr3OD6GueU98_s!kg3PtENr`&LbLD6Y*iTXCiERCGA?lYli1ClCA0WR&1* zUgb69NYJ3v+kobA`D0aShu0ka0?~#eDuTicLi9{KdPSpyMsH9usUx|s!%z#tB);`* zUDbbj#RN3JO=)eas|EaDMdu%u)VT-ngGWILCF^Xdf!ehR$EAyTW+>87Z-S@{u(d98 zjnrGRH4`y4e6-_S+r38Uf*y^jQHW?uvwLk>)5=P5TLvPzvQmSRNX^V&&9%FG_V@eG z`#kUayx;fx{XXB%r^6J*m2|{kc?KUUeyG5X=-in4PBvpn#C?9Xi{8<`W0iYEB@Iz_ z#1zD!GQss&No?Yi-D*3rKH^V6fuOYIZIrW7NwdkEc>AM|PKgFRL!X^K;`Q^Bp4^AJ zlm3gNFsGVAKYioU$4C*zr=_&$BMLpRb?2y-bm!Q4KM&h(R^*$jj-!Lj!oClFzxsUW zt7!w)I}QcIzhsP>Zep{XK5Fg8w~t?+vpnH4CqAq`R;~HHb7%3}>iVI w4_owSD>9% zie7yPX#Ki_cG*#q&Ay559;9G?Os;Zu+ipOQk@YmW%?#F}Nh;22xHwkhlagAUk??8S zFphj7l#;VHRaI=rBV!-q5tYKC^dn?WszQ{WG^h-|Uov53RrB$*JQ-H} C>9WCSLh;l>LMm@-RXo-TYa&Ca=?MEyAsV)O!Y ht5?Q4|8d{J`Tigm*!+PS3U{1OZ+aiFPhb7Dva zpJ5fN6`JHxKsF)g5yZ;jxx2QknrnqFV>larl+3bagUHP-s(F41B`8xlgeo01{r~f* zb#p|&i7wjezItqBnozDJO)18CMS2z?b?lZ4{gGjX&I +pcTO?}TeQ 9qxyA2Md`1A{8hI7cFh$6Nd zl`gqc%=}LuIcf^^UK4)ZIxJv*XORnnAGQPlW+Pav#IbQmOu}SEW`XnlSmu&Zfyu3R z+>$?whyx}M>m@5yv92CM 1ab9=t%D zV5nsYpfO}X)XyBm< 1R>0j{bY*n}Z0pW=1( z%TmH?fI;}FBK S8@1(st2IGG954E;F0o9;-Dq8B zWu)@r<`V|kvZJ4YqqWeNwKTTuSn%DZWn(X`lBaa`Fx0!a;ToV2sWejKgd57WCy!zo zu%ZedRnprRdfPWNv=QD0P+JJ )s72b;r= z?Sb*E48^Z(H>X#>>HSj HakMVno|ohNT*Oe3Wq9~Cb>Y(JsDQ2mzXtje2^hhr5etaCf3 zHl5zT<(B-N$g8yCOBP8^L44lEf8V2&jqK=s?Tytp?xshL#MnnCOHYOm-bqkzVm;5o zZ+Grbv*lin_$(heqIi6^06KA^0P=qs9Q&ke#ql5T$2WgD|L~{Bug9JIvp-Y9R-4v$ z@Cc33uNL%C`x>Bj(_7yK>Ufbi|7#9 D*bflk?kU+Zzs)T4^tAvIMYY>xPknIHeWr znoS=tL&dHh8dL)q&5-z|$(`mI0O}akFO1oZ>45`!3>Xkk%%>b^zDT97LXqxlq;3)E z^vEvFc;W4A+hXGHx~B(}L1e7t^KP6FU^I3HlNXy8k%=PBAV4dZrPH4X;BKM!*3P}q z75jEof@Z<+1HBilOi_JEbf~i9ZF4D>wot@t5t0rKJcBWpp^rXm857kd)mx*hyK~>@ dvl!POjd-~^7gR?ioHtm9Do3kC+r9t1_kW^w_gDY` literal 0 HcmV?d00001 diff --git a/images/avatar.png b/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..0630826687958e8549069e1b2c329d85f391b4c9 GIT binary patch literal 36050 zcmbTddpMJS95+65Y)+|Rm_vw-a%z|)i=2kB%_%MCWUFM9!$i*2FsF%@Gc$82$}uU2 zW+=)?awri(Dmhd-J^Mb-b3M;>{r>v>e)q2ZvFpBf-}n2zKcDye{W^U9P5zq$>_yu; z*a8Fu1OWE@58&T4z#1STBrGB dU>S2S!FoT0IIPAZZ2#AV`g2X`kL7@FGML9*-|L5&rH$X-V zcnx?>P(U33lo1e=5%~8Mpb7v82=njuzZ?GFn*dOdf4`z)dqCp+16uY1fC7SoKp{b4 zVId*@(Q*9!03jLSeFqFri0sGui>i~sMu|D)Vj5OWJ+ipbubM{!!jtxZ Hk+`|0l5j8`l&- zLQsJJ@&si7mVmEc;Wpq(W}M+>H;e fuFqnKDXHQz O?9TMxV*|$e3+usvMIyv9MNxlSNhA>N(gHB0jf333vjP$& z5M_#y7BGO+xEShDgd>%Jr3SPoGY> (& zMZ!^KhmHUX1bM~DXwkAV4~Q@_7N5YnsLZa`mErv%&XH|J3{hTL;BA%W&J#>QsH?6R z*sZB%-3g1Y*r~a5-?ZwwzzlC)C~%@U%iD_lj>^$3#-KH0$z$NmF-Ck`$bu|U0qrRR zmde)}brR*#DWiTk$>xh}`J7~Q9NVj|+OGLZmG~NNK0!3Td2frx6dc8|moHKzW2v`> zE0y-DxPXPX*#PKLcH>9Oqa(A!V;%|#M{$b|)otGUl~<}UAIc=~`G5}~L5VP1;<4vn zVU3qnd$fuKI7Y|5IeWPpX5g(lq~0!-2WxIjH9R& Yhalcf1@b?vtAQeIt3P0cs z^QCwbJSLEL4^$L8P>BE#mTHLFsxCO8J!9yI7GMfa6sM@Ms?Enx;4+H4mmZSMK~S@? zoP%W)nFKpTXY>H#J#Ad22;Y8VV+}1o@!xA}0A6ZL@bkkZI-+*UtO9w7j#R*Qb!u2e z!VwHGQOx2a7u-Id0Ixb=$N~QjpuwvI0ZeWeOr$sqi|K<1>%qIRR3WuWa2dV{CCL&f zp~n$dMI;3-8%Tx0t$vMtg^6*{M)z(YM*{SO#veGYP%IjBu2@`i>%aHrm(? z7%oEWHh?m2eI{THt}@&&p>Lj8$3Lz5M%0wN*>Z<@ih3-ZxS_^H`~` tqa{XY- zOk2Wbu>5Q`akPx1=CG|EVBcLGbHT-8b?H?P$28=Vjk% 5{Tk#?48=WPB&UV(s`mVar>@Z$8n5 z18KlMMc@+GY~gccw*!S`g5&s&sUlBaN jx8ofEK&&tERoMp1n#~9%wVa}D#bPY;(7NP(hV|Tj>1FcbCTzo06&Fy7X34S|ezxcQA!et6}Y7jPu?WsqxZOX*@U!LlD zapK;20CQJS=vHmof}mDv`=NG`@<9!oeUq$n)HLlwW^A|Qo;7FlEdP};TE2FuvoV?0 z0=@TJJza7iA_1;m<9uXa%sfl%xf$uwo8*0a=F90=qN fI7t8k%60eLx6d{hSa%&3ap>8plM(U#%DR#GVGOQe9B?&n z7>0WiYg`AmZaK(l4@EKkD~izVOU%hCyu?YU#8|I^&sIGyQqi6N7zGkJmQb?2yjF^q zO5nxr->Y(w%MGVwKKS8J62>~ );~0 nLWK!=A(` zI*f~`L&IE9jlGZQ0Zfh>__MCWv{g;Nv!x2X5ILVt?G2=cCsN&gz2irxYW97$YvW~B zR{_Sl9`clP @an(CJ)XCgG8om(|uEdwxuKZB@eBSsYs{fBk>y}l8hg7$e9jmdbK z{iMMOM5!P(%KROOSr62`Boeym{(O}sOs0z5ds&d!v*Or=>N)0So?N1XRm@2)p)BPO z*}GWSSz|V>FIR=2e86O92zH<>C?T_o5wLg@SSUMP@}|y}o0x~vvIQsh@?v5XfOlmV zs5fjQxM*wjlj_L|$~LF%f0Z *1cA*^V>33hih~ls34@S*v{Jl zOTkwo@G*W7_J>e152-mC7OOUXl_CYhf(eqk^z2d9^6K?%#FGD-^P6`FEntG-1YNp- z>ZY)U6IO -?TAkAy#^$|`sw+9%YPbQL06Lvge 8idwvSW(y4KNb60{3$~RjxaiOvRRv5>O-tpHT+Qb28yImK zov<$`7WQTZmxu;q!|PFas*Bnc_Sb75U#4!7%_aR3qJ$2`J)z^+*tp*Y?^dc;eRhVQ z+f9|XI^6(4j+(Ex^W-4|ZE(`jLeQCJ!r7=Snhr3N@r3r4J_ACbsN`Y9G(0rI!+s0Q z9}*%tUQgvyD~#-f+ xbW`7k4Y3D)Jxg;ym z6PVz*cJaZj8nnZpR(@|!^En1K>wP~-WB_@XaWX*K2mTIq=x1)tds|`kSyMaJOvH1s z_w1W8!5=oza}GNx+LGPdteC{>L*v!6An5m++=IC8zhFs;P1+}wSK+1-j}%ic_66a% zp^ 64^}`>T zMpP=VTsPA6n9rOUi+BOXJ4oL-G5feVAd88N*k<_?M3o=%vPcH-Bhy_1 Bc zQOp&|5?Hf8io55e7(%21D#4(z>NItl4yqD5W=pbJaVVGUW(x8$V)6NLM9BjN=Hpa; zngiEb5KX+TYUUFxh-*|y=OO!GbKX3^jTa$v4E9R!^SY~)Ax7LPP*Ie`zrw{z;ujl^ ze^&>SW9B(Q*JsKrp6O4-0O@d6s&);NJeeg57GM+*neJtWY( r+WVwbZ+cwLW~a_YqIT~ ze}WlipSk~%_q9eo%xO^* =J@S;k>Z{nEymr?Kb8`!NoH>`Z;3*fp=jiGot)U! z$8PRCh@OF=P4{q`dc!8@$%8EV)mKWoB7?F4T0bpTfHskto14Z*!Z4>Ey2 zN%uZbv~!-m^A+)u@XjI8H~#@ M> z@bK_=T1;Kd$G}5mj%H{6aCmv )dpt1@dXnpafJn-xY`K#--&~Jb{&H=L84ko7YxWKW3AC~ zY;$CF(nz0;z6KKs|KiX*CR7i|v5jGZVsYylOo2#+Emj;6u)|7RLyZZbEo!(8W_B%n z%-(O$k}Be@FNn~kz4wb}3M%W^AlK|g!HYgfR+wxY`x(CjZ7z#qH`zx+HyyUiKw)gX zdB3q#LByN|tWisC1ud|G0&jz<_JD0xAdfD9;RxCXOOv9Z5fd4a;5jL{3C`*wxSY%@ z5e0)BXnm0z92mc|fb~?&A*9BeBWN%kmx=stxI&af(aYMWAGTPj+pMrAubz^vgEfL` zMX%CS=%bxw74OgQy>s=hqzdXwiB531u=O%@E&@H@_~de!@X6;NvX9{F6WBuWSLBwI z4XynBGF3xeTqwA|zMA;a87T^W`h48t5`P?H4HdX9= 0c+ zi!)^K_SXA|A8s$($+^x}cih5|#xMd6)~@+Xx2 >jWa*$R zR!?b(-R?2o+lCxu23bCp64p( NRnZzL=;_29pvnd1F6h`~jLwk|y#l$0cd71r1- z3|ryw2_4irdM*Vl;4+O;=Ey6jU`yz~0}_z)bg8hA$H|RSMT}^{E;XnhK7kpYD-V{o zMq9vboQDK3?#ds9K!oK)t3Z1IOMs)cL=W073#eJ3Qnsr3+{9K{h%Xt F%mA++m9< z)J$3P(i4@eTzG|tBfps$2Ns>v2ERZiej5#IBPYFiQ7viYcux7K+mjGK13OFfJc zuMykg2j)%OpP9aQ3P3F|GN)|Uig-0wR82zKDPkRgBhL0U(nI5&kcebzIJd-MDK9Qj z{bZXjo_gC=Aa&j8<_%Q?_8kG!{piIiQ01}l+R7GGR=*9bv`Ff4Gtl~WI0cUm@@oJ+ zZFTW(ks U 7sQ_mHyzsfw1h)>G<#MahU z0BdE3-xe9IBIHfnffziVosp)z>?}T )&ja6=GgX6(=-@?naw&5&c%z4Zkr>0Eq*Q(+qPF%g{KY&k(@<5-dEa`Ma zn_!ETT8>*$nArzh+z__0pKfc!(ki #C7FvkL zuL~63H6O-4c={YAtHSI+S{wzL?E7pJ5&zg9^u*ddv(IowtmX`ICKhiR7DgF28M5zO zUW^X4LCju@T{8AoL+7AdzeP@6Q`{doIWCTtpQ_r!1(&H5Hsl#Ds&imMpzM1sFdsA4 zb7&O`L {NawD1Ek7c7Pf7A;?c^yqyiCjHSdkf6jhoa?*lmYdGwrp}^6< zb#(N3a!$6uR8b|h&{i7pUV-`CSOg_}Od6dx#L@wnRTf^&$N;&TGw85&j2LC~d0NFl z_ppGOI&->>8qP*^q~$2Gz+c-}sL?)Vh|V}?U3!H9yb?<0RSEJ+ zzOPJok2xt(G!-TWU^J P=Q_&!K7IKHm`=5?r~ zH^;C@AJeRxWo4X*p4$c%d|DL+UhfdA)PZkcRBK0r_dKO}^|5q^?PQ+>Yu%lbov%D# zN73V!?`d-t^R6HL88c-Rr?aMlH1=MZf<2Q6EkB<05I*Cqo{`dTp>wmn{jFD8cZ0o` zqs-x-Zn&fU8Ng&l)$ZQ@o)``7&%5923|)@=`4sRVZ0-T~8*M>W+~+6YbLh3BksTo0 zcT+;zw|4r8+U+w;*OK$a#H!ohMdtqImkZ<+05%-ycVj==A!6ShxJhb~?9}-JS)l1O zHz5vp++kdrny$%%d}oC|eb+&nmp&pP+F~LeA?NkIin?oLz|&XxLtI*Zex|OpLH=c= zb?E0uji^-}CCBd5-;*0mR!ss^d%hdH&z&Wd@=8yfDY&B964F*C@TX31YI9K2G?YHg zwmUIV2R3)$Q(lf20k6+kKZjB#l8pWo<$P~?+Y#cQU8IN7YW=i6J!t-67wbR64zFsp z%=dH*d$GCS^Zk`g1>d%-?x(^-;|X(@E1u@{rfKBQNzvbIg#Cz{we)@<6PdO2$HvIY zE1}p1&}^fyZcs@0qa$`fxknq=Qnc<00ggA>S0+WX?;j?ev$^0p_vam{^{aZ#)OMtZ zbvyZm)2mZ%N61zlvYj;Xez?_GFVz6n5orzJ^TuzB0A~CS2``%lFsBmmy*riQ&vJ_L zP0Z;P7j6SE+sn_}^?D6>n@b5hP9JQ7I9;g>VcUO#n=pu}+1-_hvIrMof<=vN+I)d> z>M{KWxQ|)cJ`FA TsgH>S%Nsk=D)ZS$7^>$M{;K2tMPZ!$?(9m_JPzs91YqfMuu_Mm)ku zI-Tm#gDh7IeOhWbs8y0oYmtZob? W!+TnrlUL;9B XiN23AnM-ttogMVmHB^R>x#x8O#?dY_R@ ze=QXpPi!=PU4RzD3PsLmM~Fr6=z1bH1|?F?YFDMqU7=40-ZnGSq|DtTkRQc_TzR1o zpJq+t1HTF{%lg>lk^3zbbB$KbK2XMrfQCQt=-#fO9 j@RM~ao1-T{>ZeBH7tY_ z5POyz%q5LJUe$jdUB6a0BPZT(&balVQ`UXWO{ kOh}&n)Nq<^q45iYun8cpiY5L&$+x$;*E)ig&wBlUQ zD?7)!Qw}qcyPlY~jptGv%Pb1>aynf2@%o 2yOcZbKi`vhD_BAyuaXT0N zNN2}20PJcNpJzKV3#gE2l4vzAHV|D{tdJ5oKWnTla7Ywf_f=H z0)@~fw*+jR>~Pl 3YGQ(%RnOr77Wh+7WFaQML>nLEU=0$LO z^?~LNL_5?KP!_d!Kd*q{+QAYovo>5oi$cEOgsY}kQ1!+he9gmZfeG^lWU$wr;S;Rs zH8kK0YOfIDYw;0WDpP#Trr%cB%#LX L&vLRB$ z>FA32mgn3Si-&HlO*i_*mhRFaONo$ms-EuY0MoF&3pMphW4f*L4!fQwG|dY{nV!FN zc1B>B?g$LDO{p<9_Enwzz+yq-M4QK`z#>UJ9hH#HRVHdtKRU>c6BRSNGXhu+$r;oo zCegH*`qX_|p{oC-*zxvi=gjPPrlC6S2ik!T@{4JPgU0bjJtzd>5PI)6vh1!11|CH& z8JBn>dsLi$cjI&DR=H98mTU+&k&DJc5QNB&egmo_7naRdaMJ_W`)j;=vnlRsiJh_q zf^xws*JT}MLH&idueJunyB;}pf7ZP9<2N@lro?1=Br*$49kDs{PM;z;DCax|UjE{K zK8AT~Su(r0Hh3}|J?Dmf0YSQwtzhFbwMO;>HE%EHKy>wGJ?t6&H+qIW^BjxaQu9J7 zFLPxl58iLSjRb5m^4A|dc7Nd1Hg?df5Ruw60_)SVR;9ZZ?sJH59c Wg< zsWi&0^DG~4tPIb?o_=Hd$!M0iq?u&v;oR+d+d
pl DWJc^z4DIJ-;7SFQfGYE+9)vuowy44%EGj$`Psf(~npI&^C! z?!zbJF2r+MA}>=h+WbjMzeZd3lwUMC08FA02fEQOjbTS$PN<`}?^)xE>dGU*%}>Ma!E?wR1~_q>#@J zt~IU}t0}9a9X%^}D^P|hI=ed;=%_;UiqtvuA!9$zig!iTJLBm-r$}~sb~_k&vLbXH zt!3o`aD!bJV)$FH+4SfZ!vw)|$ckHRLP`p^fe*Lg-DJ$7_Z*yz+Tp n6a`*u5h33?C!^O;qPqVrem8(e(zI%Vo&sni};#jYx;G# z3SZ4uu02!1mn?uOpN``WcVz-C(RVJ9T&2m@QSxx<0;gNbeL`f91dETl9L+v@_2Gc& z-mIZY6Hv^F)_WHgz;j{>V1gMMey(}*=XmwuE{gjjC_eJmvC-$CgXaj `9KDY|;UwploF`FBJh7FX*sY zPqbq39yFAlkMet#|HAvNE*G6SC(!|hOXn-(T7!%CK-Q>tTlaJt(&ARXCP(ax;EKu` zrjxFA*_fx^cz>o+u*JPI=ex-S#COHR%_zwzrsiCNQTB=7Fw9hHr^4u46R#^Tm^S2# zx{GhxEgnoYAKse3mv2StJB9uf6=qS}kzGF$8tViT3wODMzBK_a6Ac-#$h^d51>S`Z z2k<2J0cl$iN$F-RYH%DUx*AeUGx_U2o-Wpu_RNnme)Akipw*Z;lAK?3xpkzWfBjuP zT0OkfBud5XZpCT0;P-sp@jB*usJ&$|bb!FpS~6Dh3Wqm6l1W;pnS0`PFKhZ;{QbcF z?8}9`s&n_e&9Y$+N=orA52+;9^gB`I^R>s7GW1Z(D-QnvV!PVvN6oT-VoXj|XYUp0 z*{OMoGkyDF;Y>Zl6+0wl!7dGQhu(G8ogtZ+$&h{wEmN~k&6v*O&uC8y%*6ZS4tMIj zcl+x`I23;GTzKU{XLa40Dd#Kif{y!Oq9_411mfK{feNarx=C61z1;p2Y>n336Fqgy zF1PK5o}@ZLkCd5ZT{NflpPLL;PCMRQ%|jjG&XHDr*)Gtofr3hjJ)la4p04ciq0_v2 zPPKB!g;J*NVf{N5jQh8yQUV2>b<1nA2hX)cS8J8Ff3R>0c$l{s35s31YZIuhrge5H zy}YWwYux^LB)82wwMER%X~MuZkakUxVICQnp*mD*>VGg0MS{Ki_IzQ@ac6i&<_Cys zu94BEn&~22CJMktn(yO~J8L)AJSxqvt2Wq4-ltW)s&alLFvW=L?XEQ+O8~dCgy)O9 z`8)@KGS H}~EF=t@tz%NaEFc79-mzigK z#X@5OF2N8pjR6SoXI>fEMVW7O0j6Jv71v-#oGlf~Kzb&Hg-mfqJiLcUum&b}vjiCK z5EhL{h=j=VirXfNL=olD{8W1&(FQ5-uEET+3kiDxzRUG5C-%h57m0Skz&;C&!;dQp zE_K-p!}yv)j>^=3t^{zU^3O`_8<~sD={5XCur%3*Gi381!*%brF^o%bm6UG`&k 6>?8!r^d>gq=#ieql!{IJpQ*4$l^| zb3HoxL2!ZvEdt@l6Z}*Gny#RM@hKz`fUJRn!m#?Lok4ic1)uRWgIM>>BHqQGc^Y5u zd9FdM8GdYQD&r|D9f2`C#~}1B!VcHcVt?`O_|MjUyk64$*rfs5UE%u6K-tnev!QK& zuHi>r*OO1YvA}DW|4R^tQ!}uEP?B*B;JEi=yOz)x-ANF3??>2@l$+n@&JTr3;N *|L2~D<`K&nCFutHhvBM^ivGGkNTk6H?ToJvofQ{SK7jwhxNxhs zGCe*%|3NeE6S$-Br<3Lb`sQMdD#0!M^o-!QiiOr#&l8Ku6N#X0L`NTeiB@d>`-RT( z%frVHA5mQsH`w*GSI3SLPo6Jv!!P>}1?Ux!`U*GDQ^t2^G_RIYrYGC`MUK;x49vhC z*8{aIbPhV{*&G-R4BUL6_+|>g OxGCm#Ep{Y`USvir_2rE;AJBYp z-%ERkey<1*H;K!&i@ilUdHAQN9^L~Jtb2K>pNQRo$u1ysUi+51_!?#k{@zA!DoW8) zp>3)N9LNY$IEHWCshG=A8AS`*^L01f%nAcO#HZxD25J>IM)F01>nijzGPS^u&nBY@ z9GRMVx&)a@*S~;E;mG@)6m3ABgRoK-sIK(8eDhSAM$0ky7uzZQC38YZC~ }NMkG!oOgJ>!mqSTQj4mbSp1&jiYr|ZwdnI4<>ov^V?WCt? zChs~+M?m;~q1vg7i;v6C%Ffd34nIoP>HGFu`gPZ7amE|-RkSr~hXry?E6yV6nM Z$#pbINu7vhL2btl`J|LQS^ssLl;d-7<(w2)^|lv$^rZ3 zZM}x=f=+j`{o(TIUQjMlRvLhNDCO#|Fi%f>%l!7E`lrv