diff --git a/404.html b/404.html new file mode 100644 index 0000000..58bcadf --- /dev/null +++ b/404.html @@ -0,0 +1,89 @@ + + + + + + + Page Not Found + + + +
+
4 0 4
+
+ Page not found +
+
+ + + + + + Back + +
+
+ + + + \ No newline at end of file diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 0000000..9f25802 --- /dev/null +++ b/archives/index.html @@ -0,0 +1,657 @@ + + + + +cyanine + + + + + + + + + + + +
+
+ + + + + +
+ +

2023

+ + + +

+ uwsgi - django部署 +

+
2023-07-04
+
+ + + + +

+ nginx -- 学习 -- + uwsgi部署django项目 +

+
2023-07-04
+
+ + + + +

+ django学习 -- 前后端不分离的 -- part.2 +

+
2023-06-30
+
+ + + + +

+ mysql -- null和空的区别 +

+
2023-06-29
+
+ + + + +

+ django学习 -- 可以感受到最原始的django框架是前后端不分离的 -- part.1 +

+
2023-06-27
+
+ + + + +

+ Extra details of python - for long term - part.2 +

+
2023-06-26
+
+ + + + +

+ python网络编程 -- 建立一个http服务器+twisted模块--part.2 +

+
2023-06-26
+
+ + + + +

+ python网络编程 -- 建立一个http服务器+twisted模块--part.1 +

+
2023-06-20
+
+ + + + +

+ scrapy爬虫框架深入理解 -- 未整理完 +

+
2023-06-17
+
+ + + + +

+ scrapy item赋值/填充 细节注意 +

+
2023-06-16
+
+ + + + +

+ requests.post和requests.session.post方法的区别 +

+
2023-06-15
+
+ + + + +

+ 爬虫遇到robot.txt +

+
2023-06-06
+
+ + + + +

+ c/c++ 结构体嵌套 +

+
2023-06-02
+
+ + + + +

+ c/c++指针 +

+
2023-05-01
+
+ + + + +

+ python打开最近关闭的word文件需求解决 +

+
2023-04-23
+
+ + + + +

+ Traverse a jQuery object and the differencebetween this and $(this) +

+
2023-03-23
+
+ + + + +

+ Catch the enter key while submit a form +

+
2023-03-21
+
+ + + + +

+ ajax CORS trouble +

+
2023-03-21
+
+ + + + +

+ Some details when using URL.createObjectURL to preview images on the front end +

+
2023-03-18
+
+ + + + +

+ The error in document.write() method when its inner is "</scirpt>" +

+
2023-03-16
+
+ + + + +

+ the effect of the "mask" attribution in css +

+
2023-03-13
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

2022

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ 廖雪峰网站nodejs对于静态文件的处理 +

+
2022-11-16
+
+ + + + +

+ Asynchronization and single thread about javascript +

+
2022-11-14
+
+ + + + +

+ js promise +

+
2022-11-09
+
+ + + + +

+ CORS +

+
2022-11-08
+
+ + + + +

+ Callback function +

+
2022-11-08
+
+ + + + +

+ Anaconda can't open the anaconda navigator +

+
2022-11-01
+
+ + + + +

+ The difference between pip and conda. +

+
2022-11-01
+
+ + + + +

+ Anaconda configurate the resource of mirror downloading +

+
2022-11-01
+
+ + + + +

+ 'channels couldn't find' error while conda some packages +

+
2022-10-30
+
+ + + + +

+ Encounter a statuse code 500 error when opening jupyter notebook +

+
2022-10-30
+
+ + + + +

+ Some thoughts on saving files in the database... +

+
2022-10-28
+
+ + + + +

+ 非遗开发日志 --3 +

+
2022-10-26
+
+ + + + +

+ 非遗项目开发日志 1 +

+
2022-10-19
+
+ + + + +

+ Convert virtual conda environments in jupyter notebook +

+
2022-10-19
+
+ + + + +

+ build a static files server to avoid local CORS +

+
2022-10-18
+
+ + + + +

+ 并发,并行,串行,同步,异步? +

+
2022-10-18
+
+ + + + +

+ Some realization about 'then, catch, reject, Promise, ...' in javascript. +

+
2022-10-18
+
+ + + + +

+ Asynchronous and synchronous +

+
2022-10-18
+
+ + + + +

+ Prevent jquery animation to triggering than once. +

+
2022-10-18
+
+ + + + +

+ Install centos in virtualbox and configure the network Part.2 +

+
2022-10-09
+
+ + + + +

+ Install centos in virtualbox and configure the network Part.1 +

+
2022-10-09
+
+ + + + +

+ The solution of insert images into markdown +

+
2022-10-07
+
+ + + + +

+ Using django upload images and display +

+
2022-10-07
+
+ + + + +

+ Upload files by Django(Part.2--without ModelForm) +

+
2022-10-07
+
+ + + + +

+ Upload files by Django(Part.1--without Models, Forms, ModelForm) +

+
2022-10-05
+
+ + + + +

+ The import system of python +

+
2022-10-04
+
+ + + + +

+ The attribute of form: name,for,value +

+
2022-10-04
+
+ + + + +

+ Django's logic for finding templates folders +

+
2022-10-03
+
+ + + + +

+ Problem encountered when building a blog with Gridea +

+
2022-10-02
+
+ + + +
+ + +
+ + +
+ + + + +
+
+ + diff --git a/atom.xml b/atom.xml new file mode 100644 index 0000000..7ad8544 --- /dev/null +++ b/atom.xml @@ -0,0 +1,1881 @@ + + + https://cyanineeee.github.io + cyanine + 2024-08-14T16:22:22.282Z + https://github.com/jpmonette/feed + + + my personal blog + https://cyanineeee.github.io/images/avatar.png + https://cyanineeee.github.io/favicon.ico + All rights reserved 2024, cyanine + + <![CDATA[uwsgi - django部署]]> + https://cyanineeee.github.io/post/uwsgi-django-bu-shu/ + + + 2023-07-04T11:18:13.000Z + 以一个最基础的初始django 项目为例(不涉及数据库) +
    +
  1. 将django项目的环境打包成requirements.txt
  2. +
  3. 购买一个服务器
  4. +
  5. 将服务器安全组设置 设置入站规则全开
  6. +
  7. 配置python环境 +
      +
    • 本人使用的是ubuntu 20.04 自带python 3.8
    • +
    • 首先安装pip - 注:如果E: Package 'python3.10-venv' has no installation candidate说明你应该更新一下系统的可安装软件列表apt-get update
    • +
    • 然后安装virtualenv
    • +
    • 然后cd到想要的目录下 创建虚拟环境
    • +
    • cd到刚创建的虚拟环境目录下的bin文件夹内 source activate启动环境
    • +
    +
  8. +
  9. 配置项目环境 +
      +
    • cd到项目根目录 执行pip install -r requirements.txt (一定要在上一步启动虚拟环境之后在进行环境配置)
    • +
    • 运行结束即可
    • +
    +
  10. +
  11. 安装uwsgi +
      +
    • pip install uwsgi即可
    • +
    +
  12. +
  13. 配置uwsgi.ini文件 - 在项目根目录中的同名目录下(就是个默认wsgi.py文件同一个文件夹下)
  14. +
