<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Dennic&#39;s Blog | 小恐龙的日常</title>
  
  <subtitle>小恐龙的日常</subtitle>
  <link href="https://blog.dennic365.com/atom.xml" rel="self"/>
  
  <link href="https://blog.dennic365.com/"/>
  <updated>2022-02-24T15:14:23.000Z</updated>
  <id>https://blog.dennic365.com/</id>
  
  <author>
    <name>Dennic</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>改造REST-Framework，自定义修饰器为ViewSet中每个Action指定单独的Serializer</title>
    <link href="https://blog.dennic365.com/2019/04/19/rest-framework-viewset-action-serializer/"/>
    <id>https://blog.dennic365.com/2019/04/19/rest-framework-viewset-action-serializer/</id>
    <published>2019-04-19T09:34:09.000Z</published>
    <updated>2022-02-24T15:14:23.000Z</updated>
    
    <content type="html"><![CDATA[<p>在 REST-Framework 的 ViewSet 中，一般是通过 ViewSet 类的 serializer_class 属性来指定视图所使用的 Serializer。</p><p>但是很多时候我们需要为 ViewSet 中的某个 Action 指定不同的 Serializer。 比如在 UserViewSet中有一个修改密码的 Action：change_password，我们需要单独为它指定一个 ChangePasswordSerializer。</p><p>根据官方文档，我们可以这样做，重写 ViewSet 类的 get_serializer_class 方法，自己实现判断逻辑返回需要的 Serializer。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">get_serializer_class</span>(<span class="params">self</span>):</span><br><span class="line"><span class="keyword">if</span> self.action == <span class="string">&quot;change_password&quot;</span>:</span><br><span class="line"><span class="keyword">return</span> ChangePasswordSerializer</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">super</span>(UserViewSet, self).get_serializer_class()</span><br></pre></td></tr></table></figure><p>这样在 change_password 方法中我们就可以直接通过 self.get_serializer() 得到 ChangePasswordSerializer 对象了。</p><p><strong>但是这不是一个优雅的解决方法，我们应该劲量避免代码中出现硬编码！！！</strong></p><p>为了优雅的解决这个问题，我们需要实现一个方法装饰器和一个 Mixin 类。</p><span id="more"></span><p>首先是一个装饰器，用于为 Action 指定 Serializer。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">action_serializer</span>(<span class="params">serializer</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;用于为 Action 指定 Serializer 类的装饰器&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">decorator</span>(<span class="params">func</span>):</span><br><span class="line">func.serializer_class = serializer</span><br><span class="line"><span class="keyword">return</span> func</span><br><span class="line"><span class="keyword">return</span> decorator</span><br></pre></td></tr></table></figure><p>接下来是一个 Mixin 类，重写 get_serializer_class 方法，返回使用了 action_serializer 装饰器修饰的 Action 的 Serializer 类即可。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ActionSerializerMixin</span>(<span class="title class_ inherited__">object</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;使用 Action 指定 Serializer 的 Mixin 类&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_serializer_class</span>(<span class="params">self</span>):</span><br><span class="line">action = <span class="built_in">getattr</span>(self, self.action)</span><br><span class="line"><span class="keyword">if</span> <span class="built_in">hasattr</span>(action, <span class="string">&quot;serializer_class&quot;</span>):</span><br><span class="line"><span class="keyword">return</span> action.serializer_class</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">super</span>(ActionSerializerMixin, self).get_serializer_class()</span><br></pre></td></tr></table></figure><p>这样在我们的 UserViewSet 中，代码就可以简化成这样了，十分优雅。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">UserViewSet</span>(ActionSerializerMixin, <span class="comment"># ActionSerializerMixin类必须放在多重继承的第一个</span></span><br><span class="line">                    mixins.ListModelMixin,</span><br><span class="line">                    mixins.RetrieveModelMixin,</span><br><span class="line">                    mixins.UpdateModelMixin,</span><br><span class="line">                    viewsets.GenericViewSet):</span><br><span class="line"></span><br><span class="line">queryset = User.objects.<span class="built_in">all</span>()</span><br><span class="line">serializer_class = UserSerializer <span class="comment"># 其他Action默认的serializer</span></span><br><span class="line">permission_classes = (permissions.IsAuthenticated, IsOwnerOrReadOnly, )</span><br><span class="line"></span><br><span class="line"><span class="meta">    @action_serializer(<span class="params">ChangePasswordSerializer</span>) </span><span class="comment"># 使用装饰器为change_password指定单独的Serializer</span></span><br><span class="line"><span class="meta">@action(<span class="params">detail=<span class="literal">False</span>, methods=[<span class="string">&quot;POST&quot;</span>]</span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">change_password</span>(<span class="params">self, request</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;修改密码 Action&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">pass</span></span><br></pre></td></tr></table></figure><p>&amp;nbsp;</p><p><strong>其实还有一个更简单的方法</strong></p><p>但是我暂时没有仔细看APIView类的源码，不确定在实例化ViewSet以后修改serializer_class属性会不会造成什么问题。</p><p>这个方法就是直接在装饰器中修改ViewSet对象的serializer_class属性，这样就只需要向action方法添加一个装饰器就能实现我们想要的功能了。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> functools <span class="keyword">import</span> wraps</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">action_serializer</span>(<span class="params">serializer</span>):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">decorator</span>(<span class="params">func</span>):</span><br><span class="line"><span class="meta">        @wraps(<span class="params">func</span>)</span></span><br><span class="line">        <span class="keyword">def</span> <span class="title function_">wrapper</span>(<span class="params">self, *args, **kwargs</span>):</span><br><span class="line">            self.serializer_class = serializer</span><br><span class="line">            <span class="keyword">return</span> func(self, *args, **kwargs)</span><br><span class="line">        <span class="keyword">return</span> wrapper</span><br><span class="line">    <span class="keyword">return</span> decorator</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;在 REST-Framework 的 ViewSet 中，一般是通过 ViewSet 类的 serializer_class 属性来指定视图所使用的 Serializer。&lt;/p&gt;
&lt;p&gt;但是很多时候我们需要为 ViewSet 中的某个 Action 指定不同的 Serializer。 比如在 UserViewSet中有一个修改密码的 Action：change_password，我们需要单独为它指定一个 ChangePasswordSerializer。&lt;/p&gt;
&lt;p&gt;根据官方文档，我们可以这样做，重写 ViewSet 类的 get_serializer_class 方法，自己实现判断逻辑返回需要的 Serializer。&lt;/p&gt;
&lt;figure class=&quot;highlight python&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;title function_&quot;&gt;get_serializer_class&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;self&lt;/span&gt;):&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; self.action == &lt;span class=&quot;string&quot;&gt;&amp;quot;change_password&amp;quot;&lt;/span&gt;:&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; ChangePasswordSerializer&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;built_in&quot;&gt;super&lt;/span&gt;(UserViewSet, self).get_serializer_class()&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;这样在 change_password 方法中我们就可以直接通过 self.get_serializer() 得到 ChangePasswordSerializer 对象了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是这不是一个优雅的解决方法，我们应该劲量避免代码中出现硬编码！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了优雅的解决这个问题，我们需要实现一个方法装饰器和一个 Mixin 类。&lt;/p&gt;</summary>
    
    
    
    <category term="Django" scheme="https://blog.dennic365.com/categories/Django/"/>
    
    
    <category term="Django" scheme="https://blog.dennic365.com/tags/Django/"/>
    
    <category term="REST-Framework" scheme="https://blog.dennic365.com/tags/REST-Framework/"/>
    
  </entry>
  
  <entry>
    <title>蓝桥杯 算法训练——审美课（JAVA满分）</title>
    <link href="https://blog.dennic365.com/2019/03/23/lanqiao-java-shenmei/"/>
    <id>https://blog.dennic365.com/2019/03/23/lanqiao-java-shenmei/</id>
    <published>2019-03-23T08:56:03.000Z</published>
    <updated>2022-02-24T15:12:17.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>问题描述<br>　　《审美的历程》课上有n位学生，帅老师展示了m幅画，其中有些是梵高的作品，另外的都出自五岁小朋友之手。老师请同学们分辨哪些画的作者是梵高，但是老师自己并没有答案，因为这些画看上去都像是小朋友画的……老师只想知道，有多少对同学给出的答案完全相反，这样他就可以用这个数据去揭穿披着皇帝新衣的抽象艺术了（支持帅老师^_^）。<br>　　答案完全相反是指对每一幅画的判断都相反。<br>输入格式<br>　　第一行两个数n和m，表示学生数和图画数；<br>　　接下来是一个n*m的01矩阵A：<br>　　如果aij&#x3D;0，表示学生i觉得第j幅画是小朋友画的；<br>　　如果aij&#x3D;1，表示学生i觉得第j幅画是梵高画的。<br>输出格式<br>　　输出一个数ans：表示有多少对同学的答案完全相反。<br>样例输入<br>　　3 2<br>　　1 0<br>　　0 1<br>　　1 0<br>样例输出<br>　　2<br>样例说明<br>　　同学1和同学2的答案完全相反；<br>　　同学2和同学3的答案完全相反；<br>　　所以答案是2。<br>数据规模和约定<br>　　对于50%的数据：n&lt;&#x3D;1000；<br>　　对于80%的数据：n&lt;&#x3D;10000；<br>　　对于100%的数据：n&lt;&#x3D;50000，m&lt;&#x3D;20。</p></blockquote><p>这道题困扰了我很久，原理上看不难，但是注意有时长限制。</p><blockquote><p>时间限制：1.0s</p></blockquote><p>尝试了很多次都无法拿到满分，问题都出在运行超时上。</p><span id="more"></span><p>最后总结出三个关键点：</p><ol><li>一定要使用位运算，不要通过遍历每个同学的每个答案来进行比较。</li><li>使用Map储存每种答案出现的次数，同样用于避免过多的遍历次数。</li><li>这是最关键的一点：不要使用Scanner来读取输入，自己实现nextInt()函数，直接读取输入流。否则一定会超时！！</li></ol><p>以下是我最终的代码：</p><blockquote><p>得分：100<br>CPU使用时间：453ms<br>内存使用：27.44MB</p></blockquote><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Main</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">int</span> <span class="title function_">nextInt</span><span class="params">()</span> &#123;</span><br><span class="line"><span class="type">int</span> <span class="variable">num</span> <span class="operator">=</span> <span class="number">0</span>, read;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line"><span class="keyword">while</span> ((read = System.in.read()) != -<span class="number">1</span>) &#123;</span><br><span class="line"><span class="keyword">if</span> (Character.isDigit(read)) &#123;</span><br><span class="line">num = read - <span class="string">&#x27;0&#x27;</span>;</span><br><span class="line"><span class="keyword">while</span> ((read = System.in.read()) != -<span class="number">1</span>)</span><br><span class="line"><span class="keyword">if</span> (Character.isDigit(read)) &#123;</span><br><span class="line">num = (num * <span class="number">10</span>) + (read - <span class="string">&#x27;0&#x27;</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> num;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line"><span class="type">int</span> <span class="variable">s</span> <span class="operator">=</span> nextInt();</span><br><span class="line"><span class="type">int</span> <span class="variable">p</span> <span class="operator">=</span> nextInt();</span><br><span class="line">Map&lt;Integer, Integer&gt; map = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i=<span class="number">0</span>; i&lt;s; i++)&#123;</span><br><span class="line"><span class="type">int</span> <span class="variable">n</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">j</span> <span class="operator">=</span> <span class="number">0</span>; j &lt; p; j++) &#123;</span><br><span class="line">n = (n &lt;&lt; <span class="number">1</span>) + nextInt();</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 记录下相同答案出现的次数</span></span><br><span class="line">map.put(n, map.containsKey(n) ? map.get(n)+<span class="number">1</span> : <span class="number">1</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">int</span> <span class="variable">max</span> <span class="operator">=</span> (<span class="number">1</span> &lt;&lt; p) - <span class="number">1</span>;</span><br><span class="line"><span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span> (Map.Entry&lt;Integer, Integer&gt; entry: map.entrySet())&#123;</span><br><span class="line"><span class="type">int</span> <span class="variable">k1</span> <span class="operator">=</span> entry.getKey();</span><br><span class="line"><span class="type">int</span> <span class="variable">k2</span> <span class="operator">=</span> (~k1) &amp; max; <span class="comment">// 取反运算，得到完全相反的答案</span></span><br><span class="line"><span class="type">Integer</span> <span class="variable">v2</span> <span class="operator">=</span> map.get(k2);</span><br><span class="line"><span class="keyword">if</span> (v2 != <span class="literal">null</span>)&#123;</span><br><span class="line">count += v2 * entry.getValue();</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">System.out.println(count / <span class="number">2</span>); <span class="comment">// 除2是为了去掉重复计数</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;问题描述&lt;br&gt;　　《审美的历程》课上有n位学生，帅老师展示了m幅画，其中有些是梵高的作品，另外的都出自五岁小朋友之手。老师请同学们分辨哪些画的作者是梵高，但是老师自己并没有答案，因为这些画看上去都像是小朋友画的……老师只想知道，有多少对同学给出的答案完全相反，这样他就可以用这个数据去揭穿披着皇帝新衣的抽象艺术了（支持帅老师^_^）。&lt;br&gt;　　答案完全相反是指对每一幅画的判断都相反。&lt;br&gt;输入格式&lt;br&gt;　　第一行两个数n和m，表示学生数和图画数；&lt;br&gt;　　接下来是一个n*m的01矩阵A：&lt;br&gt;　　如果aij&amp;#x3D;0，表示学生i觉得第j幅画是小朋友画的；&lt;br&gt;　　如果aij&amp;#x3D;1，表示学生i觉得第j幅画是梵高画的。&lt;br&gt;输出格式&lt;br&gt;　　输出一个数ans：表示有多少对同学的答案完全相反。&lt;br&gt;样例输入&lt;br&gt;　　3 2&lt;br&gt;　　1 0&lt;br&gt;　　0 1&lt;br&gt;　　1 0&lt;br&gt;样例输出&lt;br&gt;　　2&lt;br&gt;样例说明&lt;br&gt;　　同学1和同学2的答案完全相反；&lt;br&gt;　　同学2和同学3的答案完全相反；&lt;br&gt;　　所以答案是2。&lt;br&gt;数据规模和约定&lt;br&gt;　　对于50%的数据：n&amp;lt;&amp;#x3D;1000；&lt;br&gt;　　对于80%的数据：n&amp;lt;&amp;#x3D;10000；&lt;br&gt;　　对于100%的数据：n&amp;lt;&amp;#x3D;50000，m&amp;lt;&amp;#x3D;20。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这道题困扰了我很久，原理上看不难，但是注意有时长限制。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;时间限制：1.0s&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;尝试了很多次都无法拿到满分，问题都出在运行超时上。&lt;/p&gt;</summary>
    
    
    
    <category term="日常" scheme="https://blog.dennic365.com/categories/%E6%97%A5%E5%B8%B8/"/>
    
    
    <category term="JAVA" scheme="https://blog.dennic365.com/tags/JAVA/"/>
    
    <category term="算法题" scheme="https://blog.dennic365.com/tags/%E7%AE%97%E6%B3%95%E9%A2%98/"/>
    
  </entry>
  
  <entry>
    <title>Django自定义认证User模型email字段不能保存为空的解决办法</title>
    <link href="https://blog.dennic365.com/2019/03/19/django-user-model-email-null/"/>
    <id>https://blog.dennic365.com/2019/03/19/django-user-model-email-null/</id>
    <published>2019-03-19T10:13:25.000Z</published>
    <updated>2022-02-24T15:09:51.000Z</updated>
    
    <content type="html"><![CDATA[<p>在一个项目中直接使用了自定义Django AdminSite作为后台管理网站。</p><p>在修改自定义User模型的信息后，点击保存按钮提交Form时，提示email字段不能为空。</p><p>这是因为我们的UserAdmin类继承自django.contrib.auth.admin.UserAdmin，在父类UserAdmin的form中将email字段定义为了required，即必填字段。</p><p>通过继承django.contrib.auth.forms.UserChangeForm类，并在__init__方法中对email字段属性进行修改，可以实现我们想要的效果。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.contrib.auth.forms <span class="keyword">import</span> UserChangeForm</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">UserForm</span>(<span class="title class_ inherited__">UserChangeForm</span>):</span><br><span class="line"><span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, *args, **kwargs</span>):</span><br><span class="line"><span class="built_in">super</span>(UserForm, self).__init__(*args, **kwargs)</span><br><span class="line">self.fields[<span class="string">&quot;email&quot;</span>].required = <span class="literal">False</span></span><br></pre></td></tr></table></figure><p>这样在保存时就不会提示email字段不能为空了。</p><p>但是测试中发现，如果不填写email字段内容，保存后发现email字段的值变成了空字符串，而不是None，但是我们在自定义User模型中指定了email字段null&#x3D;True，即我们期望的它应该是一个空值None。</p><p>经过调试后发现，在django.contrib.admin.options.ModelAdmin类的_changeform_view方法代码中，调用form.is_valid()方法后，email字段的值发生了变化。</p><span id="more"></span><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.contrib.admin.options.ModelAdmin</span></span><br><span class="line"></span><br><span class="line">ModelForm = self.get_form(request, obj)</span><br><span class="line"><span class="keyword">if</span> request.method == <span class="string">&#x27;POST&#x27;</span>:</span><br><span class="line">form = ModelForm(request.POST, request.FILES, instance=obj)</span><br><span class="line"><span class="keyword">if</span> form.is_valid(): <span class="comment"># 这句以后email字段值发生了变化</span></span><br><span class="line">form_validated = <span class="literal">True</span></span><br><span class="line">new_object = self.save_form(request, form, change=<span class="keyword">not</span> add)</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">form_validated = <span class="literal">False</span></span><br><span class="line">new_object = form.instance</span><br></pre></td></tr></table></figure><p>于是顺藤摸瓜，找到django.forms.models.BaseModelForm类中_post_clean方法，发现在此方法中调用了Model实例的full_clean方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.forms.models.BaseModelForm</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)</span><br><span class="line"><span class="keyword">except</span> ValidationError <span class="keyword">as</span> e:</span><br><span class="line">self._update_errors(e)</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">self.instance.full_clean(exclude=exclude, validate_unique=<span class="literal">False</span>)</span><br><span class="line"><span class="keyword">except</span> ValidationError <span class="keyword">as</span> e:</span><br><span class="line">self._update_errors(e)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Validate uniqueness if needed.</span></span><br><span class="line"><span class="keyword">if</span> self._validate_unique:</span><br><span class="line">self.validate_unique()</span><br></pre></td></tr></table></figure><p>继续跟进发现在自定义User模型的父类AbstractUser中，实现了clean方法，并且对email字段的值进行了修改。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.contrib.auth.models.AbstractUser</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">clean</span>(<span class="params">self</span>):</span><br><span class="line"><span class="built_in">super</span>().clean()</span><br><span class="line">self.email = self.__class__.objects.normalize_email(self.email)</span><br></pre></td></tr></table></figure><p>这里调用的normalize_email是django.contrib.auth.base_user.BaseUserManager中实现的一个静态方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># django.contrib.auth.base_user</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">BaseUserManager</span>(models.Manager):</span><br><span class="line"></span><br><span class="line"><span class="meta">@classmethod</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">normalize_email</span>(<span class="params">cls, email</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">Normalize the email address by lowercasing the domain part of it.</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">email = email <span class="keyword">or</span> <span class="string">&#x27;&#x27;</span> <span class="comment"># 就是这句话导致email值为None时被替换成了空字符串</span></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">email_name, domain_part = email.strip().rsplit(<span class="string">&#x27;@&#x27;</span>, <span class="number">1</span>)</span><br><span class="line"><span class="keyword">except</span> ValueError:</span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">email = email_name + <span class="string">&#x27;@&#x27;</span> + domain_part.lower()</span><br><span class="line"><span class="keyword">return</span> email</span><br></pre></td></tr></table></figure><p>所以我们只需要自己实现一个UserManager类并继承BaseUserManager类，并重写normalize_email方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.contrib.auth.models <span class="keyword">import</span> BaseUserManager</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">UserManager</span>(<span class="title class_ inherited__">BaseUserManager</span>):</span><br><span class="line"><span class="meta">@classmethod</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">normalize_email</span>(<span class="params">cls, email</span>):</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">super</span>().normalize_email(email) <span class="keyword">or</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><p>这样，当提交Form中缺少email值时，就能正确的把email字段值保存为None了。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在一个项目中直接使用了自定义Django AdminSite作为后台管理网站。&lt;/p&gt;
&lt;p&gt;在修改自定义User模型的信息后，点击保存按钮提交Form时，提示email字段不能为空。&lt;/p&gt;
&lt;p&gt;这是因为我们的UserAdmin类继承自django.contrib.auth.admin.UserAdmin，在父类UserAdmin的form中将email字段定义为了required，即必填字段。&lt;/p&gt;
&lt;p&gt;通过继承django.contrib.auth.forms.UserChangeForm类，并在__init__方法中对email字段属性进行修改，可以实现我们想要的效果。&lt;/p&gt;
&lt;figure class=&quot;highlight python&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt; django.contrib.auth.forms &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; UserChangeForm&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;title class_&quot;&gt;UserForm&lt;/span&gt;(&lt;span class=&quot;title class_ inherited__&quot;&gt;UserChangeForm&lt;/span&gt;):&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;title function_&quot;&gt;__init__&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;self, *args, **kwargs&lt;/span&gt;):&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		&lt;span class=&quot;built_in&quot;&gt;super&lt;/span&gt;(UserForm, self).__init__(*args, **kwargs)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		self.fields[&lt;span class=&quot;string&quot;&gt;&amp;quot;email&amp;quot;&lt;/span&gt;].required = &lt;span class=&quot;literal&quot;&gt;False&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;这样在保存时就不会提示email字段不能为空了。&lt;/p&gt;
&lt;p&gt;但是测试中发现，如果不填写email字段内容，保存后发现email字段的值变成了空字符串，而不是None，但是我们在自定义User模型中指定了email字段null&amp;#x3D;True，即我们期望的它应该是一个空值None。&lt;/p&gt;
&lt;p&gt;经过调试后发现，在django.contrib.admin.options.ModelAdmin类的_changeform_view方法代码中，调用form.is_valid()方法后，email字段的值发生了变化。&lt;/p&gt;</summary>
    
    
    
    <category term="Django" scheme="https://blog.dennic365.com/categories/Django/"/>
    
    
    <category term="Django" scheme="https://blog.dennic365.com/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>使用Django自带的session和auth应用实现Token认证</title>
    <link href="https://blog.dennic365.com/2019/03/19/django-session-auth-token/"/>
    <id>https://blog.dennic365.com/2019/03/19/django-session-auth-token/</id>
    <published>2019-03-19T09:02:00.000Z</published>
    <updated>2022-02-24T15:05:37.000Z</updated>
    
    <content type="html"><![CDATA[<p>Django的session和auth应用配合起来很方便的实现了身份认证和会话管理的功能。</p><p>现在我的项目在原有网站基础上需要另外提供一套API，只需要简单的一点拓展就可以在现有session和auth应用基础上实现基于Token的认证。</p><p>在自己的app下创建middleware.py文件，写一个自己的class，继承django.contrib.session.middleware模块下的SessionMiddleware类，并且重写process_request方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.conf <span class="keyword">import</span> settings</span><br><span class="line"><span class="keyword">from</span> django.contrib.sessions.middleware <span class="keyword">import</span> SessionMiddleware</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">SessionTokenMiddleware</span>(<span class="title class_ inherited__">SessionMiddleware</span>):</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">process_request</span>(<span class="params">self, request</span>):</span><br><span class="line">        <span class="comment"># 尝试从请求头中获取 Session Key</span></span><br><span class="line">        session_key = request.META.get(<span class="string">&quot;HTTP_&quot;</span> + <span class="built_in">getattr</span>(settings, <span class="string">&quot;ACCESS_TOKEN_NAME&quot;</span>, <span class="string">&quot;Access-Token&quot;</span>).replace(<span class="string">&quot;-&quot;</span>,<span class="string">&quot;_&quot;</span>).upper())</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> session_key:</span><br><span class="line">            <span class="built_in">super</span>().process_request(request)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            request.session = self.SessionStore(session_key)</span><br></pre></td></tr></table></figure><p>在处理请求时，先尝试从请求头中对应字段获取Token，即Session Key，并通过Session引擎获取Session对象。<br>如果请求头中没有包含Token字段，则调用父类方法。</p><span id="more"></span><p>最后，在登陆API的view中调用auth模块login方法后，从request中获取Session Key放入到响应体中即可。</p><p>这里因为是用了 REST-Framework 的 ViewSet，所以需要通过request.stream获取原始Django的HttpRequest对象。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@list_route(<span class="params">methods=[<span class="string">&quot;post&quot;</span>], permission_classes=[permissions.AllowAny]</span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">login</span>(<span class="params">self, request</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">登陆API</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">通过手机号和密码进行登陆验证</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line">serializer = UserSerializer(data=request.data)</span><br><span class="line"><span class="keyword">if</span> serializer.authenticate():</span><br><span class="line">login(request, serializer.authenticated_user)</span><br><span class="line">response_data = &#123;</span><br><span class="line"><span class="built_in">getattr</span>(settings, <span class="string">&quot;ACCESS_TOKEN_NAME&quot;</span>, <span class="string">&quot;Access-Token&quot;</span>): request.stream.session.session_key,</span><br><span class="line"><span class="string">&quot;Expires&quot;</span>: <span class="built_in">int</span>((time.time() + request.stream.session.get_expiry_age()) * <span class="number">1000</span>)</span><br><span class="line">&#125;</span><br><span class="line">response_data.update(serializer.data)</span><br><span class="line"><span class="keyword">return</span> Response(data=response_data, status=status.HTTP_200_OK)</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">logout(request)</span><br><span class="line"><span class="keyword">return</span> Response(data=serializer.errors, status=status.HTTP_400_BAD_REQUEST)</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;Django的session和auth应用配合起来很方便的实现了身份认证和会话管理的功能。&lt;/p&gt;
&lt;p&gt;现在我的项目在原有网站基础上需要另外提供一套API，只需要简单的一点拓展就可以在现有session和auth应用基础上实现基于Token的认证。&lt;/p&gt;
&lt;p&gt;在自己的app下创建middleware.py文件，写一个自己的class，继承django.contrib.session.middleware模块下的SessionMiddleware类，并且重写process_request方法。&lt;/p&gt;
&lt;figure class=&quot;highlight python&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt; django.conf &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; settings&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;from&lt;/span&gt; django.contrib.sessions.middleware &lt;span class=&quot;keyword&quot;&gt;import&lt;/span&gt; SessionMiddleware&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;title class_&quot;&gt;SessionTokenMiddleware&lt;/span&gt;(&lt;span class=&quot;title class_ inherited__&quot;&gt;SessionMiddleware&lt;/span&gt;):&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;title function_&quot;&gt;process_request&lt;/span&gt;(&lt;span class=&quot;params&quot;&gt;self, request&lt;/span&gt;):&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;comment&quot;&gt;# 尝试从请求头中获取 Session Key&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        session_key = request.META.get(&lt;span class=&quot;string&quot;&gt;&amp;quot;HTTP_&amp;quot;&lt;/span&gt; + &lt;span class=&quot;built_in&quot;&gt;getattr&lt;/span&gt;(settings, &lt;span class=&quot;string&quot;&gt;&amp;quot;ACCESS_TOKEN_NAME&amp;quot;&lt;/span&gt;, &lt;span class=&quot;string&quot;&gt;&amp;quot;Access-Token&amp;quot;&lt;/span&gt;).replace(&lt;span class=&quot;string&quot;&gt;&amp;quot;-&amp;quot;&lt;/span&gt;,&lt;span class=&quot;string&quot;&gt;&amp;quot;_&amp;quot;&lt;/span&gt;).upper())&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;not&lt;/span&gt; session_key:&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;            &lt;span class=&quot;built_in&quot;&gt;super&lt;/span&gt;().process_request(request)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;:&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;            request.session = self.SessionStore(session_key)&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;在处理请求时，先尝试从请求头中对应字段获取Token，即Session Key，并通过Session引擎获取Session对象。&lt;br&gt;如果请求头中没有包含Token字段，则调用父类方法。&lt;/p&gt;</summary>
    
    
    
    <category term="Django" scheme="https://blog.dennic365.com/categories/Django/"/>
    
    
    <category term="Django" scheme="https://blog.dennic365.com/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>selenium + PhantomJS 使用 WebDriverWait 实现延迟加载网页动态内容并截图</title>
    <link href="https://blog.dennic365.com/2018/05/02/selenium-phantomjs-delay-screenshot/"/>
    <id>https://blog.dennic365.com/2018/05/02/selenium-phantomjs-delay-screenshot/</id>
    <published>2018-05-02T05:51:57.000Z</published>
    <updated>2022-02-24T15:03:51.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近有个项目功能需要实现一个后台模拟登录网站，然后进入到二级页面进行网页截图并返回的 API。</p><p>听起来很有意思，打算使用 Python 的 selenium 包配合无界面浏览器  PhantomJS 来实现。</p><blockquote><p>要注意较新版本的 selenium 已经不再支持 PhantomJS，需安装较老版本的 selenium。</p></blockquote><span id="more"></span><p>模拟登陆使用 requests 包的 Session 类，然后将 cookie 添加到 WebDriver 里。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">session = requests.session()</span><br><span class="line">res = session.post(url=LOGIN_URL, data=payload)</span><br><span class="line">cookies = session.cookies.get_dict()</span><br><span class="line"></span><br><span class="line">driver = webdriver.PhantomJS()</span><br><span class="line">driver.implicitly_wait(<span class="number">10</span>) <span class="comment"># 设置连接超时时间为 10 秒。</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> cookies:</span><br><span class="line">driver.add_cookie(&#123;</span><br><span class="line"><span class="string">&quot;name&quot;</span>: i,</span><br><span class="line"><span class="string">&quot;value&quot;</span> : cookies[i],</span><br><span class="line"><span class="string">&quot;path&quot;</span>: <span class="string">&quot;/&quot;</span>,</span><br><span class="line"><span class="string">&quot;domain&quot;</span>: DOMAIN <span class="comment"># domain 和 path 参数是必须的，否则会报错。</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>需要截图的页面内有两个 iframe，而 iframe 内容是由页面上点击某个菜单链接时动态加载的。</p><p>所以我们在页面加载结束以后执行 JS 语句模拟点击菜单链接，网页引入了 JQuery，可以很方便的使用。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">driver.get(CONTENT_URL)</span><br><span class="line">driver.execute_script(<span class="string">&quot;$(&#x27;a.menu&#x27;).first().click()&quot;</span>)</span><br></pre></td></tr></table></figure><p>此时 iframe 开始加载，但是如果马上执行网页截图的话，iframe 可能还没有完成加载，所以我们等待想要截图的内容加载完成以后再执行截图。</p><p>这里可以方便的使用 selenium 提供的 WebDriverWait 类，用以阻塞式地循环判断条件是否成立，直到条件成立才会继续执行下面的代码。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在代码中引入 WebDriverWait 类</span></span><br><span class="line"><span class="comment"># from selenium.webdriver.support.wait import WebDriverWait</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># WebDriverWait 对象的 until 方法参数需传入一个函数，并将会循环调用这个函数用于检测条件是否成立，直到函数返回 True 时才继续执行下面的代码。</span></span><br><span class="line"><span class="comment"># 这里为了方便，将一个 lambda 匿名函数作为 until 方法的参数传入。</span></span><br><span class="line">condition = <span class="keyword">lambda</span> x: driver.execute_script(<span class="string">&quot;return document.getElementsByTagName(&#x27;iframe&#x27;)[1].contentDocument.getElementsByTagName(&#x27;table&#x27;).length&quot;</span>) &gt; <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># WebDriverWait 类构造函数第一个参数为 WebDriver 对象，第二个参数是超时时间（秒），如果超过该事件条件还未成立，将会抛出一个异常。</span></span><br><span class="line">WebDriverWait(driver, <span class="number">5</span>).until(condition)</span><br></pre></td></tr></table></figure><p>当条件成立时，执行网页截图，返回图片内容的字节数组。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">screenshot = driver.get_screenshot_as_png()</span><br></pre></td></tr></table></figure><p>最后，将图片内容写入到响应体中返回，完工。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近有个项目功能需要实现一个后台模拟登录网站，然后进入到二级页面进行网页截图并返回的 API。&lt;/p&gt;
&lt;p&gt;听起来很有意思，打算使用 Python 的 selenium 包配合无界面浏览器  PhantomJS 来实现。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;要注意较新版本的 selenium 已经不再支持 PhantomJS，需安装较老版本的 selenium。&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    <category term="Python" scheme="https://blog.dennic365.com/categories/Python/"/>
    
    
    <category term="Python" scheme="https://blog.dennic365.com/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>记录近期在 Android 开发上遇到的一些深坑</title>
    <link href="https://blog.dennic365.com/2018/01/17/android-trap/"/>
    <id>https://blog.dennic365.com/2018/01/17/android-trap/</id>
    <published>2018-01-17T12:57:22.000Z</published>
    <updated>2024-02-25T09:27:41.599Z</updated>
    
    <content type="html"><![CDATA[<p>最近在开发 App 测试的时候遇到了一些坑，在这里记录一下，防止以后再遇到时又要浪费时间。</p><h3 id="1-Fragment-中的-onAttach-方法没有被调用"><a href="#1-Fragment-中的-onAttach-方法没有被调用" class="headerlink" title="1. Fragment 中的 onAttach 方法没有被调用"></a>1. Fragment 中的 onAttach 方法没有被调用</h3><p>在 Android 5.0 (API 21) 系统手机上测试时 App 抛出了空指针异常，而在其他测试机（均等于或高于 Android 6.0）上运行正常。<br>经排查发现时 Fragment 中的 OnAttach 方法没有被调用。</p><p>原来 Android 在 API 23 以后重载了 Fragment 中的 OnAttach 方法。<br>之前的</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onAttach</span><span class="params">(Activity activity)</span> &#123;</span><br><span class="line"><span class="built_in">super</span>.onAttach(activity);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>被重载为了</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onAttach</span><span class="params">(Context context)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>.onAttach(context);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而我在继承 Fragment 的类中只重写了第二个方法，也就是 API 23 以后的方法。<br>所以我们复写的 OnAttach 方法在 API 21 的手机上没有被调用。</p><span id="more"></span><p>只需要复写 onAttach(Activity activity) 方法，并且添加一句 SDK 版本判断。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onAttach</span><span class="params">(Activity activity)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>.onAttach(activity);</span><br><span class="line"><span class="keyword">if</span> (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.M) &#123;</span><br><span class="line"><span class="comment">// Do something...</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>问题解决。</p><h3 id="2-华为手机上-Logcat-全部显示-some-logs-have-been-lost"><a href="#2-华为手机上-Logcat-全部显示-some-logs-have-been-lost" class="headerlink" title="2. 华为手机上 Logcat 全部显示 some logs have been lost"></a>2. 华为手机上 Logcat 全部显示 some logs have been lost</h3><p>在一部华为手机上（Huawei GR3 HWTAG-L6753）上测试时 App 发生了 Crash，奇怪的是查看文件没有生成 CrashLog 文件。<br>于是连上 ADB 查看 Logcat，发现输出的全是</p><blockquote><p>W&#x2F;AEE : some logs have been lost</p></blockquote><img src="/2018/01/17/android-trap/QQ%E5%9B%BE%E7%89%8720180117203514.png" class=""><p>根据 StackOverflow 上的回答 <a href="https://stackoverflow.com/questions/18124334/huawei-logcat-not-showing-the-log-for-my-app" title="android - Huawei, logcat not showing the log for my app? - Stack Overflow">android - Huawei, logcat not showing the log for my app? - Stack Overflow</a></p><blockquote><p>Dial<br><code>*#*#2846579#*#*</code><br>and you will see a hidden menu. Go to the Project Menu &gt; Background Setting &gt; Log setting and define the log availability (log switch) and level (log level setting).</p></blockquote><p>在拨号界面拨号 <code>*#*#2846579#*#*</code> 进入隐藏菜单开启 Log 输出以后，就能正常查看 Logcat 信息了。<br>（经测试开启后不需要重启手机，评论中有人提到部分手机重启手机将重置此设置）</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近在开发 App 测试的时候遇到了一些坑，在这里记录一下，防止以后再遇到时又要浪费时间。&lt;/p&gt;
&lt;h3 id=&quot;1-Fragment-中的-onAttach-方法没有被调用&quot;&gt;&lt;a href=&quot;#1-Fragment-中的-onAttach-方法没有被调用&quot; class=&quot;headerlink&quot; title=&quot;1. Fragment 中的 onAttach 方法没有被调用&quot;&gt;&lt;/a&gt;1. Fragment 中的 onAttach 方法没有被调用&lt;/h3&gt;&lt;p&gt;在 Android 5.0 (API 21) 系统手机上测试时 App 抛出了空指针异常，而在其他测试机（均等于或高于 Android 6.0）上运行正常。&lt;br&gt;经排查发现时 Fragment 中的 OnAttach 方法没有被调用。&lt;/p&gt;
&lt;p&gt;原来 Android 在 API 23 以后重载了 Fragment 中的 OnAttach 方法。&lt;br&gt;之前的&lt;/p&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;meta&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;title function_&quot;&gt;onAttach&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(Activity activity)&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;built_in&quot;&gt;super&lt;/span&gt;.onAttach(activity);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;被重载为了&lt;/p&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;meta&quot;&gt;@Override&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;title function_&quot;&gt;onAttach&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(Context context)&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;built_in&quot;&gt;super&lt;/span&gt;.onAttach(context);&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;而我在继承 Fragment 的类中只重写了第二个方法，也就是 API 23 以后的方法。&lt;br&gt;所以我们复写的 OnAttach 方法在 API 21 的手机上没有被调用。&lt;/p&gt;</summary>
    
    
    
    <category term="Android" scheme="https://blog.dennic365.com/categories/Android/"/>
    
    
    <category term="Android" scheme="https://blog.dennic365.com/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>Android — 判断点是否位于多边形内</title>
    <link href="https://blog.dennic365.com/2017/12/21/android-point-in-polygon/"/>
    <id>https://blog.dennic365.com/2017/12/21/android-point-in-polygon/</id>
    <published>2017-12-21T11:47:18.000Z</published>
    <updated>2024-02-25T09:14:06.716Z</updated>
    
    <content type="html"><![CDATA[<p>最近参与一个室内AP定位的项目做Android客户端的开发。</p><p>自己写了一个静态地图控件，遇到了需要判断触摸点是否位于多边形指定区域内的问题。</p><p>网上资料很多，主流方法是利用光投射算法。<a href="https://en.wikipedia.org/wiki/Point_in_polygon" title="Point in polygon - Wikipedia">Point in polygon - Wikipedia</a></p><img src="/2017/12/21/android-point-in-polygon/220px-RecursiveEvenPolygon.svg_.png" class="" title="point in polygon"><p>CSDN上有一篇文章把原理讲的非常详细 <a href="https://blog.csdn.net/hjh2005/article/details/9246967" title="https://blog.csdn.net/hjh2005/article/details/9246967">点在多边形内算法——判断一个点是否在一个复杂多边形的内部</a></p><p>参考 StackOverFlow 上一个回答的写法，翻译成 JAVA 如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 利用光投射算法计算点是否在多边形内</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> point 需要判断的点的坐标</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> vertices 多边形按顺时针或逆时针顺序的顶点坐标集合</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 点是否在多边形内</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isPointInPolygon</span><span class="params">(PointF point, List&lt;PointF&gt; vertices)</span> &#123;</span><br><span class="line"><span class="type">boolean</span> <span class="variable">contains</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line"><span class="keyword">for</span>(<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>, j = vertices.size() - <span class="number">1</span>; i &lt; vertices.size(); j = i++) &#123;</span><br><span class="line"><span class="keyword">if</span>(((vertices.get(i).y &gt;= point.y) != (vertices.get(j).y &gt;= point.y)) &amp;&amp;</span><br><span class="line">   (point.x &lt;= (vertices.get(j).x - vertices.get(i).x) * (point.y - vertices.get(i).y) / (vertices.get(j).y - vertices.get(i).y) + vertices.get(i).x))</span><br><span class="line">contains = !contains;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> contains;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><span id="more"></span><p>为了方便使用，我将它封装到了我的 Polygon 类里。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Polygon</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Builder</span> &#123;</span><br><span class="line">        <span class="keyword">private</span> List&lt;PointF&gt; vertices = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">public</span> Polygon.Builder <span class="title function_">addVertice</span><span class="params">(PointF point)</span>&#123;</span><br><span class="line">            <span class="built_in">this</span>.vertices.add(point);</span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">public</span> Polygon <span class="title function_">build</span><span class="params">()</span>&#123;</span><br><span class="line">            <span class="keyword">if</span> (vertices.size() &lt; <span class="number">3</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;Polygon must have at least 3 points&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Polygon</span>(<span class="built_in">this</span>.vertices);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> List&lt;PointF&gt; vertices;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Polygon</span><span class="params">(List&lt;PointF&gt; vertices)</span>&#123;</span><br><span class="line">        <span class="built_in">this</span>.vertices = vertices;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">Polygon</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.vertices = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Builder <span class="title function_">Builder</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Polygon</span>.Builder();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setVertices</span><span class="params">(List&lt;PointF&gt; vertices)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.vertices = vertices;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> List&lt;PointF&gt; <span class="title function_">getVertices</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> vertices;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Path <span class="title function_">getPolygonPath</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">        <span class="type">Path</span> <span class="variable">path</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Path</span>();</span><br><span class="line">        <span class="keyword">for</span> (PointF point : vertices) &#123;</span><br><span class="line">            <span class="keyword">if</span> (i == <span class="number">0</span>) path.moveTo(point.x, point.y);</span><br><span class="line">            <span class="keyword">else</span> path.lineTo(point.x, point.y);</span><br><span class="line">            i++;</span><br><span class="line">        &#125;</span><br><span class="line">        path.close();</span><br><span class="line">        <span class="keyword">return</span> path;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">contains</span><span class="params">(List&lt;PointF&gt; points)</span>&#123;</span><br><span class="line">        <span class="keyword">for</span> (PointF point: points)&#123;</span><br><span class="line">            <span class="keyword">if</span> (!contains(point))</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">contains</span><span class="params">(PointF point)</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Polygon.isPointInPolygon(point, <span class="built_in">this</span>.vertices);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 利用光投射算法计算点是否在多边形内</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> point 需要判断的点的坐标</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> vertices 多边形按顺时针或逆时针顺序的顶点坐标集合</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 点是否在多边形内</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isPointInPolygon</span><span class="params">(PointF point, List&lt;PointF&gt; vertices)</span> &#123;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">contains</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>, j = vertices.size() - <span class="number">1</span>; i &lt; vertices.size(); j = i++) &#123;</span><br><span class="line">            <span class="keyword">if</span>(((vertices.get(i).y &gt;= point.y) != (vertices.get(j).y &gt;= point.y)) &amp;&amp;</span><br><span class="line">                    (point.x &lt;= (vertices.get(j).x - vertices.get(i).x) * (point.y - vertices.get(i).y) / (vertices.get(j).y - vertices.get(i).y) + vertices.get(i).x))</span><br><span class="line">                contains = !contains;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> contains;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近参与一个室内AP定位的项目做Android客户端的开发。&lt;/p&gt;
&lt;p&gt;自己写了一个静态地图控件，遇到了需要判断触摸点是否位于多边形指定区域内的问题。&lt;/p&gt;
&lt;p&gt;网上资料很多，主流方法是利用光投射算法。&lt;a href=&quot;https://en.wikipedia.org/wiki/Point_in_polygon&quot; title=&quot;Point in polygon - Wikipedia&quot;&gt;Point in polygon - Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;img src=&quot;/2017/12/21/android-point-in-polygon/220px-RecursiveEvenPolygon.svg_.png&quot; class=&quot;&quot; title=&quot;point in polygon&quot;&gt;

&lt;p&gt;CSDN上有一篇文章把原理讲的非常详细 &lt;a href=&quot;https://blog.csdn.net/hjh2005/article/details/9246967&quot; title=&quot;https://blog.csdn.net/hjh2005/article/details/9246967&quot;&gt;点在多边形内算法——判断一个点是否在一个复杂多边形的内部&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;参考 StackOverFlow 上一个回答的写法，翻译成 JAVA 如下：&lt;/p&gt;
&lt;figure class=&quot;highlight java&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;14&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;15&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;16&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;/**&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt; * 利用光投射算法计算点是否在多边形内&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt; * &lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt; * &lt;span class=&quot;doctag&quot;&gt;@param&lt;/span&gt; point 需要判断的点的坐标&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt; * &lt;span class=&quot;doctag&quot;&gt;@param&lt;/span&gt; vertices 多边形按顺时针或逆时针顺序的顶点坐标集合&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt; * &lt;span class=&quot;doctag&quot;&gt;@return&lt;/span&gt; 点是否在多边形内&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt; */&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;type&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;title function_&quot;&gt;isPointInPolygon&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(PointF point, List&amp;lt;PointF&amp;gt; vertices)&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;type&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;contains&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;false&lt;/span&gt;;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt;(&lt;span class=&quot;type&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;variable&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;, j = vertices.size() - &lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;; i &amp;lt; vertices.size(); j = i++) &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;(((vertices.get(i).y &amp;gt;= point.y) != (vertices.get(j).y &amp;gt;= point.y)) &amp;amp;&amp;amp;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;		   (point.x &amp;lt;= (vertices.get(j).x - vertices.get(i).x) * (point.y - vertices.get(i).y) / (vertices.get(j).y - vertices.get(i).y) + vertices.get(i).x))&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;			contains = !contains;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;	&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; contains;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="Android" scheme="https://blog.dennic365.com/categories/Android/"/>
    
    
    <category term="Android" scheme="https://blog.dennic365.com/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>在 Django HttpResponse 响应体中返回 openpyxl 生成的 Excel 文档——源码分析</title>
    <link href="https://blog.dennic365.com/2017/10/30/django-response-openpyxl/"/>
    <id>https://blog.dennic365.com/2017/10/30/django-response-openpyxl/</id>
    <published>2017-10-30T10:14:00.000Z</published>
    <updated>2022-02-23T19:23:50.000Z</updated>
    
    <content type="html"><![CDATA[<p>现在我需要提供一个链接，访问时通过 openpyxl 动态生成一个 Excel 文档，并作为响应体返回。</p><p>在这里将动态生成的 Excel 文档保存到服务器，并重定向到静态文件链接显然不是一个合适的做法。</p><p>所以我们需要直接将 openpyx 生成的 Excel 文档写入到 Django 的 HttpResponse 对象响应体中。</p><p>查阅 openpyxl 的文档以后，并没有找到有用的信息，所以直接从源码入手。</p><p><strong>在这篇文章中，我将把源码分析的过程详细的展现出来，并总结出完美的解决方案。</strong></p><span id="more"></span><p>我们先从 openpyxl.workbook.workbook 模块下的 Workbook 类入手，找到平时我们用来保存 Excel 文档的方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">save</span>(<span class="params">self, filename</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;Save the current workbook under the given `filename`.</span></span><br><span class="line"><span class="string">Use this function instead of using an `ExcelWriter`.</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">.. warning::</span></span><br><span class="line"><span class="string">When creating your workbook using `write_only` set to True,</span></span><br><span class="line"><span class="string">you will only be able to call this function once. Subsequents attempts to</span></span><br><span class="line"><span class="string">modify or save the file will raise an :class:`openpyxl.shared.exc.WorkbookAlreadySaved` exception.</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="keyword">if</span> self.read_only:</span><br><span class="line"><span class="keyword">raise</span> TypeError(<span class="string">&quot;&quot;&quot;Workbook is read-only&quot;&quot;&quot;</span>)</span><br><span class="line"><span class="keyword">if</span> self.write_only:</span><br><span class="line">save_dump(self, filename)</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line">save_workbook(self, filename)</span><br></pre></td></tr></table></figure><p>可以看到在一般情况下调用这个方法时，实际上会调用的是 save_workbook 这个函数，这个函数在 openpyxl.writer.excel 模块中定义。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> openpyxl.writer.excel <span class="keyword">import</span> save_workbook</span><br></pre></td></tr></table></figure><p>那我们就要来看看这个模块里还定义了什么东西。</p><p>打开这个文件，突然眼前一亮，这里还定义了一个叫做 save_virtual_workbook 的函数，作者在注释里写到 “Return an in-memory workbook, suitable for a Django response.” ，看来这就是我们要找的东西。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">save_virtual_workbook</span>(<span class="params">workbook,</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;Return an in-memory workbook, suitable for a Django response.&quot;&quot;&quot;</span></span><br><span class="line">temp_buffer = BytesIO()</span><br><span class="line">archive = ZipFile(temp_buffer, <span class="string">&#x27;w&#x27;</span>, ZIP_DEFLATED, allowZip64=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">writer = ExcelWriter(workbook, archive)</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">writer.write_data()</span><br><span class="line"><span class="keyword">finally</span>:</span><br><span class="line">archive.close()</span><br><span class="line"></span><br><span class="line">virtual_workbook = temp_buffer.getvalue()</span><br><span class="line">temp_buffer.close()</span><br><span class="line"><span class="keyword">return</span> virtual_workbook</span><br></pre></td></tr></table></figure><p>这个函数将 Excel 文档写入到内存，返回一个字节数组。</p><p>Django 的 HttpResponse 类构造方法可以接受一个字节数组作为响应体，所以我们可以这样写。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.http <span class="keyword">import</span> HttpResponse</span><br><span class="line"><span class="keyword">from</span> openpyxl <span class="keyword">import</span> Workbook</span><br><span class="line"><span class="keyword">from</span> openpyxl.writer.excel <span class="keyword">import</span> save_virtual_workbook</span><br><span class="line"></span><br><span class="line">workbook = Workbook()</span><br><span class="line"></span><br><span class="line"><span class="comment"># ......</span></span><br><span class="line"></span><br><span class="line">response = HttpResponse(content=save_virtual_workbook(workbook), content_type=<span class="string">&#x27;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#x27;</span>)</span><br></pre></td></tr></table></figure><p>这样就可以很简单的实现将 Excel 文档写入到响应体中了。</p><p>指定中文文件名时遇到问题可以参考我之前的一篇文章 <a href="https://www.dennic365.com/blog/?p=82" title="Django 返回流文件时使用中文文件名的问题">Django 返回流文件时使用中文文件名的问题</a></p><h3 id="但是！还没有结束，我认为还可以有更优雅的解决方案。"><a href="#但是！还没有结束，我认为还可以有更优雅的解决方案。" class="headerlink" title="但是！还没有结束，我认为还可以有更优雅的解决方案。"></a>但是！还没有结束，我认为还可以有更优雅的解决方案。</h3><p>查阅 Django 文档，发现在 HttpResponse 类的说明下有这样一句：“you can use response as a file-like object.”，并给了一段示例代码。看来 HttpResponse 对象可以直接作为一个 file-like 对象，像对文件对象一样进行写入操作。</p><p>Excel 文档的 .xlsx 文件实际上是一个 ZIP 格式的压缩包，在 openpyxl.writer.excel 模块的 save_workbook 函数中是使用 Python 自带的 zipfile 模块来创建的文件。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">archive = ZipFile(filename, <span class="string">&#x27;w&#x27;</span>, ZIP_DEFLATED, allowZip64=<span class="literal">True</span>)</span><br></pre></td></tr></table></figure><p>查阅 zipfile 模块文档知道 ZipFile 类构造方法的第一个参数可以接受一个文件路径字符串、path-like 对象或是一个 file-like 对象。</p><p>太好了，看来我们可以直接把 Django 的 HttpResponse 对象传入到 ZipFile 的构造方法，让 ZipFile 对象直接往 HttpResponse 的响应体中写入内容。</p><p>我们当然可以在调用 openpyxl.writer.excel 模块的 save_workbook 函数时，直接将 HttpResponse 对象传入，就像这样。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.http <span class="keyword">import</span> HttpResponse</span><br><span class="line"><span class="keyword">from</span> openpyxl <span class="keyword">import</span> Workbook</span><br><span class="line"></span><br><span class="line">workbook = Workbook()</span><br><span class="line"></span><br><span class="line"><span class="comment"># ......</span></span><br><span class="line"></span><br><span class="line">response = HttpResponse(content_type=<span class="string">&#x27;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#x27;</span>)</span><br><span class="line">workbook.save(response)</span><br></pre></td></tr></table></figure><p>但是继续来读 openpyxl 的源码，发现在调用 save_workbook 函数时，创建了一个 ExcelWriter 对象，并调用了它的 save 方法。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">save</span>(<span class="params">self, filename</span>):</span><br><span class="line"><span class="string">&quot;&quot;&quot;Write data into the archive.&quot;&quot;&quot;</span></span><br><span class="line">self.write_data()</span><br><span class="line">self._archive.close()</span><br></pre></td></tr></table></figure><p>在 save 方法的最后，调用了我们传入的 HttpResponse 对象的 close 方法，而查阅 Django 源码发现在 HttpResponse 的基类 HttpResponseBase 的 close 方法处注释着，该方法应该由 WSGI 服务器在处理完请求以后调用。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># The WSGI server must call this method upon completion of the request.</span></span><br><span class="line"><span class="comment"># See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">close</span>(<span class="params">self</span>):</span><br><span class="line"><span class="keyword">for</span> closable <span class="keyword">in</span> self._closable_objects:</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">closable.close()</span><br><span class="line"><span class="keyword">except</span> Exception:</span><br><span class="line"><span class="keyword">pass</span></span><br><span class="line">self.closed = <span class="literal">True</span></span><br><span class="line">signals.request_finished.send(sender=self._handler_class)</span><br></pre></td></tr></table></figure><p>所以为了优雅，我们还是把调用 close 方法的工作交还给 WSGI 服务器来做吧。</p><h3 id="最终的解决方案"><a href="#最终的解决方案" class="headerlink" title="最终的解决方案"></a>最终的解决方案</h3><p>我们来自己实现一个 save_workbook 函数，用于将 openpyxl 的 Workbook 生成 Excel 文档，并写入到 Django 的 HttpResponse 对象响应体中。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> openpyxl.writer.excel <span class="keyword">import</span> ExcelWriter</span><br><span class="line"><span class="keyword">from</span> zipfile <span class="keyword">import</span> ZipFile, ZIP_DEFLATED</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">save_workbook_response</span>(<span class="params">workbook, response</span>):</span><br><span class="line">archive = ZipFile(response, <span class="string">&#x27;w&#x27;</span>, ZIP_DEFLATED, allowZip64=<span class="literal">True</span>)</span><br><span class="line">writer = ExcelWriter(workbook, archive)</span><br><span class="line">writer.write_data()</span><br></pre></td></tr></table></figure><p>这样，我们只需要在创建 HttpResponse 对象后，调用我们自己实现的 save_workbook_response 函数，将 openpyxl 的 Workbook 对象和 HttpResponse 对象一起传入，就可以生成 Excel 文档并写入到响应体中了。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> django.http <span class="keyword">import</span> HttpResponse</span><br><span class="line"><span class="keyword">from</span> openpyxl <span class="keyword">import</span> Workbook</span><br><span class="line"></span><br><span class="line">workbook = Workbook()</span><br><span class="line"></span><br><span class="line"><span class="comment"># ......</span></span><br><span class="line"></span><br><span class="line">response = HttpResponse(content_type=<span class="string">&#x27;application/vnd.openxmlformats-officedocument.spreadsheetml.sheet&#x27;</span>)</span><br><span class="line">save_workbook_response(workbook, response)</span><br></pre></td></tr></table></figure>]]></content>
    
    
    <summary type="html">&lt;p&gt;现在我需要提供一个链接，访问时通过 openpyxl 动态生成一个 Excel 文档，并作为响应体返回。&lt;/p&gt;
&lt;p&gt;在这里将动态生成的 Excel 文档保存到服务器，并重定向到静态文件链接显然不是一个合适的做法。&lt;/p&gt;
&lt;p&gt;所以我们需要直接将 openpyx 生成的 Excel 文档写入到 Django 的 HttpResponse 对象响应体中。&lt;/p&gt;
&lt;p&gt;查阅 openpyxl 的文档以后，并没有找到有用的信息，所以直接从源码入手。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在这篇文章中，我将把源码分析的过程详细的展现出来，并总结出完美的解决方案。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="Django" scheme="https://blog.dennic365.com/categories/Django/"/>
    
    
    <category term="Django" scheme="https://blog.dennic365.com/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>微信小程序 canvas 动画踩坑——requestAnimationFrame 帧渲染</title>
    <link href="https://blog.dennic365.com/2017/10/28/mini-program-canvas-render/"/>
    <id>https://blog.dennic365.com/2017/10/28/mini-program-canvas-render/</id>
    <published>2017-10-27T18:53:02.000Z</published>
    <updated>2022-02-24T16:55:07.000Z</updated>
    
    <content type="html"><![CDATA[<p>在浏览器上绘制 canvas 动画的时候经常使用 window.requestAnimationFrame 这个 Api 的回调来进行循环的帧渲染。</p><p>使用 requestAnimationFrame 将会把动画帧渲染周期交给浏览器统一调度，实现更好的性能优化。</p><p>可是微信小程序并没有支持这个 Api，在模拟器上测试可以正常运行，可在真机上运行时会报错，没有这个方法。</p><p>所以我们只得想办法自己来模拟这个 Api 所做的工作。</p><p>requestAnimationFrame 能够将原本零散的帧渲染序列进行梳理，使得页面上所有动画帧都在同一周期进行渲染，以最大化利用系统资源。</p><p>一般浏览器的渲染周期为 60 次每秒，即每次间隔大约 16 毫秒。因此我们只需要写一段代码，来将渲染周期控制在 16 毫秒，就能够模拟出 requestAnimationFrame 的效果了。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> lastFrameTime = <span class="number">0</span>;</span><br><span class="line"><span class="comment">// 模拟 requestAnimationFrame</span></span><br><span class="line"><span class="keyword">var</span> doAnimationFrame = <span class="keyword">function</span> (<span class="params">callback</span>) &#123;</span><br><span class="line"><span class="keyword">var</span> currTime = <span class="keyword">new</span> <span class="title class_">Date</span>().<span class="title function_">getTime</span>();</span><br><span class="line"><span class="keyword">var</span> timeToCall = <span class="title class_">Math</span>.<span class="title function_">max</span>(<span class="number">0</span>, <span class="number">16</span> - (currTime - lastFrameTime));</span><br><span class="line"><span class="keyword">var</span> id = <span class="built_in">setTimeout</span>(<span class="keyword">function</span> (<span class="params"></span>) &#123; <span class="title function_">callback</span>(currTime + timeToCall); &#125;, timeToCall);</span><br><span class="line">lastFrameTime = currTime + timeToCall;</span><br><span class="line"><span class="keyword">return</span> id;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="comment">// 模拟 cancelAnimationFrame</span></span><br><span class="line"><span class="keyword">var</span> abortAnimationFrame = <span class="keyword">function</span> (<span class="params">id</span>) &#123;</span><br><span class="line"><span class="built_in">clearTimeout</span>(id)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>现在就可以像使用 requestAnimationFrame 和 cancelAnimationFrame 一样使用 doAnimationFrame 和 abortAnimationFrame 了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;在浏览器上绘制 canvas 动画的时候经常使用 window.requestAnimationFrame 这个 Api 的回调来进行循环的帧渲染。&lt;/p&gt;
&lt;p&gt;使用 requestAnimationFrame 将会把动画帧渲染周期交给浏览器统一调度，实现更好的性能优化。</summary>
      
    
    
    
    
    <category term="微信小程序" scheme="https://blog.dennic365.com/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
    
  </entry>
  
  <entry>
    <title>Django 返回流文件时使用中文文件名的问题</title>
    <link href="https://blog.dennic365.com/2017/10/28/django-response-filename-chinese/"/>
    <id>https://blog.dennic365.com/2017/10/28/django-response-filename-chinese/</id>
    <published>2017-10-27T17:07:00.000Z</published>
    <updated>2022-02-23T19:21:09.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近有一个需求，需要根据请求参数，从数据库获取指定数据动态生成 Excel 表格文件，并以流的形式返回。</p><p>在测试的过程中发现了一个问题，无论我怎么在响应头中添加 Content-Disposition 指定文件名，在 Chrome 浏览器中访问的时候返回的文件名始终是按 url 的最后一段自动生成的文件名，而将文件名改成英文以后又是正常的。</p><p>猜测应该是字符串编码的问题，http 响应头中字符应该按照 url 百分号编码。</p><p>修改为以下代码后，终于能够正常显示中文文件名了。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Python3</span></span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> quote</span><br><span class="line">...</span><br><span class="line">response[<span class="string">&#x27;Content-Disposition&#x27;</span>] = <span class="string">&#x27;attachment; filename=&#123;0&#125;.xlsx&#x27;</span>.<span class="built_in">format</span>(quote(filename))</span><br></pre></td></tr></table></figure><p>Python2 中的 quote 方法引入位置有所不同，代码如下：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Python2</span></span><br><span class="line"><span class="keyword">from</span> urllib <span class="keyword">import</span> quote</span><br><span class="line">...</span><br><span class="line"><span class="comment"># 向下兼容 Python2.6 以前的格式化字符串</span></span><br><span class="line">response[<span class="string">&#x27;Content-Disposition&#x27;</span>] = <span class="string">&#x27;attachment; filename=%s.xlsx&#x27;</span> % quote(filename)</span><br></pre></td></tr></table></figure>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;最近有一个需求，需要根据请求参数，从数据库获取指定数据动态生成 Excel 表格文件，并以流的形式返回。&lt;/p&gt;
&lt;p&gt;在测试的过程中发现了一个问题，无论我怎么在响应头中添加 Content-Disposition 指定文件名，在 Chrome 浏览器中访问的时候返回的文件</summary>
      
    
    
    
    <category term="Django" scheme="https://blog.dennic365.com/categories/Django/"/>
    
    
    <category term="Django" scheme="https://blog.dennic365.com/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>微信小程序 Canvas 动画踩坑——棘手的椭圆</title>
    <link href="https://blog.dennic365.com/2017/10/26/mini-program-canvas-ellipse/"/>
    <id>https://blog.dennic365.com/2017/10/26/mini-program-canvas-ellipse/</id>
    <published>2017-10-25T18:19:02.000Z</published>
    <updated>2024-02-25T09:23:45.732Z</updated>
    
    <content type="html"><![CDATA[<p>最近正在给我的微信小程序开发一个天气预报的页面，页面的效果如下：</p><img src="/2017/10/26/mini-program-canvas-ellipse/result-2017-10-25-23-47-43.jpg" class="" title="最终效果"><p>计划页面有两个 canvas，一个用来实时绘制上半部分的天气动画，一个用来绘制中间的气温趋势折线图。</p><p>上半部分天气动画在不同的天气下有不同的效果，比如不同强度的降雨会有相应的雨滴从上面落下，阴天多云的时候顶部会有云朵在漂浮，多云转晴的话后面再加上个太阳。</p><p>降雨的动画不难，无非就是随机生成一些雨滴对象，每一帧实时更新位置即可。</p><p>关键在云朵的动画上，我们需要生成随机宽高的椭圆形云朵，并且云朵沿着随机椭圆轨迹运动。</p><span id="more"></span><p>根据高中数学学习的关于椭圆方程的知识，我们知道，中心点位于点 (h,k) 的椭圆方程为：</p><img src="/2017/10/26/mini-program-canvas-ellipse/20171026002201.jpg" class="" title="椭圆标准公式"><p>a 为半长轴，b 为半短轴。在这里可以简单理解成 a 为椭圆宽的 1&#x2F;2，b 为椭圆高的 1&#x2F;2。</p><p>可是仅通过这个方程我们还没办法写出动画算法，因为无法计算出 x,y 在任意时刻的值。</p><p>还好，维基百科给了我们另一个公式：</p><img src="/2017/10/26/mini-program-canvas-ellipse/20171026002259.jpg" class="" title="椭圆参数公式"><p>这是椭圆的参数方程，其中 t 为椭圆中心点到椭圆上任意一点所确定的直线与x<br>轴的夹角，可以通过三角函数计算出在任意角度时点 (x,y) 的坐标。</p><h3 id="有了这个公式就好办了，代码走起！"><a href="#有了这个公式就好办了，代码走起！" class="headerlink" title="有了这个公式就好办了，代码走起！"></a>有了这个公式就好办了，代码走起！</h3><p>我们先封装一个生成指定范围内随机数的方法，方便后面使用。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">random</span>(<span class="params">min, max</span>) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="title class_">Math</span>.<span class="title function_">random</span>() * (max - min) + min;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>好了，现在我们来写一个云的类，就叫做 Cloud。</p><p>按照我们的设想，云应该是一个绕椭圆轨迹运动的椭圆。所以我们需要给 Cloud 对象传入一个运动轨迹中点，然后让 Cloud 对象在初始化的时候随机生成一些变量，然后在每一帧的时候更新自己的位置并且绘制到 canvas 上。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">Cloud</span>(<span class="params"></span>) &#123; &#125;</span><br><span class="line"><span class="title class_">Cloud</span>.<span class="property"><span class="keyword">prototype</span></span> = &#123;</span><br><span class="line"><span class="attr">init</span>: <span class="keyword">function</span> (<span class="params">x, y</span>) &#123;</span><br><span class="line"><span class="variable language_">this</span>.<span class="property">x</span> = x <span class="comment">// 椭圆运动轨迹中点 x</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">y</span> = y <span class="comment">// 椭圆运动轨迹中点 y</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">a</span> = <span class="title function_">random</span>(<span class="number">20</span>, <span class="number">25</span>) <span class="comment">// 椭圆运动轨迹 a</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">b</span> = <span class="title function_">random</span>(<span class="number">5</span>, <span class="number">15</span>) <span class="comment">// 椭圆运动轨迹 b</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">h</span> = <span class="title function_">random</span>(<span class="number">60</span>, <span class="number">80</span>) <span class="comment">// 云的高</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">w</span> = <span class="title function_">random</span>(<span class="number">90</span>, <span class="number">120</span>) <span class="comment">// 云的宽</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">color</span> = <span class="string">&#x27;rgba(255,255,255,0.3)&#x27;</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">t</span> = <span class="title function_">random</span>(<span class="number">0</span>,<span class="title class_">Math</span>.<span class="property">PI</span> * <span class="number">2</span>) <span class="comment">// 运动起始角度</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">v</span> = <span class="title function_">random</span>(<span class="number">0.1</span>, <span class="number">0.5</span>) <span class="comment">// 运动角速度</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">d</span> = <span class="title function_">random</span>(<span class="number">0</span>,<span class="number">2</span>) &gt; <span class="number">1</span> ? <span class="literal">true</span> : <span class="literal">false</span> <span class="comment">// 运动方向</span></span><br><span class="line">&#125;,</span><br><span class="line"><span class="attr">draw</span>: <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line">ctx.<span class="title function_">setFillStyle</span>(<span class="variable language_">this</span>.<span class="property">color</span>)</span><br><span class="line"><span class="comment">// 绘制椭圆</span></span><br><span class="line"><span class="title function_">fillEllipse</span>(ctx, <span class="variable language_">this</span>.<span class="property">x</span> + <span class="variable language_">this</span>.<span class="property">a</span> * <span class="title class_">Math</span>.<span class="title function_">cos</span>(<span class="variable language_">this</span>.<span class="property">t</span>), <span class="variable language_">this</span>.<span class="property">y</span> + <span class="variable language_">this</span>.<span class="property">b</span> * <span class="title class_">Math</span>.<span class="title function_">sin</span>(<span class="variable language_">this</span>.<span class="property">t</span>), <span class="variable language_">this</span>.<span class="property">w</span> / <span class="number">2</span>, <span class="variable language_">this</span>.<span class="property">h</span> / <span class="number">2</span>)</span><br><span class="line"><span class="variable language_">this</span>.<span class="title function_">update</span>() <span class="comment">// 更新下一帧的位置</span></span><br><span class="line">&#125;,</span><br><span class="line"><span class="attr">update</span>: <span class="keyword">function</span> (<span class="params"></span>) &#123;</span><br><span class="line"><span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">d</span>) &#123;</span><br><span class="line"><span class="variable language_">this</span>.<span class="property">t</span> = <span class="variable language_">this</span>.<span class="property">t</span> &lt; <span class="title class_">Math</span>.<span class="property">PI</span> * <span class="number">2</span> ? <span class="variable language_">this</span>.<span class="property">t</span> += <span class="variable language_">this</span>.<span class="property">v</span> : <span class="number">0</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="variable language_">this</span>.<span class="property">t</span> = <span class="variable language_">this</span>.<span class="property">t</span> &gt; <span class="number">0</span> ? <span class="variable language_">this</span>.<span class="property">t</span> -= <span class="variable language_">this</span>.<span class="property">v</span> : <span class="title class_">Math</span>.<span class="property">PI</span> * <span class="number">2</span></span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码很简单，我们给了 Cloud 类三个方法。</p><p>在实例化 Cloud 对象的时候，调用 init 方法，传入云朵所围绕运动的椭圆轨迹中点坐标，并且随机生成一系列的随机变量。</p><p>在每一帧的时候调用 Cloud 对象的 draw 方法，通过角度 t 计算出新的绘制坐标，并在 canvas 上绘制一个云朵。</p><p>然后 Cloud 对象内部调用自己的 update 方法，根据自己的角速度变量计算出下一帧的角度 t。</p><h3 id="接下来开始绘制椭圆"><a href="#接下来开始绘制椭圆" class="headerlink" title="接下来开始绘制椭圆"></a>接下来开始绘制椭圆</h3><p>在 draw 方法内，我们调用了一个名叫 fillEllipse 的方法，按照设想，我们可以传入椭圆的中点坐标、a 和 b 来绘制出一个椭圆。<br>现在我们来用代码实现这个方法。</p><p>这里我首先想到的是利用之前的椭圆参数方程，沿着椭圆绘制出一条路径，然后填充路径即可。（后来发现这是一个坑，详见后文）</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">fillEllipse</span>(<span class="params">ctx, x, y, a, b</span>) &#123;</span><br><span class="line"><span class="keyword">let</span> dt = <span class="number">1</span> / <span class="title class_">Math</span>.<span class="title function_">max</span>(a, b) <span class="comment">// 绘制时的角度增量 Δt</span></span><br><span class="line">ctx.<span class="title function_">beginPath</span>()</span><br><span class="line">ctx.<span class="title function_">moveTo</span>(x + a, y) <span class="comment">// 移动到起始点</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> t = <span class="number">0</span>; t &lt; <span class="title class_">Math</span>.<span class="property">PI</span> * <span class="number">2</span>; t += dt) &#123;</span><br><span class="line"><span class="comment">// x=h+a*cos(t)</span></span><br><span class="line"><span class="comment">// y=k+b*sin(t)</span></span><br><span class="line">ctx.<span class="title function_">lineTo</span>(x + a * <span class="title class_">Math</span>.<span class="title function_">cos</span>(t), y + b * <span class="title class_">Math</span>.<span class="title function_">sin</span>(t))</span><br><span class="line">&#125;</span><br><span class="line">ctx.<span class="title function_">closePath</span>()</span><br><span class="line">ctx.<span class="title function_">fill</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码很简单，先粗略的计算出一个合适的角度增量 Δt，然后循环计算角度 t 从 0 增加到 2π 过程中的每一个点的坐标，绘制出一条路径，然后填充路径。</p><p>效果如下：</p><img src="/2017/10/26/mini-program-canvas-ellipse/2017_10_25_23_56_47_1.gif" class="" title="云朵效果动图"><p>这时遇到了问题，在微信开发工具上模拟的时候效果很不错，可是当放到真机上测试的时候发现，为什么云的运动速度变得异常的缓慢。</p><p>根据我们之前的绘制原理，每一次的增量移动是在每一帧的时候完成的，而不是根据时间增量所计算的。所以移动的速度会直接和帧率挂钩，当帧率大的时候，位置增量更新的就更快，反之就会更慢。</p><p>我在绘制的时候是采用的每绘制一帧间隔 16 毫秒，即约 60 帧每秒。<br>为了查看实际的帧率，我在每一帧的绘制方法里面加上了这样一句：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> now = <span class="keyword">new</span> <span class="title class_">Date</span>()</span><br><span class="line"><span class="keyword">let</span> fps = <span class="title class_">Math</span>.<span class="title function_">round</span>(<span class="number">1000</span> / (now - last)) <span class="comment">// 帧率 FPS</span></span><br><span class="line">last = now</span><br></pre></td></tr></table></figure><p>这样通过每一帧绘制的实际间隔时间，计算出当前的帧率，然后把这个数字绘制在 canvas 上。（高频率的输出不应使用 console.log()，会非常影响性能。）</p><p>结果如下：</p><img src="/2017/10/26/mini-program-canvas-ellipse/Screenshot_20171025-210113.jpg" class="" title="实际帧率1"><p>可以看到，实际的帧率在 20-30 帧每秒之间浮动，远远低于设定的 60 帧每秒。<br>CPU 使用率和内存占用也是出乎意料的非常高。</p><p>查看前面的代码，不难发现，问题一定是出在了绘制椭圆的那个方法上。</p><p>每秒 60 次循环计算出椭圆上各个点的位置并绘制路径，计算量非常之大，并且大量的路径数据添加到上下文中，导致内存占用也开始飙升。</p><p><strong>显然，这个方法一定是不可行的！</strong></p><h3 id="开始填坑…"><a href="#开始填坑…" class="headerlink" title="开始填坑…"></a>开始填坑…</h3><p>查阅小程序的开发文档以后，其中一个 canvas 的 Api 让我看到了希望。</p><blockquote><p><code>canvasContext.scale(scaleWidth, scaleHeight)</code><br>定义：在调用scale方法后，之后创建的路径其横纵坐标会被缩放。多次调用scale，倍数会相乘。<br>参数：scaleWidth 和 scaleHeight 分别为横纵坐标缩放的倍率。</p></blockquote><p>我们只需要计算出椭圆在横纵坐标上相较于正圆的缩放倍率，调用 scale 方法后，再绘制一个正圆，不就可以画出需要的椭圆形状了吗。</p><p>而绘制正圆的方法非常简单，用 arc 方法直接绘制一个完整的圆形路径即可。</p><p>代码如下：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">fillEllipseByScale</span>(<span class="params">ctx, x, y, a, b</span>) &#123;</span><br><span class="line"><span class="keyword">let</span> r = <span class="title class_">Math</span>.<span class="title function_">max</span>(a,b) <span class="comment">// 取 a,b 中的大者作为圆形的半径</span></span><br><span class="line"><span class="keyword">let</span> scaleX = a / r <span class="comment">// 横坐标缩放倍率</span></span><br><span class="line"><span class="keyword">let</span> scaleY = b / r <span class="comment">// 纵坐标缩放倍率</span></span><br><span class="line">ctx.<span class="title function_">scale</span>(scaleX, scaleY) <span class="comment">// 设置缩放</span></span><br><span class="line">ctx.<span class="title function_">beginPath</span>()</span><br><span class="line">ctx.<span class="title function_">arc</span>(x, y, r, <span class="number">0</span>, <span class="number">2</span> * <span class="title class_">Math</span>.<span class="property">PI</span>) <span class="comment">// 绘制圆形</span></span><br><span class="line">ctx.<span class="title function_">closePath</span>()</span><br><span class="line">ctx.<span class="title function_">scale</span>(<span class="number">1</span> / scaleX, <span class="number">1</span> / scaleY) <span class="comment">// 缩放回正常倍率</span></span><br><span class="line">ctx.<span class="title function_">fill</span>()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样每次绘制椭圆只会向 canvas 上下文中添加少量的动作，计算量也大大降低。</p><p>我们再用同一台手机运行，测试一下实际帧率。</p><img src="/2017/10/26/mini-program-canvas-ellipse/Screenshot_20171025-210154.jpg" class="" title="实际帧率2"><p>可以看到，这次帧率稳定在了 63 帧，与我们所设置的帧绘制间隔一致了，CPU 使用率和内存占用也降低到了之前的一半，性能有了很大的提升。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近正在给我的微信小程序开发一个天气预报的页面，页面的效果如下：&lt;/p&gt;
&lt;img src=&quot;/2017/10/26/mini-program-canvas-ellipse/result-2017-10-25-23-47-43.jpg&quot; class=&quot;&quot; title=&quot;最终效果&quot;&gt;

&lt;p&gt;计划页面有两个 canvas，一个用来实时绘制上半部分的天气动画，一个用来绘制中间的气温趋势折线图。&lt;/p&gt;
&lt;p&gt;上半部分天气动画在不同的天气下有不同的效果，比如不同强度的降雨会有相应的雨滴从上面落下，阴天多云的时候顶部会有云朵在漂浮，多云转晴的话后面再加上个太阳。&lt;/p&gt;
&lt;p&gt;降雨的动画不难，无非就是随机生成一些雨滴对象，每一帧实时更新位置即可。&lt;/p&gt;
&lt;p&gt;关键在云朵的动画上，我们需要生成随机宽高的椭圆形云朵，并且云朵沿着随机椭圆轨迹运动。&lt;/p&gt;</summary>
    
    
    
    
    <category term="微信小程序" scheme="https://blog.dennic365.com/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/"/>
    
  </entry>
  
  <entry>
    <title>Shell 脚本控制 uWSGI 服务器的启动、停止</title>
    <link href="https://blog.dennic365.com/2017/10/21/uwsgi-manage-shell/"/>
    <id>https://blog.dennic365.com/2017/10/21/uwsgi-manage-shell/</id>
    <published>2017-10-21T07:59:47.000Z</published>
    <updated>2024-02-25T09:29:14.591Z</updated>
    
    <content type="html"><![CDATA[<p>现在有一个 Django 应用，通过 Nginx 接受外网请求后使用 uwsgi 协议转发至内网 uWSGI 服务器，uWSGI 服务器再和 Django 应用进行通信。</p><p>为了方便管理，我决定写一个 Shell 脚本来控制 uWSGI 服务器的启动和停止。</p><p>这样以后项目中如果需要同时启动别的进程，也可以通过简单修改 Shell 脚本来实现对整个项目的一键控制。</p><p>首先把 uWSGI 服务器的各项选项参数写入到 ini 文件，并设置守护进程。</p><p>然后编写 Shell 脚本如下（dino 为项目简称）：</p><span id="more"></span><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line">usage=<span class="string">&quot;Usages: dino [start|stop|restart|status]&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> [ ! -n <span class="string">&quot;<span class="variable">$1</span>&quot;</span> ]</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$usage</span>&quot;</span>;</span><br><span class="line">        <span class="built_in">exit</span> 0</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">process=<span class="string">&quot;dino_uwsgi&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">run_start_cmd</span></span>()&#123;</span><br><span class="line">        `/usr/bin/uwsgi --ini /home/dennic/dino_uwsgi.ini`</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">is_exist</span></span>()&#123;</span><br><span class="line">        <span class="keyword">if</span> [ $(ps -ef | grep -c <span class="variable">$&#123;process&#125;</span>) -gt 1 ]</span><br><span class="line">        <span class="keyword">then</span></span><br><span class="line">                <span class="built_in">return</span> 1</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">                <span class="built_in">return</span> 0</span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">is_running</span></span>()&#123;</span><br><span class="line">        is_exist</span><br><span class="line">        <span class="keyword">if</span> [ $? == 1 ]</span><br><span class="line">        <span class="keyword">then</span></span><br><span class="line">                <span class="built_in">echo</span> -e <span class="string">&quot;\033[32mdino uWSGI service is running!\033[0m&quot;</span>;</span><br><span class="line">                <span class="built_in">return</span> 1</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">                <span class="built_in">echo</span> -e <span class="string">&quot;\033[33mdino uWSGI service is not running!\033[0m&quot;</span></span><br><span class="line">                <span class="built_in">return</span> 0</span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">kill_all</span></span>()&#123;</span><br><span class="line">        <span class="built_in">echo</span> -e <span class="string">&quot;shutting down dino uWSGI service ...... \c&quot;</span></span><br><span class="line">        pids=$(ps x | grep <span class="variable">$&#123;process&#125;</span> | grep -v grep | awk <span class="string">&#x27;&#123;print $1&#125;&#x27;</span>)</span><br><span class="line">        <span class="keyword">for</span> pid <span class="keyword">in</span> <span class="variable">$pids</span></span><br><span class="line">        <span class="keyword">do</span></span><br><span class="line">                `<span class="built_in">kill</span> -9 <span class="variable">$pid</span>`</span><br><span class="line">        <span class="keyword">done</span></span><br><span class="line">        <span class="built_in">sleep</span> 1</span><br><span class="line">        is_exist</span><br><span class="line">        <span class="keyword">if</span> [ $? == 0 ]</span><br><span class="line">        <span class="keyword">then</span></span><br><span class="line">                <span class="built_in">echo</span> -e <span class="string">&quot;\033[32m[OK]\033[0m&quot;</span>;</span><br><span class="line">                <span class="built_in">return</span> 1</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">                <span class="built_in">echo</span> -e <span class="string">&quot;\033[31m[Fail]\033[0m&quot;</span></span><br><span class="line">                <span class="built_in">return</span> 0</span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">start_service</span></span>()&#123;</span><br><span class="line">        run_start_cmd</span><br><span class="line">        <span class="built_in">echo</span> -e <span class="string">&quot;Starting dino uWSGI service      ...... \c&quot;</span>;</span><br><span class="line">        <span class="built_in">sleep</span> 1</span><br><span class="line">        is_exist</span><br><span class="line">        <span class="keyword">if</span> [ $? == 1 ]</span><br><span class="line">        <span class="keyword">then</span></span><br><span class="line">                <span class="built_in">echo</span> -e <span class="string">&quot;\033[32m[OK]\033[0m&quot;</span>;</span><br><span class="line">                <span class="built_in">return</span> 1</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">                <span class="built_in">echo</span> -e <span class="string">&quot;\033[31m[Fail]\033[0m&quot;</span></span><br><span class="line">                <span class="built_in">return</span> 0</span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">cmd=<span class="variable">$1</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># start</span></span><br><span class="line"><span class="keyword">if</span> [ <span class="variable">$cmd</span> = start ]</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">        is_running;</span><br><span class="line">        <span class="keyword">if</span> [ $? == 0 ]</span><br><span class="line">        <span class="keyword">then</span></span><br><span class="line">                start_service</span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># status</span></span><br><span class="line"><span class="keyword">elif</span> [ <span class="variable">$cmd</span> = status ];<span class="keyword">then</span></span><br><span class="line">        is_running</span><br><span class="line"></span><br><span class="line"><span class="comment"># stop</span></span><br><span class="line"><span class="keyword">elif</span> [ <span class="variable">$cmd</span> = stop ];<span class="keyword">then</span></span><br><span class="line">        kill_all</span><br><span class="line"></span><br><span class="line"><span class="comment"># restart</span></span><br><span class="line"><span class="keyword">elif</span> [ <span class="variable">$cmd</span> = restart ];<span class="keyword">then</span></span><br><span class="line">        is_running</span><br><span class="line">        <span class="keyword">if</span> [ $? == 1 ]</span><br><span class="line">        <span class="keyword">then</span></span><br><span class="line">                kill_all</span><br><span class="line">                <span class="keyword">if</span> [ $? == 1 ]</span><br><span class="line">                <span class="keyword">then</span></span><br><span class="line">                        start_service</span><br><span class="line">                <span class="keyword">fi</span></span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">                start_service</span><br><span class="line">        <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$usage</span>&quot;</span>;</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>将 Shell 脚本保存为 dino.sh，并且创建软连接到 &#x2F;usr&#x2F;bin&#x2F;dino</p><p>尝试运行脚本：</p><img src="/2017/10/21/uwsgi-manage-shell/dino-shell.png" class="" title="dino.sh 运行测试">]]></content>
    
    
    <summary type="html">&lt;p&gt;现在有一个 Django 应用，通过 Nginx 接受外网请求后使用 uwsgi 协议转发至内网 uWSGI 服务器，uWSGI 服务器再和 Django 应用进行通信。&lt;/p&gt;
&lt;p&gt;为了方便管理，我决定写一个 Shell 脚本来控制 uWSGI 服务器的启动和停止。&lt;/p&gt;
&lt;p&gt;这样以后项目中如果需要同时启动别的进程，也可以通过简单修改 Shell 脚本来实现对整个项目的一键控制。&lt;/p&gt;
&lt;p&gt;首先把 uWSGI 服务器的各项选项参数写入到 ini 文件，并设置守护进程。&lt;/p&gt;
&lt;p&gt;然后编写 Shell 脚本如下（dino 为项目简称）：&lt;/p&gt;</summary>
    
    
    
    <category term="Django" scheme="https://blog.dennic365.com/categories/Django/"/>
    
    <category term="Linux" scheme="https://blog.dennic365.com/categories/Linux/"/>
    
    
    <category term="Django" scheme="https://blog.dennic365.com/tags/Django/"/>
    
  </entry>
  
  <entry>
    <title>Linux 添加 Git 用户，开启 SSH 的 RSA 公钥认证并关闭密码认证登陆</title>
    <link href="https://blog.dennic365.com/2017/10/20/linux-add-git-user/"/>
    <id>https://blog.dennic365.com/2017/10/20/linux-add-git-user/</id>
    <published>2017-10-20T09:48:07.000Z</published>
    <updated>2022-02-23T18:11:52.000Z</updated>
    
    <content type="html"><![CDATA[<p>刚新装好的服务器，使用 CentOS 6.8 ，一堆东西需要设置。</p><p>首先是添加常用的用户，以 Git 为例：</p><p>装机自带了 Git，先 yum 更新一下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">yum update -y git</span><br></pre></td></tr></table></figure><p>接下来新建一个名为 git 的用户：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">useradd -d /home/git -m git</span><br></pre></td></tr></table></figure><p>这里的 -d 选项指定了此用户的主目录为 &#x2F;home&#x2F;git ， -m 选项表示如果目录不存在则新建目录。</p><p>然后就要开始对 git 用户进行一些安全相关的设置。</p><span id="more"></span><p>因为只需要在常用的电脑上操作远程仓库，所以开启 SSH 的 RSA 公钥认证登陆，并关闭 git 用户的密码登陆功能。</p><p>修改 &#x2F;etc&#x2F;ssh&#x2F;sshd_config 文件，这里我使用的是 vim 文本编辑器。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">HostKey /etc/ssh/ssh_host_rsa_key               <span class="comment"># 主机私钥文件位置</span></span><br><span class="line">RSAAuthentication <span class="built_in">yes</span>                           <span class="comment"># 开启 RSA 认证</span></span><br><span class="line">PubkeyAuthentication <span class="built_in">yes</span>                        <span class="comment"># 开启公钥认证</span></span><br><span class="line">AuthorizedKeysFile      .ssh/authorized_keys    <span class="comment"># 认证公钥文件位置</span></span><br></pre></td></tr></table></figure><p>找到并修改以上 4 条设置，如果被#号注释掉了，删除前面的#号。</p><p>然后再文件末尾添加：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Match User git, Group git</span><br><span class="line">        PasswordAuthentication no</span><br></pre></td></tr></table></figure><p>这样就开启了 SSH 的 RSA 公钥认证，并禁用了 git 用户的密码认证登陆功能。</p><p>认证公钥文件为用户主目录下的 .ssh&#x2F;authorized_keys 。</p><p>接下来 cd 到 git 用户的主目录，创建 .ssh 目录和 authorized_keys 文件，并设置权限。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /home/git</span><br><span class="line"><span class="built_in">mkdir</span> .ssh</span><br><span class="line"><span class="built_in">touch</span> .ssh/authorized_keys</span><br><span class="line"><span class="built_in">chmod</span> 700 -R .ssh</span><br><span class="line"><span class="built_in">chown</span> -R git:git .ssh</span><br></pre></td></tr></table></figure><p>然后编辑 &#x2F;home&#x2F;git&#x2F;.ssh&#x2F;authorized_keys 文件，将需要登陆的电脑的 RSA 公钥写入进来，每行一个。<br>并重启 ssh 服务:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/etc/init.d/sshd restart</span><br></pre></td></tr></table></figure><p>最后，<br>因为 git 用户仅用于使用 git 服务器上的管理远程仓库，所以我们应该让 git 用户的终端指向 git-shell。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">usermod -s /usr/bin/git-shell git</span><br></pre></td></tr></table></figure><p>现在就可以轻松的在自己的电脑上使用 git 管理服务器上的远程仓库了~</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;刚新装好的服务器，使用 CentOS 6.8 ，一堆东西需要设置。&lt;/p&gt;
&lt;p&gt;首先是添加常用的用户，以 Git 为例：&lt;/p&gt;
&lt;p&gt;装机自带了 Git，先 yum 更新一下：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;yum update -y git&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;接下来新建一个名为 git 的用户：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;useradd -d /home/git -m git&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;这里的 -d 选项指定了此用户的主目录为 &amp;#x2F;home&amp;#x2F;git ， -m 选项表示如果目录不存在则新建目录。&lt;/p&gt;
&lt;p&gt;然后就要开始对 git 用户进行一些安全相关的设置。&lt;/p&gt;</summary>
    
    
    
    <category term="Linux" scheme="https://blog.dennic365.com/categories/Linux/"/>
    
    
  </entry>
  
</feed>
