Search This Blog

CVX 添加自定义函数

最近考虑的一个优化问题中出现了如下的函数:
$$f(x, y)=\begin{cases}
y 2^{\frac{x}{y}}, & x > 0, y > 0\\
0, & y = 0, x \geq 0\\
+\infty, & \text{otherwise}
\end{cases}$$
首先,这个函数实际上是$2^x$的perspective function(参见Boyd 教材, P103, 3.2.6),perspective function具有保凸性质,而$2^x$为凸函数,所以$y 2^{x/y}$是凸函数。

虽然这个函数是凸函数,但是其形式并不能直接通过cvx表达式表示,因为$\frac{x}{y}$是{affine}./{affine}违背了cvx的DCP ruleset。实际上在给定的定义域内$\frac{x}{y}$是convex的,但是$y \cdot 2^{x/y}$ 为{affine}.*{convex}同样违背了DCP ruleset。总而言之,这个函数不能直接应用于cvx的语境中。

而cvx提供了增加自定义凸(凹)函数的方法,在用户手册中以 Adding new functions to the atom library 章节给出,其中提供了两种方式,一种比较直观,通过符合DCP ruleset的函数或操作符的组合所形成的函数,实际上这种方式是一种简单的封装;但在这里并不适合本函数,原因已在之前给出。另外一种方式,是把原问题转化为另一个凸(凹)问题,转换后的问题符合DCP ruleset或借助cvx提供的若干函数,诸如:rel_entr, 我理解这种方式写成的函数在新的cvx环境下就构成了cvx的嵌套。本函数可以通过第二种方式表达,如下所示:
function cvx_optval = myFoo( x, y )
cvx_begin
    variables z;
    minimize( z );
    subject to
        {x * log(2), y, z} == exponential;
cvx_end
注意到,myFoo有两个输入参数xy,这两个参数实际上都是cvx expressions,而非numerical inputs;另一方面,{x * log(2), y, z} == exponential表示的是{x * log(2), y, z}满足关系$y e^{\frac{x \ln (2)}{y}} \leq z, y>0$。因此,通过minimize z同时将结果传递给cvx_optval就可以得到这个函数的结果了。具体用法如下:
cvx_begin quiet
    variables x y
    minimize ( myFoo(x, y) )
    x >= 2; y<= 3;
cvx_end
本例中 myFoo 是作为目标函数给出,同样的,这个函数也可以出现在约束条件中,如下:
cvx_begin quiet
    variables x y
    minimize ( x + y )
    x >= 1
    myFoo(x, y) <= 10;
cvx_end
综上,我们就通过用户手册中给出的第二种方式添加了自定义的凸函数,可以用于cvx的编程了。
参考
[1]. Exponential perspective function on CVX
[2]. How to add self-defined convex function to atom library?

自定义IEEE Xplore文献列表

IEEE Xplore 数据库提供了文献列表下载服务,可以方便地导出所查询文献的相关信息,存储与csv文件中。相关信息包括:文献名称、作者、发表年份、发表刊物等共计31个标签。在上一篇博文(批量下载IEEE Xplore数据库论文)中给出了根据csv文件下载文献的方法,在此做进一步补充:自定义下载的文献列表信息,例如只选择关注其中的若干标签,如:文献名称、作者、发表年份、发表刊物、引用次数这五个标签。此外,为每个条目添加文献超链接,关联到本地已下载的文献,从而便于索引和查看。

可以通过Matlab快速实现以上两个目标,涉及的主要函数如下:
  • xlsread / xlswrite
  • actxserver
其中,xlsread xlswrite 分别读取和写入excel(csv)文件;而 axtxserver 可以创建windows的COM组件,从而操作该对象,例如:exl = axtxserver('excel.application') 可以创建一个Excel对象。以下给出通过Matlab为Excel单元格添加超链接的实现示例:
exl = actxserver('excel.application');
exlWkbk = exl.Workbooks;
exlFile = exlWkbk.Open([pwd '/' filename]);
exlSheet1 = exlFile.Sheets.Item('Sheet1');

rngObj = exlSheet1.get('Cells', row, col);
exlSheet1.Hyperlinks.Add(rngObj, 'somelink');

exlFile.Save()
exlFile.Close()
exl.Quit
exl.delete

在创建Excel对象后,可以调用Excel VBA中的方法对Excel单元格进行访问,而其中添加超链接的方式即: exlSheet1.Hyperlinks.Add(rngObj, 'somelink'); 值得注意的是以R1C1方式访问Excel单元格的方式为:rngObj = exlSheet1.get('Cells', row, col); 完整代码如下所示:
function SelectInterestTags(export, filename, savepath)
%% Select interested tags from IEEE Xplore export file
%   and generate an excel file which contains hyperlink for each entry to
%   locate the downloaded file
%
%   export: csv file downloaded from IEEE Xplore
% filename: saved excel filename
% savepath: path for downloaded pdf files
%
clc