+
[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  #主进程启动
+
+
    +
  1. 然后cd进入到uwsgi.ini同级内 +
      +
    • 执行uwsgi --ini uwsgi.ini 启动
    • +
    • 此时就能通过公网ip+端口访问到djano服务了
    • +
    +
  2. +
  3. 停止服务 +
      +
    • uwsgi --stop uwsgi.pid进入到uwsgi.ini同级文件夹运行即可
      +PS. 使用命令 ps aux|grep 'uwsgi'可以查看服务是否启动,出现如下表示正常:
    • +
    +
  4. +
+
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
+
+

10. 彻底关闭uwsgi进程

+

如果没有设置停止的pid或者启动失败导致pid生成失败,这样就会导致有uwgsi进程挂在后台,因此需要彻底关闭:pkill -f uwsgi -9

+

PS.

+
    +
  • 如果uwsgi日志报错:uwsgi invalid request block size: 4937 (max 4096)...skip
    +代表请求过大(超过默认4k)因此跳过
    +因此只要将请求接收的值方法即可:在uwsgi.ini文件添加buffer-size = 65536 即可。
  • +
  • 这样部署之后,网站是请求不到静态文件的(css,js,图片等)所以需要再搭配nginx进行静态文件请求的处理。
  • +
+

uwsgi常见报错

+
    +
  • +

    1.启动失败: 端口被占用
    +解决方法: 更换端口/停止占用端口的进程
    +sudo lsof -i:端口号 - 查看端口 kill -9 端口号 - 停止端口进程

    +
  • +
  • +

    2.停止失败 stop无法关闭uwsgi
    +原因: 重复启动uwsgi导致uwsgi.pid中进程号失效
    +解决方法 : ps 出uwsgi进程,手动kill

    +
  • +
  • +
      +
    1. 启动失败 找不到配置文件
      +大概率uwsgi.ini中的chdir和wsgi两个参数的地址配置错误
      +解决方法:修复即可,注意chdir是项目绝对路径,wsgi是wsgi.py文件的相对于项目根目录的路径
    2. +
    +
  • +
  • +

    安装uwsgi报错ERROR: Failed building wheel for uwsgi
    +搜索解决方法:更新pip即可 - 更新系统依赖 - 安装psycopg2-binary -
    +都没有用
    +最后:apt-get install build-essential python3-dev然后pip即可
    +stackoverflow

    +
  • +
  • +
      +
    1. 修改项目后再使用uwsgi启动报错
    2. +
    +
  • +
+
    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名字"
+
+

+]]>
+
+ + <![CDATA[nginx -- 学习 -- + uwsgi部署django项目]]> + https://cyanineeee.github.io/post/nginx-xue-xi/ + + + 2023-07-04T09:36:46.000Z + +
  • 教程-8.04开始
  • + +

    1. 什么是nginx

    +
      +
    • 轻量级的高性能3web服务器,提供了http代理和反向代理、负载均衡,使用c语言编写,效率极高。
    • +
    • 一般使用uwsgi协议让nginx转发接收到的http协议给uWSGI(uwsgi协议是二进制,效率更高)
    • +
    +

    2. 安装nginx

    +

    sudo apt install nginx
    +安装成功之后输入nginx -v查看版本。
    +PS. nginx安装之后会默认占用80端口

    +

    3. 配置nginx

    +

    /etc/nginx/site-enabled/default是nginx的配置文件
    +核心参数

    +
      +
    • location / 有点像路由 表示所有这个路由的请求都由下面的参数代表的服务处理
    • +
    • uwsgi_pass 将服务以uwsgi传递给***地址(例如127.0.0.1:8000)
    • +
    • include 配置参数 如果需要使用uwsgi协议需要写 /etc/nginx/uwsgi_params
    • +
    +

    4. 启动/停止nginx

    +

    sudo /etc/init.d/nginx start|stop|restart|status 分别代表启动|停止|重启|查看状态
    +PS. nginx只要修改就需要重启,否则配置不生效

    +

    5. 修改uwsgi配置文件,适配nginx,修改nginx,适配uwsgi启动

    +

    具体如下:

    +
    #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可以快速方便的告诉你配置文件是否有语法错误

    +

    6. 排错

    +
      +
    • 看日志文件! 访问日志:/var/log/nginx/error.log 错误日志:/var/log/nginx/access.log 对应uwsgi日志 - 和uwsgi.ini同级下的uwsgi.log
    • +
    • 502 代表nginx反向代理成功,但是对应的uwsgi未启动;还有一种就是uwsgi设置接受请求过小,在uwsgi.ini中添加buffer-size = 65536 - 主要还是得看日志
    • +
    • 404(分两种:django报错/nginx报错) 一可能是路由不在django项目;二可能是没有禁止掉nginx配置文件里的try_files
    • +
    +

    7. nginx静态文件配置

    +
      +
    • 创建新文件夹 - 存放所有的django静态文件 -- 例如/home/mysite_static/
    • +
    • 在django setting.py中添加配置 STATIC_ROOT代表静态文件路径 -- 例如/home/mysite_static/static/
    • +
    • 进入django项目,执行python3 mange.py collectstatic - 收集项目所有的静态文件
    • +
    • nginx配置文件新增加
    • +
    +
    location /static {
    +root /静态文件夹;  
    +}
    +
    +

    特别要注意路径,这个‘静态文件夹’下面还有一个static的文件夹,里面才是真正存放了所有的静态文件

    +

    8. 404/500 自定义报错页面

    +
      +
    • 在django的模板文件夹内添加404.html文件,当视图触发http404的时候自动显示该页面(在debug=false的时候彩起作用) -- 添加完之后记得重启nginx、uwsgi等服务
    • +
    +

    9. 给原来的静态文件夹下新增文件夹,结果404

    +

    大概是因为项目中的静态文件路径和nginx的有所区别,
    +需要注意alias和root的区别,
    +nginx会根据请求的路径,去请求静态文件夹中可能存在的,文件夹中的静态文件。简而言之,nginx是可以嵌套的,但是前提是请求的路径需要对应的上。

    +

    10. alias与root的区别

    +

    csdn参考

    +]]>
    +
    + + <![CDATA[django学习 -- 前后端不分离的 -- part.2]]> + https://cyanineeee.github.io/post/django-xue-xi-qian-hou-duan-bu-fen-chi-de-part2/ + + + 2023-06-30T12:36:08.000Z + 35.时间选择插件 +

    download_link
    +office_link
    +usage

    +
      +
    • 使用极其简单:只需要选择dom然后绑定即可,在使用方法里可以设置各种参数
    • +
    +
    $('#id_presettime').datetimepicker({
    +   format: 'yyyy-mm-dd hh:ii'
    +});   
    +
    +

    36.modelfield定义成成表单的属性样式

    +
    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属性 
    +
    +
      +
    • 扩充 这个属性可以继承 不过暂时对我没什么用处 因此pass过
    • +
    +

    37.修改dajngo的modelform类的inputtype类型为datetime-local

    +
      +
    • 第一种是通过前端js实现,一共三步:第一拿到原始的时间数据,第二选择input框然后修改type,第三讲原始的时间数据修改格式之后在赋予给input框:
    • +
    +
    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表示,但最终解析的时候空格也可以,具体参考

    +
      +
    • 第二种就是修改django的DateTimeInput
      +这种看不懂原因,但是如果只是仅仅修改input_type的话是解析不了的,回答给出的原因是后端会因为输入合法判断和不通过,detail_link_and_solution
    • +
    +
    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">
    +
    +

    38.通过validationError来为钩子方法设置非法输入提示信息

    +

    raise ValidationError

    +

    39.密码加密 通过钩子方法定义

    +
      +
    • 钩子方法返回的就是储存到数据库中
    • +
    • md5加密 加密需要“盐”,为了方便和效果 使用django在设置中生成的secret_key来当作“盐”
    • +
    +
    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()
    +
    +

    40.对于djanfo的ORM,如果搜索不到

    +

    搜索不到就返回None,可以用来判断是否是合法请求

    +

    41.对于表单验证

    +

    所有验证通过的信息都储存在对应的cleaned_data

    +

    42.django报错

    +

    'Manager' object is not callable
    +原因是objects是一个属性而不是一个方法,写成objects()会报这个错

    +

    43.用户登录

    +
      +
    • cookies 和 session
      +无状态短链接
      +通过cookies给用户发放一个“凭证”, 保存在浏览器中的键值对,发送请求的时候自动携带
      +session django默认储存在mysql中
    • +
    +

    业务过程:收到用户的提交 -> 校验(在数据库比较) -> 成功 生成随机字符串写入到用户浏览器的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的中间件高效的处理

    +

    44.django中间件

    +

    官方文档

    +
      +
    • 对于django,每一个请求都需要经过很多中间件之后才能访问到视图函数;同样的,所有的视图函数返回值都需要经过很多中间件的处理才会返回给用户。
    • +
    • django的中间件都是类,通过process_request方法对传入的请求进行处理,通过process_response对视图函数的return进行包装。如果某个请求不能通过一个中间件,那么就会直接返回给用户,而不会到达视图函数。
    • +
    • 中间件写好之后需要在setting.py文件中的MIDDLEWARE中注册(具体到类名),哪个在前面那个先执行。
    • +
    • 中间的process_request方法如果返回为None,那么继续向后执行;如果需要返回,那么返回的类型与视图函数的返回类似(例如render,redirect,HttpResponse等),并且不在向后执行。
    • +
    • 应用中间件: +
        +
      • 1.注册中间件
      • +
      • 2.中间件中返回值
      • +
      • 3.通过中间件实现登录校验
        +代码实现
      • +
      +
    • +
    +
    #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路径。

    +

    45.注销操作 - 用户退出登录

    +

    也就是将浏览器的cookies和session删除
    +同样使用request.session中的方法即可

    +

    46.通过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>
    +
    +

    47.form和modelform

    +
      +
    • form可以自定义字段
      +modeform既可以自定义 还可以在数据库中拿取字段
    • +
    • form没有save方法可以直接存入到数据库 但是modelform有
    • +
    +

    48.图片验证码

    +

    -> python中如何动态生成图片并写入值 (不做过多了解 仅仅当成黑盒使用)
    +参考文献

    +
      +
    • 需要pillow库
    • +
    +
    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. 需要注意的是:同一个文件中不要出现名字相同的函数,不管是引用的还是本文件的,会导致调用出错

    +

    49.如何验证验证码呢?

    +

    通过session! - 这样每个用户在登陆的时候对于验证码的验证就不会受到干扰,并且重复刷新也会更新session中对应的验证码。
    +这里有很多需要注意的点!

    +
      +
    • add_errors()可以向forms中指定字段添加错误提示信息,然后能在前端的{{fields.属性字段.errors.0}}中取到。
    • +
    • 由于验证码的字段是没有在用户model/数据库表中存在的,因此如果直接传入request.POST作为data参数会报错。因此应该将验证码字段单独剔除。
    • +
    • 验证码验证需要调用之前创建验证码时存入session的字段,将两个字段相比较判断是否正确。
    • +
    • 登录成功之后需要重新设置session的过期时间 ,因为为了验证码的登录时效,在生成验证码的时候会设置保存验证码对比字段的session信息时效比较短,因此登录成功之后需要重新设置。
      +最终的登录逻辑
    • +
    +
    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类,对应着储存登录用户帐号密码的数据库表。

    +]]>
    +
    + + <![CDATA[mysql -- null和空的区别]]> + https://cyanineeee.github.io/post/mysql-null-he-kong-de-qu-bie/ + + + 2023-06-29T14:14:27.000Z + +

    空值时不占用空间的; 2、null其实是占用空间的; 打个比方来说,你有一个杯子,空值代表杯子是真空的,NULL代表杯子中装满了空气,虽然杯子看起来都是空的,但是区别是很大的。 NULL 其实并不是空值,而是要占用空间,所以mysql在进行比较的时候,NULL 会参与字段比较,所以对效率有一部分影响

    + + +

    那什么时候用null什么时候用空值呢?

    +
    +

    更推荐使用空值。两者的含义不一样,前者代表不清楚,后者代表缺失值。但实际使用上null会在数据库操作方面造成很多的麻烦,因此更推荐设置为空值,并且字段设置为NOT NULL

    +
    +]]>
    +
    + + <![CDATA[django学习 -- 可以感受到最原始的django框架是前后端不分离的 -- part.1]]> + https://cyanineeee.github.io/post/lin-shi-ji-lu/ + + + 2023-06-26T16:21:01.000Z + +
  • +

    学习视频

    +
  • + +

    1.请求和响应

    +
      +
    • get post redirect
    • +
    +

    2.数据库操作

    +
      +
    • mysql+pymsql
    • +
    • ORM框架 -> 帮助我们处理sql语句 => pip install mysqlclient
      +创建修改删除数据中的表(不用写sql语句)
      +操作表中的数据
      +=> 创建数据库 django连接数据
      +PS. 需要在setting.py进行设置
    • +
    +

    3.让django帮助创建数据库表

    +
      +
    • django会通过model中的类帮助生成sql创建表和字段
      +类名代表表明 属性代表列名
    • +
    +
    class UserInfo(models.Model):
    +    name = models.CharField(max_length=32)
    +    password = models.CharField(max_length=64)
    +
    +

    4.执行命令

    +
      +
    • 需要提前注册app -> 在setting.py中注册
      +python manage.py makemigrations
      +python manage.py migrate
    • +
    +

    5.需要注意

    +
      +
    • 在已经有的model类中添加属性 可能会导致原来已经有的数据缺失新增加的列的数据
      +django给出了两个选择,一是在命令行中默认添加输入,二是在新增加的属性字段中增加 default=字段,代表给原来没有的缺失值赋值。例如:
      +age = models.CharField(defalut=18)
      +或者设置可以为空
      +age = models.CharField(null=True,blank=True)
    • +
    +

    6.通过django向表中插入数据

    +
      +
    • +

      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()进行筛选

      +
    • +
    +

    7.django表单如果不写action 那么默认提交就是向当前页面发送一个post请求

    +

    8.django中的url命名能避免对url的硬编码,

    +
      +
    • 例如修改一个url 的路径如果以前在html采用的是硬编码,直接写对应的会导致后面修改维护极为困难。
      +但是如果采用url命名空间,那么修改的url并不会直接影响到,django会通过url的命名访问到,而不是硬编码。{% url 'url_name' other_param%} 这个url_name 就是在urls.py文件中的path('url/example', views.function,name = url_name)的name为url从的重命名,这样修改path对应的url,就不用在维护前端的页面了。
    • +
    +

    9.model添加约束 (数据库的约束)

    +
      +
    • 例如员工表和部门表之间的关系,让员工表和部门表关联,通过ForeignKry方法中的toto_field参数在django中设置。用途是可以方便对输入数据的合法性判断,例如员工不能属于一个不存在的部门,就可以很方便的进行判断。
    • +
    • 在员工表中
      +department = models.ForeignKey(to = 'Department', to_field = 'id')
      +这就将员工表和部门表进行了关联,员工的部门只能属于Depatment中的一个;to表示关联的表名,to_field表明关联的表的一个列名字段。要注意的是,django在储存这样的字段时默认命名方式是关联表名_id(例如这个就是depaetment_id)。
      +于此对应,在获取的时候,如果直接通过字段/属性获取的话,例如obj.联表名_id,获取到的时储存在这张表中的对应关联表的数字,而不是我们想要的具体指代,因此django规定了如果直接.联表名就可以获取到关联表对应的那一行数据(封装为一个queryobj对象),然后就可以继续通过关联表内的属性进行查找。例如,部门表中一共有两个部门,字段为(id,部门名),内容为(1,主管部)(2,运行部),员工表关联
    • +
    • 级联删除 置空null
      +现在设想一种情况,如果部门表中的一个部门取消掉,那么与之关联的员工应该如何处理呢?常用的就是两种操作:将他们一并删除(这就是级联删除);还有一种就是将他们设置为null。对应的这两种方式在django中需要这样设置:
      +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中的操作。
    • +
    +

    10.性别存储

    +
      +
    • model的choice 与数据库无关 是django的特性 可以方便的进行固定的选择储存。例如男女,单独为男女添加一个char字段会较为浪费空间,因此使用1/2这样的数字代替,但是数字不易读,因此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()
    • +
    +

    11.Form和ModelForm

    +
      +
    • 11.1 form类 自动生成表单
    • +
    +
    #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>
    +
    +
      +
    • 11.2 ModelForm组件 +
        +
      • 实现数据库表单字段和前端form表单字段的自动生成
      • +
      +
        +
      • 对连接表/外键限制自动生成选择框
      • +
      • 定义models类中的__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']
    +
    +

    12.重定向

    +
      +
    • from django.shortcuts import redirect或者from django.http import HttpResponseRedirect
      +return一个重定向url的实例即可
    • +
    +

    13.前端url实现删除请求

    +
      +
    • 通过前端发送一个带有删除目标信息的GET请求,然后后端获取删除即可,但是这个带有删除目标信息的url该如何组装呢?通过url的组成规律,添加/?id=信息传递,代码如下:
      +<a href="/login/manage/delete/?id={{user.0}}">删除</a> #这其中的{{id.0}}通过django的模板语法动态赋予目标的id信息
      +对应处理函数(记得注册url)
    • +
    +
    def delete_user_info(request):
    +    id = request.GET.get('id')
    +    UserInfo.objects.filter(id = id).delete()
    +    return HttpResponseRedirect('/login/manage/')
    +
    +

    14.django的模板语法

    +
      +
    • 其中最开始的{% extends 'nav.html'%}必须处于子页面的第一个加载位置,意味着如果前面有哪怕{% load static %}也会报错。
    • +
    +

    15.django正则匹配url

    +
      +
    • 这样就不用前端写带有get参数的url,后端也不需要get获取,而是直接作为一个解析好的参数传递给视图进行处理,二是直接及进行请求和解析。同时也要注意,在url中添加的正则顺序不影响在命名url中的参数顺序
    • +
    +
      +
    • 具体如下:
    • +
    +
    #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请求获取。

    +

    16.在django的模板中,可以通过obj.get_联表名_display获取到和后端get_联表名_display()的效果,直接返回对照过后的结果而不是对应的关联id。

    +

    17.django的模板可以进行的额外操作:

    +
      +
    • {{number : add 1}}就是python的+=1操作,{{datetime | date :'Y-m-d}}就是python中的strftime()函数(要注意的是里面的格式不需要带%)
    • +
    +

    18.modelform的字段校验函数

    +
      +
    • is_valid()检验提交post请求中的form表单中的字段是否为空,具体的字段就是在modelformfields列表定义的那些.
    • +
    • 同时还可以在modelforms类中进行字段校验 错误的信息会保存在用post请求实例化的modelform中。
    • +
    +
    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参数进行正则筛选
    +
    +

    19.调整返回的提示信息为汉语

    +
      +
    • 通过修改setting.py文件中的语言项
    • +
    +
    #setting.py
    +# LANGUAGE_CODE = "en-us" 注释掉
    +LANGUAGE_CODE = "zh-hans" #添加
    +
    +

    20.modelform中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,
    +    })
    +
    +
      +
    • 如果想在用户输入以外的字段进行更新,那么在modelform实例化之后通过instacne属性可以修改:
    • +
    +
    userform = userModelForm(data=request.POST,instance=default_userinfo)
    +# useform.instance.字段 =  值
    +
    +

    21.django查询排序

    +
      +
    • obj.objects.all().order_by('属性名')
      +如果给属性名前面添加减号 代表逆向(高到低)排序
    • +
    +

    22.给原有的表中添加新的字段产生错误"django.core.exceptions.ValidationError" error

    +
      +
    • 产生原因:大概率是因为追加新的字段设置默认值的时候产生的格式错误,例如datetime格式但是没有设置默认或者默认格式错误,造成这样的原因。
    • +
    • 解决方法:重置所有的migrations
    • +
    • 操作:删除migrations文件夹下除了__init__.py文件以外所有的文件,然后重新执行数据库迁移命令:python manage.py makemigrations, python manage.py migrate
    • +
    • 如果还不成功可能是因为数据库中原本存在的数据导致,将数据库清库(所有表删除),然后在执行即可。
    • +
    +

    23.django报错'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 -- 感觉好多...暂时搁置一下...😖
    • +
    +

    24.关闭浏览器校验

    +

    在form表单添加novalidate

    +
    <form method="post" novalidate >
    +
    +

    25.关于models中null和black字段

    +
      +
    • 需要同时开启,但是数据库更推荐默认值为空值,因此设置default=None而不是null
      +如果设置blank=True但是不开启null=True,那么在插入的时候,如果传回来的数据为空,插入会报错不能为null字段...总之还是很迷惑的报错....但是解决方法还是最好设置两个都为True并且设置默认字段为None。如果为null,那么在数据传入到前端的时候会默认输出null或者none,而不是将那片区域置空,对于前端的用户输入逻辑不太友好。
    • +
    +

    26.modelforms便捷写法

    +
      +
    • 在添加fields的时候,可以用__all__一次性获取全部,exclude = [ 排除的字段 ]排除掉不要的字段。
    • +
    +
    class orderForm(ModelForm):
    +    class Meta():
    +        model = Orderform
    +        fields = "__all__" #注意要引号
    +        # exclude = ['字段1','字段二', .... ]
    +
    +

    27.后端表单正则校验

    +
      +
    • 在modelforms类中声明字段,然后通过validators参数设置正则表达式:
    • +
    +
    from django.core.validators import RegexValidator #需要导入正则类
    +class exampleModelForms(forms.ModelForm):
    +    moblie = forms.CharField(
    +        label = '手机号码',
    +        validactors = [RegexValidator(r'^1[3-9]\d{9}$','手机号格式错误')]       
    +        #校验失败返回后面的提示信息而不是默认的了~ 
    +    )
    +
    +

    28.钩子方法进行字段校验

    +
      +
    • 详细的
    • +
    • 这里摘列出常用的代码结果
    • +
    +
    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')
    +
    +

    29.规定modelform中不可编辑的部分

    +

    在modelform中字段中设置参数disabled= True即可,这样在前端页面渲染出来的这一条字段是不可以更改的。

    +

    30.通过modelform判断要添加的数据是否重复

    +
      +
    • order_edit_form = orderEditForm().filter(id=你要查询的).exist() 返回True/False
      +这个方法需要添加在modelform中的钩子方法中。
    • +
    • 排除自己以外的是否存在重复?(主要是在修改数据的时候)
      +通过exclude排除自己后判断:order_edit_form = orderEditForm().exclude(id='自己的id' ).filter(id=你要查询的).exist() 返回True/False
    • +
    +

    总结 编辑和添加的不同

    +

    31.查询(根据某个字的一部分进行)

    +
      +
    • filter()还支持传入字典:
    • +
    +
    order_edit_form = orderEditForm().filter(id=你要查询的,other=你要查询的)
    +#等于
    +query_dict = { 'id' : 要查询的, 'other' : 要查询的 }
    +order_edit_form = orderEditForm().filter(**query_dict)
    + #需要注意传入字典的时候需要两个**
    +
    +

    PS. 通常我们再变量前加一个星号(*)表示这个变量是元组/列表,加两个星号表示这个参数是字典

    +

    32.问题记录:modelform不能更新一行数据的部分字段

    +
      +
    • 解决方法
      +这里的解决方法很多 包括自定义钩子函数对post数据进行清晰、定义一个工厂函数处理每次不同的字段等,不过都太高端了,部分概念我还没有理解清楚😭
    • +
    • 因此我打算采取最简单的 -- 一个字段一个字段的去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类
    +
    +

    33.django的条件查询

    +

    除了固定的等于(=)查询外,还可以通过给字段/属性添加下划线的方式规定范围查询

    +
      +
    • 数字方面:gt、gte、lt、lte:大于、大于等于、小于、小于等于。
      +list = BookInfo.objects.filter(id__gt=3) 代表id大于3的数据
      +PS. 不等于可以通过exclude过滤
    • +
    • 字符串方面:startswith、endswith:以指定值开头或结尾;contains:是否包含。
      +list = BookInfo.objects.filter(btitle__endswith='部') 查询是否数据中btitle字段以‘部’这个字结尾的一行数据。
    • +
    • 日期查询: year、month、day、week_day、hour、minute、second:对日期时间类型的属性进行运算。
      +list = BookInfo.objects.filter(bpub_date__year=1980) 查询1980年发表的图书
      +list = BookInfo.objects.filter(bpub_date__gt=date(1990,1,1)) 查询1980年1月1日后发表的图书。
    • +
    • 还有很多查询: 例如比较同一行数据两个字段之间的关系(如阅读数量>=评论数量)、与或非查询、聚合等... 具体参考reference或者官网
    • +
    +

    PS. 1. filter可以连续使用 :list=BookInfo.objects.filter(bread__gt=20).filter(id__lt=3) 2. filter可以传入字典

    +

    34.设置get默认值

    +
      +
    • request.GET.get('page', '1') 获取get请求,如果get没有附带page参数,那么默认是1.
    • +
    +]]>
    +
    + + <![CDATA[Extra details of python - for long term - part.2]]> + https://cyanineeee.github.io/post/extra-details-of-python-for-long-term-part2/ + + + 2023-06-26T04:11:23.000Z + 2023-06-26 +

    9. python中的__init__.py

    +

    reference
    +在python工程中,当python在一个目录下检测到__init__.py文件时,就会把它当成一个module。__init__.py可以是空的,也可以有内容。

    +

    --未整理完

    +

    10. python中的getattr()方法

    +

    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的时候会执行该模块。

    +]]>
    +
    + + <![CDATA[python网络编程 -- 建立一个http服务器+twisted模块--part.2]]> + https://cyanineeee.github.io/post/part2/ + + + 2023-06-26T03:45:40.000Z + => +

    HTTP基础响应

    +
      +
    1. http服务器
    2. +
    +
    # 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程序
    +会使服务端的资源利用带来极大便利。

    +
      +
    1. 服务端:
    2. +
    +
    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()
    +
    +
      +
    1. 客户端
    2. +
    +
    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

    +]]>
    +
    + + <![CDATA[python网络编程 -- 建立一个http服务器+twisted模块--part.1]]> + https://cyanineeee.github.io/post/python-wang-luo-bian-cheng/ + + + 2023-06-20T03:26:07.000Z + => +

    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架构

    +
      +
    1. 服务器端socket程序
    2. +
    +
    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()
    +
    +
      +
    1. 使用telnet命令测试
      +windows需要在设置里配置telnet,他是操作系统中提供的一个测试命令。
      +telnet localhost 7000
    2. +
    3. 客户端socket程序
    4. +
    +
    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程序模型

    +
      +
    1. echo服务端
    2. +
    +
    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测试。

    +
    +
      +
    1. echo客户端
    2. +
    +
    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()。

    +
      +
    1. UDP服务端
    2. +
    +
    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()
    +
    +
      +
    1. UDP客户端
    2. +
    +
    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()
    +
    +
      +
    1. send/sendto, recv/recvfrom区别
      +PS. 1. UDP不需要建立稳定的链接 ,不受到连接的控制,不需要考虑多连接(不需要考虑并发),需要不断的接收,但只需要将响应的信息原路返回给对应的客户端即可。2. UDP是一种无连接协议,因此不能使用telnet命令进行连接测试。
    2. +
    +

    =>

    +

    UDP广播

    +
      +
    1. UDP广播接收端
    2. +
    +
    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()
    +
    +
      +
    1. UDP广播发送端(服务端)
    2. +
    +
    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()
    +
    +
      +
    1. 需要注意的点/具体解释
      +关键的函数配置项:
    2. +
    +
    +

    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**、
    +常见响应头信息

    +]]>
    +
    + + <![CDATA[scrapy爬虫框架深入理解 -- 未整理完]]> + https://cyanineeee.github.io/post/scrapy-pa-chong-kuang-jia-geng-shen-ru-li-jie-yi-ji-shi-yong/ + + + 2023-06-17T08:41:17.000Z + 1. logging模块 +

    2. 环境变量(setting)

    +

    官方文档

    +

    2.1 如何设置并访问setting

    +

    2.1.1 在spider中

    +

    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使用在方法中。

    +

    2.1.2 在pipeline等其他组件中

    +

    使用类方法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()等,用途也能从方法名显而易见出,具体请参考官方链接。

    +
    +

    1. crawler模块

    +

    2. item源码如何实现存储key字段,

    +

    https://www.cnblogs.com/twelfthing/articles/4709287.html
    +https://www.cnblogs.com/fengf233/p/11298623.html#2.field()%E7%B1%BB

    +

    5. 多个Spider以及多个Pipeline对应的设置

    +

    https://www.ziji.work/python/scrapy-many-spider-pipeline.html#SCRAPYSPIDER-4

    +

    6. 大佬的源码解析

    +

    虽然现在基本看不懂(×
    +http://kaito-kidd.com/2016/11/09/scrapy-code-analyze-entrance/

    +

    7. 访问setting

    +

    8. 为什么from_crawler方法需要设置为类方法?

    +

    提点链接
    +最后这篇博客的最后一句话简直一下子给我点透了,虽然之前专门去查了@classmethod、factory method这些东西,但为什么这么做还是很混乱。

    +
    +

    大概就是检测spider类有没有from_crawler,有的话就return一个cls()的实例化对象,产生实例化对象后会自动调__init__方法。

    +
    +

    结合官方文档的Crawler APIpipeline中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方法。
    +上面这句话有两点需要着重理解。

    +
      +
    1. 第一点,这也就是from_crawler()是一个“factory method”,scrapy会根据不同的对象为类赋予不同的from_crawler()方法。在我的理解中,就是有一个类,专门负责为其他不同的类赋予不同的from_crawler()方法,也就是类方法的类
    2. +
    3. 第二点,scrapy从Crawler创建from_crawler()的含义就是为这个方法传入一个scrapy.crawler.Crawler对象,而这个对象又是由scrapy通过一个Spider子类scrapy.settings.Settings 对象实例化而来。
    4. +
    +
    +

    官方文档
    +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对象,这里就有串起来的感觉了)

    +

    9. 设置cookies

    +

    知乎参考但是没有讲原理

    +

    10. 关闭scrapy日志

    +

    参考
    +解决方法

    +

    添加twisted定时器 以及 输出调用日志
    +https://www.jianshu.com/p/5a5cdd7f2bfb

    +

    11. 定时调度scrapy

    +

    https://blog.csdn.net/python36/article/details/82683528

    +]]>
    +
    + + <![CDATA[scrapy item赋值/填充 细节注意]]> + https://cyanineeee.github.io/post/scrapy-item-fu-zhi-tian-chong-xi-jie-zhu-yi/ + + + 2023-06-16T07:29:41.000Z + 1. 起因 +

    我在爬取到微博热搜的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    
    +
    +

    2. 调试之后

    +

    在调试之后,我发现pipeline.py文件中对应class类中的process_item()方法中的item变量并不是我想象中的是一个由字典元素组成的列表,而是一个字典,并且key是在item.py文件中设定的,value是在parse()方法中赋值的、我想要的字典元素列表。终于确定的原因,因此修改也很简单。

    +

    3. 修改

    +

    将原本的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
    +
    +

    4. 探究原因

    +

    官方文档
    +首先,开发者为了方便在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
    +
    +]]>
    +
    +
    \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..0630826 Binary files /dev/null and b/favicon.ico differ diff --git a/images/avatar.png b/images/avatar.png new file mode 100644 index 0000000..0630826 Binary files /dev/null and b/images/avatar.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..00f7118 --- /dev/null +++ b/index.html @@ -0,0 +1,291 @@ + + + + +cyanine + + + + + + + + + + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + + 下一页 + +
    + + + + +
    +
    + + diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 0000000..eb19433 --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,293 @@ + + + + +cyanine + + + + + + + + + + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + 上一页 + + + 下一页 + +
    + + + + +
    +
    + + diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 0000000..1b731a7 --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,293 @@ + + + + +cyanine + + + + + + + + + + + +
    +
    + + + +
    + + + + + + + + + +
    + +

    CORS

    +
    + + +
    + +
    +
    + + + + + + + + + + + +
    + + +
    + + 上一页 + + + 下一页 + +
    + + + + +
    +
    + + diff --git a/page/4/index.html b/page/4/index.html new file mode 100644 index 0000000..e4178ba --- /dev/null +++ b/page/4/index.html @@ -0,0 +1,293 @@ + + + + +cyanine + + + + + + + + + + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + 上一页 + + + 下一页 + +
    + + + + +
    +
    + + diff --git a/page/5/index.html b/page/5/index.html new file mode 100644 index 0000000..ba72680 --- /dev/null +++ b/page/5/index.html @@ -0,0 +1,343 @@ + + + + +cyanine + + + + + + + + + + + +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + + 上一页 + + +
    + + + + +
    +
    + + diff --git a/post/ajax-cors-trouble/index.html b/post/ajax-cors-trouble/index.html new file mode 100644 index 0000000..2a44b92 --- /dev/null +++ b/post/ajax-cors-trouble/index.html @@ -0,0 +1,176 @@ + + + + +ajax CORS trouble | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + ajax CORS trouble +

    + + +
    +
    +

    how to handle?

    +

    before your scripts, set a variblevar token = "{{ csrf_token }}";,
    +then add headers: { 'X-CSRFTOKEN': ${token} }, in your ajax request
    +then code run well~👏

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/an-zhuang-nb_conda-bao-cuo/index.html b/post/an-zhuang-nb_conda-bao-cuo/index.html new file mode 100644 index 0000000..8d39643 --- /dev/null +++ b/post/an-zhuang-nb_conda-bao-cuo/index.html @@ -0,0 +1,183 @@ + + + + + Convert virtual conda environments in jupyter notebook | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Convert virtual conda environments in jupyter notebook +

    + + +
    +
    +

    链接

    +

    结论:
    +conda install ipykernel
    +然后打开对应的环境,例如打开arcgis环境,
    +然后python -m ipykernel install --user --name 环境名称 --display-name "Python (环境名称)"
    +就能在jupyter notebook上注册这个环境下的kernel,并且可以在不同环境下在jupyetr notebook的change kernel选项下切换内核。不同的内核对应不同环境下所安装的packages和一些其他环境配置。

    +

    Ps.删除内核:
    +jupyter kernelspec list #查看内核列表
    +jupyter kernelspec remove kernel_name #删除指定内核

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/anaconda-an-zhuang-bao-shi-chu-xian-de-channels-zhao-bu-dao-wen-ti/index.html b/post/anaconda-an-zhuang-bao-shi-chu-xian-de-channels-zhao-bu-dao-wen-ti/index.html new file mode 100644 index 0000000..82a4036 --- /dev/null +++ b/post/anaconda-an-zhuang-bao-shi-chu-xian-de-channels-zhao-bu-dao-wen-ti/index.html @@ -0,0 +1,174 @@ + + + + +'channels couldn't find' error while conda some packages | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + 'channels couldn't find' error while conda some packages +

    + + +
    +
    +

    https://blog.csdn.net/zplai/article/details/108419782

    +

    还有可能是因为对应的python版本不对,所以找不到相应的packages

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/anaconda-da-bu-kai-anaconda-navigator/index.html b/post/anaconda-da-bu-kai-anaconda-navigator/index.html new file mode 100644 index 0000000..13cae8d --- /dev/null +++ b/post/anaconda-da-bu-kai-anaconda-navigator/index.html @@ -0,0 +1,203 @@ + + + + +Anaconda can't open the anaconda navigator | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Anaconda can't open the anaconda navigator +

    + + +
    +
    +

    重装了很多次 刚开始只是打开acaconda navigator无反应,最后重装了好几次 还出现了 ImportError: DLL load failed while importing shell: Can not find procedure.
    +的报错提示
    +最后找到解决方案,以下是我查找解决方法的路径

    +
      +
    1. +

      首先我在官方网站上找到了解决方案官方网站
      +它提示我首先找到.condarc文件然后删除(windows位置C://Users//
      +我找到后删除,然后启动anaconda navigator,仍然无反应,于是再次卸载重装。

      +
    2. +
    3. +

      然后再尝试使用官方的第二个方法

      +
      +

      If removing the licenses directory does not resolve the issue, manually >update Navigator from a terminal or an Anaconda Prompt:
      +conda update anaconda-navigator

      +
      +

      发现提示ImportError: DLL load failed while importing shell: Can not find procedure.错误,也尝试了许多其他的命令比如conda info等也是同样的报错提示,于是转为搜索这个错误信息。

      +
    4. +
    5. +

      在这个错误信息的搜索结果下我找到了github中一个关于这个的issue并且在后续的讨论中找到了这样一个回答:

      +
      +

      I also got the same error using the very latest version of Mini-conda (conda >version: 4.12.0, python version: 3.9.12.final.0. when I ran the
      +command:
      +conda info fails with the errors above
      +but
      +conda list works
      +I did the "pip install --upgrade pywin32==228"
      +and now
      +conda info succeeds.
      +I wonder if its the latest version of mini-conda that has this problem

      +
      +

      于是我按照他的方法pip install --upgrade pywin32==228之后,命令行中有红色的报错提示,但是conda命令已经可以执行了,于是我在使用官方的解决方法:conda update anaconda-navigator ,最后问题成功解决,Anaconda navigator可以正常打开了。

      +
    6. +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/anaconda-pei-zhi-xia-zai-jing-xiang-yuan/index.html b/post/anaconda-pei-zhi-xia-zai-jing-xiang-yuan/index.html new file mode 100644 index 0000000..7fb075b --- /dev/null +++ b/post/anaconda-pei-zhi-xia-zai-jing-xiang-yuan/index.html @@ -0,0 +1,186 @@ + + + + +Anaconda configurate the resource of mirror downloading | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Anaconda configurate the resource of mirror downloading +

    + + +
    +
    +

    安装需要的Python包非常方便,但是官方服务器在国外,下载龟速,国内清华大学提供了Anaconda的镜像仓库,我们把源改为清华大学镜像源

    +

    更改方法一:cmd后依次输入下面命令

    +

    conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
    +conda config --set show_channel_urls yes

    +

    开C盘用户目录,我这里是

    +

    C:\Users\User
    +找到.condarc文件,里面长这样就成了

    +

    ssl_verify: true
    +channels:

    +
      +
    • https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
    • +
    • defaults
      +show_channel_urls: true
    • +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/bing-fa-bing-xing-chuan-xing-tong-bu-yi-bu/index.html b/post/bing-fa-bing-xing-chuan-xing-tong-bu-yi-bu/index.html new file mode 100644 index 0000000..4de180e --- /dev/null +++ b/post/bing-fa-bing-xing-chuan-xing-tong-bu-yi-bu/index.html @@ -0,0 +1,172 @@ + + + + +并发,并行,串行,同步,异步? | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + 并发,并行,串行,同步,异步? +

    + + +
    +
    +

    https://blog.csdn.net/qq_26442553/article/details/78729793

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/catch-the-enter-key-while-submit-a-form/index.html b/post/catch-the-enter-key-while-submit-a-form/index.html new file mode 100644 index 0000000..7d87406 --- /dev/null +++ b/post/catch-the-enter-key-while-submit-a-form/index.html @@ -0,0 +1,174 @@ + + + + +Catch the enter key while submit a form | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Catch the enter key while submit a form +

    + + +
    +
    +

    (link)[https://stackoverflow.com/questions/13987300/how-to-capture-enter-key-press]

    +

    add a onkeypress event ang judge the event.Code whether is 13 which symbolize the Enter key.

    +

    pay attention to where the onkeypress event should add => the text input !! text input will automatically catch the key 'enter' as a submit action, thus, if you do not want to refreash the page, you should add a keypress function to catch the 'enter'

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/cc-jie-gou-ti-qian-tao/index.html b/post/cc-jie-gou-ti-qian-tao/index.html new file mode 100644 index 0000000..b28f74e --- /dev/null +++ b/post/cc-jie-gou-ti-qian-tao/index.html @@ -0,0 +1,249 @@ + + + + +c/c++ 结构体嵌套 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + c/c++ 结构体嵌套 +

    + + +
    +
    +

    在写图的邻接表实现的时候,发现这个数据结构的实现实在是有点复杂,看得头晕,最主要还是因为有很多的结构体嵌套,并且在看的时候没有感觉出来,但实际上实现操作的时候,多层的嵌套还是有一点让我迷糊的。

    +

    part.1

    +

    最开始遇到的就是嵌套结构体的声明,在我写初始化函数的时候就想到,如果只是声明分配最外层结构体的指针,那么内部的结构体是否正确的被创建属于它的内存地址?

    +
    #include<iostream>
    +typedef struct aaa{
    +    int a, b;
    +}*apointer;
    +typedef struct bbb{
    +    apointer a;
    +    int c,d;
    +}*bpointer,b;
    +int main(){
    +    using namespace std;
    +    bpointer btest;
    +    btest = new b;
    +    btest -> c = 1;
    +    cout<<(btest->c);
    +    cout<<endl;
    +    
    +    btest -> a -> a = 3;
    +    cout<<(btest -> a -> a);
    +}
    +
    +
    //输出
    +1
    +3
    +
    +

    输出结果一如我所料,这一步就说明,如果结构体嵌套的是另一个结构体指针,那么外部结构体创建内存的时候,内部的也就被包含进去了。

    +

    part.2

    +

    但如果内部嵌套的是一个结构体声明而不是指针呢?
    +将上面的代码微调,将apointer类型定义为结构体而不是结构体指针,结果如下:

    +
    #include<iostream>
    +typedef struct aaa{
    +    int a, b;
    +}apointer;
    +typedef struct bbb{
    +    apointer a;
    +    int c,d;
    +}*bpointer,b;
    +
    +int main(){
    +    using namespace std;
    +    bpointer btest;
    +    btest = new b;
    +    btest -> c = 1;
    +    cout<<(btest->c);
    +    cout<<endl;
    +
    +    //1.     
    +    // btest -> a -> a = 3;
    +    // cout<<(btest -> a -> a);
    +
    +    //2. 
    +    //btest -> a = new apointer;
    +
    +    //3. 
    +    (btest -> a). a = 3;
    +    cout<<((btest -> a). a);
    +}
    +
    +
    //输出
    +1
    +3
    +
    +

    1会报错error: base operand of '->' has non-pointer type 'apointer' {aka 'aaa'},这是因为btest结构体中的a并不是一个指向结构体的指针,所以使用箭头->访问成员会产生错误,因此修改使用点.来访问,那么就没有问题(第三种情况);第二钟情况会报错error: base operand of '->' has non-pointer type 'apointer' {aka 'aaa'},同样是因为btest的a不是指针,因此new会产生类型不匹配的错误。

    +

    part.3

    +

    结论:结构体内的结构体,无论是指针还是结构体声明,都会在外部的结构体声明并创建对应内存这一过程中包含在内,因此没有必要再在结构体内new/malloc申请内存。
    +不过要注意的是,根据结构体内的结构体的引用方式的不同,访问的方式也需要更改成为对应的合法方式:如果是指针,那么使用箭头访问;如果是声明,那么使用点操作符进行访问。

    + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/cczhi-zhen/index.html b/post/cczhi-zhen/index.html new file mode 100644 index 0000000..3220626 --- /dev/null +++ b/post/cczhi-zhen/index.html @@ -0,0 +1,462 @@ + + + + +c/c++指针 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + c/c++指针 +

    + + +
    +
    +

    1. 传入指针

    +

    简单讲就是,在c中,传入函数的所有东西都是一个”副本“,传入的指针实际上是该指针的副本。因此,如果如果你希望一个函数对传入的指针的指向进行修改,那么这个函数是不会作用到这个函数之外的,换而言之,你的修改不能让主函数中的指针指向发生修改。因此,如果你希望通过函数修改指针的指向,那么在c中需要通过指针的指针。
    +reference_1

    +

    2. 传入指针的指针

    +

    首先明确两点:

    +
    +
      +
    1. 自增运算符的优先级++高于解引用*
    2. +
    3. c中所有的传入都是传值(传指针是传入指针值)在函数内对值的修改都是对副本的修改,不会对函数作用外部造成影响''(第一部分)
    4. +
    +
    +

    下面是示例代码:

    +
    #include<cstdio>
    +#include<iostream>
    +void function(int **s){
    +    printf("\n");
    +    // s++;
    +    // *s++;
    +    //(*s)++;    
    +    // **s++;
    +    // (**s)++;
    +    std::cout<<s<<std::endl; //指针的指针
    +    std::cout<<*s<<std::endl; //被指向的指针
    +    std::cout<<**s<<std::endl; //被指向指针指向的值
    +}
    +int main(){
    +    int num[3] = {1,2,3};
    +    int *a = num;
    +    std::cout<<a<<std::endl;
    +    function(&a);//传入指针的地址 (或者 指向a指针地址的指针)
    +}
    +
    +
    +

    对关键部分的解释:
    +1. s++
    +代表指向指针的指针向后移动一位(指针地址加一),这将导致指针丢失,指向未知区域,也有可能报段错误(会直接终止运行)。但这一项操作不会对函数外产生影响。
    +2. *s++
    +代表指向指针的指针先向后移动一位(指针地址加一),然后在解引用。因为指向指针的指针在后移之后指向位置区域,因此可能输出未知值,也有可能报段错误(会直接终止运行)。但这一项操作不会对函数外产生影响。
    +3. (*s)++
    +代表先对指向指针的指针进行解引用,然后后移一位。在本例中,指针指向的指针指向数组的第一个元素的内存地址,所以结果是指向数组的指针后移,指向数组的第二个元素。这导致指向指针的指针的值发生改变,就是指向数组的指针发生改变。因为是产生副本的指针指向的指针发生了改变,所以会影响到主函数中a指针本身的值。
    +4. **s++
    +代表二重指针先后移,然后解引用拿到双重指针指向的指针,然后再次解引用,拿到指针指向的值。因为1的缘故,所以会产生可能的错误或位未知输出。会对住函数外产生影响。
    +5. (**s)++
    +对双重指针进行两次解引用,拿到指针指向的值,然后再加一,所以结果是s指向的值加一,也就是数组的第一个值加一。会影响到主函数中的数组。
    +6. 输出s
    +输出双重指针的内存地址。
    +7. 输出*s
    +输出双重指针解引用后的指针的内存地址,(也就是输出a指针的内存地址)。
    +8. 输出**s
    +输出对双重指针进行两次解引用的值,(理解为对双重指针指向的指针进行解引用),结果是a指针解引用后的值(或者a指针指向地址的值)。
    +于此对应,c++中补充了传引用引用,可以直接通过引用来对传入的参数进行修改(第五部分)。个人理解,这是对指针的指针的一个方便的优化,因为通过传引用引用就可以使函数中的修改直接作用于外部的变量。

    +
    +

    3. 野指针和空指针(NULL)

    +

    3.1 野指针

    +

    指向垃圾内存的指针,往往由于一些不严谨的地方产生。

    +
    +

    产生原因:
    +1.指针变量没有被初始化。这里需要注意,指针初始化后为赋值并不会指向NULL。
    +2.指针被free或者delete之后,没有设置为NULL,让人误以为这是一个合法指针。因此强调new/delete或者malloc/freeze一定要成对出现。
    +3.指针操作超越了变量的作用范围。

    +
    +

    野指针在调用的时候可能会让程序误认为是一个正常指针,因此操作的时候可能会产生意想不到的问题,因此需要避免这种问题。

    +

    3.2 空指针

    +
      +
    1. 空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。
    2. +
    3. 空指针是一个特殊的指针,因为这个指针不指向任何地方。这意味任何一个有效的指针如果和空指针进行相等的比较运算时,结果都是false。
    4. +
    5. 空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态。
    6. +
    7. NULL是一个标准规定的宏定义,用来表示空指针常量。在C++里面被直接定义成了整数立即数的0,而在没有__cplusplus定义的前提下,就被定义成一个值是0的 void* 类型的指针常量。
      +reference_2
    8. +
    +
    +

    注:指针声明一定要明确指向哪里,否则会产生“野指针”

    +
    +

    4. 结构体指针

    +

    4.1 声明结构体和声明结构体指针的区别

    +
    typedef struct pp{
    +    int *test;
    +    int *another;
    +}pp;
    +int main(){
    +    pp P;
    +    pp *P1;
    +    printf("%d\n",sizeof(P));
    +    printf("%d",sizeof(P1));
    +}
    +
    +
    //输出
    +16
    +8
    +
    +

    从上面给的实例中可以很清楚的看到声明结构体和声明结构体指针的区别:声明结构体就是在栈区分配了一个与结构体大小相同的内存,但是指针只是申请了一个指针类型的内存。如果只是声明了一个结构体指针而没有初始化(new/malloc/指定变量地址),那么该指针就是“野指针”,在调用的时候会出现错误。

    +

    4.2 结构体指针和结构体内部的指针都必须初始化

    +
        [reference](https://blog.csdn.net/qq_40285768/article/details/104607834)
    +    原因于此类似,但是需要注意定义在结构体内部的指针如果需要调用也是需要初始化。
    +
    +

    5. c++的传引用引用(&

    +
    +

    个人理解:传引用引用是对双指针的优化,他让函数可以直接对指针本身进行修改和操作

    +
    +

    5.1

    +

    如果在函数声明的时候使用了Element* &S,那么表明是一个传引用引用的指针,是可以直接对指针本身的值进行操作,也会影响到函数外部。因此如果只是想要只读的调用(例如遍历链表),那么可以使用const关键字或者创建一个新的指针指向传入的指针,然后对新的指针进行操作。

    +

    补充:声明指针一定要确保指针指向一个地址或者指针申请一块空间,否则会在传入函数指针时出现错误,或者调用的时候导致错误。

    +

    以下是对应的示例代码:

    +
    #include<cstdio>
    +#include<iostream>
    +typedef struct pointer{
    +   int data;
    +   struct pointer *next;
    +}pointer;
    +void function(pointer* &s){
    +   pointer *p = new pointer;
    +   p -> data = 5;
    +   p -> next = NULL;
    +   s -> next = p;
    +   std::cout<<s -> next -> data;
    +}
    +int main(){
    +   pointer *P;
    +   P = new pointer; //重点!!!!
    +   function(P);
    +   std::cout<<P -> next -> data;
    +   delete P;
    +}
    +
    +
    +

    如果缺少P = new pointer; //重点!!!!这一句为声明后的指针指定的区域,那么在function的调用中会因为s指向的区域不存在而导致segment error

    +
    +

    5.2

    +

    但如果不是为了使用调用的指针,而是给声明的指针赋值,那么传引用引用是可以,并且能够达到预期的效果。这种常见于数据结构中初始化一个结构的函数的操作中。
    +例如:

    +
    typedef struct pointer{
    +   int data;
    +   struct pointer *next;
    +}pointer;
    +void function(pointer* &s){
    +   // pointer *p = new pointer;
    +   // p -> data = 5;
    +   // p -> next = NULL;
    +   // s = p; //与下面的效果一样
    +
    +   s = new pointer;
    +   s -> data = 5;
    +   s -> next = NULL;
    +   std::cout<<s -> data;
    +}
    +int main(){
    +   pointer *P;
    +   function(P);
    +   std::cout<<P -> data;
    +   delete P;
    +}
    +
    +
    //输出结果
    +55
    +
    +

    自我理解: c++为了能简化在函数中能对指针地址的操作,因此可以使用pointer* &来表示传引用引用一个指针,在函数内对指针的地址进行操作是可以影响到主函数中。在c中传入指针只是传入了指针副本,不能对传入指针本身进行有效操作(这时候就需要双重指针)。

    +

    5.3 c++不允许传引用引用一个数组元素

    +
    void function(int &edges[6][2]) //这样是不允许的 编译会报错
    +void function(int &test[6][) //这样是不允许的 编译会报错
    +
    +

    具体在第七部分(向函数传入数组指针)

    +

    6. 多维数组与指针

    +

    首先请记住理解:

    +
    +

    多维数组实际上是数组的数组/数组元素为数组的数组

    +
    +

    6.1 总结

    +

    对于二维数组可以分为以下三种赋值方式:

    +
    int a[3][5]; 
    +...//进行赋值操作
    +
    +int (*d)[3][5] = &a;
    +int *e = &a[0][0];
    +int (*f)[5] = a;
    +
    +

    区别在于:
    +第一种赋值得到的指针的操作和原来的多维数组操作没有区别;
    +第二种指定的是a数组第一个数组元素的第一个元素,遍历的时候是线性的;
    +第三种相当于规定了指向a数组元素类型的一个指针,因为a数组的元素类型是一个长度为五的整型数组,因此规定了一个这样类型的指针指向a。

    +

    6.2 详细探究过程如下(很长,基于个人理解):

    +
        #include<iostream>
    +	int main(){
    +	    using namespace std;
    +	    int a[3][5]; //a是长度为3的数组 每个元素又都是一个长度为5的数组
    +	    int p = 0;
    +	    for(int i = 0; i < 3 ; i++){
    +	        for(int j = 0; j < 5 ; j++){
    +	            a[i][j] = p;
    +	            cout<<p<<'\t';
    +	            p++;
    +	        }
    +	        cout<<endl;
    +	    }
    +    //需要谨记 自增运算符/自减的运算优先级都要比*(取值运算符)优先级高  又高于加号/减号运算符==============================
    +	    int (*d)[3][5] = &a;  //对整个二维数组的第一个元素取地址
    +	    cout<<(*d)<<"\t"; //输出的是a数组第一个元素的地址
    +	    cout<<(*d)[0]<<"\t"; //输出的是a数组第一个元素的地址 与上方的输出相同
    +	    cout<<*d[1]<<"\t";  // 输出的是一个指针 
    +	    cout<<*(*d)[1]<<"\t";  // 上面的指针指向a的第二元素数组的第一个元素 即a[1][0]
    +	    cout<<(*d)[2][2]<<'\t'; //输出的是a[2][2]
    +	    cout<<(*d+1)[2][2]<<'\t'; //未知输出  
    +	//=============================================
    +	    cout<<(*d+1)<<'\t'; // 输出一个指针 
    +	    cout<<*(*d+1)<<'\t'; // 还是一个指针
    +	    cout<<*(*(*d+1))<<'\t'; // 是a[1][0] => 可以得出 对于赋值给二维数组第一个元素的地址的指针 对其进行运算 就是将a数组的指针向后移动一位
    +	    cout<<*(*d+1)<<"   "<<&a[1][0]; //不放心 检查一下地址是否一样
    +	    // => 由上面四个输出推测 对二维数组的第一个元素指针进行运算 那么就是其实就是将指向a数组的指针向后运算 不涉及a数组内的(数组)元素
    +	    //因此 如果想要遍历a数组的第一个数组元素 那么应该对第一个元素的指针进行运算然后解析
    +	    cout<<*((*d)[1]+1)<<"\t"; //输出6
    +	    //检查是6还是5+1 使用地址
    +	    cout<<(*d)[1]+1<<"\t"<<&a[1][1]; // 地址一样 所以就是
    +	
    +	    //其他的尝试输出
    +	    cout<<*(*(d+1))<<'\t'; // 是一个指针
    +	    cout<<*(*(*(d+1)))<<'\t'; // 未知输出
    +	
    +	    int *e = &a[0][0]; //这样会导致对上面操作的数组发生错误 因此两个赋值并不一致
    +	    // => 可以理解为 上方是声明了一个[3][5]类型的指针,然后指向a
    +	    // 下方就是简单的指向a数组第一个元素数组的第一个元素的指针 为了对比 来输出地址进行对比
    +	    cout<<endl<<e<<"  "<<d<<endl; //地址一致 因此猜测正确 指向的地址一样 但是指针类型不一样
    +	
    +	    //接下来对e进行运算
    +	    cout<<*(e+1)<<"\t"; //输出1,即a[0][1]
    +	    cout<<*(e+5)<<"\t"<<endl; //输出5 即a[1][0] 发现直接输出a数组第二个数组元素的第一个元素 
    +	    //  其实也可以解释为 实际上再内存中还是呈线性的 逻辑上还是线性储存的
    +	
    +	    // => 猜想d指针是否也能够进行类似操作 而不是解引用之后再对指针进行操作再进行解引用 -> 显而易见不可以
    +	    //对d指针的更深一步理解 他就是将a数组整体进行了copy 因此他的操作不想e指针那么丝滑(指可以直接线性运算迭代)
    +	    // 他的操作也和a数组原本的操作更加接近 只不过都换成了指针
    +	
    +	//===============================================================
    +	    cout<<endl;
    +	    //再来看c++ primer中的二维数组指针
    +	    int (*f)[5] = a;
    +	    //通过上面的操作 因此这一下理解更加清晰
    +	    // => 自我理解:这是一个类型为[指向五个元素数组]的指针 因此对他进行指针运算相当于对a数组进行运算
    +	    cout<<*f<<"\t"<<**f<<"\t"; //说明这样赋值的指针也是指向a数组第一个数组元素的第一个元素
    +	    cout<<*((*f)+1)<<endl; // 因此这样就是对第一个数组元素的指针后移后再解引用 所以就是1
    +	    // 所以现在我希望通过进行指针运算访问a数组内第二个数组的元素
    +	    cout<<*(*(f+1)+1)<<"\t"; // 猜测正确 f指针就是一个指向a数组的指针 直接对f指针运算+1 得到a数组第二个元素的指针 然后再次+1 代表a的第二个数组的指针向后移 然后解引用 得到6 也就是a[1][1]
    +	    // 但是为什么二维数组的赋值要这样写? 
    +	    // 首先需要明确二维数组实际上是数组元素为数组类型的数组
    +	    // 因此a数组内的元素类型就是一个数组 因此指向a数组的指针就是数组指针的指针 所以才需要这样规定
    +	    
    +	    //=> 与此同时 还需要注意区别数组指针和指针的数组
    +	    //ex. int (*pointer)[10];
    +	    //ex. int *pointer[10];
    +	    // 前者是多维数组指针 后者是指针的数组(即pointer这个数组内的元素都是int*(int型指针))
    +	
    +	    cout<<endl<<"运行结束"<<endl;
    +	}
    +
    +

    7. 向函数传入数组指针

    +

    reference

    +

    reference_2
    +reference_3
    +reference_4

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/conda-he-pip-de-qu-bie/index.html b/post/conda-he-pip-de-qu-bie/index.html new file mode 100644 index 0000000..c6550ae --- /dev/null +++ b/post/conda-he-pip-de-qu-bie/index.html @@ -0,0 +1,172 @@ + + + + +The difference between pip and conda. | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + The difference between pip and conda. +

    + + +
    +
    +

    https://www.zhihu.com/question/279152320

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/da-jian-jing-tai-wen-jian-fu-wu-qi-lai-gui-bi-ben-di-kua-yu-fang-wen-wen-jian/index.html b/post/da-jian-jing-tai-wen-jian-fu-wu-qi-lai-gui-bi-ben-di-kua-yu-fang-wen-wen-jian/index.html new file mode 100644 index 0000000..036d076 --- /dev/null +++ b/post/da-jian-jing-tai-wen-jian-fu-wu-qi-lai-gui-bi-ben-di-kua-yu-fang-wen-wen-jian/index.html @@ -0,0 +1,172 @@ + + + + +build a static files server to avoid local CORS | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + build a static files server to avoid local CORS +

    + + +
    +
    +

    vscode 插件 live server

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/django-xue-xi-qian-hou-duan-bu-fen-chi-de-part2/index.html b/post/django-xue-xi-qian-hou-duan-bu-fen-chi-de-part2/index.html new file mode 100644 index 0000000..07d098c --- /dev/null +++ b/post/django-xue-xi-qian-hou-duan-bu-fen-chi-de-part2/index.html @@ -0,0 +1,572 @@ + + + + +django学习 -- 前后端不分离的 -- part.2 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + django学习 -- 前后端不分离的 -- part.2 +

    + + +
    +
    +

    35.时间选择插件

    +

    download_link
    +office_link
    +usage

    +
      +
    • 使用极其简单:只需要选择dom然后绑定即可,在使用方法里可以设置各种参数
    • +
    +
    $('#id_presettime').datetimepicker({
    +   format: 'yyyy-mm-dd hh:ii'
    +});   
    +
    +

    36.modelfield定义成成表单的属性样式

    +
    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属性 
    +
    +
      +
    • 扩充 这个属性可以继承 不过暂时对我没什么用处 因此pass过
    • +
    +

    37.修改dajngo的modelform类的inputtype类型为datetime-local

    +
      +
    • 第一种是通过前端js实现,一共三步:第一拿到原始的时间数据,第二选择input框然后修改type,第三讲原始的时间数据修改格式之后在赋予给input框:
    • +
    +
    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表示,但最终解析的时候空格也可以,具体参考

    +
      +
    • 第二种就是修改django的DateTimeInput
      +这种看不懂原因,但是如果只是仅仅修改input_type的话是解析不了的,回答给出的原因是后端会因为输入合法判断和不通过,detail_link_and_solution
    • +
    +
    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">
    +
    +

    38.通过validationError来为钩子方法设置非法输入提示信息

    +

    raise ValidationError

    +

    39.密码加密 通过钩子方法定义

    +
      +
    • 钩子方法返回的就是储存到数据库中
    • +
    • md5加密 加密需要“盐”,为了方便和效果 使用django在设置中生成的secret_key来当作“盐”
    • +
    +
    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()
    +
    +

    40.对于djanfo的ORM,如果搜索不到

    +

    搜索不到就返回None,可以用来判断是否是合法请求

    +

    41.对于表单验证

    +

    所有验证通过的信息都储存在对应的cleaned_data

    +

    42.django报错

    +

    'Manager' object is not callable
    +原因是objects是一个属性而不是一个方法,写成objects()会报这个错

    +

    43.用户登录

    +
      +
    • cookies 和 session
      +无状态短链接
      +通过cookies给用户发放一个“凭证”, 保存在浏览器中的键值对,发送请求的时候自动携带
      +session django默认储存在mysql中
    • +
    +

    业务过程:收到用户的提交 -> 校验(在数据库比较) -> 成功 生成随机字符串写入到用户浏览器的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的中间件高效的处理

    +

    44.django中间件

    +

    官方文档

    +
      +
    • 对于django,每一个请求都需要经过很多中间件之后才能访问到视图函数;同样的,所有的视图函数返回值都需要经过很多中间件的处理才会返回给用户。
    • +
    • django的中间件都是类,通过process_request方法对传入的请求进行处理,通过process_response对视图函数的return进行包装。如果某个请求不能通过一个中间件,那么就会直接返回给用户,而不会到达视图函数。
    • +
    • 中间件写好之后需要在setting.py文件中的MIDDLEWARE中注册(具体到类名),哪个在前面那个先执行。
    • +
    • 中间的process_request方法如果返回为None,那么继续向后执行;如果需要返回,那么返回的类型与视图函数的返回类似(例如render,redirect,HttpResponse等),并且不在向后执行。
    • +
    • 应用中间件: +
        +
      • 1.注册中间件
      • +
      • 2.中间件中返回值
      • +
      • 3.通过中间件实现登录校验
        +代码实现
      • +
      +
    • +
    +
    #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路径。

    +

    45.注销操作 - 用户退出登录

    +

    也就是将浏览器的cookies和session删除
    +同样使用request.session中的方法即可

    +

    46.通过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>
    +
    +

    47.form和modelform

    +
      +
    • form可以自定义字段
      +modeform既可以自定义 还可以在数据库中拿取字段
    • +
    • form没有save方法可以直接存入到数据库 但是modelform有
    • +
    +

    48.图片验证码

    +

    -> python中如何动态生成图片并写入值 (不做过多了解 仅仅当成黑盒使用)
    +参考文献

    +
      +
    • 需要pillow库
    • +
    +
    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. 需要注意的是:同一个文件中不要出现名字相同的函数,不管是引用的还是本文件的,会导致调用出错

    +

    49.如何验证验证码呢?

    +

    通过session! - 这样每个用户在登陆的时候对于验证码的验证就不会受到干扰,并且重复刷新也会更新session中对应的验证码。
    +这里有很多需要注意的点!

    +
      +
    • add_errors()可以向forms中指定字段添加错误提示信息,然后能在前端的{{fields.属性字段.errors.0}}中取到。
    • +
    • 由于验证码的字段是没有在用户model/数据库表中存在的,因此如果直接传入request.POST作为data参数会报错。因此应该将验证码字段单独剔除。
    • +
    • 验证码验证需要调用之前创建验证码时存入session的字段,将两个字段相比较判断是否正确。
    • +
    • 登录成功之后需要重新设置session的过期时间 ,因为为了验证码的登录时效,在生成验证码的时候会设置保存验证码对比字段的session信息时效比较短,因此登录成功之后需要重新设置。
      +最终的登录逻辑
    • +
    +
    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类,对应着储存登录用户帐号密码的数据库表。

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/djangos-logic-for-finding-templates-folders/index.html b/post/djangos-logic-for-finding-templates-folders/index.html new file mode 100644 index 0000000..4124a7f --- /dev/null +++ b/post/djangos-logic-for-finding-templates-folders/index.html @@ -0,0 +1,210 @@ + + + + +Django's logic for finding templates folders | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Django's logic for finding templates folders +

    + + +
    +
    +

    今天创建新app后,配置路由时发现renturn的html页面串了

    +

    即app:show/views.py里面的upload方法return的upload.html
    +而不是我在show/templates下面创建的,
    +而是app:picture/templates下面的upload.html文件。
    +最开始是怀疑app:show/views.py或者show/urls.py里面设置的路由或者文件名字错误,
    +将其全部修改成为upload2之后发现正常导入了。

    +

    但是问题本身并没有被解决,本着继续探索的根源,就继续探索问题根源。

    +

    之后将upload2改为upload之后,猜测可能是django命名空间的原因,
    +于是在官方文档中先后查找
    +为url添加命名空间(https://docs.djangoproject.com/zh-hans/4.1/intro/tutorial03/#namespacing-url-names)、
    +URL命名空间(https://docs.djangoproject.com/zh-hans/4.1/topics/http/urls/)
    +结果并没有找到解决方法,并且了解url命名空间只是为了在html页面寻找路由时做区分,
    +而不是返回渲染html时寻找html文件做区分

    +

    之后猜想可能是django加载templates文件夹的逻辑
    +于是寻找到如下三篇博客解决了我的疑惑:

    +

    第一篇

    +

    第二篇

    +

    第三篇

    +

    其中第三篇最为详细,做简单总结与摘要:

    +
      +
    1. Django默认会在配置文件setting.py的TEMPLATE_LOADERS中开启'django.template.loaders.filesystem.Loader',
      +开启该选项后可以按照TEMPLATE_DIRS中列出的路径的先后顺序从中查找并载入模板(templates文件夹)
    2. +
    3. 可以在setting.py文件的TEMPLATE钟添加 'django.template.loaders.app_directories.Loader',这样就会覆盖掉默认方法,加载逻辑就会变成从APP对应的templates文件夹中加载对应的html文件
    4. +
    5. 但第二条列出的方法并不是常用的做法,更常用的做法是在app/templates文件夹内在建立一个app文件夹(app/templates/app),然后将html文件放入内,然后在引用时将路径写为“app/html”。并且在创建app/static文件夹时也采取同样的方法,就相当于是提供了一个命名空间,这样便不用担心Django载入的模板究竟是不是对的、需要的那个模板。
    6. +
    +

    最终问题解决
    +Ps.在更改html文件的地址之后,使用django的模板语言的继承时,也需要修改地址。
    +example:
    +{%extends 'base.html'%} -> {%extend 'app/base.html%}

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/extra-details-of-python-for-long-term-part2/index.html b/post/extra-details-of-python-for-long-term-part2/index.html new file mode 100644 index 0000000..d06cf28 --- /dev/null +++ b/post/extra-details-of-python-for-long-term-part2/index.html @@ -0,0 +1,218 @@ + + + + +Extra details of python - for long term - part.2 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Extra details of python - for long term - part.2 +

    + + +
    +
    +

    2023-06-26

    +

    9. python中的__init__.py

    +

    reference
    +在python工程中,当python在一个目录下检测到__init__.py文件时,就会把它当成一个module。__init__.py可以是空的,也可以有内容。

    +

    --未整理完

    +

    10. python中的getattr()方法

    +

    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的时候会执行该模块。

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/fei-yi-kai-fa-ri-zhi-3/index.html b/post/fei-yi-kai-fa-ri-zhi-3/index.html new file mode 100644 index 0000000..d9f3508 --- /dev/null +++ b/post/fei-yi-kai-fa-ri-zhi-3/index.html @@ -0,0 +1,204 @@ + + + + +非遗开发日志 --3 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + 非遗开发日志 --3 +

    + + +
    +
    +
      +
    1. object-fit属性
      +https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit
    2. +
    3. li嵌套a嵌套img时
      +object-fit属性的cover只针对父节点,所以如果对li外层的div进行限制高度,那img作用的object-fit属性将不起作用。
    4. +
    5. 深刻理解inline 、inline-block 、block之间的区别于关系
    6. +
    7. 深刻理解relative 、absolute的效果
      +absolute定位的依据是 阶层离自己最近且具有设定的父层空间,因此一般需要搭配父节点的relabtive属性使用
      +https://www.runoob.com/css/css-positioning.html#position-relative
    8. +
    9. 深入理解float属性
      +https://www.runoob.com/css/css-float.html
    10. +
    11. setInterva()函数+jq改变css实现图片轮播
    12. +
    13. 深入理解flex布局
    14. +
    15. #html标签使用多个class
      +html标签是允许定义多个class类名的。比如:
      +<div class = "exp1 exp2"></div>
      +上边这个div就用到了exp1、exp2这两个class类,对于个数是没有限制,只需每个类名之间用空格隔开就好。
    16. +
    17. 关于django的url配置
      +django会自动在url路径前面添加/,所以不需要再路径前面添加反斜杠,如果添加会产生错误
    18. +
    19. django不能及时更新修改的css和js文件等
      +https://blog.csdn.net/qq_38388811/article/details/105625981
      +因为浏览器会缓存,需要清除缓存后加载
    20. +
    21. 深入理解django的URL和ROOT路径配置
      +包括STATIC_URL、STATICFILES_DIRS、MEDIA_URL、MEDIAFILES_DIRS
    22. +
    23. django中利用模板再style属性中添加css的写法(注意引号的使用)
      +style='background-image:url("{% static 'pic1.jpg' %}") '
      +或者
      +style='background-image: url("{% static "pic2.jpg" %}")'
      +但最好还是不要这样子写 因为特别容易出错
    24. +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/fei-yi-xiang-mu-kai-fa-ri-zhi-1/index.html b/post/fei-yi-xiang-mu-kai-fa-ri-zhi-1/index.html new file mode 100644 index 0000000..d7886a3 --- /dev/null +++ b/post/fei-yi-xiang-mu-kai-fa-ri-zhi-1/index.html @@ -0,0 +1,189 @@ + + + + +非遗项目开发日志 1 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + 非遗项目开发日志 1 +

    + + +
    +
    +
      +
    1. 简单搭建django项目,发现好多又给忘得差不多了。。
      +所以简单复习了一下,主要就是 设置新app、安装app、添加路由、设置模板文件以及路径、设置静态文件以及路径、创建views、引用html、引用css
    2. +
    3. 因为静态文件在django中调试十分麻烦,于是决定先在外面把html和css写好在放进django
    4. +
    5. 先写了一个大概的base.html以及css
    6. +
    7. 一些小细节~ +
      +

      1)p 标签会横向填充,而span不会,所以在制作导航栏时需要display属性调整为inline时需要注意里面的文字所在的标签(块级元素和内联元素)
      +2) 字体设置
      +3) :not()选择器
      +4) 横向等距分布:`display:inline-block'
      +flex布局
      +去除顶部白边

      +
      +
    8. +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/guan-yu-kua-yu/index.html b/post/guan-yu-kua-yu/index.html new file mode 100644 index 0000000..2943ff2 --- /dev/null +++ b/post/guan-yu-kua-yu/index.html @@ -0,0 +1,176 @@ + + + + +CORS | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + CORS +

    + + +
    +
    +

    之前就研究过这个问题,在学习ajax的时候看到廖雪峰老师的教程说实话也是半知半解,后来在vsc上安装了一个叫live server的插件,才对跨域有了一点体会。这个插件的作用就是可以把文件地址转换为http协议地址,这样在ajax请求的时候,可以直接请求一个http的url。当然,ajax请求的main文件和被请求的txt文件都在live server有对应的http地址。
    +然后就是今天,在学nodejs,它自带一个http的module,然后再廖雪峰的网站上简单创建了一个解析文件地址为http然后在浏览器展示的方法,在尝试的时候,我打开之前的那个main函数,然后再打开ajax请求的txt文件,前者是本地打开,后者是http地址,然后ajax请求的地址是http地址,然后就有了Ensure CORS response header values are valid的报错,发现跨域错误,最后转了一圈发现,只要把ajax请求的地址变为本地或者把这个main文件也变为http的话就可以正常请求了。这次算是更深入了一点了解跨域的问题。
    +一个教程

    +

    另一个教程
    +摘要:
    +Chrome浏览器不允许ajax在本地文件系统中进行请求(经测试edge浏览器也不允许),但是firefox浏览器允许。可能是因为ajax有一定的安全问题

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/hui-diao-han-shu/index.html b/post/hui-diao-han-shu/index.html new file mode 100644 index 0000000..210631c --- /dev/null +++ b/post/hui-diao-han-shu/index.html @@ -0,0 +1,172 @@ + + + + +Callback function | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Callback function +

    + + +
    +
    +

    https://www.zhihu.com/question/19801131

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/install-centos-in-virtualbox-and-configure-the-network-part1/index.html b/post/install-centos-in-virtualbox-and-configure-the-network-part1/index.html new file mode 100644 index 0000000..1c1615b --- /dev/null +++ b/post/install-centos-in-virtualbox-and-configure-the-network-part1/index.html @@ -0,0 +1,246 @@ + + + + +Install centos in virtualbox and configure the network Part.1 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Install centos in virtualbox and configure the network Part.1 +

    + + +
    +
    +

    在virtualbox安装centos并且调配好网络 Part.1

    +

    之前暑假就因为项目原因要配置一次virtualbox+centos7+MobaXterm作为开发环境,没想到一到学校连接校园网之前调配的就全都断掉了(心态崩了),而且由于之前做完没有写个博客之类的记录,这下相当于重头再来了。不过好在还是有一点经验,外加上之前也保存的了一些有用的网站。这次配置起来还是快了不少。

    +

    目标:在virtualbox上安装运行centos,并且虚拟机可以连接互联网,主机和虚拟机之间也可以互相连接。最后连接虚拟机SSH。

    +

    Step1. 下载virtualbox和centos7

    +

    virtualbox下载地址,选择VirtualBox 6.1.38 platform packages的本windows版本以及
    +rtualBox 6.1.38 Oracle VM VirtualBox Extension PackAll supported platforms下载
    +centos下载地址 选择**CentOS-7-x86_64-DVD-2009.iso **下载(大概有4.5GB)

    +

    Step2. 安装virtualbox

    +

    安装提示自选安装地址安装完成后,打开【管理】-【全局设定】-【拓展】安装刚才下载的Extension Pack。
    +centos下载好之后是一个文件,只要你能找得到地址就可以了。

    +

    Step3. 在virtualbox上安装centos7(输入的内容可以完全照抄)

    +
    3.1 点击【新建】
    +

    然后输入如下配置:

    +
    +
    插入图片
    +
    +

    之后一路默认,最后点击创建。参考教程

    +
    3.2 安装配置
    +

    选择左侧刚创建好的虚拟机,然后点击【启动】
    +之后的窗口会让你选择要安装的系统文件,然后找到下载好的centos系统文件,点击启动

    +
    +
    +
    找到刚才下载的centos系统 +
    +

    然后选择【Install CentOS 7】(就是上下键把高亮调到第一个然后按回车)。
    +等待基本配置完成之后,第一个页面是选择语言,翻到最底下选择中文,然后点击【继续】。
    +然后是安装信息摘要页面,把每一个选项都点击进去,但是不要做任何配置(除了下面的网络和主机名),点击进去之后立马点完成。

    +

    但是注意!!!!网络和主机名选项要选择【打开】,然后再点击完成。

    +
    +
    网络配置一定要打开!
    +
    这一步对之后的网络配置很重要 +
    +

    完成后的配置如下:

    +
    +

    配置
    +配置

    +
    +

    然后点击【开始安装】
    +之后设置root密码(Ps. 如果是用108键盘数字,记得打开Num Lock,virtualbox启动虚拟机会默认关闭),设置完之后就会开始安装,我们不需要创建新用户,所以忽略右侧的选项。

    +

    安装完成之后点击重新启动。
    +localhost login提示我们输入用户名,这里输入root,就是刚才黄健的默认管理员用户。
    +Paasword提示输入密码,这里的密码输入不可见。
    +正确输入之后,就成功进入了~

    +
    +
    如下
    +
    如果输入错误会提示login incorrect +
    成功进入左侧的提示就会变成类似于最后一行 +
    +

    最后附上参考 参考教程

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/install-centos-in-virtualbox-and-configure-the-network-part2/index.html b/post/install-centos-in-virtualbox-and-configure-the-network-part2/index.html new file mode 100644 index 0000000..5150896 --- /dev/null +++ b/post/install-centos-in-virtualbox-and-configure-the-network-part2/index.html @@ -0,0 +1,239 @@ + + + + +Install centos in virtualbox and configure the network Part.2 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Install centos in virtualbox and configure the network Part.2 +

    + + +
    +
    +

    在virtualbox安装centos并且调配好网络 Part.2

    +

    第一部分完成了安装virtualbox和centos,接下来就是要配置网络。

    +

    目标:在virtualbox上安装运行centos,并且虚拟机可以连接互联网,主机和虚拟机之间也可以互相连接。最后连接虚拟机SSH。

    +

    Step1. 下载MobaXterm
    +连接,选择【installer edition】。
    +下载好之后,选择安装地址,然后一路默认。

    +

    Step2. 配置虚拟机网络
    +这里涉及到几种连接方式,但本文只着重于操作方法,具体概念会在以后再写一篇,同时这篇文章最后也会提供这些概念的一些参考链接。
    +这里采用NAT+ host-only的方法。前者负责虚拟机连接互联网,后者负责虚拟机和主机通信。
    +2.1 NAT配置
    +首先退出虚拟机,然后在virtualbox界面选择【管理】-【全局设置】-【网络】-右侧的第一个图标,然后设置如下:

    +
    +
    插入图片
    +
    +

    然后选择左侧创建的虚拟机,然后点击【设置】-【网络】-【网卡1】,配置如下:

    +
    +
    插入图片
    +
    +

    这时再次打开虚拟机之后,尝试
    +ping baicu.com发现可以ping通,表明配置成功。
    +然后再次退出,进行下一步配置。

    +

    2.2 host-only配置
    +在virtualbox界面选择【管理】-【主机网络管理器】然后设置如下:

    +
    +
    插入图片
    +
    插入图片
    +
    +

    配置完之后点击【应用】,然后选择左侧的虚拟机,点击【设置】-【网络】-【网卡2】,配置如下:

    +
    +
    插入图片
    +
    +

    然后再次启动,输入 ip addr

    +
    +
    插入图片
    +
    +

    lo 是本地网络(127.0.0.1),默认配置就可以
    +enp0s3是配置的NAT网卡,负责虚拟机和互联网联通,默认配置就可以,我们也不需要知道他的IP地址
    +enp0s8是我们现在配置的host-only网卡,蓝色线对应的就是虚拟机负责与主机连通的ip地址。
    +我们尝试用主机ping这个地址,在用虚拟机ping主机,两边连通正常。表示配置完成。

    +

    Step3. 配置SSH连接
    +打开安装好的MobaXterm,点击左上角的session,选择SSH,在Remote host输入刚才蓝线表示的虚拟机IP地址(要保证虚拟机运行),然后点击【ok】
    +login as提示输入账户名,我们只使用了管理员root账户,所以输入root
    +password提示我们输入密码。
    +全部正确之后,出现如下页面表示连接正常。

    +
    +
    插入图片
    +
    +

    这时我们就相当于通过SSH于虚拟机远程连接,可以在MobaXterm上运行主机,包括安装删除文件、调整配置等。

    +

    Ps.我没有设置静态ip,所以可能会在之后的连接中因为动态ip分配而导致连接错误,不过因为主机配置的局域网ip基本不会发生改变,所以配置的ip也不会经常改变,所以暂时不去设置静态ip。

    +

    virtualbox几种连接方式详解:详解连接
    +MobaXterm安装参考:详解连接
    +配置NAT和host-only参考:详解连接

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/jquery-dong-hua-fang-zhi-duo-ci-hong-fa-chong-fu/index.html b/post/jquery-dong-hua-fang-zhi-duo-ci-hong-fa-chong-fu/index.html new file mode 100644 index 0000000..8f7992b --- /dev/null +++ b/post/jquery-dong-hua-fang-zhi-duo-ci-hong-fa-chong-fu/index.html @@ -0,0 +1,215 @@ + + + + +Prevent jquery animation to triggering than once. | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Prevent jquery animation to triggering than once. +

    + + +
    +
    +

    https://blog.csdn.net/u013239476/article/details/103155634
    +同时调整外部的div,让其跟着内部的图片动画一同改变大小,防止出界等不美观的情况‘
    +还有jquery的each函数 同一个动画绑定多个元素

    +
    +
    + + + + +
    +
    +
    + +
    +

    动画效果

    +
    +
    +

    $(function(){
    +var anime = $('div.part-2 .pic img'),
    +div = $('.part-2');

    +
    anime.each(function(){
    +    $(this).mouseenter(function(){
    +        $(this).stop(true,false).animate({
    +            width:'200px',
    +            height:'200px'        
    +        },250)
    +        div.stop(true,false).animate({
    +            height:'308px',
    +        },250)
    +    })
    +    $(this).mouseleave(function(){
    +        $(this).stop(true,false).animate({
    +            width:'100px',
    +            height:'100px'  
    +        },200)
    +        div.stop(true,false).animate({
    +            height:'208px',
    +        },200)
    +        
    +    })
    +})
    +
    +

    })

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/js-promise/index.html b/post/js-promise/index.html new file mode 100644 index 0000000..ae0dc1a --- /dev/null +++ b/post/js-promise/index.html @@ -0,0 +1,179 @@ + + + + +js promise | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + js promise +

    + + +
    +
    +

    建立 Promise
    +Promise 是一種物件,它的建構函式接受一個執行函式 (executor),用來定義非同步行為。執行函式會被馬上呼叫並傳入兩個參數:(resolve, reject)。

    +

    Promise 物件的初始狀態為 pending,當我們完成了非同步流程,在執行函式中呼叫 resolve(),會將 Promise 的狀態轉變成 resolved。當錯誤發生時,我們呼叫 reject() 會將 Promise 的狀態轉變為 rejected。

    +

    ----------------2022.11.22 学习Cesium.js又再次遇到,进行补充--------------
    +Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和 Rejected(已失败)。

    +

    通过回调里的 resolve(data) 将这个 promise 标记为 resolverd,然后进行下一步 then((data)=>{//do something}),resolve 里的参数就是你要传入 then 的数据。

    +

    参考链接

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/js-yi-bu-he-dan-xian-cheng/index.html b/post/js-yi-bu-he-dan-xian-cheng/index.html new file mode 100644 index 0000000..71ad3d7 --- /dev/null +++ b/post/js-yi-bu-he-dan-xian-cheng/index.html @@ -0,0 +1,201 @@ + + + + +Asynchronization and single thread about javascript | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Asynchronization and single thread about javascript +

    + + +
    +
    +

    需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。 引用原文

    +

    setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。原文2

    +

    理解:
    +js单线程存储两个调用栈:执行栈和事件栈。js在运行的时候永远只有一处代码在执行,但是这段代码来自于执行栈还是事件栈是根据代码内容决定,浏览器中的js引擎中有一个异步进程处理,用来判断js的异步进程是否满足执行条件,如果不满足则不执行,满足就将其放入事件栈中,等待执行栈所有任务执行完成之后在进行调用。

    +

    理解引用:三、异步是如何实现的:异步是靠浏览器的两个或者两个以上的常驻线程来完成的,第一个就是js的执行线程,第二个就是事件触发线程,js执行线程发出异步请求给浏览器,浏览器开一个新的请求线程来处理,js执行线程继续执行其他任务。那么事件触发线程呢?他就监听这个请求,如果这个请求完成了,就将该事件插入到JS执行队列的尾部等待JS处理。
    +链接
    +黑马课程 p278 279 280
    +深入理解后的代码:

    +
    setTimeout(function(){
    +    console.log('定时器开始啦')
    +});
    +
    +new Promise(function(resolve){
    +    console.log('马上执行for循环啦');
    +    for(var i = 0; i < 10000; i++){
    +        i == 99 && resolve();
    +    }
    +}).then(function(){
    +    console.log('执行then函数啦')
    +});
    +
    +console.log('代码执行结束');
    +
    +
    +

    输出结果:

    +
    +

    马上执行for循环啦
    +代码执行结束
    +执行then函数啦
    +定时器开始啦

    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/jupyter-notebook-qi-dong-500-wen-ti/index.html b/post/jupyter-notebook-qi-dong-500-wen-ti/index.html new file mode 100644 index 0000000..d23d606 --- /dev/null +++ b/post/jupyter-notebook-qi-dong-500-wen-ti/index.html @@ -0,0 +1,178 @@ + + + + +Encounter a statuse code 500 error when opening jupyter notebook | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Encounter a statuse code 500 error when opening jupyter notebook +

    + + +
    +
    +

    https://stackoverflow.com/questions/36851746/jupyter-notebook-500-internal-server-error

    +

    更新conda相关组件

    +

    pip install --upgrade jupyterhub
    +pip install --upgrade --user nbconvert

    +

    或者(在环境里面)
    +conda install nbconvert==5.4.1

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/li-jie-js-de-then-catch-reject-promise-deng/index.html b/post/li-jie-js-de-then-catch-reject-promise-deng/index.html new file mode 100644 index 0000000..3b6abe1 --- /dev/null +++ b/post/li-jie-js-de-then-catch-reject-promise-deng/index.html @@ -0,0 +1,172 @@ + + + + +Some realization about 'then, catch, reject, Promise, ...' in javascript. | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Some realization about 'then, catch, reject, Promise, ...' in javascript. +

    + + +
    +
    +

    https://zhuanlan.zhihu.com/p/94805682

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/liao-xue-feng-wang-zhan-nodejs-dui-yu-jing-tai-wen-jian-de-chu-li/index.html b/post/liao-xue-feng-wang-zhan-nodejs-dui-yu-jing-tai-wen-jian-de-chu-li/index.html new file mode 100644 index 0000000..e17c861 --- /dev/null +++ b/post/liao-xue-feng-wang-zhan-nodejs-dui-yu-jing-tai-wen-jian-de-chu-li/index.html @@ -0,0 +1,172 @@ + + + + +廖雪峰网站nodejs对于静态文件的处理 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + 廖雪峰网站nodejs对于静态文件的处理 +

    + + +
    +
    +

    他是从url中提取文件名称然后再本地查找,。?感觉有点奇怪,我印象里应该是先将本地文件设置一个url然后其他文件再引用,他这个刚好相反,,感觉有点奇怪,

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/lin-shi-ji-lu/index.html b/post/lin-shi-ji-lu/index.html new file mode 100644 index 0000000..fae3316 --- /dev/null +++ b/post/lin-shi-ji-lu/index.html @@ -0,0 +1,615 @@ + + + + +django学习 -- 可以感受到最原始的django框架是前后端不分离的 -- part.1 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + django学习 -- 可以感受到最原始的django框架是前后端不分离的 -- part.1 +

    + + +
    +
    + +

    1.请求和响应

    +
      +
    • get post redirect
    • +
    +

    2.数据库操作

    +
      +
    • mysql+pymsql
    • +
    • ORM框架 -> 帮助我们处理sql语句 => pip install mysqlclient
      +创建修改删除数据中的表(不用写sql语句)
      +操作表中的数据
      +=> 创建数据库 django连接数据
      +PS. 需要在setting.py进行设置
    • +
    +

    3.让django帮助创建数据库表

    +
      +
    • django会通过model中的类帮助生成sql创建表和字段
      +类名代表表明 属性代表列名
    • +
    +
    class UserInfo(models.Model):
    +    name = models.CharField(max_length=32)
    +    password = models.CharField(max_length=64)
    +
    +

    4.执行命令

    +
      +
    • 需要提前注册app -> 在setting.py中注册
      +python manage.py makemigrations
      +python manage.py migrate
    • +
    +

    5.需要注意

    +
      +
    • 在已经有的model类中添加属性 可能会导致原来已经有的数据缺失新增加的列的数据
      +django给出了两个选择,一是在命令行中默认添加输入,二是在新增加的属性字段中增加 default=字段,代表给原来没有的缺失值赋值。例如:
      +age = models.CharField(defalut=18)
      +或者设置可以为空
      +age = models.CharField(null=True,blank=True)
    • +
    +

    6.通过django向表中插入数据

    +
      +
    • +

      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()进行筛选

      +
    • +
    +

    7.django表单如果不写action 那么默认提交就是向当前页面发送一个post请求

    +

    8.django中的url命名能避免对url的硬编码,

    +
      +
    • 例如修改一个url 的路径如果以前在html采用的是硬编码,直接写对应的会导致后面修改维护极为困难。
      +但是如果采用url命名空间,那么修改的url并不会直接影响到,django会通过url的命名访问到,而不是硬编码。{% url 'url_name' other_param%} 这个url_name 就是在urls.py文件中的path('url/example', views.function,name = url_name)的name为url从的重命名,这样修改path对应的url,就不用在维护前端的页面了。
    • +
    +

    9.model添加约束 (数据库的约束)

    +
      +
    • 例如员工表和部门表之间的关系,让员工表和部门表关联,通过ForeignKry方法中的toto_field参数在django中设置。用途是可以方便对输入数据的合法性判断,例如员工不能属于一个不存在的部门,就可以很方便的进行判断。
    • +
    • 在员工表中
      +department = models.ForeignKey(to = 'Department', to_field = 'id')
      +这就将员工表和部门表进行了关联,员工的部门只能属于Depatment中的一个;to表示关联的表名,to_field表明关联的表的一个列名字段。要注意的是,django在储存这样的字段时默认命名方式是关联表名_id(例如这个就是depaetment_id)。
      +于此对应,在获取的时候,如果直接通过字段/属性获取的话,例如obj.联表名_id,获取到的时储存在这张表中的对应关联表的数字,而不是我们想要的具体指代,因此django规定了如果直接.联表名就可以获取到关联表对应的那一行数据(封装为一个queryobj对象),然后就可以继续通过关联表内的属性进行查找。例如,部门表中一共有两个部门,字段为(id,部门名),内容为(1,主管部)(2,运行部),员工表关联
    • +
    • 级联删除 置空null
      +现在设想一种情况,如果部门表中的一个部门取消掉,那么与之关联的员工应该如何处理呢?常用的就是两种操作:将他们一并删除(这就是级联删除);还有一种就是将他们设置为null。对应的这两种方式在django中需要这样设置:
      +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中的操作。
    • +
    +

    10.性别存储

    +
      +
    • model的choice 与数据库无关 是django的特性 可以方便的进行固定的选择储存。例如男女,单独为男女添加一个char字段会较为浪费空间,因此使用1/2这样的数字代替,但是数字不易读,因此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()
    • +
    +

    11.Form和ModelForm

    +
      +
    • 11.1 form类 自动生成表单
    • +
    +
    #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>
    +
    +
      +
    • 11.2 ModelForm组件 +
        +
      • 实现数据库表单字段和前端form表单字段的自动生成
      • +
      +
        +
      • 对连接表/外键限制自动生成选择框
      • +
      • 定义models类中的__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']
    +
    +

    12.重定向

    +
      +
    • from django.shortcuts import redirect或者from django.http import HttpResponseRedirect
      +return一个重定向url的实例即可
    • +
    +

    13.前端url实现删除请求

    +
      +
    • 通过前端发送一个带有删除目标信息的GET请求,然后后端获取删除即可,但是这个带有删除目标信息的url该如何组装呢?通过url的组成规律,添加/?id=信息传递,代码如下:
      +<a href="/login/manage/delete/?id={{user.0}}">删除</a> #这其中的{{id.0}}通过django的模板语法动态赋予目标的id信息
      +对应处理函数(记得注册url)
    • +
    +
    def delete_user_info(request):
    +    id = request.GET.get('id')
    +    UserInfo.objects.filter(id = id).delete()
    +    return HttpResponseRedirect('/login/manage/')
    +
    +

    14.django的模板语法

    +
      +
    • 其中最开始的{% extends 'nav.html'%}必须处于子页面的第一个加载位置,意味着如果前面有哪怕{% load static %}也会报错。
    • +
    +

    15.django正则匹配url

    +
      +
    • 这样就不用前端写带有get参数的url,后端也不需要get获取,而是直接作为一个解析好的参数传递给视图进行处理,二是直接及进行请求和解析。同时也要注意,在url中添加的正则顺序不影响在命名url中的参数顺序
    • +
    +
      +
    • 具体如下:
    • +
    +
    #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请求获取。

    +

    16.在django的模板中,可以通过obj.get_联表名_display获取到和后端get_联表名_display()的效果,直接返回对照过后的结果而不是对应的关联id。

    +

    17.django的模板可以进行的额外操作:

    +
      +
    • {{number : add 1}}就是python的+=1操作,{{datetime | date :'Y-m-d}}就是python中的strftime()函数(要注意的是里面的格式不需要带%)
    • +
    +

    18.modelform的字段校验函数

    +
      +
    • is_valid()检验提交post请求中的form表单中的字段是否为空,具体的字段就是在modelformfields列表定义的那些.
    • +
    • 同时还可以在modelforms类中进行字段校验 错误的信息会保存在用post请求实例化的modelform中。
    • +
    +
    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参数进行正则筛选
    +
    +

    19.调整返回的提示信息为汉语

    +
      +
    • 通过修改setting.py文件中的语言项
    • +
    +
    #setting.py
    +# LANGUAGE_CODE = "en-us" 注释掉
    +LANGUAGE_CODE = "zh-hans" #添加
    +
    +

    20.modelform中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,
    +    })
    +
    +
      +
    • 如果想在用户输入以外的字段进行更新,那么在modelform实例化之后通过instacne属性可以修改:
    • +
    +
    userform = userModelForm(data=request.POST,instance=default_userinfo)
    +# useform.instance.字段 =  值
    +
    +

    21.django查询排序

    +
      +
    • obj.objects.all().order_by('属性名')
      +如果给属性名前面添加减号 代表逆向(高到低)排序
    • +
    +

    22.给原有的表中添加新的字段产生错误"django.core.exceptions.ValidationError" error

    +
      +
    • 产生原因:大概率是因为追加新的字段设置默认值的时候产生的格式错误,例如datetime格式但是没有设置默认或者默认格式错误,造成这样的原因。
    • +
    • 解决方法:重置所有的migrations
    • +
    • 操作:删除migrations文件夹下除了__init__.py文件以外所有的文件,然后重新执行数据库迁移命令:python manage.py makemigrations, python manage.py migrate
    • +
    • 如果还不成功可能是因为数据库中原本存在的数据导致,将数据库清库(所有表删除),然后在执行即可。
    • +
    +

    23.django报错'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 -- 感觉好多...暂时搁置一下...😖
    • +
    +

    24.关闭浏览器校验

    +

    在form表单添加novalidate

    +
    <form method="post" novalidate >
    +
    +

    25.关于models中null和black字段

    +
      +
    • 需要同时开启,但是数据库更推荐默认值为空值,因此设置default=None而不是null
      +如果设置blank=True但是不开启null=True,那么在插入的时候,如果传回来的数据为空,插入会报错不能为null字段...总之还是很迷惑的报错....但是解决方法还是最好设置两个都为True并且设置默认字段为None。如果为null,那么在数据传入到前端的时候会默认输出null或者none,而不是将那片区域置空,对于前端的用户输入逻辑不太友好。
    • +
    +

    26.modelforms便捷写法

    +
      +
    • 在添加fields的时候,可以用__all__一次性获取全部,exclude = [ 排除的字段 ]排除掉不要的字段。
    • +
    +
    class orderForm(ModelForm):
    +    class Meta():
    +        model = Orderform
    +        fields = "__all__" #注意要引号
    +        # exclude = ['字段1','字段二', .... ]
    +
    +

    27.后端表单正则校验

    +
      +
    • 在modelforms类中声明字段,然后通过validators参数设置正则表达式:
    • +
    +
    from django.core.validators import RegexValidator #需要导入正则类
    +class exampleModelForms(forms.ModelForm):
    +    moblie = forms.CharField(
    +        label = '手机号码',
    +        validactors = [RegexValidator(r'^1[3-9]\d{9}$','手机号格式错误')]       
    +        #校验失败返回后面的提示信息而不是默认的了~ 
    +    )
    +
    +

    28.钩子方法进行字段校验

    +
      +
    • 详细的
    • +
    • 这里摘列出常用的代码结果
    • +
    +
    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')
    +
    +

    29.规定modelform中不可编辑的部分

    +

    在modelform中字段中设置参数disabled= True即可,这样在前端页面渲染出来的这一条字段是不可以更改的。

    +

    30.通过modelform判断要添加的数据是否重复

    +
      +
    • order_edit_form = orderEditForm().filter(id=你要查询的).exist() 返回True/False
      +这个方法需要添加在modelform中的钩子方法中。
    • +
    • 排除自己以外的是否存在重复?(主要是在修改数据的时候)
      +通过exclude排除自己后判断:order_edit_form = orderEditForm().exclude(id='自己的id' ).filter(id=你要查询的).exist() 返回True/False
    • +
    +

    总结 编辑和添加的不同

    +

    31.查询(根据某个字的一部分进行)

    +
      +
    • filter()还支持传入字典:
    • +
    +
    order_edit_form = orderEditForm().filter(id=你要查询的,other=你要查询的)
    +#等于
    +query_dict = { 'id' : 要查询的, 'other' : 要查询的 }
    +order_edit_form = orderEditForm().filter(**query_dict)
    + #需要注意传入字典的时候需要两个**
    +
    +

    PS. 通常我们再变量前加一个星号(*)表示这个变量是元组/列表,加两个星号表示这个参数是字典

    +

    32.问题记录:modelform不能更新一行数据的部分字段

    +
      +
    • 解决方法
      +这里的解决方法很多 包括自定义钩子函数对post数据进行清晰、定义一个工厂函数处理每次不同的字段等,不过都太高端了,部分概念我还没有理解清楚😭
    • +
    • 因此我打算采取最简单的 -- 一个字段一个字段的去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类
    +
    +

    33.django的条件查询

    +

    除了固定的等于(=)查询外,还可以通过给字段/属性添加下划线的方式规定范围查询

    +
      +
    • 数字方面:gt、gte、lt、lte:大于、大于等于、小于、小于等于。
      +list = BookInfo.objects.filter(id__gt=3) 代表id大于3的数据
      +PS. 不等于可以通过exclude过滤
    • +
    • 字符串方面:startswith、endswith:以指定值开头或结尾;contains:是否包含。
      +list = BookInfo.objects.filter(btitle__endswith='部') 查询是否数据中btitle字段以‘部’这个字结尾的一行数据。
    • +
    • 日期查询: year、month、day、week_day、hour、minute、second:对日期时间类型的属性进行运算。
      +list = BookInfo.objects.filter(bpub_date__year=1980) 查询1980年发表的图书
      +list = BookInfo.objects.filter(bpub_date__gt=date(1990,1,1)) 查询1980年1月1日后发表的图书。
    • +
    • 还有很多查询: 例如比较同一行数据两个字段之间的关系(如阅读数量>=评论数量)、与或非查询、聚合等... 具体参考reference或者官网
    • +
    +

    PS. 1. filter可以连续使用 :list=BookInfo.objects.filter(bread__gt=20).filter(id__lt=3) 2. filter可以传入字典

    +

    34.设置get默认值

    +
      +
    • request.GET.get('page', '1') 获取get请求,如果get没有附带page参数,那么默认是1.
    • +
    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/mysql-null-he-kong-de-qu-bie/index.html b/post/mysql-null-he-kong-de-qu-bie/index.html new file mode 100644 index 0000000..0fb32d2 --- /dev/null +++ b/post/mysql-null-he-kong-de-qu-bie/index.html @@ -0,0 +1,181 @@ + + + + +mysql -- null和空的区别 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + mysql -- null和空的区别 +

    + + +
    +
    +
    +

    空值时不占用空间的; 2、null其实是占用空间的; 打个比方来说,你有一个杯子,空值代表杯子是真空的,NULL代表杯子中装满了空气,虽然杯子看起来都是空的,但是区别是很大的。 NULL 其实并不是空值,而是要占用空间,所以mysql在进行比较的时候,NULL 会参与字段比较,所以对效率有一部分影响

    +
    + +

    那什么时候用null什么时候用空值呢?

    +
    +

    更推荐使用空值。两者的含义不一样,前者代表不清楚,后者代表缺失值。但实际使用上null会在数据库操作方面造成很多的麻烦,因此更推荐设置为空值,并且字段设置为NOT NULL

    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/nginx-xue-xi/index.html b/post/nginx-xue-xi/index.html new file mode 100644 index 0000000..d085ec2 --- /dev/null +++ b/post/nginx-xue-xi/index.html @@ -0,0 +1,260 @@ + + + + +nginx -- 学习 -- + uwsgi部署django项目 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + nginx -- 学习 -- + uwsgi部署django项目 +

    + + +
    +
    + +

    1. 什么是nginx

    +
      +
    • 轻量级的高性能3web服务器,提供了http代理和反向代理、负载均衡,使用c语言编写,效率极高。
    • +
    • 一般使用uwsgi协议让nginx转发接收到的http协议给uWSGI(uwsgi协议是二进制,效率更高)
    • +
    +

    2. 安装nginx

    +

    sudo apt install nginx
    +安装成功之后输入nginx -v查看版本。
    +PS. nginx安装之后会默认占用80端口

    +

    3. 配置nginx

    +

    /etc/nginx/site-enabled/default是nginx的配置文件
    +核心参数

    +
      +
    • location / 有点像路由 表示所有这个路由的请求都由下面的参数代表的服务处理
    • +
    • uwsgi_pass 将服务以uwsgi传递给***地址(例如127.0.0.1:8000)
    • +
    • include 配置参数 如果需要使用uwsgi协议需要写 /etc/nginx/uwsgi_params
    • +
    +

    4. 启动/停止nginx

    +

    sudo /etc/init.d/nginx start|stop|restart|status 分别代表启动|停止|重启|查看状态
    +PS. nginx只要修改就需要重启,否则配置不生效

    +

    5. 修改uwsgi配置文件,适配nginx,修改nginx,适配uwsgi启动

    +

    具体如下:

    +
    #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可以快速方便的告诉你配置文件是否有语法错误

    +

    6. 排错

    +
      +
    • 看日志文件! 访问日志:/var/log/nginx/error.log 错误日志:/var/log/nginx/access.log 对应uwsgi日志 - 和uwsgi.ini同级下的uwsgi.log
    • +
    • 502 代表nginx反向代理成功,但是对应的uwsgi未启动;还有一种就是uwsgi设置接受请求过小,在uwsgi.ini中添加buffer-size = 65536 - 主要还是得看日志
    • +
    • 404(分两种:django报错/nginx报错) 一可能是路由不在django项目;二可能是没有禁止掉nginx配置文件里的try_files
    • +
    +

    7. nginx静态文件配置

    +
      +
    • 创建新文件夹 - 存放所有的django静态文件 -- 例如/home/mysite_static/
    • +
    • 在django setting.py中添加配置 STATIC_ROOT代表静态文件路径 -- 例如/home/mysite_static/static/
    • +
    • 进入django项目,执行python3 mange.py collectstatic - 收集项目所有的静态文件
    • +
    • nginx配置文件新增加
    • +
    +
    location /static {
    +root /静态文件夹;  
    +}
    +
    +

    特别要注意路径,这个‘静态文件夹’下面还有一个static的文件夹,里面才是真正存放了所有的静态文件

    +

    8. 404/500 自定义报错页面

    +
      +
    • 在django的模板文件夹内添加404.html文件,当视图触发http404的时候自动显示该页面(在debug=false的时候彩起作用) -- 添加完之后记得重启nginx、uwsgi等服务
    • +
    +

    9. 给原来的静态文件夹下新增文件夹,结果404

    +

    大概是因为项目中的静态文件路径和nginx的有所区别,
    +需要注意alias和root的区别,
    +nginx会根据请求的路径,去请求静态文件夹中可能存在的,文件夹中的静态文件。简而言之,nginx是可以嵌套的,但是前提是请求的路径需要对应的上。

    +

    10. alias与root的区别

    +

    csdn参考

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/part2/index.html b/post/part2/index.html new file mode 100644 index 0000000..c2ec893 --- /dev/null +++ b/post/part2/index.html @@ -0,0 +1,535 @@ + + + + +python网络编程 -- 建立一个http服务器+twisted模块--part.2 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + python网络编程 -- 建立一个http服务器+twisted模块--part.2 +

    + + +
    +
    +

    =>

    +

    HTTP基础响应

    +
      +
    1. http服务器
    2. +
    +
    # 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程序
    +会使服务端的资源利用带来极大便利。

    +
      +
    1. 服务端:
    2. +
    +
    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()
    +
    +
      +
    1. 客户端
    2. +
    +
    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

    + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/preview-pictures-in-front-end-by-using-urlcreateobjecturl-and-some-details-in-this-means/index.html b/post/preview-pictures-in-front-end-by-using-urlcreateobjecturl-and-some-details-in-this-means/index.html new file mode 100644 index 0000000..9a2ed7c --- /dev/null +++ b/post/preview-pictures-in-front-end-by-using-urlcreateobjecturl-and-some-details-in-this-means/index.html @@ -0,0 +1,179 @@ + + + + +Some details when using URL.createObjectURL to preview images on the front end | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Some details when using URL.createObjectURL to preview images on the front end +

    + + +
    +
    +
      +
    1. URL.createObjectURL
    2. +
    3. define img.src to create an img element that will display the picture
    4. +
    5. use appendChild instead of innerHTML
    6. +
    7. URL.revokeObjectURL
    8. +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/python-da-kai-zui-jin-guan-bi-de-word-wen-jian-xu-qiu-jie-jue/index.html b/post/python-da-kai-zui-jin-guan-bi-de-word-wen-jian-xu-qiu-jie-jue/index.html new file mode 100644 index 0000000..105fed4 --- /dev/null +++ b/post/python-da-kai-zui-jin-guan-bi-de-word-wen-jian-xu-qiu-jie-jue/index.html @@ -0,0 +1,299 @@ + + + + +python打开最近关闭的word文件需求解决 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + python打开最近关闭的word文件需求解决 +

    + + +
    +
    +

    1. python关于进程的操作

    +
    - os.fork()	
    +- os.getpid()
    +- ...
    +
    +初始思路:再打开word的时候托管一个python进程,捕捉用户的word操作日志,记录上一次关闭的窗口
    +                                  <center>**问题**</center>
    +    1.  获取不到进程中的word的进程名称 -- 因为获取的都是进程数字(或者会有方法 但是我关于window系统/python系统部分的熟悉程度不足)
    +    2. 获取进程也不知道怎么获取word进程中打开的文档名字/地址等信息
    +
    +

    => 意识到了python关于系统的库os

    +

    2. 尝试寻找python标准库 - os库中可能会用到的提供的函数

    +
    粗略浏览后并没有找到可用的解决方法(也有可能我忽略了)
    +>还发现了python关于日志的处理 - logging库,但于目标并不相关。
    +
    +

    => 意识到了python可能会有关于支持word操作的库

    +

    3. python-docx库

    +
    粗略浏览后发现只是通过python向word中写入/读取内容(包括word文字、标题、图片、表格、排版等)
    +并没有找到可用的思路或者解决方法
    +
    +

    =>思路转到word软件本身/windows本身

    +

    4. word/windows是怎么恢复上一次打开的文件的?

    +
    -> [window怎么查看最近修改/打开的文件?](https://blog.csdn.net/kaige8312/article/details/121898000?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-121898000-blog-113621828.235%5Ev28%5Epc_relevant_default_base1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-121898000-blog-113621828.235%5Ev28%5Epc_relevant_default_base1&utm_relevant_index=1)
    +
    +**`recent`**
    +
    +搜索运行 (win+r ) 输入recent => 发现了最近打开的文件
    +=>  发现recent文件夹中的文件类型都是.lnk
    +=>  通过python脚本打开这些快捷方式以恢复最近打开的文件	
    +=>  关键问题:
    +    q1. 这个文件位置在哪里? 
    +    q2. python怎么打开文件?
    +
    +

    5. 解决关键问题

    +
    Q1.
    +[参考链接](https://zhuanlan.zhihu.com/p/401313556)
    +    -> 复制文件地址 
    +    -> "C:\Users\sapph\Recent\新建 文本文档.txt.lnk"
    +    -> 根据复制的地址 并没有看到recent文件夹
    +    -> 实际上在          "C:\\Users\\sapph\\AppData\\Roaming\\Microsoft\\Windows\\Recent"
    +
    +

    注意AppData是一个隐藏文件夹
    +注:不过在文件系统查找还是可以直接使用”C:\Users\sapph\Recent“直接访问到recent文件夹的
    +Q2.
    +python的标准os库中提供了相关的函数

    +
    >os.open() -> ×
    +os.system() -> 可以 但是在测试的时候遇到带有空格的文件路径会分开执行,导致bug
    +
    +>	=> [subprocess 模块](https://docs.python.org/zh-cn/3/library/subprocess.html#module-subprocess)
    +		仔细阅读并测试得到下面的:
    +		**subprocess.Popon(args[,shell=])**
    +            1. args 应当是一个程序参数的序列或者是一个单独的字符串或 path-like object.
    +            2.  在 Windows,如果 args 是一个序列,他将通过一个在 在 Windows 上将参数列表转换为一个字符串 描述的方式被转换为一个字符串。这是因为底层的 CreateProcess() 只处理字符串。
    +            3.  在 Windows,使用 shell=True,环境变量 COMSPEC 指定了默认 shell。在 Windows 你唯一需要指定 shell=True 的情况是你想要执行内置在 shell 中的命令(例如 dir 或者 copy)。
    +			在运行一个批处理文件或者基于控制台的可执行文件时,不需要 shell=True。
    +
    +

    6. 开始规划需求,做出解决方案

    +

    6.1 最终解决方案

    +
    import subprocess
    +import os
    +import time
    +root_dir = "C:\\Users\\sapph\\%userprofile%\\Roaming\\Microsoft\\Windows\\Recent")
    +dirs = os.listdir("C:\\Users\\sapph\\%userprofile%\\Roaming\\Microsoft\\Windows\\Recent")
    +# userprofile修改为自己的
    +docx_file = []
    +for filename in dirs:
    +    if filename[-9:] == '.docx.lnk':
    +        docx_file.append(filename)
    +# print(docx_file)
    +docx_file_time = {}
    +for f in docx_file:
    +    filepath = os.path.join(root_dir,f)
    +    open_time = os.path.getatime(filepath) #时间戳
    +    timeStruct = time.localtime(open_time)
    +    open_time = time.strftime('%Y%m%d-%H:%M:%S',timeStruct)
    +    docx_file_time[filepath] = open_time
    +# print(docx_file_time)
    +docx_file_time = dict(sorted(docx_file_time.items(),key=lambda item: item[1],reverse=True))
    +print("排序完成",docx_file_time)
    +file_list = list(docx_file_time.keys())
    +for val in file_list[:3]:
    +    print("打开",val)
    +    subprocess.Popen(val,shell=True)
    +print("启动完成...")
    +
    +

    6.2 优化思路

    +
    1. 图形界面、
    +2. 提高自定义水平(使用配置文件xml/yuml/json/ENV等)、
    +3. 操作的文件种类更多(excel/docs/ppt/txt/pdf)、
    +4. 根据软件定制(word/特定pdf阅读器/excel)
    +
    +

    6.3 打包exe

    +

    pyinstaller库

    +

    7. (个人认为)更好的方案/设想/回顾补充

    +
    1. 通过托管进程监控用户对于word的操作,记录日志,并保存为链接,下次可以直接打开(更加灵活,可操作性更高,类似Aplue.js)
    +2.  获取windows日志,可能会有关于word的操作记录,捕捉并解析,记录,下一次打开
    +3.  [使用更加贴近底层的语言(c/c++/java)](https://blog.csdn.net/u014677109/article/details/127444425)
    +
    +

    8. others

    +

    os.ctermid()
    +os.getcwd()
    +os.link()
    +等...

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/python-wang-luo-bian-cheng/index.html b/post/python-wang-luo-bian-cheng/index.html new file mode 100644 index 0000000..287dcac --- /dev/null +++ b/post/python-wang-luo-bian-cheng/index.html @@ -0,0 +1,453 @@ + + + + +python网络编程 -- 建立一个http服务器+twisted模块--part.1 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + python网络编程 -- 建立一个http服务器+twisted模块--part.1 +

    + + +
    +
    +

    =>

    +

    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架构

    +
      +
    1. 服务器端socket程序
    2. +
    +
    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()
    +
    +
      +
    1. 使用telnet命令测试
      +windows需要在设置里配置telnet,他是操作系统中提供的一个测试命令。
      +telnet localhost 7000
    2. +
    3. 客户端socket程序
    4. +
    +
    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程序模型

    +
      +
    1. echo服务端
    2. +
    +
    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测试。

    +
    +
      +
    1. echo客户端
    2. +
    +
    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()。

    +
      +
    1. UDP服务端
    2. +
    +
    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()
    +
    +
      +
    1. UDP客户端
    2. +
    +
    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()
    +
    +
      +
    1. send/sendto, recv/recvfrom区别
      +PS. 1. UDP不需要建立稳定的链接 ,不受到连接的控制,不需要考虑多连接(不需要考虑并发),需要不断的接收,但只需要将响应的信息原路返回给对应的客户端即可。2. UDP是一种无连接协议,因此不能使用telnet命令进行连接测试。
    2. +
    +

    =>

    +

    UDP广播

    +
      +
    1. UDP广播接收端
    2. +
    +
    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()
    +
    +
      +
    1. UDP广播发送端(服务端)
    2. +
    +
    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()
    +
    +
      +
    1. 需要注意的点/具体解释
      +关键的函数配置项:
    2. +
    +
    +

    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**、
    +常见响应头信息

    + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/requestspost-he-requestssessionpost-fang-fa-de-qu-bie/index.html b/post/requestspost-he-requestssessionpost-fang-fa-de-qu-bie/index.html new file mode 100644 index 0000000..5cf8801 --- /dev/null +++ b/post/requestspost-he-requestssessionpost-fang-fa-de-qu-bie/index.html @@ -0,0 +1,236 @@ + + + + +requests.post和requests.session.post方法的区别 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + requests.post和requests.session.post方法的区别 +

    + + +
    +
    +

    1. requests.post()

    +

    调用形式:requests.post(url, data={key: value}, json={key: value}, args)
    +args表示还有很多可选参数,例如:

    +

    1.1 file : 一个文件字典列表

    +
    myfiles = {'file': open('myfirstreact.png' ,'rb')}
    +x = requests.post(url, files = myfiles)
    +
    +

    表示向目标链接post一个图片('rb'表示以二进制格式读取文件)

    +

    1.2 allow_redirects : 表示是否允许网页重定向

    +

    默认为True

    +

    1.3 cookies : 一个字典格式的cookies

    +
    myobj = {'somekey': 'somevalue'}
    +x = requests.post(url, data = myobj, cookies = {"favcolor": "Red"}) #根据目标url的cookies
    +
    +

    1.4 headers : 请求头

    +
    x = requests.post(url,data={},headers = {
    +    'header_one':'content',
    +    'header_two':'content',
    +    ...
    +})
    +
    +

    1.5 timeout : 等待服务器响应的时间

    +

    默认为None,表示一直等待直到服务器响应。
    +也可以自定义,如果超出时间表示“在这一次请求中响应超时”。

    +
    x = requests.post(url, data = myobj, timeout=0.001)#表示等待0.001秒
    +
    +

    1.5 data和json的区别

    +

    根据不同的url的http请求,需要发送对应的数据,比如有的网站是直接以字典格式发送数据,而有的可能是在前端打包成json格式在发送请求。
    +具体需要看请求头中的content-type参数:如果请求头中content-typeapplication/json, 为json形式,post请求使用json参数。
    +如果为application/x-www-form-urlencoded,为表单形式,post请求时使用使用data参数。
    +reference_1

    +

    2. requests.session.post()

    +

    The Session object allows you to persist certain parameters across requests. It also persists cookies across all requests made from the Session instance, and will use urllib3's connection pooling. So if you’re making several requests to the same host, the underlying TCP connection will be reused, which can result in a significant performance increase (see <HTTP persistent connection).

    +

    -- reference_2

    +

    这段话也就表明,session中的post方法可以维持一个实例中的某些参数,保持会话连接,让你在实例中保存cookies。
    +其余的参数和requests.post()相同。
    +而且从reference_3中对源码的分析中也可以看出,requests.post()在调用完成之后,连接池就会被关闭;而requests.session.post()会将链接保存,将cookies保存在同一个实例中。

    +
    #官方参考中的示例代码
    +s = requests.Session()
    +s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')
    +r = s.get('https://httpbin.org/cookies')
    +print(r.text)
    +# '{"cookies": {"sessioncookie": "123456789"}}'
    +
    +

    表明在r这一个实例中,两次对不同链接的请求的cookies是相同的。

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/robottxt/index.html b/post/robottxt/index.html new file mode 100644 index 0000000..39bd68b --- /dev/null +++ b/post/robottxt/index.html @@ -0,0 +1,258 @@ + + + + +爬虫遇到robot.txt | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + 爬虫遇到robot.txt +

    + + +
    +
    +

    在做大作业的时候需要爬微博的热搜,用scrapy框架,按照quick start操作,但是示例网址可以爬取,我想要爬取微博的热搜却不可以,在检索问题的时候,看到用python直接爬取却可以。然后仔细查看运行日志,发现了以下两行:

    +
    +
    [scrapy.core.engine] DEBUG: Crawled (200) <GET https://weibo.com/robots.txt> (referer: None)
    +[scrapy.downloadermiddlewares.robotstxt] DEBUG: Forbidden by robots.txt: <GET https://weibo.com/ajax/side/hotSearch>
    +
    +

    然后意识到可能是因为这个robot.txt导致的,但是robot.txt到底是什么呢?

    +

    1. 简述

    +

    robot.txt文件是搜索引擎和网站之间做的一个非强制执行的访问限制。robot.txt对按照一定的语法规范,对每个搜索引擎能(例如谷歌、百度等),访问到的内容进行规定。但这种并不是一种强制要求,各种巨头搜索引擎会按照robot.txt文件中的要求访问网站,但是对于个人或者某些组织来讲,可以不遵守robot.txt文件继续访问。

    +

    2. robot.txt简要介绍

    +

    微博为例:

    +
    Sitemap: http://weibo.com/sitemap.xml
    +User-Agent: Baiduspider
    +Disallow:
    +User-agent: 360Spider
    +Disallow:
    +User-agent: Googlebot
    +Disallow:
    +User-agent: *
    +Allow: /ads.txt
    +User-agent: Sogou web spider
    +Disallow:
    +User-agent: bingbot
    +Disallow:
    +User-agent: smspider
    +Disallow:
    +User-agent: HaosouSpider
    +Disallow:
    +User-agent: YisouSpider
    +Disallow:
    +User-agent: *
    +Disallow: /
    +
    +

    2.1

    +

    Sitemap是站点地图,网站通过它向搜索引擎说明网站的重要页面,是由Sitemap:+url组成,一般是有多个,但是微博只列举了一个。

    +

    2.2

    +

    User-agent是用户代理,对于正常用户来说,这个包括這包括浏览器类型、系统版本等信息,但是对于爬虫来说,如果是单独写的爬虫,那么一般会提前设定好head(请求头)中的user-agent属性,伪装成一个正常的用户;不过对流搜索引擎爬虫来说,会在这里注明自己的“身份”,例如常见的搜索引擎爬虫代理名称:

    +
    +

    Google:
    +Googlebot
    +Googlebot-Image(用於影像)
    +Googlebot-News(用於新聞)
    +Googlebot-Video(用於影片)
    +Bing
    +Bingbot
    +MSNBot-Media(用於影像和影片)
    +百度
    +Baiduspider

    +
    +

    当然,如果是User-agent : *,表示所有的代理用户都可以访问。
    +因为爬虫读取是按照顺序读取,对于scapy来说,前几个允许的搜索引擎爬虫不包括自己,所以选择遵守最后一个,就是

    +
    User-agent: *
    +Disallow: /
    +
    +

    因此就“被屏蔽”掉了。

    +

    2.3

    +

    Disallow对爬虫机器人声明后面的内容是不允许访问的。
    +例如Disallow: /example/bots/表示网站根目录下的example/bots/页面(也就是'https://websiteRoot/example/bots/)不允许访问;
    +也可以禁止访问一个目录,例如Disallow: /example/,表示网站目录example(https://websiteRoot/example/)下的所有页面都不允许爬虫访问;
    +如果只是一个Disallow: /,那么表示该对机器人隐藏整个网站;
    +如果为空,Disallow: ,表示爬虫机器人可以访问储存网站内的所有内容。
    +不过以上都只是对于善意的爬虫机器人,如果爬虫不遵守这个规则,那么就相当于没有。

    +

    2.4

    +

    Allow就是你所想的那个样子,告诉爬虫允许访问的页面。

    +

    2.5 还有什么?

    +

    Crawl-delay:crawl delay 命令旨在阻止搜索引擎蜘蛛爬虫使使服务器负担过重。它让爬虫管理指定每个爬虫访问的时间间隔(以毫秒为单位)。例如:Crawl-delay: 8

    +

    PS. 还注意到微博的文件中由一个ads.txt文件,可以从名字看出该文件和广告业务相关,简而言之,它可以避免广告欺诈行为,让网站所有者可以决定其他哪些公司可以在其的网站上进行广告。【reference

    +

    3. 解决方法

    +

    在scrapy项目中的setting.py文件中找到# Obey robots.txt rules下方的设置项修改为False即可。同时也可以在setting文件中的# Override the default request headers:设置默认请求头;可以在Crawl responsibly by identifying yourself (and your website) on the user-agent:设置用户代理

    +

    reference

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/scrapy-item-fu-zhi-tian-chong-xi-jie-zhu-yi/index.html b/post/scrapy-item-fu-zhi-tian-chong-xi-jie-zhu-yi/index.html new file mode 100644 index 0000000..e82b0d5 --- /dev/null +++ b/post/scrapy-item-fu-zhi-tian-chong-xi-jie-zhu-yi/index.html @@ -0,0 +1,291 @@ + + + + +scrapy item赋值/填充 细节注意 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + scrapy item赋值/填充 细节注意 +

    + + +
    +
    +

    1. 起因

    +

    我在爬取到微博热搜的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    
    +
    +

    2. 调试之后

    +

    在调试之后,我发现pipeline.py文件中对应class类中的process_item()方法中的item变量并不是我想象中的是一个由字典元素组成的列表,而是一个字典,并且key是在item.py文件中设定的,value是在parse()方法中赋值的、我想要的字典元素列表。终于确定的原因,因此修改也很简单。

    +

    3. 修改

    +

    将原本的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
    +
    +

    4. 探究原因

    +

    官方文档
    +首先,开发者为了方便在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
    +
    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/scrapy-pa-chong-kuang-jia-geng-shen-ru-li-jie-yi-ji-shi-yong/index.html b/post/scrapy-pa-chong-kuang-jia-geng-shen-ru-li-jie-yi-ji-shi-yong/index.html new file mode 100644 index 0000000..7d5cbc9 --- /dev/null +++ b/post/scrapy-pa-chong-kuang-jia-geng-shen-ru-li-jie-yi-ji-shi-yong/index.html @@ -0,0 +1,289 @@ + + + + +scrapy爬虫框架深入理解 -- 未整理完 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + scrapy爬虫框架深入理解 -- 未整理完 +

    + + +
    +
    +

    1. logging模块

    +

    2. 环境变量(setting)

    +

    官方文档

    +

    2.1 如何设置并访问setting

    +

    2.1.1 在spider中

    +

    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使用在方法中。

    +

    2.1.2 在pipeline等其他组件中

    +

    使用类方法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()等,用途也能从方法名显而易见出,具体请参考官方链接。

    +
    +

    1. crawler模块

    +

    2. item源码如何实现存储key字段,

    +

    https://www.cnblogs.com/twelfthing/articles/4709287.html
    +https://www.cnblogs.com/fengf233/p/11298623.html#2.field()%E7%B1%BB

    +

    5. 多个Spider以及多个Pipeline对应的设置

    +

    https://www.ziji.work/python/scrapy-many-spider-pipeline.html#SCRAPYSPIDER-4

    +

    6. 大佬的源码解析

    +

    虽然现在基本看不懂(×
    +http://kaito-kidd.com/2016/11/09/scrapy-code-analyze-entrance/

    +

    7. 访问setting

    +

    8. 为什么from_crawler方法需要设置为类方法?

    +

    提点链接
    +最后这篇博客的最后一句话简直一下子给我点透了,虽然之前专门去查了@classmethod、factory method这些东西,但为什么这么做还是很混乱。

    +
    +

    大概就是检测spider类有没有from_crawler,有的话就return一个cls()的实例化对象,产生实例化对象后会自动调__init__方法。

    +
    +

    结合官方文档的Crawler APIpipeline中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方法。
    +上面这句话有两点需要着重理解。

    +
      +
    1. 第一点,这也就是from_crawler()是一个“factory method”,scrapy会根据不同的对象为类赋予不同的from_crawler()方法。在我的理解中,就是有一个类,专门负责为其他不同的类赋予不同的from_crawler()方法,也就是类方法的类
    2. +
    3. 第二点,scrapy从Crawler创建from_crawler()的含义就是为这个方法传入一个scrapy.crawler.Crawler对象,而这个对象又是由scrapy通过一个Spider子类scrapy.settings.Settings 对象实例化而来。
    4. +
    +
    +

    官方文档
    +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对象,这里就有串起来的感觉了)

    +

    9. 设置cookies

    +

    知乎参考但是没有讲原理

    +

    10. 关闭scrapy日志

    +

    参考
    +解决方法

    +

    添加twisted定时器 以及 输出调用日志
    +https://www.jianshu.com/p/5a5cdd7f2bfb

    +

    11. 定时调度scrapy

    +

    https://blog.csdn.net/python36/article/details/82683528

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/shi-yong-gridea-jian-li-bo-ke-shi-yu-dao-de-wen-ti/index.html b/post/shi-yong-gridea-jian-li-bo-ke-shi-yu-dao-de-wen-ti/index.html new file mode 100644 index 0000000..91a09d7 --- /dev/null +++ b/post/shi-yong-gridea-jian-li-bo-ke-shi-yu-dao-de-wen-ti/index.html @@ -0,0 +1,179 @@ + + + + +Problem encountered when building a blog with Gridea | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Problem encountered when building a blog with Gridea +

    + + +
    +
    +

    #使用gridea+github建立自己的博客

    +
    +

    教程链接地址:https://www.zhihu.com/question/20962496/answer/872441482
    +gridea地址:https://gridea.dev/
    +gridea的GitHub地址:https://github.com/getgridea/gridea

    +
    +

    我遇到的问题:
    +点击预览的时候并没有像教程那样弹出窗口
    +后来发现预览是直接生成一个output文件夹,在文件夹中通过浏览器预览index.html文件
    +文件夹地址:C:\Users\sapph\.gridea\output

    + +
    +
    + +
    +
    +
    +
    + + + + + + + +
    +
    + + + + diff --git a/post/the-attribute-of-form-nameforvalue/index.html b/post/the-attribute-of-form-nameforvalue/index.html new file mode 100644 index 0000000..39b4a2d --- /dev/null +++ b/post/the-attribute-of-form-nameforvalue/index.html @@ -0,0 +1,208 @@ + + + + +The attribute of form: name,for,value | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + The attribute of form: name,for,value +

    + + +
    +
    +

    form表单中的一些元素和属性

    +
    +
      +
    1. label标签中for属性的作用:用于将label与input关联起来。简单说就是点击了for关联的label标签会弹出与点击input标签一样的效果。并且for的命名要与input标签的id一致才能表示关联。
      +Ps.如果label包含input,可以不适用for依然与input关联(隐形关联)
    2. +
    3. name属性用于对提交到服务器后的表单数据进行标识,只有设置了name属性的表单元素才能被js或django或者其他语言捕捉。
    4. +
    5. value属性
    6. +
    +
    +

    value是控件的值,input 标签的 value 属性的作用是由 input 标签的 type 属性的值决定的

    +
      +
    • 当 type 的取值为 button、reset、submit 中的其中一个时,此时 value 属性的值表示的是按钮上显示的文本。
    • +
    • 当type 的取值为 text、password、hidden 中的其中一个时,此时 value属性的值表示的是输入框中显示的初始值,此初始值可以更改,并且在提交表单时,value属性的值会发送给服务器(既是初始值,也是提交给服务器的值)。
    • +
    • 当 type 的取值为 checkbox、radio 中的其中一个时,此时value 属性的值表示的是提交给服务器的值 当 type 的取值为 image时,点击它提交表单后,会将用户的点击位置相对于图像左上角的 x 坐标和 y 坐标提交给服务器。
    • +
    • checkbox 型的 input标签的不足之处在于:提交表单时,只有处于勾选状态的复选框的数据值才会发送给服务器。也就是说,如果没有任何一个复选框被选中,那么服务器就不会收到与其相关的数据项。
      +当设置 input 标签的 type 属性值为checkbox 或者 radio 时,必须同时设置 input 标签的 value 属性
    • +
    • 当 type=“file” 时,不能使用 value 属性
    • +
    +
    +
    +
    +

    ————————————————
    +参考来源name,for,value区别

    +
    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/the-effect-of-the-mask-attribution-in-css/index.html b/post/the-effect-of-the-mask-attribution-in-css/index.html new file mode 100644 index 0000000..fca9f87 --- /dev/null +++ b/post/the-effect-of-the-mask-attribution-in-css/index.html @@ -0,0 +1,192 @@ + + + + +the effect of the "mask" attribution in css | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + the effect of the "mask" attribution in css +

    + + +
    +
    +

    css mask的原理:只会把遮罩图里透明像素所对应的原图部分进行隐藏。
    +所以一般通过mask-image:url()的形式引入svg格式的图片进行遮盖。
    +还可以搭配background-image: linear-gradient(direction, color-stop1, transparents);进行渐变的隐藏效果。

    +
    +

    例如
    + background-image: linear-gradient(to right, red , yellow);表示创建一个从由右红到左黄的渐变背景色。

    +
    +

    而transparent表示完全透明。

    +
    +

    所以全部加在一起

    +
    +
        .mask {
    +      width: 200px;
    +      height: 200px;
    +      background-color: blue;
    +      -webkit-mask-image: linear-gradient(to bottom,transparent 50%,black);
    +      mask-image: linear-gradient(to bottom,white,white);
    +    }
    +
    +

    效果是一个从上隐到下面变为蓝色的正方形。

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/the-error-in-documentwrite-method-when-its-inner-is-lessscirptgreater/index.html b/post/the-error-in-documentwrite-method-when-its-inner-is-lessscirptgreater/index.html new file mode 100644 index 0000000..c129cbc --- /dev/null +++ b/post/the-error-in-documentwrite-method-when-its-inner-is-lessscirptgreater/index.html @@ -0,0 +1,174 @@ + + + + +The error in document.write() method when its inner is "</scirpt>" | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + The error in document.write() method when its inner is "</scirpt>" +

    + + +
    +
    +

    While browser parses the html documents, the '<>' means start tag and the '</' implies the end tag. After browser has parsed one start tag, it starts to find the end tag, which even was in the quotation marks. And unfortunately when an end tag in quotes was detected, the error happened. Sometime it can run correctly in browser but highlighted with a red underline in your compiler (yes, is you, vscode!) which is fretted.
    +Depending on the reasons, the solution is clear, use escape character in end tag. Such as below:
    +document.write('<script>console.log("sth" + "another thing");<\/script>');
    +The '\' makes borwser parse the end tag as a right character, and your code will run validly.

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/the-import-system-of-python/index.html b/post/the-import-system-of-python/index.html new file mode 100644 index 0000000..ce6efee --- /dev/null +++ b/post/the-import-system-of-python/index.html @@ -0,0 +1,209 @@ + + + + +The import system of python | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + The import system of python +

    + + +
    +
    +

    python的import系统

    +

    import 模块
    +导入了一个模块,相当于岛屿了一个文件夹,是一个相对路径。
    +使用时需要 模块.函数
    +from x import y
    +导入了一个模块中的一个函数,又或者相当于一个文件夹中的一个文件。使用时直接使用import后面的函数名就可以。
    +from x import y
    +导入一个模块中所有的函数
    +from . import y
    +从当前文件夹中导入y
    +Ps. 较为复杂,目前暂不深入。

    +
    +

    简单摘录
    +假设该from . import x语句存在于proj/test.py中,它会导入同级目录下的__init__.pyproj/__init__.py中的x对象,如果没有proj/__init__.py或有那个文件但是文件中不存在x对象,就导入proj/x/__init__.py,如果还没有,就导入proj/x.py,还没有就报错。其中同级是按__name__的前一部分或 __package__ 确定的。如果用python proj/test.py运行,name__就是__mainpackage__是None,就会报错。如果用python -m proj.test运行,虽然__name__还是__main,但是__package__是proj,就能成功导入,即使proj/init.py不存在也行。

    +
    +

    from ... import y
    +从上一级文件夹导入y
    +from ...x import y
    +从上一级文件夹中的x文件夹里的__init__.py程序里导入y

    +

    import x as b
    +将x命名为b导入
    +from x import y as b
    +导入x中的y并命名为b

    + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/the-solution-of-insert-images-into-markdown/index.html b/post/the-solution-of-insert-images-into-markdown/index.html new file mode 100644 index 0000000..7f15258 --- /dev/null +++ b/post/the-solution-of-insert-images-into-markdown/index.html @@ -0,0 +1,176 @@ + + + + +The solution of insert images into markdown | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + The solution of insert images into markdown +

    + + +
    +
    +

    原先在知乎上找到的通过Picgo+Github仓库的方法貌似在我这用不了(也不知道为什么)
    +不过在博客插入图片的本质就是引入图片的url,所以只要能将图片上传到网络并且给他一个url连接就可以通过markdown插入图片的方法引入了。
    +目前采用的是imgURL这个图床网站,免费每天最多20张,每月最多400张,虽然很少,不过对于我写博客来说已经够用了。上传之后会自动生成markdown、url等连接格式。

    +

    Ps. 虽然是上传到图床上了,但是考虑到以后还有可能重新自建一个博客,所以还是将一些图片根据实践和上传的文章对应起来保存吧。(虽然现在想的好,估计以后就懒得做了😕)

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/tong-bu-he-yi-bu/index.html b/post/tong-bu-he-yi-bu/index.html new file mode 100644 index 0000000..231b01d --- /dev/null +++ b/post/tong-bu-he-yi-bu/index.html @@ -0,0 +1,172 @@ + + + + +Asynchronous and synchronous | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Asynchronous and synchronous +

    + + +
    +
    +

    https://zhuanlan.zhihu.com/p/67452727

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/traverse-a-jquery-object-and-the-differencebetween-this-and-dollarthis/index.html b/post/traverse-a-jquery-object-and-the-differencebetween-this-and-dollarthis/index.html new file mode 100644 index 0000000..f906f86 --- /dev/null +++ b/post/traverse-a-jquery-object-and-the-differencebetween-this-and-dollarthis/index.html @@ -0,0 +1,193 @@ + + + + +Traverse a jQuery object and the differencebetween this and $(this) | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Traverse a jQuery object and the differencebetween this and $(this) +

    + + +
    +
    +

    I want to traverse a jq object (which is a type of object used in jQuery) and print the inner text in every selected items. However, when I used either for (let x in jq_obj) nor for (let x of jq_obj), I received an error that 'x.text() is not a function'. Then I tried the foraEach() method, browser showed that jq_obj is not iterable. Then I searched error message, I gained a clearer understanding.
    +Firstly, jq_obj is not an iterable object. If we want to access the inner elements, jq_obj has a each() method which can be used to iterate over a jq_obj, and is equivalent to forEach() which belongs to DOM object.
    +Secondly, while each() method traverses a jq_obj, it will pass a DOM object in the loop, and the DOM object does not have access to the text() method because it is jq_obj's method. Therefore, we need to use a different way to get the inner text to use DOM's method which is innerText or innerHTML.
    +Nevertheless, by wrapping the DOM in $ to convert it to be a jq_obj, we are able to invoke the methods of jq_obj. As a conclusion, the whole samples to solve this trouble is below:

    +
    jq_obj.each(function(){
    +    console.log($(this).text());
    +});
    +
    +
    $("li").each(function(){
    +        console.log(this.innerText)
    +    });
    +
    +
    for (let x of jq_obj){
    +    console.log(x.textContent); 
    +    //textContent is a property of DOM whose value is text of this dom, similar to innerText() method. 
    +}
    +
    +
    for (let x of jq_obj){
    +    console.log($(x).text());
    +}
    +
    +

    ----------------------------in addition--------------------
    +it seems that an array function can not transform in each() means.

    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/upload-files-by-django/index.html b/post/upload-files-by-django/index.html new file mode 100644 index 0000000..f19fb4a --- /dev/null +++ b/post/upload-files-by-django/index.html @@ -0,0 +1,364 @@ + + + + +Upload files by Django(Part.1--without Models, Forms, ModelForm) | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Upload files by Django(Part.1--without Models, Forms, ModelForm) +

    + + +
    +
    +

    Django上传文件(Part.1)

    +

    这几天一直因为这个问题捣鼓了半天,各种问题都出来了。因为太多了而且最后也解决的,于是就只写一个最后总结出来的方法吧。而且因为问题太多,就把写的时候能想到的遇到的问题顺便就列出来。

    +

    只演示上传文件这一单一功能,所以就新建一个项目,一边做一边记录下来。

    +

    Ps.在windows系统下

    +

    不使用models和forms进行上传(自定义上传文件/图片)

    +

    需求:最终创建两个页面,一个是上传主页面,一个是上传结果页面,结果页面分为上传成功和上传失败,两个结果页面都可以再次继续上传。上传的文件保存在与manage.py存在目录下的media/upload文件夹内。

    +
      +
    1. 新建项目temptation,并新建app。在有django环境下的windows命令窗口中输入
      +django-admin startproject temptation
      +python manage.py startapp upload
      +生成的项目结构:
    2. +
    +
    +
    D:temptation
    +│  manage.py
    +│
    +├─temptation│  │  asgi.py
    +│  │  settings.py
    +│  │  urls.py
    +│  │  wsgi.py
    +│  │  __init__.py
    +│  │
    +│  └─__pycache__
    +│          settings.cpython-39.pyc
    +│          __init__.cpython-39.pyc
    +│
    +└─upload
    +   │  admin.py
    +   │  apps.py
    +   │  models.py
    +   │  tests.py
    +   │  views.py
    +   │  __init__.py
    +   │
    +   └─migrations
    +           __init__.py
    +
    +
    +
      +
    1. 然后在temptation/settings.py中
    2. +
    +
    +
    INSTALLED_APPS = [
    +   "django.contrib.admin",
    +		......
    +	 'upload',//添加
    +
    +
    +

    Ps. 此时不对TEMPALTES中的DIRS进行配置,这样在之后配置html文件时就可以直接在upload下创建templates文件夹,然后直接将html文件放入。因为不对路径进行配置django会先寻找manage.py所在的文件夹中是否有templates文件夹,然后再在每个app下寻找templates文件夹,直到找到为止。
    +3. temptation/urls.py中:

    +
    +
    from django.contrib import admin
    +from django.urls import path,include//添加
    +
    +urlpatterns = [
    +  path("admin/", admin.site.urls),
    +   path("upload/", include('upload.urls')),//添加
    +]
    +
    +
    +
      +
    1. 在upload文件中创建urls.py文件,并配置:
    2. +
    +
    +
    from django.urls import path
    +from . import views
    +
    +urlpatterns = [
    +   path('index/',views.index,name='index'),
    +   path('result/',views.result,name='result')
    +]
    +
    +
    +
      +
    1. 填写视图方法
    2. +
    +
    +
    from django.shortcuts import render
    +def index(request):    //配置基础页面 
    +   return render(request,'index.html')
    +
    +def result(request):
    +   if request.method == "POST": 
    +       file = request.FILES['file']
    +       filename = file.name
    +       filepath = "media/upload/" + filename  //创建文件保存路径,是一个相对路径,相对于manage.py所在的文件夹
    +       with open(filepath,'wb+') as f:  //写入文件
    +           for chunk in file.chunks():  //分块写入文件,避免文件太大占满内存
    +               f.write(chunk)
    +           f.close()
    +       return render(request,'result.html',{'result':'上传文件成功!'})
    +   else:
    +       return render(request,'result.html',{"result":'上传文件失败!'})
    +
    +
    +
      +
    1. 创建templates文件夹,并创建index.html和result.html页面
      +index.html
    2. +
    +
    +
    <form name="show" action="{% url 'result'%}" method="POST" enctype ="multipart/form-data">   /*创建form表单*/
    +/*action表示表单上传至result.html页面,如果action为空那么不会有页面转跳,文件也上传失败。*/
    +   {% csrf_token %}   /*django内置的上传用户审查,目前只需要记住上传文件的时候要加上*/
    +   <label for="fileupload" >请选择要上传的文件</label>  /*for属性表示将label标签和id为fileupload的input标签关联*/
    +   <input type="file" name="file" id="fileupload">
    +   <input type="submit" value="上传">
    +</form>
    +{% block result%}  /*django的继承模板,用于做出类似于不刷新页面返回结果的效果*/
    +{%endblock%}
    +
    +
    +

    result.html

    +
    +
    {% extends 'index.html' %}   /*继承index.html模板*/
    +
    +{% block result%}
    +{{result}}   /*显示上传结果*/
    +{%endblock%}
    +
    +
    +
      +
    1. 到此结束,列举一下最后的项目结构
    2. +
    +
    +
    D:temptation
    +│  db.sqlite3
    +│  manage.py
    +│
    +├─media
    +│  └─upload
    +│          img401.png
    +│          img402.png
    +│
    +├─temptation
    +│  │  asgi.py
    +│  │  settings.py
    +│  │  urls.py
    +│  │  wsgi.py
    +│  │  __init__.py
    +│  │
    +│  └─__pycache__
    +│          settings.cpython-39.pyc
    +│          urls.cpython-39.pyc
    +│          wsgi.cpython-39.pyc
    +│          __init__.cpython-39.pyc
    +│
    +└─upload
    +   │  admin.py
    +   │  apps.py
    +   │  models.py
    +   │  tests.py
    +   │  urls.py
    +   │  views.py
    +   │  __init__.py
    +   │
    +   ├─migrations
    +   │  │  __init__.py
    +   │  │
    +   │  └─__pycache__
    +   │          __init__.cpython-39.pyc
    +   │
    +   ├─templates
    +   │      index.html
    +   │      result.html
    +   │
    +   └─__pycache__
    +           admin.cpython-39.pyc
    +           apps.cpython-39.pyc
    +           models.cpython-39.pyc
    +           urls.cpython-39.pyc
    +           views.cpython-39.pyc
    +           __init__.cpython-39.pyc
    +
    +
    +

    评价:这种方法最简单,不需要很多其他的配置,文件路径和名字也都可以自定义(比如换成上传时间)。但是缺少对上传文件格式的判断、对表单传来数据的审查、没有存储到数据库不方便管理等。
    +Ps. 这个项目完全可以不用创建数据库(也就是使用python manage.py makemigrationspython manage.py migrate),但是强迫症忍不了红色的提示😳

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/upload-files-by-djangopart2-without-modelform/index.html b/post/upload-files-by-djangopart2-without-modelform/index.html new file mode 100644 index 0000000..95f95cd --- /dev/null +++ b/post/upload-files-by-djangopart2-without-modelform/index.html @@ -0,0 +1,366 @@ + + + + +Upload files by Django(Part.2--without ModelForm) | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Upload files by Django(Part.2--without ModelForm) +

    + + +
    +
    +

    Django上传文件(Part.2)

    +

    只演示上传文件这一单一功能,所以就新建一个项目,一边做一边记录下来。在windows系统下

    +

    与第一部分一样,创建一个新的项目,然后一边做一边记录,然后想起之前的问题就补充说明。

    +

    不使用ModelForm上传文件

    +
      +
    1. 创建项目、应用
      +django-admin startproject mysite
      +python manage.py startapp show
    2. +
    3. 配置mysite/settings.py文件
    4. +
    +
    +
    INSTALLED_APPS = [
    +   "django.contrib.admin",
    +			...
    +   'show',
    +]
    +
    +
    MEDIA_ROOT = "D:/Aproject/django-project/mysite/media" #要创建对应文件夹,不过就算忘记创建django也会在上传时自动创建。不过还是建议提前创建,这样对路径更加清楚。这个路径是一个绝对路径。
    +MEDIA_URL = '/media/'     #创建这个路由之后,django会自动对每个上传的文件分配路由,具体展示请看接着看。
    +
    +
    +
      +
    1. 配置show/urls.py
      +思路:最终创建两个页面,一个是上传主页面,一个是上传结果页面,结果页面分为上传成功和上传失败,两个结果页面都可以再次继续上传。上传的文件保存在与manage.py存在目录下的media/show/文件夹内。
    2. +
    +
    +
    from django.urls import path
    +from . import views
    +urlpatterns = [
    +   path("upload/", views.index,name='index'),
    +   path("result/", views.result,name='result'),
    +]
    +
    +
    +
      +
    1. 配置forms.py文件和
    2. +
    +
    +

    forms.py文件

    +
    from django import forms
    +class UploadFiles(forms.Form): 
    +   file = forms.FileField()  
    +   #django的forms类可以帮助我们验证上传表单中的数据是否符合要求。我们现在只需要上传一个文件,因此只需要配置FileField字段。
    +
    +
    +

    自定义的UploadFiles类的属性要和表单中提交的input的name属性相对应,不然在”is_valid()"验证时会失败!!

    +
      +
    1. 配置models.py文件
    2. +
    +
    +

    models.py文件

    +
    from django.db import models
    +
    +class files(models.Model):
    +   filename = models.CharField(max_length=255)
    +   documents = models.FileField(upload_to='show')
    +#配置文件字段,filename用来储存文件名字;documents储存文件对象,后面的upload_to参数用来规定上传文件的储存地址
    +
    +
    +

    配置完成之后记得迁移数据库,不然之后运行会报错(python manage.py makemigrationspython manage.py migrate)
    +5. 配置views.py方法

    +
    +
    from django.shortcuts import render
    +from .forms import UploadFiles
    +from .models import files
    +
    +def index(request):
    +   return render(request,'index.html')
    +
    +def result(request):
    +   if request.method == 'POST':
    +       form = UploadFiles(request.POST,request.FILES) #将表单信息传递给UploadFiles类进行验证
    +       name = 'file' 
    +       if form.is_valid(): #验证正确返回True
    +           filename = request.FILES[name].name
    +           tempfile = >files(filename=filename,documents=request.FILES[name]) #实例化files类,并传入上传文件信息初始化
    +           tempfile.save()  #使用django内置的文件类保存,不用自己编写保存步骤
    +           return render(request,'result.html',{'result':'文件上传成功!'})
    +       else:
    +           message = form.errors  #如果UploadFiles类(forms表单)验证错误,会返回错误对象
    +           return render(request,'result.html',{'result':'文件上传失败!',
    +           'message':message,
    +           })
    +   else:
    +       return render(request,'result.html',{'result':'文件上传失败!'})
    +
    +
    +
      +
    1. 创建show/templates文件夹,在里面配置index.html和result.html
    2. +
    +
    +

    index.html

    +
    <form name="show" action="{% url 'result'%}" >method="POST" enctype ="multipart/form-data">  
    +   {% csrf_token %}   
    +   <label for="fileupload" >请选择要上传的文件</label>  
    +   <input type="file" name="file" id="fileupload"> /*name属性一定要和forms定义的类一样,不然会判断错误*/
    +   <input type="submit" value="上传">
    +</form>
    +{% block result%}  
    +{%endblock%}
    +
    +

    result.html

    +
    {% extends 'index.html' %}   
    +
    +{% block result%}
    +{{result}}   
    +{{message}}
    +{%endblock%}
    +
    +
    +
      +
    1. 最后的项目结构
    2. +
    +
    +
    D:.
    +│  db.sqlite3
    +│  manage.py
    +│
    +├─media
    +│  └─show
    +│          img401.png #尝试上传的结果
    +│
    +├─mysite
    +│  │  asgi.py
    +│  │  settings.py
    +│  │  urls.py
    +│  │  wsgi.py
    +│  │  __init__.py
    +│  │
    +│  └─__pycache__
    +│          settings.cpython-39.pyc
    +│          urls.cpython-39.pyc
    +│          wsgi.cpython-39.pyc
    +│          __init__.cpython-39.pyc
    +│
    +└─show
    +   │  admin.py
    +   │  apps.py
    +   │  forms.py
    +   │  models.py
    +   │  tests.py
    +   │  urls.py
    +   │  views.py
    +   │  __init__.py
    +   │
    +   ├─migrations
    +   │  │  0001_initial.py
    +   │  │  __init__.py
    +   │  │
    +   │  └─__pycache__
    +   │          0001_initial.cpython-39.pyc
    +   │          __init__.cpython-39.pyc
    +   │
    +   ├─templates
    +   │      index.html
    +   │      result.html
    +   │
    +   └─__pycache__  
    +           admin.cpython-39.pyc
    +           apps.cpython-39.pyc
    +           forms.cpython-39.pyc
    +           models.cpython-39.pyc
    +           urls.cpython-39.pyc
    +           views.cpython-39.pyc
    +           __init__.cpython-39.pyc
    +
    +
    +

    总结

    +

    在重新做一遍注意到了之前没有注意到的事情:forms类的属性要和前端的form表单的input的name属性相对应。
    +在解决问题的时候还有很多的ajax实现方法,但现在js掌握不熟练,因此暂时不去实践,希望我能快一点掌握好实现!

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/using-django-upload-images-and-display/index.html b/post/using-django-upload-images-and-display/index.html new file mode 100644 index 0000000..bef63ed --- /dev/null +++ b/post/using-django-upload-images-and-display/index.html @@ -0,0 +1,248 @@ + + + + +Using django upload images and display | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Using django upload images and display +

    + + +
    +
    +

    Django展示图片

    +

    上传图片参考前两个,现在只考虑将图片返回到前端展示(同样只是用django,因为ajax等掌握不熟练XD,过几天一定要拿js重新完成一次这些功能!)
    +这个算是前两个(upload-files-by-django part1pload-files-by-django part2)的延申,代码已经上传至github,这里只记录一下遇到的问题。

    +
    +

    little-test-main

    +
    +

    Q1. 在forms.py文件和models.py文件的CharField字段没有设置
    +问题原因:在forms.py文件和models.py文件的CharField字段没有设置必要参数max_length
    +解决方法:补充上即可。

    +

    Q2. 命名空间的关键词设置错误
    +问题原因:在app display中的urls.py文件中命名空间的关键词设置错误,将app_name记成了namespace。同时在前端页面index.html中使用了命名空间。

    +
    +
    问题来源
    +
    +
    问题来源
    +解决方法:正确关键字`app_name='display'` +

    Q3. 缺少{%csrf_token%}
    +问题原因:没有在form表单中添加{%csrf_token%}
    +解决方法:补上即可。

    +

    Q4. views.py中方法定义错误,返回的不是文件名而是数据库中的文件对象
    +问题原因:views.py中方法定义错误,返回的不是文件名而是数据库中的文件对象
    +解决方法:

    +
    +

    view.py修改之后

    +
    from re import U
    +from django.shortcuts import render
    +from .models import UploadImages
    +from .forms import ImageIdentify
    +
    +def upload(request):
    +   tmpimg = ImageIdentify(request.POST,request.FILES)
    +   if tmpimg.is_valid():
    +       imgname = request.FILES['image'].name
    +       image = >UploadImages(imagename=imgname,images=request.FILES['image'])
    +       image.save()
    +def display():
    +   content = UploadImages.objects.all()
    +   namelist = []
    +   for con in content:
    +       temp=con.get_img_name()
    +       namelist.append(temp)
    +   if namelist == []:
    +       return []
    +   else:
    +       return namelist
    +
    +def index(request):
    +   if request.method == 'POST':
    +       upload(request)
    +   content = display()
    +   print(content)
    +   return render(request,'index.html',{'content':content})
    +
    +
    +

    最后

    +

    原测试项目已经上传至githun仓库(little-test-main),并且在我的硬盘上进行了备份~
    +总体这个感觉已经是非常熟练~ 不愧于弄了那么久,同时算是对django的熟悉程度更深了一层。

    + +
    +
    + + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/uwsgi-django-bu-shu/index.html b/post/uwsgi-django-bu-shu/index.html new file mode 100644 index 0000000..a359fa4 --- /dev/null +++ b/post/uwsgi-django-bu-shu/index.html @@ -0,0 +1,298 @@ + + + + +uwsgi - django部署 | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + uwsgi - django部署 +

    + + +
    +
    +

    以一个最基础的初始django 项目为例(不涉及数据库)

    +
      +
    1. 将django项目的环境打包成requirements.txt
    2. +
    3. 购买一个服务器
    4. +
    5. 将服务器安全组设置 设置入站规则全开
    6. +
    7. 配置python环境 +
        +
      • 本人使用的是ubuntu 20.04 自带python 3.8
      • +
      • 首先安装pip - 注:如果E: Package 'python3.10-venv' has no installation candidate说明你应该更新一下系统的可安装软件列表apt-get update
      • +
      • 然后安装virtualenv
      • +
      • 然后cd到想要的目录下 创建虚拟环境
      • +
      • cd到刚创建的虚拟环境目录下的bin文件夹内 source activate启动环境
      • +
      +
    8. +
    9. 配置项目环境 +
        +
      • cd到项目根目录 执行pip install -r requirements.txt (一定要在上一步启动虚拟环境之后在进行环境配置)
      • +
      • 运行结束即可
      • +
      +
    10. +
    11. 安装uwsgi +
        +
      • pip install uwsgi即可
      • +
      +
    12. +
    13. 配置uwsgi.ini文件 - 在项目根目录中的同名目录下(就是个默认wsgi.py文件同一个文件夹下)
    14. +
    +
    [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  #主进程启动
    +
    +
      +
    1. 然后cd进入到uwsgi.ini同级内 +
        +
      • 执行uwsgi --ini uwsgi.ini 启动
      • +
      • 此时就能通过公网ip+端口访问到djano服务了
      • +
      +
    2. +
    3. 停止服务 +
        +
      • uwsgi --stop uwsgi.pid进入到uwsgi.ini同级文件夹运行即可
        +PS. 使用命令 ps aux|grep 'uwsgi'可以查看服务是否启动,出现如下表示正常:
      • +
      +
    4. +
    +
    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
    +
    +

    10. 彻底关闭uwsgi进程

    +

    如果没有设置停止的pid或者启动失败导致pid生成失败,这样就会导致有uwgsi进程挂在后台,因此需要彻底关闭:pkill -f uwsgi -9

    +

    PS.

    +
      +
    • 如果uwsgi日志报错:uwsgi invalid request block size: 4937 (max 4096)...skip
      +代表请求过大(超过默认4k)因此跳过
      +因此只要将请求接收的值方法即可:在uwsgi.ini文件添加buffer-size = 65536 即可。
    • +
    • 这样部署之后,网站是请求不到静态文件的(css,js,图片等)所以需要再搭配nginx进行静态文件请求的处理。
    • +
    +

    uwsgi常见报错

    +
      +
    • +

      1.启动失败: 端口被占用
      +解决方法: 更换端口/停止占用端口的进程
      +sudo lsof -i:端口号 - 查看端口 kill -9 端口号 - 停止端口进程

      +
    • +
    • +

      2.停止失败 stop无法关闭uwsgi
      +原因: 重复启动uwsgi导致uwsgi.pid中进程号失效
      +解决方法 : ps 出uwsgi进程,手动kill

      +
    • +
    • +
        +
      1. 启动失败 找不到配置文件
        +大概率uwsgi.ini中的chdir和wsgi两个参数的地址配置错误
        +解决方法:修复即可,注意chdir是项目绝对路径,wsgi是wsgi.py文件的相对于项目根目录的路径
      2. +
      +
    • +
    • +

      安装uwsgi报错ERROR: Failed building wheel for uwsgi
      +搜索解决方法:更新pip即可 - 更新系统依赖 - 安装psycopg2-binary -
      +都没有用
      +最后:apt-get install build-essential python3-dev然后pip即可
      +stackoverflow

      +
    • +
    • +
        +
      1. 修改项目后再使用uwsgi启动报错
      2. +
      +
    • +
    +
        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名字"
    +
    +

    + +
    + +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/post/wen-jian-chu-cun-shu-ju-ku/index.html b/post/wen-jian-chu-cun-shu-ju-ku/index.html new file mode 100644 index 0000000..19984cf --- /dev/null +++ b/post/wen-jian-chu-cun-shu-ju-ku/index.html @@ -0,0 +1,183 @@ + + + + +Some thoughts on saving files in the database... | cyanine + + + + + + + + + + + + + + +
    +
    + + +
    +
    +

    + Some thoughts on saving files in the database... +

    + + +
    +
    +
    +

    网页上传直接将文件上传到固定目录下,或者直接上传到指定的文件服务器上,将文件相对路径或者访问>key存入数据库就行。数据库直接存文件,小规模可以,大规模不可取。

    +
    +

    或者

    +
    +

    将文件存储在 mysql 中,可以使用 blob 类型,mysql 提供了四个类型来存储不同大小的文件:>TINYBLOB,BLOB,MEDIUMBLOB, LONGBLOB。
    +但是,还是劝你不要这样做,一般项目开发都不会在数据库中存文件,否则访问文件可能会对数据库造成很高的负载。你可以将文件存储在服务器的其它位置,或者使用单独文件存储服务。

    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + + + + +
    +
    + + + + diff --git a/styles/main.css b/styles/main.css new file mode 100644 index 0000000..24241ce --- /dev/null +++ b/styles/main.css @@ -0,0 +1,1002 @@ +/*! modern-normalize | MIT License | https://github.com/sindresorhus/modern-normalize */ +/* Document + ========================================================================== */ +/** + * Use a better box model (opinionated). + */ +html { + box-sizing: border-box; +} +*, +*::before, +*::after { + box-sizing: inherit; +} +/** + * Use a more readable tab size (opinionated). + */ +:root { + -moz-tab-size: 4; + tab-size: 4; +} +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ +html { + line-height: 1.15; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ +} +/* Sections + ========================================================================== */ +/** + * Remove the margin in all browsers. + */ +body { + margin: 0; +} +/** + * Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) + */ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +} +/* Grouping content + ========================================================================== */ +/* Text-level semantics + ========================================================================== */ +/** + * Add the correct text decoration in Chrome, Edge, and Safari. + */ +abbr[title] { + text-decoration: underline dotted; +} +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ +b, +strong { + font-weight: bolder; +} +/** + * 1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) + * 2. Correct the odd `em` font sizing in all browsers. + */ +code, +kbd, +samp, +pre { + font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} +/** + * Add the correct font size in all browsers. + */ +small { + font-size: 80%; +} +/** + * Prevent `sub` and `sup` elements from affecting the line height in all browsers. + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +/* Forms + ========================================================================== */ +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: 1.15; + /* 1 */ + margin: 0; + /* 2 */ +} +/** + * Remove the inheritance of text transform in Edge and Firefox. + * 1. Remove the inheritance of text transform in Firefox. + */ +button, +select { + /* 1 */ + text-transform: none; +} +/** + * Correct the inability to style clickable types in iOS and Safari. + */ +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} +/** + * Remove the inner border and padding in Firefox. + */ +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + border-style: none; + padding: 0; +} +/** + * Restore the focus styles unset by the previous rule. + */ +button:-moz-focusring, +[type='button']:-moz-focusring, +[type='reset']:-moz-focusring, +[type='submit']:-moz-focusring { + outline: 1px dotted ButtonText; +} +/** + * Correct the padding in Firefox. + */ +fieldset { + padding: 0.35em 0.75em 0.625em; +} +/** + * Remove the padding so developers are not caught out when they zero out `fieldset` elements in all browsers. + */ +legend { + padding: 0; +} +/** + * Add the correct vertical alignment in Chrome and Firefox. + */ +progress { + vertical-align: baseline; +} +/** + * Correct the cursor style of increment and decrement buttons in Safari. + */ +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + height: auto; +} +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} +/* Interactive + ========================================================================== */ +/* + * Add the correct display in Chrome and Safari. + */ +summary { + display: list-item; +} +*, +*:before, +*:after { + margin: 0; + padding: 0; +} +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + border: 0; + vertical-align: baseline; +} +html { + font-size: 58%; +} +body { + color: rgba(0, 0, 0, 0.86); + font: 400 16px/1.42 -apple-system, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", sans-serif; + letter-spacing: 0.05em; +} +a { + color: rgba(0, 0, 0, 0.98); + text-decoration: none; + transition: all 0.3s; +} +a:hover { + color: #006CFF; +} +body, +div, +a, +p, +ul, +li, +ol, +h1, +h2, +h3, +h4, +h5, +h6, +table, +tr, +td { + box-sizing: border-box; + margin: 0; + padding: 0; +} +.main { + max-width: 800px; + min-height: 100vh; + margin: 0 auto; + background: #fff; +} +.main .main-content { + flex: 1; + display: flex; + min-height: 100vh; + flex-direction: column; + padding: 0 24px; +} +.site-header { + padding: 48px 0; + text-align: center; +} +.site-header .site-title { + font-size: 32px; + font-weight: bold; +} +.site-header .site-description { + font-size: 16px; + padding: 24px; + color: #495057; + font-weight: lighter; +} +.site-header .menu-container { + display: flex; + justify-content: center; + flex-wrap: wrap; +} +.site-header .menu-container a.menu { + font-size: 16px; + padding: 8px 16px; + flex-shrink: 0; + font-weight: 600; +} +.site-header .avatar { + margin-bottom: 24px; + border-radius: 50%; + width: 120px; + height: 120px; +} +.site-header .social-container { + padding: 16px; + font-size: 18px; +} +.site-header .social-container a { + margin: 4px 8px; + color: #868e96; +} +@media (max-width: 600px) { + .site-header { + padding: 24px 0 0; + } + .site-header .avatar { + width: 80px; + height: 80px; + } +} +.post-container { + flex: 1; +} +.post-container .post { + padding-bottom: 32px; +} +.post-container .post .post-title { + font-size: 28px; + text-align: center; + padding: 24px 0; + font-weight: 900; + letter-spacing: 0.02em; +} +.post-container .post .post-info { + text-align: center; + font-size: 12px; + padding-bottom: 24px; +} +.post-container .post .post-info > span { + color: #5E5E5E; +} +.post-container .post .post-info > span:not(:first-child):before { + content: "/ "; + font-size: 10px; + color: rgba(0, 0, 0, 0.1); + margin: 0 4px; +} +.post-container .post .post-info .post-tag { + padding: 8px 8px; +} +.post-container .post .post-feature-image { + display: block; + width: 100%; + padding-top: 32.6%; + border-radius: 2px; + overflow: hidden; + background-size: cover; + background-position: center; + transition: all 0.3s; +} +.post-container .post .post-feature-image img { + width: 100%; +} +.post-container .post .post-feature-image:hover { + transform: scale(1.0082); +} +.post-container .post .post-abstract { + padding: 24px 0; + line-height: 1.5; + font-size: 16px; +} +.post-container .post .post-abstract strong { + font-weight: bolder; +} +.post-container .post .post-abstract a { + color: #006CFF; + transition: all 0.3s; +} +.post-container .post .post-abstract a:hover { + color: #0061e6; + border-bottom: 1px dotted #0061e6; +} +.post-container .post .post-abstract code { + font-family: monospace; + font-size: inherit; + background-color: rgba(0, 0, 0, 0.06); + padding: 0 2px; + border: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 2px 2px; + line-height: initial; + word-wrap: break-word; + text-indent: 0; +} +.pagination-container { + padding: 32px 16px; + overflow: hidden; +} +.pagination-container .prev-page { + float: left; +} +.pagination-container .next-page { + float: right; +} +.pagination-container .prev-page, +.pagination-container .next-page { + padding: 6px 12px; + font-weight: bold; + border-bottom: 2px solid transparent; +} +.pagination-container .prev-page:hover, +.pagination-container .next-page:hover { + border-bottom: 2px solid; +} +@media (max-width: 600px) { + .post-container .post { + padding: 16px 16px; + } + .post-container .post .post-title { + padding: 16px 0; + font-size: 24px; + } + .post-container .post .post-abstract { + padding: 16px 0; + } + .post-container .post .post-feature-image { + padding-top: 56.25%; + } +} +.post-detail { + flex: 1; +} +.post-detail .post { + padding: 24px 32px; +} +.post-detail .post .post-feature-image { + width: 100%; + height: auto; + margin-bottom: 24px; + border-radius: 2px; +} +.post-detail .post .post-title { + font-size: 32px; + text-align: center; + padding: 24px 0; + font-weight: 900; + letter-spacing: 0.02em; +} +.post-detail .post .post-info { + text-align: center; + font-size: 12px; + padding-bottom: 24px; +} +.post-detail .post .post-info > span { + color: #5E5E5E; +} +.post-detail .post .post-info > span:not(:first-child):before { + content: "/ "; + font-size: 10px; + color: rgba(0, 0, 0, 0.1); + margin: 0 4px; +} +.post-detail .post .post-info .post-tag { + padding: 8px 8px; +} +.post-detail .post .post-content-wrapper { + display: flex; +} +.post-detail .post .post-content { + width: 100%; + flex-shrink: 0; + font-family: "Droid Serif", "PingFang SC", "Hiragino Sans GB", "Droid Sans Fallback", "Microsoft YaHei", sans-serif; +} +.post-detail .post .post-content a { + color: rgba(0, 0, 0, 0.98); + word-wrap: break-word; + text-decoration: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.26); +} +.post-detail .post .post-content a:hover { + color: #0061e6; + border-bottom: 1px solid #0061e6; +} +.post-detail .post .post-content img { + display: block; + box-shadow: 0 0 30px #eee; + max-width: 100%; + border-radius: 2px; + margin: 24px auto; +} +.post-detail .post .post-content p { + line-height: 1.62; + margin-bottom: 1.12em; + font-size: 16px; + letter-spacing: 0.05em; + hyphens: auto; +} +.post-detail .post .post-content p code, +.post-detail .post .post-content li code { + font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace; + line-height: initial; + word-wrap: break-word; + border-radius: 0; + background-color: #fff5f5; + color: #c53030; + padding: 0.2em 0.33333333em; + margin-left: 0.125em; + margin-right: 0.125em; +} +.post-detail .post .post-content pre { + margin-bottom: 1.5rem; + padding: 0; + position: relative; +} +.post-detail .post .post-content pre code { + font-size: 0.96em; + font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', monospace; + padding: 1em; + border-radius: 5px; + line-height: 1.5; +} +.post-detail .post .post-content blockquote { + color: #9a9a9a; + position: relative; + padding: 0.4em 0 0 2.2em; + font-size: 0.96em; +} +.post-detail .post .post-content blockquote:before { + position: absolute; + top: -4px; + left: 0; + content: "\201c"; + font: 700 62px/1 serif; + color: rgba(0, 0, 0, 0.1); +} +.post-detail .post .post-content table { + border-collapse: collapse; + margin: 1rem 0; + display: block; + overflow-x: auto; +} +.post-detail .post .post-content tr { + border-top: 1px solid #dfe2e5; +} +.post-detail .post .post-content td, +.post-detail .post .post-content th { + border: 1px solid #dfe2e5; + padding: 0.6em 1em; +} +.post-detail .post .post-content ul, +.post-detail .post .post-content ol { + padding-left: 35px; + line-height: 1.725; + margin-bottom: 16px; +} +.post-detail .post .post-content ul { + list-style-type: square; +} +.post-detail .post .post-content h1, +.post-detail .post .post-content h2, +.post-detail .post .post-content h3, +.post-detail .post .post-content h4, +.post-detail .post .post-content h5, +.post-detail .post .post-content h6 { + margin: 16px 0; + font-weight: 700; + padding-top: 16px; +} +.post-detail .post .post-content h1 { + font-size: 1.8em; +} +.post-detail .post .post-content h2 { + font-size: 1.42em; +} +.post-detail .post .post-content h3 { + font-size: 1.17em; +} +.post-detail .post .post-content h4 { + font-size: 1em; +} +.post-detail .post .post-content h5 { + font-size: 1em; +} +.post-detail .post .post-content h6 { + font-size: 1em; + font-weight: 500; +} +.post-detail .post .post-content hr { + display: block; + border: 0; + margin: 2.24em auto 2.86em; +} +.post-detail .post .post-content hr:before { + color: rgba(0, 0, 0, 0.2); + font-size: 1.1em; + display: block; + content: "* * *"; + text-align: center; +} +.post-detail .post .post-content mark { + background: #faf089; + color: #744210; + padding: 0.2em; +} +.post-detail .post .post-content .footnotes { + margin-left: auto; + margin-right: auto; + max-width: 760px; + padding-left: 18px; + padding-right: 18px; +} +.post-detail .post .post-content .footnotes:before { + content: ""; + display: block; + border-top: 4px solid rgba(0, 0, 0, 0.1); + width: 50%; + max-width: 100px; + margin: 40px 0 20px; +} +.post-detail .post .post-content .contains-task-list { + list-style-type: none; + padding-left: 30px; +} +.post-detail .post .post-content .task-list-item { + position: relative; +} +.post-detail .post .post-content .task-list-item-checkbox { + position: absolute; + cursor: pointer; + width: 16px; + height: 16px; + margin: 4px 0 0; + top: -1px; + left: -22px; + transform-origin: center; + transform: rotate(-90deg); + transition: all 0.2s ease; +} +.post-detail .post .post-content .task-list-item-checkbox:checked { + transform: rotate(0); +} +.post-detail .post .post-content .task-list-item-checkbox:checked:before { + border: transparent; + background-color: #51cf66; +} +.post-detail .post .post-content .task-list-item-checkbox:checked:after { + transform: rotate(-45deg) scale(1); +} +.post-detail .post .post-content .task-list-item-checkbox:checked + .task-list-item-label { + color: #a0a0a0; + text-decoration: line-through; +} +.post-detail .post .post-content .task-list-item-checkbox:before { + content: ""; + width: 16px; + height: 16px; + box-sizing: border-box; + display: inline-block; + border: 1px solid #9ae6b4; + border-radius: 2px; + background-color: #fff; + position: absolute; + top: 0; + left: 0; + transition: all 0.2s ease; +} +.post-detail .post .post-content .task-list-item-checkbox:after { + content: ""; + transform: rotate(-45deg) scale(0); + width: 9px; + height: 5px; + border: 1px solid #fff; + border-top: none; + border-right: none; + position: absolute; + display: inline-block; + top: 4px; + left: 4px; + transition: all 0.2s ease; +} +.next-post { + text-align: center; + padding: 24px 32px; +} +.next-post .next { + margin-bottom: 24px; + color: #343a40; + font-weight: lighter; +} +.next-post .post-title { + font-size: 20px; + font-weight: bold; + letter-spacing: 0.02em; +} +#gitalk-container, +#disqus_thread { + padding: 24px 32px; +} +.toc-container .markdownIt-TOC { + position: sticky; + top: 32px; + width: 200px; + font-size: 12px; + list-style: none; + padding-left: 0; + padding: 16px 8px; +} +.toc-container .markdownIt-TOC:before { + content: ""; + position: absolute; + top: 0; + left: 8px; + bottom: 0; + width: 1px; + background-color: #ebedef; + opacity: 0.5; +} +.toc-container ul { + list-style: none; +} +.toc-container li { + padding-left: 16px; +} +.toc-container li a { + color: #868e96; + padding: 4px; + display: block; + transition: all 0.3s; +} +.toc-container li a:hover { + background: #fafafa; +} +.toc-container li a.current { + color: #006CFF; + background: #fafafa; +} +@media (max-width: 600px) { + .post-detail .post { + padding: 16px; + } + .post-detail .post .post-title { + font-size: 24px; + padding: 16px 0; + } +} +@media (max-width: 1150px) { + .toc-container { + display: none; + } +} +.archives-container { + padding: 32px; + flex: 1; +} +.archives-container .year { + font-size: 1.375rem; + font-weight: bold; + margin: 24px 0 16px; + color: #868e96; + padding: 0 24px; +} +.archives-container .post { + padding: 16px 24px; + display: block; +} +.archives-container .post .post-title { + font-size: 16px; + font-weight: 900; + letter-spacing: 0.02em; +} +.archives-container .post .time { + font-size: 0.75rem; + margin-top: 8px; + color: #ced4da; +} +@media (max-width: 600px) { + .archives-container { + padding: 16px; + } +} +.tags-container { + padding: 32px 32px; + flex: 1; + text-align: center; +} +.tags-container .tag { + display: inline-block; + padding: 8px 16px; + margin: 8px; + background: #f8f9fa; + color: #495057; + border-radius: 2px; + font-size: 14px; +} +.tags-container .tag:hover { + background: #e9ecef; + color: #212529; +} +.current-tag-container .title { + text-align: center; + font-size: 18px; + margin-bottom: 24px; +} +.about-page { + padding: 24px 32px; +} +.site-footer { + font-size: 12px; + text-align: center; + padding: 40px 24px; + color: #868e96; + display: flex; + justify-content: center; + align-items: center; +} +.rss { + display: inline-flex; + align-items: center; + margin-left: 24px; +} +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + color: #333; + background: #f9f7f3; +} +.hljs-comment, +.hljs-quote { + color: #998; + font-style: italic; +} +.hljs-keyword, +.hljs-selector-tag, +.hljs-subst { + color: #333; + font-weight: bold; +} +.hljs-number, +.hljs-literal, +.hljs-variable, +.hljs-template-variable, +.hljs-tag .hljs-attr { + color: #008080; +} +.hljs-string, +.hljs-doctag { + color: #d14; +} +.hljs-title, +.hljs-section, +.hljs-selector-id { + color: #900; + font-weight: bold; +} +.hljs-subst { + font-weight: normal; +} +.hljs-type, +.hljs-class .hljs-title { + color: #458; + font-weight: bold; +} +.hljs-tag, +.hljs-name, +.hljs-attribute { + color: #000080; + font-weight: normal; +} +.hljs-regexp, +.hljs-link { + color: #009926; +} +.hljs-symbol, +.hljs-bullet { + color: #990073; +} +.hljs-built_in, +.hljs-builtin-name { + color: #0086b3; +} +.hljs-meta { + color: #999; + font-weight: bold; +} +.hljs-deletion { + background: #fdd; +} +.hljs-addition { + background: #dfd; +} +.hljs-emphasis { + font-style: italic; +} +.hljs-strong { + font-weight: bold; +} + + .post-container .post .post-title { + text-align: center; + } + .post-container .post .post-info { + text-align: center; + } + .post-detail .post .post-title { + text-align: center; + } + .post-detail .post .post-info { + text-align: center; + } + + body { + font-family: -apple-system,BlinkMacSystemFont,'Helvetica Neue','PingFang SC','Hiragino Sans GB','Droid Sans Fallback','Microsoft YaHei',sans-serif; + } + + .main { + background: #d0ebff; + } + + body { + background: #e7f5ff; + } + \ No newline at end of file diff --git a/tag/-uPahkw58/index.html b/tag/-uPahkw58/index.html new file mode 100644 index 0000000..c835515 --- /dev/null +++ b/tag/-uPahkw58/index.html @@ -0,0 +1,170 @@ + + + + +tool | cyanine + + + + + + + + + + + +
    +
    + + +
    +

    + 标签:# tool +

    +
    +
    + + + + + + + +
    + +
    + + +
    + + + +
    +
    + + diff --git a/tag/M8-hjmU6C/index.html b/tag/M8-hjmU6C/index.html new file mode 100644 index 0000000..925ef9d --- /dev/null +++ b/tag/M8-hjmU6C/index.html @@ -0,0 +1,209 @@ + + + + +Django | cyanine + + + + + + + + + + + +
    +
    + + +
    +

    + 标签:# Django +

    +
    +
    + + + + + + + + + +
    + +
    + + +
    + + + +
    +
    + + diff --git a/tag/axGgidGI4/index.html b/tag/axGgidGI4/index.html new file mode 100644 index 0000000..00bd6db --- /dev/null +++ b/tag/axGgidGI4/index.html @@ -0,0 +1,232 @@ + + + + +python | cyanine + + + + + + + + + + + +
    +
    + + +
    +

    + 标签:# python +

    +
    +
    + + + + + + + + + + + +
    + +
    + + +
    + + + +
    +
    + + diff --git a/tag/p8kCxqhZ6/index.html b/tag/p8kCxqhZ6/index.html new file mode 100644 index 0000000..3fe633d --- /dev/null +++ b/tag/p8kCxqhZ6/index.html @@ -0,0 +1,124 @@ + + + + +html | cyanine + + + + + + + + + + + +
    +
    + + +
    +

    + 标签:# html +

    +
    +
    + + + +
    + +
    + + +
    + + + +
    +
    + + diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 0000000..260a9c8 --- /dev/null +++ b/tags/index.html @@ -0,0 +1,100 @@ + + + + +cyanine + + + + + + + + + + + +
    +
    + + + +
    + + tool + + Django + + python + + html + +
    + + + +
    +
    + +