‘ ; ’
分隔符
每个命令之间用 ;
隔开,各命令的执行给果,不会影响其它命令的执行。换句话说,各个命令都会执行,但不保证每个命令都执行成功。
‘ && ’
分隔符 每个命令之间用 &&
隔开,若前面的命令执行成功,才会去执行后面的命令。这样可以保证所有的命令执行完毕后,执行过程都是成功的。
‘ || ’
分隔符 每个命令之间用 ||
隔开,||
是或的意思,只有前面的命令执行失败后才去执行下一条命令,直到执行成功一条命令为止。
‘ ; ’
分隔符
每个命令之间用 ;
隔开,各命令的执行给果,不会影响其它命令的执行。换句话说,各个命令都会执行,但不保证每个命令都执行成功。
在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”(Divide and Conquer),就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1< k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
分治法所能解决的问题一般具有以下几个特征:
该问题的规模缩小到一定的程度就可以容易地解决。绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。这条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用。
分治法在每一层递归上都有三个步骤:
step1 分解
:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决
:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
step3 合并
:将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,...,yk) △ 合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
$T\left ( n \right )= kT\left ( n/m \right )+f\left ( n \right )$
通过迭代法求得方程的解:
递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当 mi≤n< mi+1 时,T(mi)≤T(n)< T(mi+1)。
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。以二路归并为例:
代码示例:
|
|
实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
1、一定是先找到最小问题规模时的求解方法;
2、然后考虑随着问题规模增大时的求解方法;
3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
auther: 红脸书生
]]>在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”(Divide and Conquer),就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直]]>
根据自己的系统下载相应的版本。解压或安装后文件夹名字太长,这里将“mysql-connector-c++-noinstall-1.0.5-win32”改为“mysql”。
VS2008 以上的版本都适用
下面是在 VS2010 版本下的配置过程。
3. 菜单 Project ->property ->linker ->input ->additional include directories 添加下面三项。
mysqlcppconn.lib;
mysqlcppconn-static.lib;
libmysql.lib;
4. 将mysql\lib下的mysqlcppconn.dll文件和 \$MySQL\bin\libmysql.dll 复制到Windows\system32 文件夹底下。
这样就配置好了。
这里简单建立一个连接测试数据库 contest
。
|
|
头文件定义(系统默认生成):
|
|
函数定义:
|
|
有时候需要操作非本地其它 PC 上的 MySQL 数据库,在连接前需要对访问的计算机进行配置,添加访问权限。这里提供两种方法:
|
|
这样应该可以进入MySQL服务器,代码如下:
|
|
|
|
|
|
|
|
这样就可以在其它任何的主机上以root身份登录啦!
|
|
3. 在mysql控制台执行命令中的 ‘root’@’%’ 可以这样理解: root是用户名,%是主机名或IP地址,这里的%代表任意主机或IP地址,你也可替换成任意其它用户名或指定唯一的IP地址;’yourPassword’是给授权用户指定的登录数据库的密码;另外需要说明一点的是我这里的都是授权所有权限,可以指定部分权限,GRANT具体操作详情见:http://dev.mysql.com/doc/refman/5.1/en/grant.html。
4. 不放心的话可以 在mysql 控制台执行下面命令检查一下用户表里的内容。
|
|
设置好后通过 TCP 远程计算机的IP 即可。
|
|
数据库操作如上节所述。
Text 文本文档文件,扩展名 .txt
,要注意其编码方式。
Excel 电子表格格式,扩展名 .xls
或 .xlsx
。
日常的文本、数据存储和处理文件。
CSV(Comma-Separated Values)逗号分隔值文件格式,有时也称为字符分隔值,因为分隔字符也可以不是逗号。其文件以纯文本形式存储表格数据(数字和文本)。纯文本意味着该文件是一个字符序列,不含必须像二进制数字那样被解读的数据。CSV文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符。通常,所有记录都有完全相同的字段序列。
Example - Movies.csv
The Hobbit:The Battle of Five Armies,2015,America
Transformers: Age of Extinction,2014,America
Lucy,2014, America
Intouchables,2011,France
4条记录,每条记录为电影名,上映时间,国家,用“,”分隔。
“当 XML(扩展标记语言)于 1998 年 2 月被引入软件工业界时,它给整个行业带来了一场风暴。有史以来第一次,这个世界拥有了一种用来结构化文档和数据的通用且适应性强的格式,它不仅仅可以用于 WEB,而且可以被用于任何地方。”
——《Designing With Web Standards Second Edition》, Jeffrey Zeldman
XML(eXtensible Markup Language)可扩展标记语言,是一种标记语言, 扩展名 .csv
。标记指计算机所能理解的信息符号,通过此种标记,计算机之间可以处理包含各种信息的文章等。如何定义这些标记,既可以选择国际通用的标记语言,比如HTML,也可以使用像XML这样由相关人士自由决定的标记语言,这就是语言的可扩展性。XML是从标准通用标记语言(SGML)中简化修改出来的。它主要用到的有可扩展标记语言、可扩展样式语言(XSL)、XBRL和XPath等。但 XML 需要存储标签,需要额外的内存。
查看 XML 教程。
Example - Books.xml
<bookstore>
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<author>Giada De Laurentiis</author>
<year>2005</year>
<price>30.00</price>
</book>
<book category="CHILDREN">
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
<book category="WEB">
<title lang="en">Learning XML</title>
<author>Erik T. Ray</author>
<year>2003</year>
<price>39.95</price>
</book>
</bookstore>
例子中的根元素是 <bookstore>
。文档中的所有 <book>
元素都被包含在 <bookstore>
中。<book>
元素有 4 个子元素:<title>
、<author>
、<year>
、<price>
。
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,扩展名 .json
。它基于JavaScript(Standard ECMA-262 3rd Edition - December 1999)的一个子集。 JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C++, C#, Java, JavaScript, Perl, Python等)。这些特性使JSON成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成(网络传输速度快)。
JSON 是存储和交换文本信息的语法,类似 XML。JSON 比 XML 更小、更快,更易解析。
查看 JSON 教程。
Example - employees.json
{
"employees": [
{ "firstName":"Bill" , "lastName":"Gates" },
{ "firstName":"George" , "lastName":"Bush" },
{ "firstName":"Thomas" , "lastName":"Carter" }
]
}
这个 employee 对象是包含 3 个员工记录(对象)的数组。
MySQL是最流行的关系型数据库管理系统,在WEB应用方面MySQL是最好的RDBMS(Relational Database Management System:关系数据库管理系统)应用软件之一。
MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司。MySQL是一种关联数据库管理系统,关联数据库将数据保存在不同的表中,而不是将所有数据放在一个大仓库内,这样就增加了速度并提高了灵活性。MySQL 主要有一下特点:
查看MySQL 教程。
MongDB是一个高性能,易部署,开源,无模式的文档型非关系数据库,是当前NoSql数据库中比较热门的一种。它在许多场景下可用于替代传统的关系型数据库或键/值存储方式。Mongo使用C++开发。
- 什么是NoSql?
NoSql,全称是 Not Only Sql,指的是非关系型的数据库。下一代数据库主要解决几个要点:非关系型的、分布式的、开源的、水平可扩展的。原始的目的是为了大规模web应用,这场运动开始于2009年初,通常特性应用如:模式自由、支持简易复制、简单的API、最终的一致性(非ACID)、大容量数据等。NoSQL被我们用得最多的当数key-value存储,当然还有其他的文档型的、列存储、图型数据库、xml数据库等。
MongDB 主要有一下特点:
量化投资( Quantitative Investment ),简单地说,就是利用数学、统计学、信息技术的量化投资方法来管理投资组合。数量化投资的组合构建注重的是对宏观数据、市场行为、企业财务数据、交易数据进行分析,利用数据挖掘技术、统计技术、计算方法等处理数据,以得到最优的投资组合和投资机会。
量化投资就是借助现代统计学、数学的方法,从海量历史大数据中寻找能够带来股票上涨的多种“大概率”策略和规律,并在此基础上,综合归纳成因子和模型程序,最终纪律严明地按照这些数量化模型组合来进行独立投资,力求取得稳定的、可持续的、高于平均的超额回报。
海外量化投资已经非常成熟,而中国采用量化技术的进行投资才起步不久,相对来说量化对冲基金捕捉套利机会用的多一些,而主动管理的纯股票型基金还是少数。此外中国市场无效性和波动性更大,可挖掘的规律更多,故机会与前景看好,可以说,量化投资正步入黄金时代。
量化投资的运作和FOF(基金中的基金)有点像类似,基金经理构建出一篮子模型组操作公募基金。每个模型根据自己的构建原理自主选股,好像是一个独立的子基金。在运作过程中,基金经理根据实际情况和业绩因地制宜的对因子或模型进行调整,使之不断迭代,持续有效。一般来说,真正的量化投资,基金经理是模型的构建者和资金的给与者(仓位控制),至于模型选出什么股票,基金经理不是无权干预,而是无法干预。
这么一来,量化投资保证了模型的独立自主和自我学习的有效性,最大限度的降低了人性弱点可能带来的错误干预。于是从结果和宏观层面来看,量化投资和普通的主动管理基金就形成了巨大的差别,如图:
左边第一列有点夸张,不是说量化投资基金经理很闲,这里主要说的是工作模式。主动投资管理的基金经理要不停的挖掘、调研、选股票,靠的是人;而量化投资基金经理更多的是进行数据分析、构建新的模型、研究外方股东有啥最新最先进的模型进行本土化尝试,股票交给模型来挖,靠的是电脑。
此外,主动投资管理基金经理都希望挖掘到牛股,那感觉就像杨过对小龙女,不说天长地久吧,想至少赚个好几倍的心还是有的。量化投资则不同,电脑自动买卖,换手率高,股票基本不长期持有,一旦达到目的触发相应规则立刻卖出。那感觉像楚留香,说的直白一点,人家就是来谈恋爱的,不是来结婚的。
所以一切结果最后体现在持仓上,就是一般量化投资产品没有“十大重仓股”的概念,比如某些量化投资产品持仓300多只股票每个股票占比都小于2%,非常分散,也就保证了量化投资产品可以覆盖多个行业的多只股票,分散投资、分散风险,也就能圈住各个热点板块的轮动。
1、αα……α:量化投资获取超额收益的逻辑是:不求在每只股票上都赚很多钱,但求在很多股票上每只都赚一点钱。一句话,积小胜为大胜,积全市场很多股票的小α为整个基金的大α。原理在于,量化投资的优势不是对单一上市公司的深度挖掘,而是对全市场股票进行的大样本、大数据、大强度的广度挖掘。这一点,“人脑”做不到,只能用“电脑”。
2、β(市场风险):这点不用细说,去年以来市场整体向上,自然给量化产品带来很高的系统性收益。
持续性的业绩非常重要,一般来讲,某一个基金前一年表现的很好后一年就会很差,只有少数“神一般”的基金经理才能做到改变逻辑、改对逻辑、最终还能获利。量化投资的运作原理决定了对于市场热点而言,它不会赚翻,但是绝不会错过。从业绩上来看,量化投资连续多年把握住了机会,未来如何?可以期待并加以验证!
同时,量化投资的有效性在未来依然会持续很长时间,原因有三:
后续:市场若好,便是晴天;那市场转熊呢…
说了这么多好听的,不好听的也必须要说:单纯的量化投资方式,产品表现好和中小盘的轻舞飞扬不无关系,那么万一中小盘行情结束了呢?万一经济和市场没大家想的那么美好呢?风险无处不在,风险永远是一场说来就来的意外。
因此,对于量化这种“锋利”的投资方式,在瞬息万变的市场环境中,对冲将是控制风险的最好选择!
量化投资( Quantitative Investment ),简单地说,就是利用数学、统计学、信息技术的量化投资方法来管理投资组合。数量化投资的组合构建注重的是对宏观数据、市场行为、企业财务数据、交易数据进行分析,利用数据挖掘技术、统计技术、计算]]>
30分钟内让你明白正则表达式是什么,并对它有一些基本的了解,让你可以在自己的程序或网页里使用它。
别被下面那些复杂的表达式吓倒,只要跟着我一步一步来,你会发现正则表达式其实并没有想像中的那么困难。当然,如果你看完了这篇教程之后,发现自己明白了很多,却又几乎什么都记不得,那也是很正常的——我认为,没接触过正则表达式的人在看完这篇教程后,能把提到过的语法记住 80% 以上的可能性为零。这里只是让你明白基本的原理,以后你还需要多练习,多使用,才能熟练掌握正则表达式。
最重要的是——请给我30分钟,如果你没有使用正则表达式的经验,请不要试图在30秒内入门——除非你是超人 :)
除了作为入门教程之外,本文还试图成为可以在日常工作中使用的正则表达式语法参考手册。就作者本人的经历来说,这个目标还是完成得不错的——你看,我自己也没能把所有的东西记下来,不是吗?
在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
字符是计算机软件处理文字时最基本的单位,可能是字母,数字,标点符号,空格,换行符,汉字等等。字符串是0个或更多个字符的序列。文本也就是文字,字符串。说某个字符串匹配某个正则表达式,通常是指这个字符串里有一部分(或几部分分别)能满足表达式给出的条件。
很可能你使用过 Windows/Dos 下用于文件查找的通配符 (wildcard),也就是*
和?
。如果你想查找某个目录下的所有的Word文档的话,你会搜索*.doc
。在这里,*
会被解释成任意的字符串。和通配符类似,正则表达式也是用来进行文本匹配的工具,只不过比起通配符,它能更精确地描述你的需求——当然,代价就是更复杂——比如你可以编写一个正则表达式,用来查找所有以0开头,后面跟着2-3个数字,然后是一个连字号“-”,最后是7或8位数字的字符串(像010-12345678或0376-7654321)。
学习正则表达式的最好方法是从例子开始,理解例子之后再自己对例子进行修改,实验。下面给出了不少简单的例子,并对它们作了详细的说明。
假设你在一篇英文小说里查找hi,你可以使用正则表达式hi。
这几乎是最简单的正则表达式了,它可以精确匹配这样的字符串:由两个字符组成,前一个字符是h,后一个是i。通常,处理正则表达式的工具会提供一个忽略大小写的选项,如果选中了这个选项,它可以匹配hi, HI, Hi, hI这四种情况中的任意一种。
不幸的是,很多单词里包含hi这两个连续的字符,比如 him, history, high 等等。用 hi 来查找的话,这里边的 hi 也会被找出来。如果要精确地查找 hi 这个单词的话,我们应该使用\bhi\b
。
\b是正则表达式规定的一个特殊代码(好吧,某些人叫它元字符,metacharacter),代表着单词的开头或结尾,也就是单词的分界处。虽然通常英文的单词是由空格,标点符号或者换行来分隔的,但是\b
并不匹配这些单词分隔字符中的任何一个,它只匹配一个位置。
如果需要更精确的说法,\b匹配这样的位置:它的前一个字符和后一个字符不全是(一个是,一个不是或不存在)\w。
假如你要找的是 hi 后面不远处跟着一个 Lucy,你应该用 \bhi\b.*\bLucy\b
。
这里,.
是另一个元字符,匹配除了换行符以外的任意字符。*
同样是元字符,不过它代表的不是字符,也不是位置,而是数量——它指定 *
前边的内容可以连续重复使用任意次以使整个表达式得到匹配。因此,.*
连在一起就意味着任意数量的不包含换行的字符。现在 \bhi\b.*\bLucy\b
的意思就很明显了:先是一个单词hi,然后是任意个任意字符(但不能是换行),最后是Lucy这个单词。
如果同时使用其它元字符,我们就能构造出功能更强大的正则表达式。比如下面这个例子:
0\d\d-\d\d\d\d\d\d\d\d 匹配这样的字符串:以0开头,然后是两个数字,然后是一个连字号“-”,最后是8个数字(也就是中国的电话号码。当然,这个例子只能匹配区号为3位的情形)。
这里的 \d
是个新的元字符,匹配一位数字(0,或1,或2,或……)。-
不是元字符,只匹配它本身——连字符(或者减号,或者中横线,或者随你怎么称呼它)。
为了避免那么多烦人的重复,我们也可以这样写这个表达式:0\d{2}-\d{8}
。这里\d
后面的 {2}({8})
的意思是前面\d
必须连续重复匹配2次(8次)。
现在你已经知道几个很有用的元字符了,如 \b
, .
, *
,还有\d.
正则表达式里还有更多的元字符,比如\s
匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等。\w
匹配字母或数字或下划线或汉字等。
下面来看看更多的例子:
\ba\w*\b
匹配以字母 a 开头的单词——先是某个单词开始处 (\b),然后是字母 a,然后是任意数量的字母或数字 (\w*),最后是单词结束处 (\b)。
好吧,现在我们说说正则表达式里的单词是什么意思吧:就是不少于一个的连续的\w。不错,这与学习英文时要背的成千上万个同名的东西的确关系不大 :)
\d+
匹配1个或更多连续的数字。这里的+
是和*
类似的元字符,不同的是*
匹配重复任意次(可能是0次),而+
则匹配重复1次或更多次。\b\w{6}\b
匹配刚好6个字符的单词(表1.常用的元字符)。
代码/语法 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
元字符 ^
(和数字6在同一个键位上的符号)和$
都匹配一个位置,这和 \b
有点类似。^匹配你要用来查找的字符串的开头,$
匹配结尾。这两个代码在验证输入的内容时非常有用,比如一个网站如果要求你填写的QQ号必须为5位到12位数字时,可以使用:^\d{5,12}$
。
这里的{5,12}
和前面介绍过的{2}
是类似的,只不过{2}
匹配只能不多不少重复2次,{5,12}
则是重复的次数不能少于5次,不能多于12次,否则都不匹配。
因为使用了 ^
和 $
,所以输入的整个字符串都要用来和\d{5,12}
来匹配,也就是说整个输入必须是5到12个数字,因此如果输入的QQ号能匹配这个正则表达式的话,那就符合要求了。
和忽略大小写的选项类似,有些正则表达式处理工具还有一个处理多行的选项。如果选中了这个选项,^
和$
的意义就变成了匹配行的开始处和结束处。
如果你想查找元字符本身的话,比如你查找.
,或者*
,就出现了问题:你没办法指定它们,因为它们会被解释成别的意思。这时你就得使用\
来取消这些字符的特殊意义。因此,你应该使用\.
和\*
。当然,要查找\
本身,你也得用\\.
例如:deerchao\.net
匹配 deerchao.net,C:\\Windows
匹配 C:\Windows。
你已经看过了前面的*
,+
,{2}
,{5,12}
这几个匹配重复的方式了。下面是正则表达式中所有的限定符(指定数量的代码,例如*
,{5,12}
等),表2.常用的限定符:
代码/语法 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
下面是一些使用重复的例子:Windows\d+
匹配Windows后面跟1个或更多数字^\w+
匹配一行的第一个单词(或整个字符串的第一个单词,具体匹配哪个意思得看选项设置)。
要想查找数字,字母或数字,空白是很简单的,因为已经有了对应这些字符集合的元字符,但是如果你想匹配没有预定义元字符的字符集合(比如元音字母a,e,i,o,u),应该怎么办?
很简单,你只需要在方括号里列出它们就行了,像 [aeiou]
就匹配任何一个英文元音字母,[.?!]
匹配标点符号(.或?或!)。
我们也可以轻松地指定一个字符范围,像[0-9]
代表的含意与\d
就是完全一致的:一位数字;同理[a-z0-9A-Z_]
也完全等同于\w
(如果只考虑英文的话)。
下面是一个更复杂的表达式:\(?0\d{2}[) -]?\d{8}
。
这个表达式可以匹配几种格式的电话号码,像(010)88886666,或 022-22334455,或 02912345678等。我们对它进行一些分析吧:首先是一个转义字符\(
,它能出现0次或1次(?),然后是一个0,后面跟着2个数字(\d{2}),然后是)或-或空格中的一个,它出现1次或不出现(?),最后是8个数字(\d{8})。
不幸的是,刚才那个表达式也能匹配 010)12345678 或 (022-87654321 这样的“不正确”的格式。要解决这个问题,我们需要用到分枝条件。正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用|把不同的规则分隔开。听不明白?没关系,看例子:
0\d{2}-\d{8}|0\d{3}-\d{7}
这个表达式能匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)。
\(?0\d{2}\)?[- ]?\d{8}|0\d{2}[-]?\d{8}
这个表达式匹配3位区号的电话号码,其中区号可以用小括号括起来,也可以不用,区号与本地号间可以用连字号或空格间隔,也可以没有间隔。你可以试试用分枝条件把这个表达式扩展成也支持4位区号的。
\d{5}-\d{4}|\d{5}
这个表达式用于匹配美国的邮政编码。美国邮编的规则是5位数字,或者用连字号间隔的9位数字。之所以要给出这个例子是因为它能说明一个问题:使用分枝条件时,要注意各个条件的顺序。如果你把它改成 \d{5}|\d{5}-\d{4}
的话,那么就只会匹配5位的邮编(以及9位邮编的前5位)。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。
我们已经提到了怎么重复单个字符(直接在字符后面加上限定符就行了);但如果想要重复多个字符又该怎么办?你可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了,你也可以对子表达式进行其它一些操作(后面会有介绍)。
(\d{1,3}\.){3}\d{1,3}
是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\d{1,3}
匹配1到3位的数字,(\d{1,3}\.){3}
匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字 (\d{1,3})
。
IP地址中每个数字都不能大于255. 经常有人问我, 01.02.03.04 这样前面带有0的数字, 是不是正确的IP地址呢? 答案是: 是的, IP 地址里的数字可以包含有前导 0 (leading zeroes).
不幸的是,它也将匹配 256.300.888.999 这种不可能存在的IP地址。如果能使用算术比较的话,或许能简单地解决这个问题,但是正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
。
理解这个表达式的关键是理解 2[0-4]\d|25[0-5]|[01]?\d\d?
,这里我就不细说了,你自己应该能分析得出来它的意义。
有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义(表3.常用的反义代码):
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
例子:\S+
匹配不包含空白符的字符串。
<a[^>]+>
匹配用尖括号括起来的以a开头的字符串。
使用小括号指定一个子表达式后,匹配这个子表达式的文本(也就是此分组捕获的内容)可以在表达式或其它程序中作进一步的处理。默认情况下,每个分组会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。
呃……其实,组号分配还不像我刚说得那么简单:
- 分组0对应整个正则表达式
- 实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号
- 你可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权.
后向引用用于重复搜索前面某个分组匹配的文本。例如,\1
代表分组1匹配的文本。难以理解?请看示例:
\b(\w+)\b\s+\1\b
可以用来匹配重复的单词,像go go, 或者 kitty kitty。这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字 (\b(\w+)\b)
,这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符 (\s+)
,最后是分组1中捕获的内容(也就是前面匹配的那个单词) (\1)
。
你也可以自己指定子表达式的组名。要指定一个子表达式的组名,请使用这样的语法:(?<Word>\w+)
(或者把尖括号换成’也行:(?'Word'\w+))
,这样就把 \w+
的组名指定为 Word 了。要反向引用这个分组捕获的内容,你可以使用 \k<Word>
,所以上一个例子也可以写成这样:\b(?<Word>\w+)\b\s+\k<Word>\b
。
使用小括号的时候,还有很多特定用途的语法。下面列出了最常用的一些(表4.常用分组语法):
分类 | 代码/语法 | 说明 |
---|---|---|
捕获 | (exp) | 匹配exp,并捕获文本到自动命名的组里 |
(? |
匹配exp,并捕获文本到名称为name的组里,也可以写成(?’name’exp) | |
(?:exp) | 匹配exp,不捕获匹配的文本,也不给此分组分配组号 | |
零宽断言 | (?=exp) | 匹配exp前面的位置 |
(?<=exp) | 匹配exp后面的位置 | |
(?!exp) | 匹配后面跟的不是exp的位置 | |
(?<!exp) | 匹配前面不是exp的位置 | |
注释 | (?#comment) | 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读 |
我们已经讨论了前两种语法。第三个(?:exp)
不会改变正则表达式的处理方式,只是这样的组匹配的内容不会像前两种那样被捕获到某个组里面,也不会拥有组号。“我为什么会想要这样做?”——好问题,你觉得为什么呢?
接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b
,^
,$
那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧:
断言用来声明一个应该为真的事实。正则表达式中只有当断言为真时才会继续进行匹配。
`(?=exp)`也叫**零宽度正预测先行断言**,它断言自身出现的位置的后面能匹配表达式exp。比如 `\b\w+(?=ing\b)`,匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找*I'm singing while you're dancing.* 时,它会匹配*sing*和*danc*。
(?<=exp)
也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如 (?<=\bre)\w+\b
会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找 reading a book 时,它匹配 ading 。
假如你想要给一个很长的数字中每三位间加一个逗号(当然是从右边加起了),你可以这样查找需要在前面和里面添加逗号的部分:((?<=\d)\d{3})+\b
,用它对 1234567890 进行查找时结果是 234567890。
下面这个例子同时使用了这两种断言:(?<=\s)\d+(?=\s)
匹配以空白符间隔的数字(再次强调,不包括这些空白符)。
前面我们提到过怎么查找不是某个字符或不在某个字符类里的字符的方法(反义)。但是如果我们只是想要确保某个字符没有出现,但并不想去匹配它时怎么办?例如,如果我们想查找这样的单词—它里面出现了字母q,但是q后面跟的不是字母u,我们可以尝试这样:
\b\w*q[^u]\w*\b
匹配包含后面不是字母u的字母q的单词。但是如果多做测试(或者你思维足够敏锐,直接就观察出来了),你会发现,如果q出现在单词的结尾的话,像Iraq,Benq,这个表达式就会出错。这是因为[^u]
总要匹配一个字符,所以如果q是单词的最后一个字符的话,后面的[^u]
将会匹配q后面的单词分隔符(可能是空格,或者是句号或其它的什么),后面的\w*\b
将会匹配下一个单词,于是 b\w*q[^u]\w*\b
就能匹配整个 Iraq fighting 。负向零宽断言能解决这样的问题,因为它只匹配一个位置,并不消费任何字符。现在,我们可以这样来解决这个问题:\b\w*q(?!u)\w*\b
。
零宽度负预测先行断言(?!exp)
,断言此位置的后面不能匹配表达式exp。例如:\d{3}(?!\d)
匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b
匹配不包含连续字符串abc的单词。
同理,我们可以用(?<!exp)
,零宽度负回顾后发断言来断言此位置的前面不能匹配表达式exp:(?<![a-z])\d{7}
匹配前面不是小写字母的七位数字。
请详细分析表达式 `(?<=<(\w+)>).*(?=<\/\1>)`,这个表达式最能表现零宽断言的真正用途。
一个更复杂的例子:(?<=<(\w+)>).*(?=<\/\1>) 匹配不包含属性的简单 HTML 标签内里的内容。
(?<=<(\w+)>)指定了这样的**前缀**:被尖括号括起来的单词(比如可能是
),然后是
.`(任意的字符串),最后是一个*后缀(?=<\/\1>)
。注意后缀里的\/
,它用到了前面提过的字符转义;\1
则是一个反向引用,引用的正是捕获的第一组,前面的(\w+)
匹配的内容,这样如果前缀实际上是<b>
的话,后缀就是</b>
了。整个表达式匹配的是<b>
和</b>
之间的内容(再次提醒,不包括前缀和后缀本身)。
小括号的另一种用途是通过语法 (?#comment)
来包含注释。例如:2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199)
。
要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样:
(?<= # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
) # 前缀结束
.* # 匹配任意文本
(?= # 断言要匹配的文本的后缀
<\/\1> # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
) # 后缀结束
当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b
,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。
有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?
。这样.*?
就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:
a.*?b
匹配最短的,以 a 开始,以 b 结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。(表5.懒惰限定符)
代码/语法 | 说明 |
---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复 |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |
上面介绍了几个选项如忽略大小写,处理多行等,这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项(表6.常用的处理选项):
名称 | 说明 |
---|---|
IgnoreCase(忽略大小写) | 匹配时不区分大小写。 |
Multiline(多行模式) | 更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.) |
Singleline(单行模式) | 更改.的含义,使它与每一个字符匹配(包括换行符\n)。 |
IgnorePatternWhitespace(忽略空白) | 忽略表达式中的非转义空白并启用由#标记的注释。 |
ExplicitCapture(显式捕获) | 仅捕获已被显式命名的组。 |
一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案是:不是。这两个选项之间没有任何关系,除了它们的名字比较相似(以至于让人感到疑惑)以外。
有时我们需要匹配像 ( 100 * ( 50 + 15 ) ) 这样的可嵌套的层次性结构,这时简单地使用 \(.+\)
则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式,懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等,比如 ( 5 / ( 3 + 2 ) ) ),那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的,配对的括号之间的内容呢?
为了避免(和\(
把你的大脑彻底搞糊涂,我们还是用尖括号代替圆括号吧。现在我们的问题变成了如何把xx <aa <bbb> <bbb> aa> yy
这样的字符串里,最长的配对的尖括号内的内容捕获出来?
这里需要用到以下的语法构造:
(?'group')
把捕获的内容命名为group,并压入堆栈(Stack)(?'-group')
从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败(?(group)yes|no)
如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分(?!)
零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败
如果你不是一个程序员(或者你自称程序员但是不知道堆栈是什么东西),你就这样理解上面的三种语法吧:第一个就是在黑板上写一个"group",第二个就是从黑板上擦掉一个"group",第三个就是看黑板上写的还有没有"group",如果有就继续匹配yes部分,否则就匹配no部分。
我们需要做的是每碰到了左括号,就在压入一个”Open”,每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就证明左括号比右括号多,那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。
< #最外层的左括号
[^<>]* #最外层的左括号后面的不是括号的内容
(
(
(?'Open'<) #碰到了左括号,在黑板上写一个"Open"
[^<>]* #匹配左括号后面的不是括号的内容
)+
(
(?'-Open'>) #碰到了右括号,擦掉一个"Open"
[^<>]* #匹配右括号后面不是括号的内容
)+
)*
(?(Open)(?!)) #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败
> #最外层的右括号
平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的< div >标签。
|
|
30分钟内让你明白正则表达式是什么,并对它有一些基本的了解,让你可以在自己的程序或网页里使用它。
别被下面那些复杂的表达式吓倒,只要跟着我一步一]]>
抓取前先了解一些概念和工具。
动态网页是指跟静态网页相对的一种网页编程技术。静态网页,随着html代码的生成,页面的内容和显示效果就基本上不会发生变化了——除非你修改页面代码。而动态网页则不然,页面代码虽然没有变,但是显示的内容却是可以随着时间、环境或者数据库操作的结果而发生改变的。与静态网页相对应的,能与后台数据库进行交互,数据传递。也就是说,网页 URL的后缀不是.htm、.html、.shtml、.xml等静态网页的常见形动态网页制作格式,而是以.aspx、.asp、.jsp、.php、.perl、.cgi等形式为后缀,并且在动态网页网址中有一个标志性的符号——“?”。可以通过以下方式简单验证某网页是否为动态网页。
在页面上右键查看源代码,和右键审查元素所看到的html代码是不一样的,如果后者中能看到商品数据信息,而前者没有的话,就说明这个页面是动态生成的。
Selenium是Thoughtworks公司的一个集成测试的强大工具。Selenium 是 ThoughtWorks 专门为 Web 应用程序编写的一个验收测试工具。与其他测试工具相比,使用 Selenium 的最大好处是: Selenium 测试直接在浏览器中运行,就像真实用户所做的一样。在浏览器加载js后,便可以通过xpath来解析网页了。可以先用 pip install 或 easy_install 安装 Selenium package。
|
|
这里就不详细说明 item
、pipeline
、setting
文件的编写了。如果对这些模块不熟的话可以先看看 Our first spider。
搜索某一款产品,如 Iphone6,我们就可以得到该产品检索结果的起始页面的 start_urls
。但通常情况下,我们可能要得到很多商品相应的信息,那该怎么处理呢?容易想到的是让浏览器模拟我们手动输入,自动响应检索事件,从而得到目标页面的 start_urls
。另一种方法是,我们将目标产品存到一个配置文件中,直接将http://gouwu.sogou.com/shop?query=
+ ItemList
作为 start_urls
。这里采用后一种方法,简单粗暴有效。
|
|
首先启用 selenium,这里用本地浏览器 Firefox:
|
|
得到所有产品的 start_urls
后,我们便可以通过 Xpath
提取想要的数据了。这里抓取的内容有标题、价格和来源网站。
|
|
然后提取页面(下一页),定义提取和过滤 url
:
|
|
这样我们的 Spider 就差不多定义好了。完整 Spider 程序如下:
|
|
本文介绍了利用 selenium 实现动态网站数据抓取的一种方法。但需要注意的是 selenium 需要运行本地浏览器,比较耗时,不太适合大规模网页抓取。因此可以尝试其它的 Javascript 加载工具,如 webkit、spynner,也可以调用无界面依赖的浏览器引擎 Casperjs、Phantomjs等。
]]>__future__
模块 Python 3.x 介绍的 一些Python 2 不兼容的关键字和特性可以通过在 Python 2 的内置 __future__
模块导入。如果你计划让你的代码支持 Python 3.x,建议你使用 __future__
模块导入。例如,如果我想要 在Python 2 中表现 Python 3.x 中的整除,我们可以通过如下导入
|
|
更多的 __future__
模块可被导入的特性被列在下表中:
feature | optional in | mandatory in | effect |
---|---|---|---|
nested_scopes | 2.1.0b1 | 2.2 | PEP 227: Statically Nested Scopes |
generators | 2.2.0a1 | 2.3 | PEP 255: Simple Generators |
division | 2.2.0a2 | 3.0 | PEP 238: Changing the Division Operator |
absolute_import | 2.5.0a1 | 3.0 | PEP 328: Imports: Multi-Line and Absolute/Relative |
with_statement | 2.5.0a1 | 2.6 | PEP 343: The “with” Statement |
print_function | 2.5.0a2 | 3.0 | PEP 3105: Make print a function |
unicode_literals | 2.5.0a2 | 3.0 | PEP 3112: Bytes literals in Python 3000 |
(Source: https://docs.python.org/2/library/future.html)
|
|
print
函数 很琐碎,而 print
语法的变化可能是最广为人知的了,但是仍值得一提的是: Python 2 的 print 声明已经被 print()
函数取代了,这意味着我们必须包装我们想打印在小括号中的对象。
Python 2 不具有额外的小括号问题。但对比一下,如果我们按照 Python 2 的方式不使用小括号调用 print
函数,Python 3 将抛出一个语法异常(SyntaxError
)。
Python 2
|
|
run result:
Python 2.7.6
Hello, World!
Hello, World!
text print more text on the same line
Python 3
|
|
run result:
Python 3.4.1
Hello, World!
some text, print more text on the same line
|
|
run result:
File “
print ‘Hello, World!’
^
SyntaxError: invalid syntax
Note:
以上通过 Python 2 使用 Printing "Hello, World"
是非常正常的,尽管如此,如果你有多个对象在小括号中,我们将创建一个元组,因为 print
在 Python 2 中是一个声明,而不是一个函数调用。
|
|
run result:
Python 2.7.7
(‘a’, ‘b’)
a b
如果你正在移植代码,这个变化是特别危险的。或者你在 Python 2 上执行 Python 3 的代码。因为这个整除的变化表现在它会被忽视(即它不会抛出语法异常)。
因此,我还是倾向于使用一个 float(3)/2
或 3/2.0
代替在我的 Python 3 脚本保存在 Python 2 中的 3/2
的一些麻烦(并且反而过来也一样,我建议在你的 Python 2 脚本中使用 from __future__ import division
)
Python 2
|
|
run result:
Python 2.7.6
3 / 2 = 1
3 // 2 = 1
3 / 2.0 = 1.5
3 // 2.0 = 1.0
Python 3
|
|
run result:
Python 3.4.1
3 / 2 = 1.5
3 // 2 = 1
3 / 2.0 = 1.5
3 // 2.0 = 1.0
Unicode
Python 2 有 ASCII str() 类型,unicode()
是单独的,不是 byte
类型。
现在, 在 Python 3,我们最终有了 Unicode (utf-8)
字符串,以及一个字节类:byte
和 bytearrays
。
Python 2
|
|
run result:
Python 2.7.6
|
|
run result:
< type ‘unicode’ >
|
|
run result:
< type ‘str’ >
|
|
run result:
they are really the same
|
|
run result:
< type ‘bytearray’ >
Python 3
|
|
run result:
Python 3.4.1
strings are now utf-8 μnicoΔé!
|
|
run result:
Python 3.4.1 has < class ‘bytes’ >
|
|
run result:
and Python 3.4.1 also has < class ‘bytearray’>
|
|
run result:
-—————————————————————————————————————
TypeError Traceback (most recent call last)
< ipython-input-13-d3e8942ccf81> in < module>()
——> 1 ‘note that we cannot add a string’ + b’bytes for data’
TypeError: Can’t convert ‘bytes’ object to str implicitly
xrange
模块 在 Python 2 中 xrange()
创建迭代对象的用法是非常流行的。比如: for
循环或者是列表/集合/字典推导式。
这个表现十分像生成器(比如。“惰性求值”)。但是这个 xrange-iterable
是无穷的,意味着你可以无限遍历。
由于它的惰性求值,如果你不得仅仅不遍历它一次,xrange()
函数 比 range()
更快(比如 for
循环)。尽管如此,对比迭代一次,不建议你重复迭代多次,因为生成器每次都从头开始。
在 Python 3 中,range()
是像 xrange()
那样实现以至于一个专门的 xrange()
函数都不再存在(在 Python 3 中 xrange()
会抛出命名异常)。
|
|
Python 2
|
|
run result:
Python 2.7.6
timing range()
1000 loops, best of 3: 433 µs per loop
timing xrange()
1000 loops, best of 3: 350 µs per loop
Python 3
|
|
run result:
Python 3.4.1
timing range()
1000 loops, best of 3: 520 µs per loop
|
|
run result:
-—————————————————————————————————————
NameError Traceback (most recent call last)
——> 1 print(xrange(10))
NameError: name ‘xrange’ is not defined
range
对象的__contains__
方法 另外一件值得一提的事情就是在 Python 3 中 range
有一个新的 __contains__
方法(感谢 Yuchen Ying 指出了这个),__contains__
方法可以加速 “查找” 在 Python 3.x 中显著的整数和布尔类型。
|
|
run result:
Python 3.4.1
1 loops, best of 3: 742 ms per loop
1000000 loops, best of 3: 1.19 µs per loop
基于以上的 timeit 的结果,当它使一个整数类型,而不是浮点类型的时候,你可以看到执行查找的速度是 60000 倍快。尽管如此,因为 Python 2.x 的 range
或者是 xrange
没有一个 __contains__
方法,这个整数类型或者是浮点类型的查询速度不会相差太大。
|
|
run result:
Python 2.7.7
1 loops, best of 3: 285 ms per loop
1 loops, best of 3: 179 ms per loop
1 loops, best of 3: 658 ms per loop
1 loops, best of 3: 556 ms per loop
下面说下 __contain__
方法并没有加入到 Python 2.x 中的证据:
|
|
run result:
Python 3.4.1
< slot wrapper ‘contains‘ of ‘range’ objects >
|
|
run result:
Python 2.7.7
-—————————————————————————————————————
AttributeError Traceback (most recent call last)
< ipython-input-7-05327350dafb> in < module>()
1 print ‘Python’, pythonversion()
——> 2 range.`_contains`
AttributeError: ‘builtinfunctionor_method’ object has no attribute `’__contains‘`
|
|
run result:
Python 2.7.7
-—————————————————————————————————————
AttributeError Traceback (most recent call last)
< ipython-input-8-7d1a71bfee8e> in < module>()
1 print ‘Python’, pythonversion()
——> 2 xrange.`_contains`
AttributeError: type object ‘xrange’ has no attribute '__contains__'
注意在 Python 2 和 Python 3 中速度的不同
有些人指出了 Python 3 的 range()
和 Python 2 的 xrange()
之间的速度不同。因为他们是用相同的方法实现的,因此期望相同的速度。尽管如此,这事实在于 Python 3 倾向于比 Python 2 运行的慢一点。
|
|
Python 3
|
|
run result:
Python 3.4.1
100 loops, best of 3: 2.68 ms per loop
Python 2
|
|
run result:
Python 2.7.6
1000 loops, best of 3: 1.72 ms per loop
Python 2 接受新旧两种语法标记,在 Python 3 中如果我不用小括号把异常参数括起来就会阻塞(并且反过来引发一个语法异常)。
Python 2
|
|
run result:
Python 2.7.6
|
|
run result:
-—————————————————————————————————————
IOError Traceback (most recent call last)
< ipython-input-8-25f049caebb0> in < module>()
——> 1 raise IOError, “file error”
IOError: file error
|
|
run result:
-—————————————————————————————————————
IOError Traceback (most recent call last)
< ipython-input-9-6f1c43f525b2> in < module>()
——> 1 raise IOError(“file error”)
IOError: file error
Python 3
|
|
run result:
Python 3.4.1
|
|
run result:
File “
raise IOError, “file error”
^
SyntaxError: invalid syntax
在 Python 3 中,可以这样抛出异常:
|
|
run result:
Python 3.4.1
-—————————————————————————————————————
OSError Traceback (most recent call last)
< ipython-input-11-c350544d15da> in < module>()
1 print(‘Python’, python_version())
——> 2 raise IOError(“file error”)
OSError: file error
在 Python 3 中处理异常也轻微的改变了,在 Python 3 中我们现在使用 as
作为关键词。
Python 2
|
|
run result:
Python 2.7.6
name ‘let_us_cause_a_NameError’ is not defined —> our error message
Python 3
|
|
run result:
Python 3.4.1
name ‘let_us_cause_a_NameError’ is not defined —> our error message
next()
函数 and .next()
方法 因为 next() (.next())
是一个如此普通的使用函数(方法),这里有另外一个语法改变(或者是实现上改变了),值得一提的是:在 Python 2.7.5 中函数和方法你都可以使用,next()
函数在 Python 3 中一直保留着(调用 .next()
抛出属性异常)。
Python 2
|
|
run result:
Python 2.7.6
‘b
Python 3
|
|
run result:
Python 3.4.1
‘a’
|
|
run result:
-—————————————————————————————————————
AttributeError Traceback (most recent call last)
< ipython-input-14-125f388bb61b> in < module>()
——> 1 my_generator.next()
AttributeError: ‘generator’ object has no attribute ‘next’
For
循环变量和全局命名空间泄漏 好消息:在 Python 3.x 中 for
循环变量不会再导致命名空间泄漏。
在 Python 3.x 中做了一个改变,在 What’s New In Python 3.0 中有如下描述:
“列表推导不再支持 [... for var in item1, item2, ...]
这样的语法。使用 [... for var in (item1, item2, ...)]
代替。也需要提醒的是列表推导有不同的语义: 他们关闭了在 list()
构造器中的生成器表达式的语法糖, 并且特别是循环控制变量不再泄漏进周围的作用范围域.”
Python 2
|
|
run result:
Python 2.7.6
before: i = 1
comprehension: [0, 1, 2, 3, 4]
after: i = 4
Python 3
|
|
run result:
Python 3.4.1
before: i = 1
comprehension: [0, 1, 2, 3, 4]
after: i = 1
在 Python 3 中的另外一个变化就是当对不可排序类型做比较的时候,会抛出一个类型错误。
Python 2
|
|
run result:
Python 2.7.6
[1, 2] > ‘foo’ = False
(1, 2) > ‘foo’ = True
[1, 2] > (1, 2) = False
Python 3
|
|
run result:
Python 3.4.1
-—————————————————————————————————————
TypeError Traceback (most recent call last)
< ipython-input-16-a9031729f4a0> in < module>()
1 print(‘Python’, python_version())
——> 2 print(“[1, 2] > ‘foo’ = “, [1, 2] > ‘foo’)
3 print(“(1, 2) > ‘foo’ = “, (1, 2) > ‘foo’)
4 print(“[1, 2] > (1, 2) = “, [1, 2] > (1, 2))
TypeError: unorderable types: list() > str()
input()
解析用户的输入 幸运的是,在 Python 3 中已经解决了把用户的输入存储为一个 str
对象的问题。为了避免在 Python 2 中的读取非字符串类型的危险行为,我们不得不使用 raw_input()
代替。
Python 2
Python 2.7.6
[GCC 4.0.1 (Apple Inc. build 5493)] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> my_input = input('enter a number: ')
enter a number: 123
>>> type(my_input)
<type 'int'>
>>> my_input = raw_input('enter a number: ')
enter a number: 123
>>> type(my_input)
<type 'str'>
Python 3
Python 3.4.1
[GCC 4.2.1 (Apple Inc. build 5577)] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> my_input = input('enter a number: ')
enter a number: 123
>>> type(my_input)
<class 'str'>
如果在 xrange 章节看到的,现在在 Python 3 中一些方法和函数返回迭代对象 — 代替 Python 2 中的列表
因为我们通常那些遍历只有一次,我认为这个改变对节约内存很有意义。尽管如此,它也是可能的,相对于生成器 —- 如需要遍历多次。它是不那么高效的。
而对于那些情况下,我们真正需要的是列表对象,我们可以通过 list()
函数简单的把迭代对象转换成一个列表。
Python 2
|
|
run result:
Python 2.7.6
[0, 1, 2]
< type ‘list’>
Python 3
|
|
run result:
Python 3.4.1
range(0, 3)
< class ‘range’>
[0, 1, 2]
在 Python 3 中一些经常使用到的不再返回列表的函数和方法:
zip()
map()
filter()
.keys()
method.values()
method.items()
method下面是我建议后续的关于 Python 2 和 Python 3 的一些好文章。
移植到 Python 3
Python 3 的拥护者和反对者
]]> Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。
Scrapy 使用了 Twisted异步网络库来处理网络通讯。整体架构大致如下:
Scrapy主要包括了以下组件:
引擎
:用来处理整个系统的数据流处理,触发事务。调度器
:用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。下载器
:用于下载网页内容,并将网页内容返回给蜘蛛。蜘蛛
:蜘蛛是主要干活的,用它来制订特定域名或网页的解析规则。项目管道
:负责处理有蜘蛛从网页中抽取的项目,他的主要任务是清晰、验证和存储数据。当页面被蜘蛛解析后,将被发送到项目管道,并经过几个特定的次序处理数据。下载器中间件
:位于Scrapy引擎和下载器之间的钩子框架,主要是处理Scrapy引擎与下载器之间的请求及响应。蜘蛛中间件
:介于Scrapy引擎和蜘蛛之间的钩子框架,主要工作是处理蜘蛛的响应输入和请求输出。调度中间件
:介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。使用Scrapy可以很方便的完成网上数据的采集工作,它为我们完成了大量的工作,而不需要自己费大力气去开发。
在本文中,假定您已经安装好Scrapy
。 如若不然,请参考 Installation guide。
接下来以爬取饮水思源BBS数据为例来讲述爬取过程,详见 bbsdmoz代码。
本篇教程中将带您完成下列任务:
1. 创建一个Scrapy项目
2. 定义提取的Item
3. 编写爬取网站的 spider 并提取 Item
4. 编写 Item Pipeline 来存储提取到的Item(即数据)
Scrapy由Python编写。如果您刚接触并且好奇这门语言的特性以及Scrapy的详情, 对于已经熟悉其他语言并且想快速学习Python的编程老手, 我们推荐 Learn Python The Hard Way , 对于想从Python开始学习的编程新手, 非程序员的Python学习资料列表 将是您的选择。
在开始爬取之前,您必须创建一个新的Scrapy项目。进入您打算存储代码的目录中,运行下列命令:
|
|
该命令将会创建包含下列内容的 bbsDmoz 目录:
bbsDmoz/
scrapy.cfg
bbsDmoz/
__init__.py
items.py
pipelines.py
settings.py
spiders/
__init__.py
...
这些文件分别是:
scrapy.cfg
: 项目的配置文件bbsDmoz/
: 该项目的python
模块。之后您将在此加入代码。bbsDmoz/items.py
: 项目中的item
文件.bbsDmoz/pipelines.py
: 项目中的pipelines
文件.bbsDmoz/settings.py
: 项目的设置文件.bbsDmoz/spiders/
: 放置spider
代码的目录. Item 是保存爬取到的数据的容器;其使用方法和python字典类似,并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。
类似在ORM中做的一样,您可以通过创建一个 scrapy.Item
类,并且定义类型为 scrapy.Field
的类属性来定义一个Item。(如果不了解ORM,不用担心,您会发现这个步骤非常简单)
首先根据需要从bbs网站获取到的数据对item进行建模。 我们需要从中获取url,发帖板块,发帖人,以及帖子的内容。 对此,在item中定义相应的字段。编辑 bbsDmoz 目录中的 items.py
文件:
|
|
一开始这看起来可能有点复杂,但是通过定义item, 您可以很方便的使用Scrapy的其他方法。而这些方法需要知道您的item的定义。
Spider是用户编写用于从单个网站(或者一些网站)爬取数据的类。
其包含了一个用于下载的初始URL,如何跟进网页中的链接以及如何分析页面中的内容, 提取生成 item 的方法。
为了创建一个Spider,保存在 bbsDmoz/spiders,您必须继承 scrapy.Spider 类,且定义以下三个属性:
name
: 用于区别Spider。该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。start_urls
: 包含了Spider在启动时进行爬取的url列表。因此,第一个被获取到的页面将是其中之一。后续的URL则从初始的URL获取到的数据中提取。我们可以利用正则表达式定义和过滤需要进行跟进的链接。parse()
是spider的一个方法。被调用时,每个初始URL完成下载后生成的 Response
对象将会作为唯一的参数传递给该函数。该方法负责解析返回的数据(response data),提取数据(生成item)以及生成需要进一步处理的URL的 Request
对象。 从网页中提取数据有很多方法。Scrapy使用了一种基于 XPath 和 CSS 表达式机制: Scrapy Selectors 。 关于selector和其他提取机制的信息请参考 Selector文档 。
我们使用XPath来从页面的HTML源码中选择需要提取的数据。这里给出XPath表达式的例子及对应的含义:
/html/head/title
: 选择HTML文档中 <head>
标签内的 <title>
元素/html/head/title/text()
: 选择上面提到的 <title>
元素的文字//td
: 选择所有的 <td>
元素//div[@class="mine"]
: 选择所有具有 class="mine"
属性的 div
元素 以饮水思源BBS一页面为例:https://bbs.sjtu.edu.cn/bbstcon?board=PhD&reid=1406973178&file=M.1406973178.A
观察HTML页面源码并创建我们需要的数据(种子名字,描述和大小)的XPath表达式。
通过观察,我们可以发现poster
是包含在 pre/a
标签中的,这里是userid=jasperstream
:
|
|
因此可以提取jasperstream
的 XPath 表达式为:
|
|
同理我可以提取其他内容的XPath,并最好在提取之后验证其正确性。上边仅仅是几个简单的XPath例子,XPath实际上要比这远远强大的多。 如果您想了解的更多,我们推荐 这篇XPath教程。
为了配合XPath,Scrapy除了提供了 Selector 之外,还提供了方法来避免每次从response中提取数据时生成selector的麻烦。
Selector有四个基本的方法(点击相应的方法可以看到详细的API文档):
xpath()
: 传入xpath表达式,返回该表达式所对应的所有节点的selector list
列表 。css()
: 传入CSS表达式,返回该表达式所对应的所有节点的selector list
列表.extract()
: 序列化该节点为unicode字符串并返回list
。re()
: 根据传入的正则表达式对数据进行提取,返回unicode字符串list列表。 如提取上述的poster
的数据:
|
|
Item
对象是自定义的python字典。您可以使用标准的字典语法来获取到其每个字段的值(字段即是我们之前用Field赋值的属性)。一般来说,Spider将会将爬取到的数据以 Item
对象返回。
以下为我们的第一个Spider代码
,保存在 bbsDmoz/spiders
目录下的 forumSpider.py
文件中:
|
|
当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。
每个item pipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并通过它执行一些行为,同时也决定此Item是否继续通过pipeline,或是被丢弃而不再进行处理。
以下是item pipeline
的一些典型应用:
编写你自己的item pipeline
很简单,每个item pipeline
组件是一个独立的Python类,同时必须实现以下方法:
process_item(item, spider)
每个item pipeline组件都需要调用该方法,这个方法必须返回一个 Item (或任何继承类)对象,或是抛出 DropItem异常,被丢弃的item将不会被之后的pipeline组件所处理。
参数:item (Item object) – 由 parse 方法返回的 Item 对象
spider (Spider object) – 抓取到这个 Item 对象对应的爬虫对象
此外,他们也可以实现以下方法:
open_spider(spider)
当spider被开启时,这个方法被调用。
参数: spider (Spider object) – 被开启的spider
close_spider(spider)
当spider被关闭时,这个方法被调用,可以再爬虫关闭后进行相应的数据处理。
参数: spider (Spider object) – 被关闭的spider
本文爬虫的item pipeline
如下,保存为XML文件:
|
|
为了启用一个Item Pipeline
组件,你必须将它的类添加到 ITEM_PIPELINES
就像下面这个例子:
|
|
分配给每个类的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,通过pipeline,通常将这些数字定义在0-1000范围内。
Scrapy设定(settings
)提供了定制Scrapy组件的方法。您可以控制包括核心(core),插件(extension),pipeline及spider组件。
设定为代码提供了提取以key-value映射的配置值的的全局命名空间(namespace)。 设定可以通过下面介绍的多种机制进行设置。
设定(settings)同时也是选择当前激活的Scrapy项目的方法(如果您有多个的话)。
在setting
配置文件中,你可一定以抓取的速率、是否在桌面显示抓取过程信息等。详细请参考内置设定列表请参考 。
本爬虫的setting
配置如下:
|
|
写好爬虫程序后,我们就可以运行程序抓取数据。进入项目的根目录bbsDomz/
下,执行下列命令启动spider:
|
|
这样就等程序运行结束就还可以啦。
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中]]>
在现实世界中,可获取的大部信息是以文本形式存储在文本数据库中的,由来自各种数据源的大量文档组成,如新闻文档、研究论文、书籍、数字图书馆、电子邮件和Web页面。由于电子形式的文本信息飞速增涨,文本挖掘已经成为信息领域的研究热点。
文本数据库中存储的数据可能是高度非结构化的,如WWW上的网页。也可能是半结构化的,如e-mail消息和一些XML网页:而其它的则可能是良结构化的。良结构化文本数据的典型代表是图书馆数据库中的文档,这些文档可能包含结构字段,如标题、作者、出版日期、长度、分类等等,也可能包含大量非结构化文本成分,如摘要和内容。通常,具有较好结构的文本数据库可以使用关系数据库系统实现,而对非结构化的文本成分需要采用特殊的处理方法对其进行转化。
文本挖掘(Text Mining)是一个从非结构化文本信息中获取用户感兴趣或者有用的模式的过程。其中被普遍认可的文本挖掘定义如下:文本挖掘是指从大量文本数据中抽取事先未知的、可理解的、最终可用的知识的过程,同时运用这些知识更好地组织信息以便将来参考。
文本挖掘的主要用途是从原本未经处理的文本中提取出未知的知识,但是文本挖掘也是一项非常困难的工作,因为它必须处理那些本来就模糊而且非结构化的文本数据,所以它是一个多学科混杂的领域,涵盖了信息技术、文本分析、模式识别、统计学、数据可视化、数据库技术、机器学习以及数据挖掘等技术 。文本挖掘是从数据挖掘发展而来,因此其定义与我们熟知的数据挖掘定义相类似。但与传统的数据挖掘相比,文本挖掘有其独特之处,主要表现在:文档本身是半结构化或非结构化的,无确定形式并且缺乏机器可理解的语义;而数据挖掘的对象以数据库中的结构化数据为主,并利用关系表等存储结构来发现知识。因此,有些数据挖掘技术并不适用于文本挖掘,即使可用,也需要建立在对文本集预处理的基础之上。
文本挖掘是应用驱动的。它在商业智能、信息检索、生物信息处理等方面都有广泛的应用;例如,客户关系管理,自动邮件回复,垃圾邮件过滤,自动简历评审,搜索引擎等等。
有些人把文本挖掘视为另一常用术语文本知识发现(KDD)的同义词,而另一些人只是把文本挖掘视为文本知识发现过程的一个基本步骤。文本知识发现主要由以下步骤组成:
- 获取文本数据源
- 文本预处理
- 挖掘与分析
- 评估与可视化
本篇文章将在接下来的篇幅中,详细的介绍以上几个步骤。
各文档数据库,语料库,论文集
web网页,如通过爬虫抓取,RSS订阅
等
去标签,去停用词,分词,生成数据集
常用表达模型
特征表达模型种类较多,大体可分为基于集合论的模型、基于代数论的模型和基于概率统计的模型。下面将分别介绍其中比较有代表性和常用的模型。
布尔模型
布尔模型(Bool model)。一个文档表示为文档中出现特征词的集合,也可以表示为一个特征空间上的向量,向量空间的每个分量的权值为0或1。
例
词条数据集 wordset = [‘love’, ‘have’, ‘dream’]
文档1: doc_1 = [‘I’, ‘have’, ‘a’, ‘dream’]
文档2: doc_2= [‘Cats’, ‘love’, ‘fish’]
那么文档1的布尔模型表达为vector_doc_1 = (0, 1, 0, 1),同理文档2的布尔模型表达为vector_doc_2 = (0, 1, 0, 0)
向量空间模型
向量空间模型(VSM:Vector Space Model) (或者 词组向量模型) 作为向量的标识符(比如索引),是一个用来表示文本文件的代数模型。如词频向量模型,TF/IDF权重模型。
例
词频向量模型
词条数据集 words = [‘love’, ‘have’, ‘dream’]
文档1: doc_1 = [‘I’, ‘have’, ‘a’, ‘dream’]
文档2: doc_2= [‘Cats’, ‘love’, ‘fish’]
那么文档1的布尔模型表达为vector_doc_1 = (0, 1, 0, 1),同理文档2的布尔模型表达为vector_doc_2 = (0, 1, 0, 0)
TF/IDF权重模型
Logistic回归模型
基本思想:为了求 P(R=1|Q,D),定义多个特征函数fi(Q,D),认为求 P(R=1|Q,D)是这些函数的组合。
通过训练集合拟合得到相应的系数,对于新的文档带入公式得到概率 P。
开方检验
信息增益
前文提到过,除了开方检验(CHI)以外,信息增益(IG,Information Gain)也是很有效的特征选择方法。信息增益(Kullback–Leibler divergence)又称information divergence,information gain,relative entropy 或者KLIC。但凡是特征选择,总是在将特征的重要程度量化之后再进行选择,而如何量化特征的重要性,就成了各种方法间最大的不同。开方检验中使用特征与类别间的关联性来进行这个量化,关联性越强,特征得分越高,该特征越应该被保留。
在信息增益中,衡量标准是看特征能够为分类系统带来多少信息,带来的信息越多,该特征越重要。对一个特征而言,系统有它和没它时信息量将发生变化,而前后信息量的差值就是这个特征给系统带来的信息量。所谓信息量,就是熵。
假如有变量X,分别是x1,x2,……,xn,每一种取到的概率分别是P1,P2,……,Pn,,那么X的熵就定义为:
也就是说X可能的变化越多,X所携带的信息量越大,熵也就越大。对于文本分类或聚类而言,就是说文档属于哪个类别的变化越多,类别的信息量就越大。所以特征T给聚类C或分类C带来的信息增益为IG(T)=H(C)-H(C|T)。
H(C|T)包含两种情况:一种是特征T出现,标记为t,一种是特征T不出现,标记为t’。所以H(C|T)=P(t)H(C|t) + P(t’)H(C|t’),再由熵的计算公式便可推得特征与类别的信息增益公式。
信息增益最大的问题在于它只能考察特征对整个系统的贡献,而不能具体到某个类别上,这就使得它只适合用来做所谓“全局”的特征选择(指所有的类都使用相同的特征集合),而无法做“本地”的特征选择(每个类别有自己的特征集合,因为有的词,对这个类别很有区分度,对另一个类别则无足轻重)。
主成分分析
主成分分析(Principal Component Analysis,PCA), 将多个变量通过线性变换以选出较少个数重要变量的一种多元统计分析方法。又称主分量分析。在很多情形,变量之间是有一定的相关关系的,当两个变量之间有一定相关关系时,可以解释为这两个变量反映某特征的信息有一定的重叠。PCA通过少数几个主成分来揭示多个变量间的内部结构,即从原始变量中导出少数几个主成分,将许多相关性较高的变量转化成彼此相互独立或不相关的变量,并使它们尽可能多地保留原始变量的信息。
线性判别分析
线性判别式分析(Linear Discriminant Analysis, LDA),也叫做Fisher线性判别(Fisher Linear Discriminant ,FLD)。LDA基本思想是将高维的模式样本投影到最佳鉴别矢量空间,以达到抽取分类信息和压缩特征空间维数的效果,投影后保证模式样本在新的子空间有最大的类间距离和最小的类内距离,即模式在该空间中有最佳的可分离性。因此,它是一种有效的特征抽取方法。使用这种方法能够使投影后模式样本的类间散布矩阵最大,并且同时类内散布矩阵最小。就是说,它能够保证投影后模式样本在新的空间中有最小的类内距离和最大的类间距离,即模式在该空间中有最佳的可分离性。
[1] Ronen Feldman and James Sanger, The Text Mining Handbook, Cambridge University Press.
[2] Kao Anne, Poteet, Steve R. (Editors), Natural Language Processing and Text Mining, Springer.
在现实世界中,可获取的大部信息是以文本形式存储在文本数据库中的,由来自各种数据源的大量文档组成,如新闻文档、研究]]>
memoryError
错误和文件读取太慢的问题,后来找到了两种比较快Large File Reading
的方法,本文将介绍这两种读取方法。
我们谈到“文本处理”时,我们通常是指处理的内容。Python 将文本文件的内容读入可以操作的字符串变量非常容易。文件对象提供了三个“读”方法: .read()
、.readline()
和 .readlines()
。每种方法可以接受一个变量以限制每次读取的数据量,但它们通常不使用变量。 .read()
每次读取整个文件,它通常用于将文件内容放到一个字符串变量中。然而 .read()
生成文件内容最直接的字符串表示,但对于连续的面向行的处理,它却是不必要的,并且如果文件大于可用内存,则不可能实现这种处理。下面是read()
方法示例:
|
|
调用read()
会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)
方法,每次最多读取size个字节的内容。另外,调用readline()
可以每次读取一行内容,调用readlines()
一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。
如果文件很小,read()
一次性读取最方便;如果不能确定文件大小,反复调用read(size)
比较保险;如果是配置文件,调用readlines()
最方便:
|
|
处理大文件是很容易想到的就是将大文件分割成若干小文件处理,处理完每个小文件后释放该部分内存。这里用了 iter & yield
:
|
|
with open()
with
语句打开和关闭文件,包括抛出一个内部块异常。for line in f
文件对象f
视为一个迭代器,会自动的采用缓冲IO
和内存管理,所以你不必担心大文件。
|
|
在使用python进行大文件读取时,应该让系统来处理,使用最简单的方式,交给解释器,就管好自己的工作就行了。
memoryError
错误和文件读取太慢的问题,后来找到了两种比较快Large File Reading
的方法,本文将介绍这两种读取方法。
A computer program is said to learn from experience E with respect to some class of tasks T and performance measure P, if its performance at tasks in T, as measured by P, improves with experience E.
Updating…
]]>单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式,也叫单子模式,是一种简单和常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
单例模式主要有3个特点:
(1). 单例类确保自己只有一个实例。
(2). 单例类必须自己创建自己的实例。
(3). 单例类必须为其他对象提供唯一的实例。
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance
这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。
单例模式是Java中最常用的模式之一,它通过阻止外部实例化和修改,来控制所创建的对象的数量。这个概念可以被推广到仅有一个对象能更高效运行的系统,或者限制对象实例化为特定的数目的系统中(From:Java Design Pattern: Singleton)。例如:
单例模式的实现方式有五种方法:懒汉,恶汉,静态内部类,枚举和双重校验锁。
懒汉方式,指全局的单例实例在第一次被使用时构建。注意线程安全与否。
线程不安全
|
|
这种写法lazy loading
很明显,但是致命的是在多线程不能正常工作。
线程安全
|
|
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading
,但是,遗憾的是,效率很低,99%情况下不需要同步。
饿汉方式,指全局的单例实例在类装载时构建。
|
|
这种方式基于classloder
机制避免了多线程的同步问题,不过,instance
在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance
方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance
显然没有达到lazy loading
的效果。
因为单例是静态的final
变量,当类第一次加载到内存中的时候就初始化了,其thread-safe
性由 JVM 来负责保证。
|
|
首先,其他类在引用这个Singleton
的类时,只是新建了一个引用,并没有开辟一个的堆空间存放(对象所在的内存空间)。接着,当使用Singleton.getInstance()
方法后,Java虚拟机(JVM)会加载SingletonHolder.class
(JLS规定每个class对象只能被初始化一次),并实例化一个Singleton
对象。这种方式的缺点是需要在Java的另外一个内存空间(Java PermGen 永久代内存,这块内存是虚拟机加载class文件存放的位置)占用一个大块的空间。
枚举单例(Enum Singleton)是实现单例模式的一种新方式,枚举这个特性是在Java5才出现的。《Effective Java》一书中有介绍这个特性,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
|
|
默认枚举实例的创建是线程安全的,但是在枚举中的其他任何方法由程序员自己负责。如果你正在使用实例方法,那么你需要确保线程安全(如果它影响到其他对象的状态的话)。传统单例存在的另外一个问题是一旦你实现了序列化接口,那么它们不再保持单例了,但是枚举单例,JVM对序列化有保证。枚举实现单例的好处:有序列化和线程安全的保证,代码简单。
|
|
当两个线程执行完第一个 singleton == null
后等待锁, 其中一个线程获得锁并进入synchronize
后,实例化了,然后退出释放锁,另外一个线程获得锁,进入又想实例化,会判断是否进行实例化了,如果存在,就不进行实例化了。
单例模式有五种写法:懒汉、饿汉、静态内部类、枚举、双重检验锁。一般来说,推荐使用饿汉和静态内部类两种方式。
Singleton 是最简单也最被滥用的模式。和其他设计模式一样,需要不断的实践经验的积累。对于 Beginner 来说了解一些基本的概念和模式即可,不要刻意追求设计模式。不断地看优秀的代码,努力写出高可阅读性代码才是王道。
References
]]>单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
circular reasoning
)。它也不是一个直观的过程;当我们指挥别人做事的时候,我们极少会递归地指挥他们。
递归算法是一种直接或者间接调用自身函数或者方法的算法。递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。递归算法对解决一大类问题很有效,它可以使算法简洁和易于理解。递归算法,其实说白了,就是程序的自身调用。它表现在一段程序中往往会遇到调用自身的那样一种coding策略,这样我们就可以利用大道至简的思想,把一个大的复杂的问题层层转换为一个小的和原问题相似的问题来求解的这样一种策略。递归往往能给我们带来非常简洁非常直观的代码形势,从而使我们的编码大大简化,然而递归的思维确实很我们的常规思维相逆的,我们通常都是从上而下的思维问题, 而递归趋势从下往上的进行思维。这样我们就能看到我们会用很少的语句解决了非常大的问题,所以递归策略的最主要体现就是小的代码量解决了非常复杂的问题。
递归算法解决问题的特点:
递归算法要求。递归算法所体现的“重复”一般有三个要求:
(1) 是每次调用在规模上都有所缩小(通常是减半);
(2) 是相邻两次重复之间有紧密的联系,前一次要为后一次做准备(通常前一次的输出就作为后一次的输入);
(3) 是在问题的规模极小时必须用直接给出解答而不再进行递归调用,因而每次递归调用都是有条件的(以规模未达到直接解答的大小为条件),无条件递归调用将会成为死循环而不能正常结束。
计算阶乘是递归程序设计的一个经典示例。计算某个数的阶乘就是用那个数去乘包括 1 在内的所有比它小的数。例如,factorial(5)
等价于 5*4*3*2*1
,而 factorial(3)
等价于 3*2*1
。
阶乘的一个有趣特性是,某个数的阶乘等于起始数(starting number)乘以比它小一的数的阶乘。例如,factorial(5)
与 5 * factorial(4)
相同。您很可能会像这样编写阶乘函数:
|
|
(注:本文的程序示例用C语言编写)
不过,这个函数的问题是,它会永远运行下去,因为它没有终止的地方。函数会连续不断地调用 factorial
。 当计算到零时,没有条件来停止它,所以它会继续调用零和负数的阶乘。因此,我们的函数需要一个条件,告诉它何时停止。
由于小于 1 的数的阶乘没有任何意义,所以我们在计算到数字 1 的时候停止,并返回 1 的阶乘(即 1)。因此,真正的递归函数类似于:
|
|
可见,只要初始值大于零,这个函数就能够终止。停止的位置称为 基线条件(base case)。基线条件是递归程序的 最底层位置,在此位置时没有必要再进行操作,可以直接返回一个结果。所有递归程序都必须至少拥有一个基线条件,而且 必须确保它们最终会达到某个基线条件;否则,程序将永远运行下去,直到程序缺少内存或者栈空间。
斐波纳契数列(Fibonacci Sequence),最开始用于描述兔子生长的数目时用上了这数列。从数学上,费波那契数列是以递归的方法来定义:
这样斐波纳契数列的递归程序就可以非常清晰的写出来了:
|
|
每一个递归程序都遵循相同的基本步骤:
(1) 初始化算法。递归程序通常需要一个开始时使用的种子值(seed value)。要完成此任务,可以向函数传递参数,或者提供一个入口函数, 这个函数是非递归的,但可以为递归计算设置种子值。
(2) 检查要处理的当前值是否已经与基线条件相匹配。如果匹配,则进行处理并返回值。
(3) 使用更小的或更简单的子问题(或多个子问题)来重新定义答案。
(4) 对子问题运行算法。
(5) 将结果合并入答案的表达式。
(6) 返回结果。
有时候,编写递归程序时难以获得更简单的子问题。 不过,使用 归纳定义的(inductively-defined
)数据集 可以令子问题的获得更为简单。归纳定义的数据集是根据自身定义的数据结构 —— 这叫做 归纳定义(inductive definition
)。
例如,链表就是根据其本身定义出来的。链表所包含的节点结构体由两部分构成:它所持有的数据,以及指向另一个节点结构体(或者是 NULL,结束链表)的指针。 由于节点结构体内部包含有一个指向节点结构体的指针,所以称之为是归纳定义的。
使用归纳数据编写递归过程非常简单。注意,与我们的递归程序非常类似,链表的定义也包括一个基线条件 —— 在这里是 NULL 指针。 由于 NULL 指针会结束一个链表,所以我们也可以使用 NULL 指针条件作为基于链表的很多递归程序的基线条件。
下面看两个例子。
让我们来看一些基于链表的递归函数示例。假定我们有一个数字列表,并且要将它们加起来。履行递归过程序列的每一个步骤,以确定它如何应用于我们的求和函数:
(1) 初始化算法。这个算法的种子值是要处理的第一个节点,将它作为参数传递给函数。
(2) 检查基线条件。程序需要检查确认当前节点是否为 NULL 列表。如果是,则返回零,因为一个空列表的所有成员的和为零。
(3) 使用更简单的子问题重新定义答案。我们可以将答案定义为当前节点的内容加上列表中其余部分的和。为了确定列表其余部分的和, 我们针对下一个节点来调用这个函数。
(4) 合并结果。递归调用之后,我们将当前节点的值加到递归调用的结果上。
这样我们就可以很简单的写出链表求和的递归程序,实例如下:
|
|
汉诺塔(Hanoi Tower)问题也是一个经典的递归问题,该问题描述如下:
汉诺塔问题:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上(如图)。有一个和尚想把这64个盘子从A座移到B座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上。
|
|
在下表中了解循环的特性,看它们可以如何与递归函数的特性相对比。
Properties | Loops | Recursive functions |
---|---|---|
重复 | 为了获得结果,反复执行同一代码块;以完成代码块或者执行 continue 命令信号而实现重复执行。 | 为了获得结果,反复执行同一代码块;以反复调用自己为信号而实现重复执行。 |
终止条件 | 为了确保能够终止,循环必须要有一个或多个能够使其终止的条件,而且必须保证它能在某种情况下满足这些条件的其中之一。 | 为了确保能够终止,递归函数需要有一个基线条件,令函数停止递归。 |
状态 | 循环进行时更新当前状态。 | 当前状态作为参数传递。 |
可见,递归函数与循环有很多类似之处。实际上,可以认为循环和递归函数是能够相互转换的。 区别在于,使用递归函数极少被迫修改任何一个变量 —— 只需要将新值作为参数传递给下一次函数调用。 这就使得您可以获得避免使用可更新变量的所有益处,同时能够进行重复的、有状态的行为。
下面还是以阶乘为例子,循环写法为:
|
|
递归写法在第二节中已经介绍过了,这里就不重复了,可以比较一下。
对于递归函数的使用,人们所关心的一个问题是栈空间的增长。确实,随着被调用次数的增加,某些种类的递归函数会线性地增加栈空间的使用 —— 不过,有一类函数,即尾部递归函数,不管递归有多深,栈的大小都保持不变。尾递归属于线性递归,更准确的说是线性递归的子集。
函数所做的最后一件事情是一个函数调用(递归的或者非递归的),这被称为 尾部调用(tail-call
)。使用尾部调用的递归称为 尾部递归。当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
让我们来看一些尾部调用和非尾部调用函数示例,以了解尾部调用的含义到底是什么:
|
|
可见,要使调用成为真正的尾部调用,在尾部调用函数返回之前,对其结果 不能执行任何其他操作。
注意,由于在函数中不再做任何事情,那个函数的实际的栈结构也就不需要了。惟一的问题是,很多程序设计语言和编译器不知道 如何除去没有用的栈结构。如果我们能找到一个除去这些不需要的栈结构的方法,那么我们的尾部递归函数就可以在固定大小的栈中运行。
在尾部调用之后除去栈结构的方法称为 尾部调用优化 。
那么这种优化是什么?我们可以通过询问其他问题来回答那个问题:
(1) 函数在尾部被调用之后,还需要使用哪个本地变量?哪个也不需要。
(2) 会对返回的值进行什么处理?什么处理也没有。
(3) 传递到函数的哪个参数将会被使用?哪个都没有。
好像一旦控制权传递给了尾部调用的函数,栈中就再也没有有用的内容了。虽然还占据着空间,但函数的栈结构此时实际上已经没有用了,因此,尾部调用优化就是要在尾部进行函数调用时使用下一个栈结构 覆盖 当前的栈结构,同时保持原来的返回地址。
我们所做的本质上是对栈进行处理。再也不需要活动记录(activation record
),所以我们将删掉它,并将尾部调用的函数重定向返回到调用我们的函数。 这意味着我们必须手工重新编写栈来仿造一个返回地址,以使得尾部调用的函数能直接返回到调用它的函数。
递归是一门伟大的艺术,使得程序的正确性更容易确认,而不需要牺牲性能,但这需要程序员以一种新的眼光来研究程序设计。对新程序员 来说,命令式程序设计通常是一个更为自然和直观的起点,这就是为什么大部分程序设计说明都集中关注命令式语言和方法的原因。 不过,随着程序越来越复杂,递归程序设计能够让程序员以可维护且逻辑一致的方式更好地组织代码。
References
]]>circular reasoning
)。它也不是一个直观的过程;当我们指挥别人做事的时候,我们极少会递归地指挥他们。
640K ought to be enough for everybody — Bill Gates 1981
程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆:就是那些由 new
分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new
就要对应一个 delete
。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区:就是那些由malloc
等分配的内存块,他和堆是十分相似的,不过它是用free
来结束自己的生命的。
全局/静态存储区:全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
堆与栈的区分问题,似乎是一个永恒的话题,由此可见,初学者对此往往是混淆不清的,所以我决定拿他第一个开刀。
首先,我们举一个例子:
|
|
这条短短的一句话就包含了堆与栈,看到new
,我们首先就应该想到,我们分配了一块堆内存,那么指针p
呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p
。在程序会先确定在堆中分配内存的大小,然后调用operator new
分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
这里,我们为了简单并没有释放内存,那么该怎么去释放呢?是delete p
么?澳,错了,应该是delete []p
,这是为了告诉编译器:我删除的是一个数组,编译器就会根据相应的Cookie
信息去进行释放内存的工作。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
主要的区别由以下几点:
(1). 管理方式不同
(2). 空间大小不同
(3). 能否产生碎片不同
(4). 生长方向不同
(5). 分配方式不同
(6). 分配效率不同
管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak
。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:
打开工程,依次操作菜单如下:Project->Setting->Link
,在Category
中选中Output
,然后在Reserve
中设定堆栈的最大值和commit
。
注意:reserve最小值为4Byte;commit
是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题:对于堆来讲,频繁的new/delete
势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca
函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete
的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候debug
可是相当困难的:)
在嵌入式系统中使用C++的一个常见问题是内存分配,即对new
和 delete
操作符的失控。
具有讽刺意味的是,问题的根源却是C++对内存的管理非常的容易而且安全。具体地说,当一个对象被消除时,它的析构函数能够安全的释放所分配的内存。
这当然是个好事情,但是这种使用的简单性使得程序员们过度使用new
和 delete
,而不注意在嵌入式C++环境中的因果关系。并且,在嵌入式系统中,由于内存的限制,频繁的动态分配不定大小的内存会引起很大的问题以及堆破碎的风险。
作为忠告,保守的使用内存分配是嵌入式环境中的第一原则。
但当你必须要使用new
和delete
时,你不得不控制C++中的内存分配。你需要用一个全局的new
和delete
来代替系统的内存分配符,并且一个类一个类的重载new
和delete
。
一个防止堆破碎的通用方法是从不同固定大小的内存持中分配不同类型的对象。对每个类重载new
和delete
就提供了这样的控制。
可以很容易地重载new 和 delete 操作符,如下所示:
|
|
这段代码可以代替默认的操作符来满足内存分配的请求。出于解释C++的目的,我们也可以直接调用malloc()
和free()
。
也可以对单个类的new
和 delete
操作符重载。这是你能灵活的控制对象的内存分配。
|
|
所有TestClass
对象的内存分配都采用这段代码。更进一步,任何从TestClass
继承的类也都采用这一方式,除非它自己也重载了new
和 delete
操作符。通过重载new
和 delete
操作符的方法,你可以自由地采用不同的分配策略,从不同的内存池中分配不同的类对象。
必须小心对象数组的分配。你可能希望调用到被你重载过的new
和 delete
操作符,但并不如此。内存的请求被定向到全局的new[]
和delete[]
操作符,而这些内存来自于系统堆。
C++将对象数组的内存分配作为一个单独的操作,而不同于单个对象的内存分配。为了改变这种方式,你同样需要重载new[]
和 delete[]
操作符。
|
|
但是注意:对于多数C++的实现,new[]
操作符中的个数参数是数组的大小加上额外的存储对象数目的一些字节。在你的内存分配机制重要考虑的这一点。你应该尽量避免分配对象数组,从而使你的内存分配策略简单。
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:
NULL
。如果指针p
是函数的参数,那么在函数的入口处用assert(p!=NULL)
进行检查。如果是用malloc
或new
来申请内存,应该用if(p==NULL)
或if(p!=NULL)
进行防错处理。for
循环语句中,循环次数很容易搞错,导致数组操作越界。malloc
与free
的使用次数一定要相同,否则肯定有错误(new/delete
同理)。 有三种情况:
(1). 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2). 函数的return
语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3). 使用free
或delete
释放了内存后,没有将指针设置为NULL
。导致产生“野指针”。
那么如何避免产生野指针呢?这里列出了5条规则,平常写程序时多注意一下,养成良好的习惯。
规则1:用
malloc
或new
申请内存之后,应该立即检查指针值是否为NULL
。防止使用指针值为NULL
的内存。
规则2:不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
规则3:避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
规则4:动态内存的申请与释放必须配对,防止内存泄漏。
规则5:用free
或delete
释放了内存之后,立即将指针设置为NULL
,防止产生“野指针”。
C++/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
下面以字符串为例比较指针与数组的特性。
下面示例中,字符数组a的容量是6个字符,其内容为 hello。a的内容可以改变,如a[0]= ‘X’
。指针p指向常量字符串“world”(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’
有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。
|
|
不能对数组名进行直接复制与比较。若想把数组a的内容复制给数组b,不能用语句 b = a
,否则将产生编译错误。应该用标准库函数strcpy
进行复制。同理,比较b和a的内容是否相同,不能用if(b==a)
来判断,应该用标准库函数strcmp
进行比较。
语句 p = a
并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc
为p申请一块容量为strlen(a)+1
个字符的内存,再用strcpy
进行字符串复制。同理,语句if(p==a)
比较的不是内容而是地址,应该用库函数strcmp
来比较。
|
|
用运算符sizeof
可以计算出数组的容量(字节数)。如下示例中,sizeof(a)
的值是12(注意别忘了’’)。指针p指向a,但是sizeof(p)
的值却是4。这是因为sizeof(p)
得到的是一个指针变量的字节数,相当于sizeof(char*)
,而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
|
|
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。如下示例中,不论数组a的容量是多少,sizeof(a)
始终等于sizeof(char *)
。
|
|
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如下示例中,Test函数的语句GetMemory(str, 200)
并没有使str
获得期望的内存,str
依旧是NULL
,为什么?
|
|
毛病出在函数GetMemory
中。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p
,编译器使 _p=p
。如果函数体内的程序修改了_p
的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p
申请了新的内存,只是把 _p
所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory
并不能输出任何东西。事实上,每执行一次GetMemory
就会泄露一块内存,因为没有用free
释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,见示例:
|
|
由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单,见示例:
|
|
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return
语句用错了。这里强调不要用return
语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,见示例:
|
|
用调试器逐步跟踪Test4
,发现执行str = GetString
语句后str
不再是NULL
指针,但是str
的内容不是“hello world”
而是垃圾。
如果把上述示例改写成如下示例,会怎么样?
|
|
函数Test5
运行虽然不会出错,但是函数GetString2
的设计概念却是错误的。因为GetString2
内的“hello world”
是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2
,它返回的始终是同一个“只读”的内存块。
“野指针”不是NULL
指针,是指向“垃圾”内存的指针。人们一般不会错用NULL
指针,因为用if
语句很容易判断。但是“野指针”是很危险的,if
语句对它不起作用。 “野指针”的成因主要有三种:
(1). 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如:
|
|
(2). 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
(3). 指针操作超越了变量的作用域范围。这种情况让人防不胜防,示例程序如下:
|
|
函数Test
在执行语句p->Func()
时,对象a已经消失,而p是指向a的,所以p就成了“野指针”。但奇怪的是我运行这个程序时居然没有出错,这可能与编译器有关。
malloc
与free
是C++/C语言的标准库函数,new/delete
是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free
无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free
是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free
。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new
,以及一个能完成清理与释放内存工作的运算符delet
e。注意new/delete
不是库函数。我们先看一看malloc/free
和new/delete
如何实现对象的动态内存管理,见示例:
|
|
类Obj
的函数Initialize
模拟了构造函数的功能,函数Destroy
模拟了析构函数的功能。函数UseMallocFree
中,由于malloc/free
不能执行构造函数与析构函数,必须调用成员函数Initialize
和Destroy
来完成初始化与清除工作。函数UseNewDelete
则简单得多。
所以我们不要企图用malloc/free
来完成动态对象的内存管理,应该用new/delete
。由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free
和new/delete
是等价的。
既然new/delete
的功能完全覆盖了malloc/free
,为什么C++不把malloc/free
淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free
管理动态内存。
如果用free
释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete
释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。所以new/delete
必须配对使用,malloc/free
也一样。
如果在申请动态内存时找不到足够大的内存块,malloc
和new
将返回NULL
指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
(1). 判断指针是否为NULL
,如果是则马上用return
语句终止本函数。例如:
|
|
(2). 判断指针是否为NULL
,如果是则马上用exit(1)
终止整个程序的运行。例如:
|
|
(3). 为new
和malloc
设置异常处理函数。例如Visual C++可以用_set_new_hander
函数为new
设置用户自己定义的异常处理函数,也可以让malloc
享用与new
相同的异常处理函数。详细内容请参考C++使用手册。
上述 (1)、(2) 方式使用最普遍。如果一个函数内有多处需要申请动态内存,那么方式 (1) 就显得力不从心(释放内存很麻烦),应该用方式 (2) 来处理。
很多人不忍心用exit(1)
,问:“不编写出错处理程序,让操作系统自己解决行不行?”
不行。如果发生“内存耗尽”这样的事情,一般说来应用程序已经无药可救。如果不用exit(1)
把坏程序杀死,它可能会害死操作系统。道理如同:如果不把歹徒击毙,歹徒在老死之前会犯下更多的罪。
有一个很重要的现象要告诉大家。对于32位以上的应用程序而言,无论怎样使用malloc与new
,几乎不可能导致“内存耗尽”。对于32位以上的应用程序,“内存耗尽”错误处理程序毫无用处。这下可把Unix和Windows程序员们乐坏了:反正错误处理程序不起作用,我就不写了,省了很多麻烦。
必须强调:不加错误处理将导致程序的质量很差,千万不可因小失大。
|
|
函数malloc
的原型如下:
|
|
用malloc
申请一块长度为length
的整数类型的内存,程序如下:
|
|
我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
* malloc
返回值的类型是void*
,所以在调用malloc
时要显式地进行类型转换,将void *
转换成所需要的指针类型。
* malloc
函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住int
, float
等数据类型的变量的确切字节数。例如int
变量在16位系统下是2个字节,在32位下是4个字节;而float
变量在16位系统下是4个字节,在32位下也是4个字节。最好用以下程序作一次测试:
|
|
在malloc
的“()”中使用sizeof
运算符是良好的风格,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))
这样的程序来。
函数free
的原型如下:
|
|
为什么free
函数不象mallo
c函数那样复杂呢?这是因为指针p
的类型以及它所指的内存的容量事先都是知道的,语句free(p)
能正确地释放内存。如果p
是NULL
指针,那么free
对p
无论操作多少次都不会出问题。如果p
不是NULL
指针,那么free
对p
连续操作两次就会导致程序运行错误。
运算符new
使用起来要比函数malloc
简单得多,例如:
|
|
这是因为new
内置了sizeof
、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new
在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new
的语句也可以有多种形式。例如:
|
|
如果用new
创建对象数组,那么只能使用对象的无参数构造函数。例如:
|
|
不能写成:
|
|
在用delete
释放对象数组时,留意不要丢了符号‘[]’。例如:
|
|
后者有可能引起程序崩溃和内存泄漏。