%% initial
switch(nargin)
    case 2
        savepath = pwd;
    case 3
        % do nothing
    otherwise
        error('Wrong for number of inputs.')
end

%% load csv file
[raw_numerical, raw_text, RAW] = xlsread(export);
NameList = raw_text(3:end, 1);
YearList = raw_numerical(1:end, 2);

pat = '[\\/:*?"<>|]';
NameList = regexprep(NameList, pat, ' ');

%% disp all tags and choose interest tags
Tags = raw_text(2, :);
for k = 1 : length(Tags)
    fprintf('\t%d\t%s\n', k, Tags{k});
end

prompt = 'Please Select Your Interest Tags (-1 for all, 0 for default): ';
interestTags = input(prompt);

%% parameter check
if interestTags == -1
    interestTags = 1:length(Tags);
else
    if interestTags == 0 % default selection
        interestTags = [1, 22, 4, 6, 11, 17, 24, 2];
    end
end

disp('The following tags are selected: ')
disp(Tags(interestTags)')

%% write xls file
InterestArray = RAW([2:end], interestTags);
xlswrite(filename, InterestArray);

%% add hyperlink for each paper

exl = actxserver('excel.application');
exlWkbk = exl.Workbooks;
exlFile = exlWkbk.Open([pwd '/' filename]);
exlSheet1 = exlFile.Sheets.Item('Sheet1');

for k = 1 : length(InterestArray) - 1
    pdfFile = [savepath '/' num2str(YearList(k)) ' ' NameList{k} '.pdf'];
    if exist(pdfFile, 'file') == 2
        rngObj = exlSheet1.get('Cells', k + 1, 1);
        exlSheet1.Hyperlinks.Add(rngObj, pdfFile);
    end
end
disp([filename ' generated!'])

%% save file and close activex excel com
exlFile.Save()
exlFile.Close()
exl.Quit
exl.delete

参考


批量下载IEEE Xplore数据库论文

在文献调研初期一般需要大量下载相关论文,IEEE Xplore提供了简单的批量下载功能,一次最多10篇,不算方便;此外,短时间内连续批量下载会遭到服务器拒绝,导致一段时间内无法下载文献。因此,考虑写个小脚本下载所需查阅的文献。

好在Xplore提供了导出文献列表的功能,如下图所示:
勾选所需的文献后,点击导出即会自动下载一个csv文件,保存有文献的相关信息,如:标题、年份、作者、文献链接等,如下图所示:

而文献链接为URL地址,打开后为PDF的浏览界面,PDF文件封装于一个<iframe></iframe>标签中。
基于以上的信息,可以设计如下的脚本逻辑:
采用Matlab实现,主要用到两个网络函数:webread 和 websave (于R2014b后引入,低于此版本的Matlab可以采用urlread 和 urlwrite 替代)
  • webread(url) 输入参数为url地址,访问该地址并以字符串的形式返回html代码;
  • websave(filename, url) 将url地址内容保存到本地并命名为filename。
以上逻辑描述如下:读取csv函数,提取出文献的链接以及文献相关信息;其中链接作为webread输入参数,调用后获得html内容,字符串提取出pdf的下载地址,然后通过websave下载pdf文件;结合csv读取的文献信息为文献命名。

注意事项

  • 以上讨论基于具备IEEE Xplore访问权限的前提,一般校园网均具备;
  • 在以文献标题作为文件名保存时需要注意通配符的问题,例如:“\/:*?"<>|”这些字符是无法存在于文件名中的,所以需要考虑将这些字符替换,比如替换为空格。可以通过正则表达式实现,Matlab中regexprep可用;
  • 下载时最好设置相邻下载间的等待时间从而模拟人工操作避免被封IP,例如可以用: pause(30 * rand() + 30) 模拟随机的等待时间。

代码

function DownloadPDFfromXplore(export, skip)
%% download pdf from IEEE EXplore export file
% if termites at any exception, we can restart and skip those downloaded 
if nargin == 1
    skip = 0;
end

[raw_numerical, raw_text, ~] = xlsread(export);
UrlList = raw_text(skip+3:end, 16);
NameList = raw_text(skip+3:end, 1);
YearList = raw_numerical(skip+1:end, 2);

pat = '[\\/:*?"<>|]';
NameList = regexprep(NameList, pat, ' ');

for k = 1 : length(NameList)
   html = webread(UrlList{k});
   first = strfind(html, '<iframe src="h');
   last = strfind(html, '" frameborder=0>');
   url = html(first+13:last-1);
   disp(url)
   filename = [num2str(YearList(k)) ' ' NameList{k} '.pdf'];
   disp(filename)
   websave(filename, url);
   waitTime = 30 * rand() + 30;
   pause(waitTime);
end

Chrome加载PDF


问题描述:

Chrome在网上查阅PDF文档时经常出现如图中的情况,无法加载PDF文档(本机装有Acrobat),但是使用IE访问相同的链接可以成功加载。原因在于Adobe Acrobat/Reader使用NPAPI技术在Chrome中加载PDF文档,而Google在2015年宣布废弃对该技术的支持。

"Adobe Acrobat and Acrobat Reader run as a plug-in to display PDF files in a web browser. For Google Chrome and Mozilla Firefox, the plug-in is based on the Netscape Plug-In API (NPAPI) technology. "

"Google announced that in April 2015 NPAPI plug-in support is disabled by default in the Google Chrome web browser with an override capability for advanced users. In September 2015, NPAPI support in the Google Chrome web browser was removed entirely."


解决方案:

考虑到IE可以加载PDF文档,因此可以在Chrome中打开IE标签页,而正好Chrome中提供了这样的插件:IE Tab,安装后再安装一次其提供的exe程序即可在在Chrome中以IE内核加载网页。当遇到PDF无法加载的情况时点击“IE Tab”图标即可切换至IE内核从而正确加载PDF文档。

参考链接:

[1]. Change in support for Acrobat and Reader plug-ins in modern web browsers

Solution for Calculator: the Game

动机

最近玩到了一款手机游戏,名字叫做 Calculator: the Game,游戏玩法很简单,通过给定的操作符在给定的步数以内将原始的数字变换到目标数。起初的一些关卡较为容易,但是后续的关卡短时间内无法想到求解思路,所以产生了“暴力求解”的想法。为了快速实现需求,采用Matlab实现。

Demo


思路

基本思路是遍历求解空间,此时需要解决两个主要问题:
  • 如何不重复的遍历求解空间?
  • 如何确定一组求解策略的计算结果?
针对这两个问题,分别有如下的考虑:
  • 由于步数($m$)是已知的,每一步可以选择的操作符个数($n$)也是固定的,那么全体解空间的总数也是确定的,为:$n^m$;另一方面,对于$n$进制的$m$位数总共的可能性也同样为$n^m$,因此可以将$0 \sim n^m-1$的数一一映射到$n$进制的$m$位数。Matlab中提供了10位数到任意进制的转换函数dec2base
  • 另一方面,需要在已知策略的情况下,计算出最终结果并且和目标进行比对;为了实现这一功能,需要明确根据输入的操作符提取并实现相应的操作,这可以通过匿名函数实现。
综上,设计了两个函数,分别命名为parsersolution,其中parser负责解析输入的操作符,并且将所有的操作符保存到cell中;而solution负责遍历求解空间,找出可行解。

这款游戏提供的操作符列表如下:
% operator list:
%       '-3': minues 3
%       '+5': plus 5
%       'x2': times 2
%       '/3': divides 3
%         10: add '10' at the end of current number
%      '+/-': change sign
%       '<<': backspace
%  'reverse': reverse number, keep negetive sign ('-') if it exists
%   'mirror': 32 -> 2332
%    '5=>13': replace
%   'shift>': 231 -> 123
%   '<shift': -150 -> -501
%     '[+]2': increase each number of operators 2 (e.g. +3 -> +5)
%      'sum': 1023 -> (1+0+2+3) -> 6
%      'x^3': power
%    'inv10': 1250 -> (10-1, 10-8, 10-5, 0) -> 9850
%    'store': store current result as a number operator (ignore negetive)
%     [4, 2]: portal indicator, 4th digital will added to the 2nd digital
%             1123 -> (123 + 10) -> 133

其他

代码是一边玩一边扩展的,以为所有的操作符都是改变当前的值,直到遇到了store这个元素,其操作规则是存储当前的值,并且变为一个数字按键,但是store本身并不消耗游戏步数;这一设定着实让我费了一番脑筋。考虑到store本身不会对当前值产生影响且不消耗步数,因此只需要考虑每一步操作之前是否需要执行store即可。而对于每一步之前都有“执行”或“不执行”两种操作,因此对于每一组策略,共可以衍生出$2^m$种可能。(注:其中包含重复或无效情况,后续分析)可以类比遍历求解空间的方法,为store添加一个m位的mask从而标记每一步之前是否需要store

代码

项目代码参见:CalculatorSolution

三点定位

问题来源

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

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

数学描述

设目标点为$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!