Search This Blog

三点定位

问题来源

在火灾报警电话时,基站一般难以确定与呼叫手机的距离,但是可以确定呼叫手机到两个临近基站的距离差值(原理待考证)。问题在于:能否根据呼叫手机到三个临近基站的两两距离差值确定呼叫手机的位置。

简化描述为:根据目标点到三个锚点间两两的差值确定空间中目标点的位置(轨迹)

数学描述

设目标点为$P$,三个锚点分别为$A,B,C$,目标点到三个锚点的距离差满足如下关系:
$$\left\{\begin{aligned}  PA-PB =& d_1\\ PB - PC=& d_2 \\ PC - PA=& -(d_1+d_2)  \end{aligned}\right.$$
注意:(1) $d_1,d_2$均可以是负数;(2) 以上三个方程实际上等价为两个方程,因为任意一个方程均可以由另外两个导出。
设$P$坐标为$(x,y,z)$,$A,B,C$坐标分别为$(a_1,b_1,c_1)$,$(a_2,b_2,c_2)$,$(a_3,b_3,c_3)$,则以上问题转化为:
$$
\left\{
\begin{aligned}
    & \sqrt{(x-a_1)^2+(y-b_1)^2+(z-c_1)^2} - \sqrt{(x-a_2)^2+(y-b_2)^2+(z-c_2)^2} = d_1 \\
    & \sqrt{(x-a_2)^2+(y-b_2)^2+(z-c_2)^2} - \sqrt{(x-a_3)^2+(y-b_3)^2+(z-c_3)^2} = d_2
\end{aligned}
\right.
$$
注意:以上方程包含三个未知数,而只有两个方程,因此该方程并不只有唯一解。

需求分析

  • 可视化描述目标点到锚点距离差值一定的轨迹方程
  • 计算出目标点的轨迹(数值解或解析解)

解决思路一

Mathematica提供了Solve函数可以用于求解以上方程,但计算结果极为冗长,几乎无法拷贝至其他软件中实现。并且在Mathematica中改变参数进行计算时计算效率也很低,同时存在“除零”的情况。该方法可以用于Demo,但几乎不具备实用性。Demo效果图如图所示。

如图所示,三种颜色的曲面表示分别表示到两锚点距离差一定的轨迹,其中蓝色曲线即为三个曲面的交线。(橙色曲线为对称的交线)

解决思路二

1. 设三锚点为$A, B, C$, 以$A, B, C$形成的平面为$XOY$平面,且以$\overrightarrow{AB}$为$x$轴正方向,$AB$的中点为原点,中垂线为$y$轴正方向;
2. 根据锚点间距离,确定参数$c$(两锚点间距离为$2c$);
3. 根据动点到两锚点间距离差, 确定参数$a$(距离差为$2a$);
4. 相应地确定参数$b = \sqrt{c^2 - a^2}$;
5. 确定两锚点方向相对于$x$轴正方向的旋转角度与平移量;
6. 根据$2 \sim 4$确定相应双叶双曲面的标准方程,然后根据$5$中确定的旋转角度与平移量确定旋转平移后的双叶双曲面方程;
7. 固定$z_0$,即选定平面$Z=z_0$,代入双曲面方程,确定双曲面与该平面的交线(为双曲线)方程;
8. 求两双曲线方程的交点坐标(将$x(y)$代入,转化为关于$y$的一元四次方程[3, 4]);
9. 利用一元四次方程的求根公式计算出求出$y$的解,带回原方程验证,确定最终解即为两双曲面在平面$Z=z_0$上的交点坐标;
10. 遍历$z_0$, 重复步骤$7 \sim 9$。

思路二中的关键步骤

坐标转换顺序

按照以上解决思路中给出的顺序,其中涉及到的坐标转换顺序如图所示。
坐标转换顺序

平面旋转变换

由前面的分析可以知道,三锚点位于$XOY$平面, 那么相应的形成的双叶双曲面的分割面均垂直于$XOY$平面。因此旋转变换仅限于平面$XOY$中,对$z$方向不涉及旋转变换。下面介绍平面旋转变换。
平面直角坐标系中左边平移与旋转变换
如图所示,给出了原始坐标系$XOY$到旋转平移后的坐标系$X'O'Y'$之间坐标的相互转换关系。
注意:从$XOY$到$X'O'Y'$需要先进行平移变换,再进行旋转变换,旋转的角度为顺时针$\alpha$,因此在图中标识为$-\alpha$;反之,从$X'O'Y'$到$XOY$坐标系需要先进行旋转变换,旋转角为逆时针$\alpha$,然后进行平移转换。

子问题

平面(空间)中到两定点距离差固定的动点轨迹为双曲线(双曲面)

证明:假设两定点间的举例为$2c$,而动点到两定点的举例差为$2a$(根据三角形两边之差小于第三边,可知:当$2a>2c$时, 是不存在的;而当$2a=2c$时,动点只可能位于两定点中的任意一个;以下的讨论均假定$2c>2a$)
为了便于讨论,假设两个定点(焦点)的坐标分别位于$(-c, 0)$和$(c, 0)$。设动点坐标为$(x, y)$, 则已知: $$\left| \sqrt{(x-c)^2 + y^2} - \sqrt{(x+c)^2 + y^2}\right| = 2a$$确定$(x, y)$的轨迹方程。
因为$\sqrt{(x-c)^2 + y^2} - \sqrt{(x+c)^2 + y^2} = \pm 2a$,移项可得
$$\sqrt{(x-c)^2 + y^2} = \pm 2a + \sqrt{(x+c)^2 + y^2}$$
两边平方可得
$$ (x-c)^2 + y^2 = 4a^2 + (x+c)^2 + y^2 \pm 4a \sqrt{(x+c)^2 + y^2}$$
化简可得
$$ -4cx - 4a^2= \pm 4a \sqrt{(x+c)^2 + y^2}$$
$$ -cx - a^2= \pm a \sqrt{(x+c)^2 + y^2}$$
两边平方
$$ (cx + a^2)^2= a^2 \left[(x+c)^2 + y^2\right]$$
化简可得
$$x^2(c^2 - a^2) - a^2 y^2 = a^2 (c^2 - a^2)$$
另$b^2 = c^2 - a^2$,可得
$$x^2 b^2 - a^2 y^2 = a^2 b^2$$
两边同时除以$a^2 b^2$,即可得到双曲线的标准方程:
$$\frac{x^2}{a^2} - \frac{y^2}{b^2} = 1$$
将以上的问题推广到空间中是类似的,假设焦点坐标位于$(-c, 0, 0)$与$(c, 0, 0)$,动点坐标为$(x, y, z)$,则动点满足如下关系:
$$\left| \sqrt{(x-c)^2 + y^2 + z^2} - \sqrt{(x+c)^2 + y^2 + z^2}\right| = 2a$$
经过与上述类似的变换(将其中的$y^2$替换为$y^2+z^2$即可)可以得到如下方程:
$$\frac{x^2}{a^2} - \frac{y^2+z^2}{b^2} = 1$$
该方程为双叶双曲线的标准方程(焦点位于$x$轴),以下给出一个示例。
双叶双曲面:$x^2-y^2-z^2=1$

平面与双叶双曲面所形成的交线为双曲线?

如图,给出一个双叶双曲面的切割demo。
Demo:双叶双曲面“切割”
图中分别给出了一个双叶双曲面和三个平面,其中:红色和绿色的平面是双叶双曲面的“渐进平面”即在$XOY$平面上的投影对应为该双叶双曲线在$XOY$平面投影(双曲线)的渐近线。而蓝色平面为切割平面,为了便于观察,绘制蓝色平面与双曲面的交线如下图所示。
双叶双曲面切割平面及其交线
从图中可以看出,蓝色交线为双曲线,下面给出证明以及给出该交线的方程。
首先这一结论是有前提条件的,考虑的“切割”平面是垂直于$XOY$平面的(对于焦点坐标在$X$轴上时,即标准方程为$\frac{x^2}{a^2} - \frac{y^2+z^2}{b^2} = 1$)
证明:切割平面在本例中垂直于$XOY$平面,平面方程可以表述为:$y = kx+m$其中“斜率$k$”的范围在上述“渐进平面”对应的“渐近线”的斜率范围内,即有:$|k| < \frac{b}{a}$
将平面方程$y=kx+m$代入双曲面标准方程$\frac{x^2}{a^2} - \frac{y^2+z^2}{b^2} = 1$,可得:
$$\left(\frac{1}{a^2} - \frac{k^2}{b^2}\right)x^2 - \frac{2km}{b^2}x + \frac{m^2}{b^2} = 1 + \frac{z^2}{b^2}$$
进一步整理可得:
$$\left(\frac{1}{a^2} - \frac{k^2}{b^2}\right)\left(x - \frac{a^2 km}{b^2 - a^2 k^2}\right)^2 - \frac{z^2}{b^2} = 1 + \frac{m^2}{b^2 - a^2 k^2}$$
注意到:$|k| < \frac{b}{a}$,故:$b^2 - a^2k^2 > 0$,从而上式中$\left(\frac{1}{a^2} - \frac{k^2}{b^2}\right)>0$,等式右侧$1 + \frac{m^2}{b^2 - a^2 k^2} > 1$,故:上式可以写为:
$$\frac{(x-m')^2}{(a')^2} - \frac{z^2}{(b')^2} = 1$$
其中:$m' = \frac{a^2 km}{b^2 - a^2 k^2}$, $a' = \sqrt{\frac{a^2 b^2}{b^2-a^2 k^2}\left(1+\frac{m^2}{b^2-a^2k^2}\right)}$,$b' = \sqrt{b^2 \left(1 + \frac{m^2}{b^2 - a^2 k^2}\right)}$
同时还需要满足$y=kx+m$。
示例:以$x^2 - y^2 - z^2 = 1$被平面$x+2y=0$切割为例,可以绘制出解如下
图中绿色曲面即为$\frac{(x-m')^2}{(a')^2} - \frac{z^2}{(b')^2} = 1$表示的曲面。

参考

为 Blogger HTML 编辑页面添加代码标签的快捷方式

动机

在blogger中写博客的时候经常需要插入代码,通过在HTML编辑模式下为代码段添加<code>...</code>标签实现,一旦插入代码较多这个过程就显得比较繁琐了;因此考虑开发一个chrome的插件实现这个功能。主要的需求描述如下:选中需要添加代码HTML标签的代码段,右键菜单中显示需要添加代码段的模式,点击后将添加了HTML标签的代码段替换选中的原代码段

Demo


设计思路

预期实现的功能:为所选的可编辑内容弹出右键菜单,根据相应的标签选择替换所选内容。
为了实现这个功能,首先要对chrome插件的架构有一定的了解。下图给出了Chrome插件的插件的一个基本框架图。(由来源作者总结,感觉很清晰)
值得注意的是:Chrome的插件是有一个独立的运行环境的,可以是popup或者background,均可以是由HTML/CSS/JavaScript编写,用于呈现插件的显示或者功能。而此外Chrome还提供了content script的方式,用于在“匹配的”网页中注入(injection)content script脚本,从而获取或者操作页面上的DOM(Document Object Model)对象。注意:处于安全性的考虑,content script只对页面上的DOM对象有操作的权限,而对页面中的JavaScript脚本或其中出现的变量等均无访问的权限。
Message机制:目前看来,content script与插件之间是相互隔离的,无法交互;而实际上Chrome还提供了一个Message的方法为content script与插件之间提供了通信的方式。例如:可以在content script中发送消息(sendMessage),而在background script中注册该消息的监听(onMessage.addListener),如此便可以实现content script通知background script的需求了;反之类似。
来源: Chrome插件(Extensions)开发攻略
插件框架:有了以上对Chrome插件架构的基本了解以后,就可以设计本插件的框架图了,如下图所示。在extension的background页面调用了contextMenus这个API将插件功能添加到右键菜单中,为“selection”类型的内容(DOM对象)创建了该插件的右键菜单,并且在创建时绑定了“onclick”方法,在其中调用了sendMessage方法,以此通知content script当前选择了需要替换的内容以及需要添加的标签类型。另一方面,在content script中,注册了onMessage方法,从而可以监听background中发送的消息。在onMessage方法中,实现了对“selection”内容的添加标签后替换的功能。
设计思路:插件框架图

实现

根据以上的设计思路,完成代码如下。(完整项目地址:CodeTag)
Chrome 插件下载地址: Code Tag
  • manifest.json
  • {
        "name": "Code Tag",
        "description": "This extension helps add html tag for editable selected context",
        "version": "0.2",
        "permissions": [
            "contextMenus"
        ],
        "content_scripts": [{
                "matches": ["<all_urls>"],
                "js": ["addTag.js"]
            }
        ],
        "background": {
            "scripts": [
                "background.js"
            ]
        },
        "icons": {
            "128": "icon.png"
        },
        "manifest_version": 2
    }
  • background.js
  • // parent menu
    var parent = chrome.contextMenus.create({
            "title": "Code Tag",
            "contexts": ["selection"]
        });
    
    // sub-menu for block style
    chrome.contextMenus.create({
        "title": "Block",
        "parentId": parent,
        "contexts": ["selection"],
        "onclick": function (info, tab) {
            if (info.editable) {
                chrome.tabs.query({
                    "active": true,
                    "currentWindow": true
                }, function (tabs) {
                    chrome.tabs.sendMessage(tabs[0].id, {
                        // codetag message, indicating block style
                        "codetag": "block"
                    });
                });
            }
        }
    });
    
    // sub-menu for inline style
    chrome.contextMenus.create({
        "title": "Inline",
        "parentId": parent,
        "contexts": ["selection"],
        "onclick": function (info, tab) {
            if (info.editable) {
                chrome.tabs.query({
                    "active": true,
                    "currentWindow": true
                }, function (tabs) {
                    chrome.tabs.sendMessage(tabs[0].id, {
                        // codetag message, indicating inline style
                        "codetag": "inline"
                    });
                });
            }
        }
    });
  • addTag.js (content script)
  • // trancode "<" and ">" in code snippet into html coding
    function transCode(text) {
        var newStr;
        newStr = text.replace(/</g, "<");
        newStr = newStr.replace(/>/g, ">");
        return newStr;
    }
    
    // register listener on message, fired when a sendMessage called in background
    // in this function, selection is recognized and replaced with tag added context
    // different tags are determined by the message sent from background
    chrome.extension.onMessage.addListener(function (message, sender, callback) {
        // get selection
        var sel = window.getSelection();
        var codeWithTag;
    
        if (message.codetag == "block") {
            codeWithTag = "<pre><code>" + transCode(sel.toString()) + "</code></pre>";
        }
        if (message.codetag == "inline") {
            codeWithTag = "<code>" + transCode(sel.toString()) + "</code>";
        }
        var elem = document.activeElement;
        var start = elem.selectionStart;
        var end = elem.selectionEnd;
        elem.value = elem.value.slice(0, start) + codeWithTag + elem.value.substr(end);
        // Set cursor after selected text
        elem.selectionStart = start + codeWithTag.length;
        elem.selectionEnd = elem.selectionStart;
    });
目前实现的功能是两个,分别可以添加<code>...</code> 或者<pre><code>...</code></pre>标签,从而实现Inline(嵌入行内)和Block(代码块)模式。(:为了实现语法高亮,可以参考之前的文章:使用 highlight.js 高亮博文中的代码

遇到的问题

1. getSelection无法获取预期的内容
起初,因为没有搞清楚Chrome插件的架构,导致踩了不少雷,其中就包括遇到getSelection方法无法获取预期内容的问题;最初设计插件的时候只考虑了background,而未添加content script,这样一来实际上是无法通过JavaScript代码访问到页面内容的。而getSelection是属于JavaScript中的方法。准确来说,在background中调用getSelection也只是在background的页面中进行操作,也就是说如果访问DOM对象,例如document,指代的是background页面,而我当时以为是当前的网页,这也就造成了getSelection无法获取预期内容的现象。

2. info.selectionText 将换行符替换为空格
接上一个问题,由于无法获取getSelection的预期内容,那么后续的操作也就无从进行下去。改变思路找到Chrome的contextMenus中的info.selectionText属性,可以获取在菜单创建时所选择的文本。看上去可以解决需求。但是当选中的文本中出现换行符时,就不能如愿了,换行符被替换为空格。这个方案再次失效。其中selectionText将换行符替换为空格的原因可能在于Chrome在菜单的显示中提供了“%s”直接转换选中文本的方式,如果保留换行符那么在菜单显示的时候就不方便了。

3. 插件的Debug窗口无法看到content script
最终找到了 Chrome插件(Extensions)开发攻略 这篇文章,对Chrome插件的架构有了重新的认识,意识到可以通过content script实现需求。与此同时也了解了Chrome的Debug工具。但是在调试的过程中又遇到了新的问题。原本是在插件的背景页启动了Debug页面,而在其中的content script部分并未发现所写的content script代码,无从调试。原因在于content script代码是注入到匹配的页面中,而不是插件的背景页,所以要调试content script需要在网页中进行审查(Inspector),启动Debug。

4. contextMenus同时满足两个条件
需求中是要对可编辑editable)的选中内容selection)进行替换,contextMenus可以根据点击发生的对象类型弹出相应的右键菜单,并且可以对不同的对象类型进行“OR”操作,但是并不支持“AND”操作,因此仅对于可编辑的选中内容右键弹出该插件菜单的操作是不能“直接”实现的。简单地变通方式可以如下:在contextMenus中只关注“selection”类型的对象,并且检查对象的“info.editable”属性进行判断,如果为真则触发sendMessage方法,否则不进行处理。

参考

Python 正则表达式

语法表

语法说明表达式实例完整匹配的字符串
字符
一般字符匹配自身abcabc
.匹配任意除换行符“\n”外的字符。
在DOTALL模式中也能匹配换行符。
a.cabc
\转义字符,使后一个字符改变原来的意思。
如果字符串中有字符*需要匹配,可以使用\*或者字符集[*]。
a\.c
a\\c
a.c
a\c
[...]字符集(字符类)。对应的位置可以是字符集中任意字符。
字符集中的字符可以逐个列出,或者给出范围,如[abc]或
[a-c]。第一个字符如果是^则表示取反,如[^abc]表示不是
abc的其他字符。
所有的特殊字符在字符集中都失去其原有的特殊含义。在字
符集中如果要使用 ]、-或^,可以在前面加上转义字符(\),
或把 ]、-放在第一个字符,把^放在非第一个字符。
a[bcd]eabe
ace
ade
预定义字符集(可以写在字符集[...]中)
\d数字:[0-9]a\dca1c
\D非数字:[^\d]a\Dcabc
\s空白字符:[<空格>\t\r\n\f\v]a\sca c
\S非空白字符:[^\s]a\Scabc
\w单词字符:[A-Za-z0-9]a\wcabc
\W非单词字符:[^\w]a\Wca c
数量词(用在字符或(...)之后)
*匹配前一个字符0或无限次。abc*ab
abccc
+匹配前一个字符1次或无限次。abc+abc
abccc
?匹配前一个字符0次或1次。abc?ab
abc
{m}匹配前一个字符m次。ab{2}cabbc
{m,n}匹配前一个字符m至n次。
m和n可以省略:若省略m,则匹配0至n次;若省略n,则匹
配m至无限次。
ab{1,2}cabc
abbc
*?, +?, ??
{m,n}?
使*, +, ?, {m,n}变成非贪婪模式abc{2,4}?abcc
边界匹配(不消耗待匹配字符串中的字母)
^匹配字符串开头。
在多行模式中匹配每一行的开头。
^abcabc
$匹配字符串末尾。
在多行模式中匹配每一行的末尾。
abc$abc
\A匹配字符串开头(对多行也仅匹配第一行开头)。\Aabcabc
\Z匹配字符串末尾(对多行也仅匹配最后一行末尾)。abc\Zabc
\b匹配\w和\W之间的字符。\bfoo\bfoo
bar foo bar
(foo)
foo2 (not matched)
\B匹配\w字符,但是不位于单词的开头或结尾。py\Bpython (matched)
py 3 (not matched)
逻辑、分组
|代表左右表达式任意匹配一个。
总是先尝试匹配左边的表达式,一旦成功则跳过匹配
右侧的表达式。如果 | 未被包括在 () 中,
则它的范围是整个正则表达式。
abc|defabc
def
(...)被括起来的表达式将作为分组,从表达式左边开始每遇到一
个分组的左括号 '(' ,编号+1.
另外,分组表达式作为一个整体,可以后接数量词。
表达式中的 | 仅在该组中有效。
(abc){2}
a(123|456)c
abcabc
a456c
(?P<name>...)分组,除了原有的编号外再额外指定一个别名(name)(?P<id>abc){2}abcabc
\<number>引用编号为<number>的分组匹配到的字符串。(\d)abc\11abc1
5abc5
(?P=name)引用别名为<name>的分组匹配到的字符串。(?P<id>\d)abc(?P=id)1abc1
5abc5
特殊构造(不作为分组,即不增加分组号)
(?:...)(...)的不分组版本(?:\d)(\d)abc\112abc2 (matched)
12abc1 (not matched)
(?#...)#后为注释内容,不形成匹配模式(?#comment)abcabc
(?=...)之后的字符串需要匹配表达式中的内容。
不消耗字符串内容。
abc(?=123)abc123 (matched)
匹配结果为 abc,
而不是 abc123。
abc12 (not matched)
(?!...)之后的字符串不是表达式中的内容,才匹配。
不消耗字符串内容。
abc(?!123)abc123 (not matched)
abc12 (matched)
匹配结果为 abc,
而不是 abc12。
(?<=...)之前的字符串需要匹配表达式中的内容。
不消耗字符串内容。
(?<=123)abc123abc (matched)
(?<!...)之前的字符串不是表达式中的内容,才匹配。
不消耗字符串内容。
(?!123)abc12abc (matched)
(?(id/name)
yes-pattern|
no-pattern)
类似C语言中的三元操作符(condition ? x : y),此处的含义
即当第id个分组或者名称为name的分组匹配到结果的时候
就采用yes-pattern,否则采用no-pattern,
其中no-pattern可以省略
(<)?(\w+@\w+(?:\.\w+)+)(?(1)>)<user@host.com>
user@host.com
---------------------------
<user@host.com
(not matched)

Python测试

Python中的正则表达式库为 re 。而常用的函数如下
  • compile
  • match
  • search
其中compile函数负责根据生成正则表达式生成相应的匹配模式,而matchsearch函数则根据匹配模式对目标字符串进行匹配。matchsearch的区别在于match是从字符串的开头匹配,相当于对匹配模式强制实施了\A,而search可以从字符串的任意位置匹配正则表达式。
import re

pattern = re.compile(r'world')
match1 = pattern.match('hello, world!')
match2 = pattern.search('hello, world!')

if match1:
    print(match1.group())
else:
    print("No match")

if match2:
    print(match2.group())
else:
    print("No match")
输出结果如下:
No match
world

Raw string

注意到上面的例子中声明pattern时使用了如下的方式 pattern = re.compile(r'world'),其中的 r'...' 表示字符串为 raw string,即不对反斜杠 (\) 做转义处理。例如:r'\n' 表示的正是两个字符 \n 的组合,而如果不声明为raw string的话 '\n' 表示的就是换行符。Raw string的最大好处在于声明正则表达式时可以简化书写。举例说明如下:
pattern1 = re.compile('\\w') # 需要第一个反斜杠对第二个反斜杠转义
pattern2 = re.compile(r'\w') # 反斜杠就是其本来的含义,因此无需两个

综合测试

结合前面的语法表,此处给出一个简单的综合测试案例。要求是从字符串中筛选出电子邮箱地址。实现的方式如下:
(<)?(\w+@\w+(?:\.\w+)+)(?(1)>)
用到的语法包括 \w, +, (?:...), (...), ?, (?(id) yes-pattern | no-pattern)
分析:我们知道邮箱的格式一般为 xxx@yyy.zzz
首先关注以上正则表达式的中间部分 (\w+@\w+(?:\.\w+)+)@前的\w+表示至少有一个字母或数字,指代了xxx部分;而@后的\w+指代了yyy部分;(?:\.\w+)+表明了.zzz模式至少有一个(并且不记入分组计数),例如:.com又或者.edu.cn
然后,关注以上表达式首末的语法,(<)? 表示左尖括号没有或者仅有一个,并且由于是第一个分组,所以分组的序号为1;而末尾的语法 (?(1)>) 是 (?(id) yes-pattern | no-pattern) 的省略写法,省略了no-pattern部分,以第一个分组的匹配结果为条件,若找到左尖括号,则相应整体的正则表达式最后会添加上右尖括号从而形成配对;否则无需指定多余的匹配模式;如此一来便可实现对以下两种形式的邮箱地址进行匹配 user@host.com 或者 <user@host.com>

测试源码

更多详细的测试案例参见下方测试代码
Python-Study/regex_study.py

参考

Git 与 GitHub (二)

正文

本篇博文主要介绍如下的功能
  • 从 GitHub 上 clone 项目
  • 使用 .gitignore 排除若干不需要跟踪的文件
第一个功能通过如下命令即可实现
git clone https://github.com/zouyu4524/Python-Study
以上语句将从 GitHub 上 clone 指定的 Repository 到本地当前路径下

第二个同样是非常实用的功能之一,例如在本地进行编译或者修改文件产生的一些临时文件或者日志文件不需要上传,又或者是一些隐私文件,例如密钥等不便于上传到 GitHub 时,可以通过添加 .gitignore 文件来排除不想上传的文件。.gitignore 文件一般具有如下的形式
# Python:
*.pyc

# IDE:
.idea/*
即只需要指明排除的文件名称即可(支持正则表达式),例如上面的例子表明排除文件后缀为 .pyc 的文件以及 .idea 目录下的所有文件。
如果不下心把不需要跟踪的文件已经提交(add)到了暂存区,或者甚至已经commit,再或者已经push到了GitHub,此时想删除该文件怎么办呢?可以通过如下的命令实现
git rm --cached .idea/vcs.xml
其中, .idea/vcs.xml 是之前误操作被跟踪的文件。通过此命令实际上完成了一个删除操作,再次查看改动时,git会给出如下的提示
D:\Github\Python-Study>git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD ..." to unstage)

        deleted:    .idea/vcs.xml
此后,再commit、push即可完成对该文件在本地及远程(GitHub)的Repository中的删除。

参考


Git 与 GitHub (一)

背景

Git是目前最优秀的分布式版本控制系统,而GitHub是一个通过Git进行版本控制的软件源代码托管服务。作为一个个人用户,需求如下
  • 将已有的本地项目上传至GitHub进行保管
本文正是介绍如何快速地实现需求。

安装与注册

首先,需要在本地安装Git客户端,以Windows为例,在Git下载Windows版本的Git客户端并默认安装即可。其次,为了使用GitHub提供的服务,需要注册一个GitHub账号。在Git客户端安装好以后以及GitHub注册以后,需要做的事情是建立本地与GitHub的安全连接(SSH),以便可以在本地通过命令行实现与GitHub的交互。具体的操作如下

设置本地Git

Git为分布式版本控制系统,所谓分布式就是你可以在任何机器上对同一份代码进行管理,那如何确定就是“你”呢?这就需要进行用户名和邮箱的设置了,在commit阶段用户名和邮箱就成为了提交者的标识。
  • 设置用户名
  • 设置邮箱
设置用户名如下
$ git config --global user.name "Mona Lisa"
设置邮箱如下
$ git config --global user.email "email@example.com"
以上命令中 --global 标识表示为全局(所有的Repository)设置用户名和邮箱。如果去除该标识,表示为当前的Repository设置用户名和邮箱。

本地生成密钥

在程序列表中找到Git Bash,打开后输入
$ ssh-keygen -t rsa -C "youremail@example.com"
其中 youremail@example.com 为GitHub的注册邮箱
然后一路回车,等待密钥生成。默认生成的密钥将会保存在 ~/.ssh 路径下,其中包括三个文件,分别是:id_rsaid_rsa.pub 其中,id_rsa为私钥,而id_rsa.pub是公钥,接下来我们需要的是id_rsa.pub

在GitHub中添加公钥

本地生成了公钥以后,在GitHub中添加相应的公钥即可实现本地与GitHub的安全连接。如下图所示:选择Setting -> SSH and GPG keys -> New SSH Key -> 添加Title描述(可任意填写) -> 复制公钥中的内容到Key选框 -> Add SSH Key

按照以上步骤生成了SSH 密钥以后就可以实现本地与GitHub的安全连接了。

快速上手

建立安全连接为将本地的项目上传至GitHub,步骤如下
  • 在GitHub上创建一个空的Repository
  • 本地项目关联至GitHub所创建的Repository
第一步,如图所示,创建Repository。New Repository -> 添加 Repository 名称 -> 添加Repository描述(可选) -> Create Repository。
主要注意的是,在创建该Repository的时候最好不要添加 .gitignore 或者 LICENSE文件,避免在导入本地项目时发生冲突。(即创建一个“干净”的空库)
创建好以后,会跳转到如下界面,GitHub为新创建的Repository提供了四种完善的方法:分别是 ① 通过GitHub桌面程序;② 通过在本地创建新的Repository,然后将其推送至该Repository; ③ 将本地以后的Repository推送至该Repository; ④ 从其他的非Git库中导入。这里,假设本地的项目此前并未创建过Repository,那么我们遵循第二个方法进行操作。
第二步,将本地项目关联至刚刚创建的Repository,方法如下:在cmd窗口中输入以下命令:
git init
git add *
git commit -m "first commit"
git remote add origin https://github.com/zouyu4524/firstRepo.git
git push -u origin master
注意到,前三句是在本地当前项目目录中创建一个本地Repository,并且将所有的文件添加到暂存区然后提交至HEAD,简而言之就是前三句话实现了将本地当前目录下的所有文件转移至了本地的Repository。
第四句的作用在于将本地Repository与GitHub上远程的Repository关联起来。其中 origin 是远程Repository的别名,而这句话的含义就是为远程Repository (https://github.com/zouyu4524/firstRepo.git)添加到 origin 中,这样一来在本地git中使用 origin 时就可以等效为远程的Repository。
第五句实现了将本地Repository推送至GitHub远程Repository。其中正是用 origin 指代了远程的Repository,后面 master 表示将本地内容推送至远程Repository的 master 分支。至此,我们的目标就实现了。

参考

LaTeX on Blogger

Blogger中插入LaTeX

与blogger中高亮代码块类似,同样是通过在html编辑模式(或编辑模板的html)下引入JavaScript脚本,实现对LaTeX公式的渲染。这里采用的是MathJax;同样的该工具的js文件也在CDN中存有备份,可以直接引用。详细的代码如下
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js">
MathJax.Hub.Config({
 extensions: ["tex2jax.js","TeX/AMSmath.js","TeX/AMSsymbols.js"],
 jax: ["input/TeX", "output/HTML-CSS"],
 tex2jax: {
     inlineMath: [ ['$','$'], ["\\(","\\)"] ],
     displayMath: [ ['$$','$$'], ["\\[","\\]"] ],
 },
 "HTML-CSS": { availableFonts: ["TeX"] }
});
</script>

用法

\[
... % LaTeX code, 居中显示 (display mode)
\]
或者
$...$ % 其中 ... 为 LaTeX code, 线性显示 (inline mode)

示例

居中显示 \[
\phi(x) = \frac{1}{\sqrt{2 \pi}} e^{-\frac{x^2}{2}} \] 或者也可以通过$...$线性显示公式 (inline mode), 效果为: $\phi(x) = \frac{1}{\sqrt{2 \pi}} e^{-\frac{x^2}{2}}$

性能

注意到,在页面打开时,基本可以看到LaTeX渲染的过程,即从原始的LaTeX表达式到公式显示。这说明该脚本加载速度较慢,具体原因和解决方案后续有待进一步查阅官方文档或Google。

参考

1. MathJax
2. How to use LaTeX on blogspot?
3. Understanding mathjax performance

谷歌博客 Blogger 搭建

前言

近来考虑记录一下平时的工作或者生活,写写博客、学习笔记。之所以选择了Blogger也完全是因为偶然的机会在谷歌的产品列表中看到了。以前试图在github通过github.io搭建自己的个人主页,但是有一定的学习成本,简单尝试以后没有获得理想的结果遂搁置了。这次看到Blogger,上手快,很容易就创建了自己的主页,并且可以把自己的名字作为三级域名,还是很有感觉的。

Blogger具有三个层面:后台管理,负责定制博客的样式、查看博客的浏览量;内容编辑,负责创建编辑博客文章;前端显示,通过创建Blogger时申请的域名可以直接访问,查看博客。上传图片、视频,支持html编辑等都很方便,将资料上载到谷歌还是值得信赖的。

当然,Blogger也有一些不太人性化的地方:不支持Markdown,不支持直接插入代码等。这对于需要经常性编辑、插入代码的人来说的确是一件头疼的事。不过这个问题也可以相应地解决。

模板定制

Blogger有多种模板可供选择,一般来说也是够用且足够好看的;但是对于某些个性化(功能性)的需求就需要稍作修改了,当然Blogger也提供了足够的自由度。
如图所示,可以对博客的页面、布局、主题背景(Template)、设置等进行个性化的修改。而其中,主题背景提供了“自定义”和“修改html”两种主要的修改方式。
其中自定义方式中包含对主题、背景、各个组件的样式、字体等进行修改。
注意,其中的“高级”下有添加css的功能,可以作为对页面样式的补充控制,下面将会介绍简单的应用。

CSS样式控制

目前所选择的样式中,会在各个小工具的又下角出现工具按钮提供快速的修改,实在有碍观瞻,考虑将其去除;可以通过修改模板的html文件实现,也可以通过添加CSS样式控制实现。此外,在博客的最下方会出现“订阅:文章(Atom)”的字样,为读者提供RSS订阅服务,但是也并不需要这个功能,因此同样考虑将其去除,同样可以通过CSS样式控制实现。一并将这两个需求通过CSS样式控制实现,具体的操作如下
其中在自定义添加CSS栏中填入的代码为
.quickedit{
    display:none;
}
.blog-feeds, .post-feeds {
    display :none;
}
其中第一部分是隐藏快速编辑的按钮(小工具按钮);第二部分是同时隐藏博文订阅和评论订阅的功能。

代码高亮

前面提到Blogger的编辑页面没有直接插入代码的选项,只能通过html的编辑模式,在其中插入代码段。有一些现成的网址提供了代码块转换为html代码段的功能,例如:hilite.me,也确实比较方便,但是问题在于生成的html代码段较为冗长,贴到blogger的html编辑页面后不太方便阅读。目前最有效简介的方法莫过于使用highlight.js了,这个工具实在是太强大方便了,具体的使用方法参考另一篇博文:使用 highligh.js 高亮博文中的代码

插入图片

最后,常常需要在博客中插入图片(截图)以示说明,而这里提供一个Windows下方便的截图工具WinSnap,截图后可进行简单的编辑,并且可以设置图片的阴影效果。
下载地址:WinSnap 4.5.9

参考

使用 highlight.js 高亮博文中的代码

Method

将以下代码嵌入到blogger的html编辑页面即可。注意到其中的风格是hybrid.css。其他风格可以参考styles directory (注意在style名称前添加min标注,例如风格名为hybrid.css则引入时的名称为hybrid.min.css)
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/hybrid.min.css" rel="stylesheet"> </link>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script> 
1. 第一行href的值为代码高亮风格文件(.css)的路径,以上给出了当前版本(9.12.0)的hybrid风格的样式文件路径。可以修改hybrid.min.css为其他风格文件名称从而获得不同的高亮风格。
2. 第二行src的值为执行页面代码高亮操作的JavaScript脚本文件路径。
3. 第三行声明了高亮脚本在页面加载时启动,从而渲染页面内的所有代码块,按照指定样式进行高亮
注意事项:以上的两个链接中最好加上https:构成完整的url,以免无法加载css风格或js文件

Usage

使用时,在需要插入代码的地方以<pre><code>...</code></pre>包含代码块即可

Testing

以下给出一段Scrapy代码,采用了hybrid.css的风格,看起来比较舒服。其他风格预览可以在demo中查看,确定风格以后可以在styles directory中查看相应的名称,并修改引入的路径名称即可获得相应的高亮风格显示效果。
import scrapy

class WeatherSpider(scrapy.Spider):
    name = "yangzhongWeather"
 
    def start_requests(self):
        url = "http://lishi.tianqi.com/yangzhong/201501.html"
        yield scrapy.Request(url, self.parse)
 
    def parse(self, response):
        weathers = iter(response.css('div.tqtongji2 ul'))
        next(weathers)
        for weather in weathers:
            yield {
                "date": weather.css('li a::text').extract_first(),
                "high_degree": weather.css('li::text').extract()[0],
                "low_degree": weather.css('li::text').extract()[1],
                "weather": weather.css('li::text').extract()[2],
            }
  
        next_page = response.css('span.tqxiangqing a::attr(href)').extract()[-1]
        datemonth = str(next_page.split("/")[-1]) 
        if datemonth != "201609.html":
            yield scrapy.Request(next_page, callback=self.parse)
 

Custom

以上介绍了博客中插入代码段的方式,但是有时需要在文中(inline)插入代码,该议题等待更新... 对于inline模式的代码,可以直接使用<code>...</code>标签,但是默认情况下仅仅只是字体不同,样式上比较单一。而一般可以为inline模式的代码加上一个灰色的背景色块会比较美观,实现的方式是添加自定义的CSS。但是需要注意的是,由于highlight.js中的代码块标签是<pre><code>...</code></pre>层叠的形式,其中也包裹了<code>标签对,如果直接对<code>标签应用CSS,那么对于代码块同样被应用了CSS,这是不希望看到的。为了解决这一问题,可以为inline模式的标签对添加一个class标注,如:<code class="inline">...</code>,与此同时在自定义CSS中添加如下的代码段:
code.inline {
    background-color: #f5f5f5!important;
    border: 1px solid #ccc!important;
    padding: 0 5px!important;
    border-radius: 3px!important;
    margin: 1px 5px;
    vertical-align: middle;
    display: inline-block;
}

Tips

为了方便起见,不必要对每一篇博文都在其html页面中添加上述的三行代码;可以自定义blogger的主题,为html模板添加上述三行代码,即可为所有的博文启用相同的代码高亮风格。具体的操作如下:在blogger的主界面左侧选择“主题背景”--> “修改html”
在打开的html文件中最下方(其他地方也适用)插入上述代码,如下:
保存。使用该样式风格时,只需要为代码块加上<pre><code>...</code></pre> 即可。

Problems

以上Tips中可能会遇到一个问题:修改了blogger Template的html后无法保存(保存以后再回到Template会发现其html文件被重置了),这样就会导致以上代码插入不成功。这个是与模板相关的,有的模板可以成功保存修改,而有的不可以。解决的办法是,搜索Template的html文件,找到如下代码段
<b:widget id='Blog1' locked='true' title='Blog Posts' type='Blog'> 
中的<true>改为<false>,如下
<b:widget id='Blog1' locked='false' title='Blog Posts' type='Blog'> 

Reference

1.highlight.js
2.Trouble saving template (edit HTML) - help!

Scrapy学习(一)

动机

近期参加阿里天池大数据竞赛 -- 大航杯“智造扬中”电力AI大赛,赛题提示中提及除已公布的用电量数据以外,可以额外使用例如天气、经济等数据。因此考虑通过爬虫的方式从天气网站上抓取历史天气数据。考虑到整体项目采用Python实现,因此爬虫也同样采用Python,而其中最为常用的即为Scrapy框架。

安装

依赖项

安装流程

1. 下载安装 VCForPython27.msi
2. 通过pip安装
pip2 install scrapy
在cmd窗口中输入以上命令,基于Python 2.x环境获取安装Scrapy框架。

第一个Scrapy程序

创建项目

在cmd窗口中定位到需要创建项目的目录,然后输入以下命令
code>scrapy startproject tutorial
将在当前目录下创建一个名为Tutorial的目录,具有如下的结构
tutorial/
    scrapy.cfg            # deploy configuration file

    tutorial/             # project's Python module, you'll import your code from here
        __init__.py

        items.py          # project items definition file

        pipelines.py      # project pipelines file

        settings.py       # project settings file

        spiders/          # a directory where you'll later put your spiders
            __init__.py

第一个Spider

注意到在tutorial目录下有一个spiders文件夹,spiders文件夹用于存放spiders,而每一个spider就是一个用户自定义的供Scrapy调用的类,这些spiders用于从关注的网址(或网址列表)中爬取相应感兴趣的数据。这些用户自定义的类必须 1) 继承自scrapy.Spider 2) 定义初始化的请求(链接)以及可选地 1) 定义如何跟踪页面上的链接 2) 如何提取所下载页面的内容。以下给出第一个spider的代码,将其保存为quotes_spider.py并存放在tutorial/spiders目录下。
import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)
注意:在类定义的首行注明了name = "quotes",作为项目中spider的唯一标识,用于后续的调用。我们来逐一对照前述的两个必要与两个可选项。首先,所定义的Spider名称为QuotesSpider并且继承自scrapy.Spider;其次,定义了start_requests方法,在其中生成了初始的Request;注意到,该Request方法请求了以此请求urls中的两个网址,并且将爬取的页面交由parse方法进行处理。对于可选项1)提到的定义跟踪其他链接的方法在这个例子中并未给出。最后,关注parse方法,实际的功能是将爬取的页面保存到本地,暂时为进行页面的解析,后续将会给出页面解析的示例。

运行spider

在了解该spider的作用以后,我们来测试运行一下,运行的方法为,在cmd窗口中输入
scrapy crawl quotes
以上命令会在当前项目中搜素名为quotes的spider,找到后调用,开始爬取并保存网页。得到的结果与下面类似
D:\Tutorial\Python\Scrapy\tutorial>scrapy crawl quotes
2017-06-06 17:42:17 [scrapy.utils.log] INFO: Scrapy 1.4.0 started (bot: tutorial)
2017-06-06 17:42:17 [scrapy.utils.log] INFO: Overridden settings: {'NEWSPIDER_MODULE': 'tutorial.spiders', 'SPIDER_MODULES': 
2017-06-06 17:42:18 [scrapy.core.engine] INFO: Spider opened
2017-06-06 17:42:19 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2017-06-06 17:42:19 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2017-06-06 17:42:20 [scrapy.core.engine] DEBUG: Crawled (404) <get http:="" quotes.toscrape.com="" robots.txt=""> (referer: None)
2017-06-06 17:42:21 [scrapy.core.engine] DEBUG: Crawled (200) <get http:="" quotes.toscrape.com=""> (referer: None)
...

提取数据

以上的例子中仅仅是保存了url列表中所有url对应的网页内容,这对于爬取而言并不足够,Scrapy还提供了强大的提取功能,可以不必直接整个将网页下载而是提取部分所关注的内容。总体而言,Scrapy提供了两种提取的方式
  • CSS
  • Xpath
为了了解如何通过这两种方式进行内容的提取,我们最好从Scrapy的Shell模式入手。可以把Shell模式作为一个调试的手段,逐条语句的测试。Ok,下面开始动手吧
在cmd窗口中输入以下语句
scrapy shell "http://quotes.toscrape.com/page/1/"
将会看到类似如下的结果
C:\Windows\system32>scrapy shell "http://quotes.toscrape.com/page/1/"
2017-06-08 16:07:24 [scrapy.utils.log] INFO: Scrapy 1.4.0 started (bot: scrapybot)
2017-06-08 16:07:24 [scrapy.utils.log] INFO: Overridden settings: {'LOGSTATS_INTERVAL': 0, 'DUPEFILTER_CLASS': 'scrapy.dupefilters.BaseDupeFilter'}
2017-06-08 16:07:25 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2017-06-08 16:07:25 [scrapy.core.engine] INFO: Spider opened
2017-06-08 16:07:26 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x044E5EF0>
[s]   item       {}
[s]   request    <GET http://quotes.toscrape.com/page/1/>
[s]   response   <200 http://quotes.toscrape.com/page/1/>
[s]   settings   <scrapy.settings.Settings object at 0x044E5FF0>
[s]   spider     <DefaultSpider 'default' at 0x47b1c10>
[s] Useful shortcuts:
[s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
[s]   fetch(req)                  Fetch a scrapy.Request and update local objects
[s]   shelp()           Shell help (print this help)
[s]   view(response)    View response in a browser
>>>
此时已经将网页内容爬取,接下来可以尝试按照指定规则提取内容,例如,获取网页的标题
>>> response.css('title')
[<Selector xpath=u'descendant-or-self::title' data=u'<title>Quotes to Scrape</title>'>]
通过调用responsecss方法,其中的参数为'title',可以获得类Python中list的数据结构,返回的是网页中所有类型为title的对象。为了获取以上对象中的文本而不是对象本身的方法如下:
>>> response.css('title::text').extract()
[u'Quotes to Scrape']
此处和上一行命令的区别在于两处,第一css方法中的参数多了::text,并且多了一个extract()方法。首先通过增加::text告诉Scrapy仅仅提取title对象的文本对象;此外通过调用extract()方法将文本对象中的内容提取出来。需要注意的是这个方法仍然是作用于整个列表的(此处因为只有一个元素),如果只想提取一个首个元素,那么可以通过以下的方法实现。
>>> response.css('title::text').extract_first()
u'Quotes to Scrape'
除了这种方法以外,还可以通过Xpath的方式进行内容提取,如下
>>> response.xpath("///title")
[<Selector xpath='///title' data=u'<title>Quotes to Scrape</title>'>]
>>> response.xpath("///title/text()")
[<Selector xpath='///title/text()' data=u'Quotes to Scrape'>]
>>> response.xpath("///title/text()").extract()
[u'Quotes to Scrape']
关于Xpath的用法在以后需要更细致的学习。

产生下一个Request

截止目前,我们可以通过爬取url列表中的网页,并提取所需内容,已经迈出了关键一步,但是如果我们想爬取很多的网页,把所有的网址列表全部添加到url列表的方法显然是不实际的,所以就引出了下一个问题,如何在运行过程中产生新的Request,从而获取其他的网页,这样就可以让爬虫真正的“爬”起来。看下面的例子
import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').extract_first(),
                'author': quote.css('small.author::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }

        next_page = response.css('li.next a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)
以上代码的关键在于以下语句
next_page = response.css('li.next a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)
注意以上代码段是在parse方法中的。首先,通过css的提取方式获取了当前页面中指向下一页的url地址;判断这个对象是否存在,如果存在的话就调用response.urljoin方法创建一个url地址,然后通过scrapy.Request方法再次生成对“下一页”这个链接的请求,与此同时将获取的内容传递给回调函数parse,如此便形成了一个迭代的调用,可以让爬虫不停的爬取下去,直到没有下一页为止。

保存爬取的结果

通过以上的示例,我们现在可以从一个网页出发,迭代式地爬取相同性质的网页中所需要的内容,最后一个问题就是如何对爬取的结果进行保存呢?Scrapy同样提供了简单的方式
scrapy crawl quotes -o quotes.json
通过在cmd窗口中调用该方法即可将爬取的结果保存到quotes.json文件中。
需要注意的是,由于历史遗留原因Scrapy在写入json文件时是追加方式写入而不是覆盖方法,所以如果多次调用该命令会破坏json的格式,因此在多次调用前记得删除原文件或者更改保存的文件名。 至此,第一个爬虫程序就较为完整的呈现出来了。 

扬中市历史天气数据的抓取

仿照以上的例子,通过对扬中市历史天气网站的结构的查看,就可以实现以下的爬虫程序。
import scrapy
class QuotesSpider(scrapy.Spider):
    name = "yangzhongWeather"
 
    def start_requests(self):
        url = "http://lishi.tianqi.com/yangzhong/201501.html"
        yield scrapy.Request(url, self.parse)
 
    def parse(self, response):
        weathers = iter(response.css('div.tqtongji2 ul'))
        next(weathers)
        for weather in weathers:
            yield {
                "date": weather.css('li a::text').extract_first(),
                "high_degree": weather.css('li::text').extract()[0],
                "low_degree": weather.css('li::text').extract()[1],
                "weather": weather.css('li::text').extract()[2],
            }
  
         next_page = response.css('span.tqxiangqing a::attr(href)').extract()[-1]
         datemonth = str(next_page.split("/")[-1]) 
         if datemonth != "201609.html":
             yield scrapy.Request(next_page, callback=self.parse)

参考资料