开学前接了一个任务,内容是从网上爬取特定属性的数据。正好之前学了python,练练手。
因为涉及到中文,所以必然地涉及到了编码的问题,这一次借这个机会算是彻底搞清楚了。
问题要从文字的编码讲起。原本的英文编码只有0~255,刚好是8位1个字节。为了表示各种不同的语言,自然要进行扩充。中文的话有GB系列。可能还听说过Unicode和UTF-8,那么,它们之间是什么关系呢?
Unicode是一种编码方案,又称万国码,可见其包含之广。但是具体存储到计算机上,并不用这种编码,可以说它起着一个中间人的作用。你可以再把Unicode编码(encode)为UTF-8,或者GB,再存储到计算机上。UTF-8或者GB也可以进行解码(decode)还原为Unicode。
在python中Unicode是一类对象,表现为以u打头的,比如u'中文',而string又是一类对象,是在具体编码方式下的实际存在计算机上的字符串。比如utf-8编码下的'中文'和gbk编码下的'中文',并不相同。可以看如下代码:
1
2
3
4
5
6
7
8
9
|
>>>
str
=
u
'中文'
>>> str1
=
str
.encode(
'utf8'
)
>>> str2
=
str
.encode(
'gbk'
)
>>>
print
repr
(
str
)
u
'\u4e2d\u6587'
>>>
print
repr
(str1)
'\xe4\xb8\xad\xe6\x96\x87'
>>>
print
repr
(str2)
'\xd6\xd0\xce\xc4'
|
可以看到,其实存储在计算机中的只是这样的编码,而不是一个一个的汉字,在print的时候要知道当时是用的什么样的编码方式,才能正确的print出来。有一个说法提得很好,python中的Unicode才是真正的字符串,而string是字节串
既然有不同的编码,那么如果在代码文件中直接写string的话,那么它到底是哪一种编码呢?这个就是由文件的编码所决定的。文件总是以一定的编码方式保存的。而python文件可以写上coding的声明语句,用来说明这个文件是用什么编码方式保存的。如果声明的编码方式和实际保存的编码方式不一致就会出现异常。可以见下面例子: 以utf-8保存的文件声明为gbk
1
2
3
4
5
6
7
8
9
|
#coding:gbk
str
=
u
'汉'
str1
=
str
.encode(
'utf8'
)
str2
=
str
.encode(
'gbk'
)
str3
=
'汉'
print
repr
(
str
)
print
repr
(str1)
print
repr
(str2)
print
repr
(str3)
|
提示错误 File "test.py", line 1 SyntaxError: Non-ASCII character '\xe6' in file test.py on line 1, but no encodi ng declared; see http://www.python.org/peps/pep-0263.html for details 改为
1
2
3
4
5
6
7
8
9
|
#coding:utf8
str
=
u
'汉'
str1
=
str
.encode(
'utf8'
)
str2
=
str
.encode(
'gbk'
)
str3
=
'汉'
print
repr
(
str
)
print
repr
(str1)
print
repr
(str2)
print
repr
(str3)
|
输出正常结果 u'\u6c49' '\xe6\xb1\x89' '\xba\xba' '\xe6\xb1\x89'
更多内容可参见这篇文章http://www.cnblogs.com/huxi/archive/2010/12/05/1897271.html
其实用python爬取网页很简单,只有简单的几句话
1
2
|
import
urllib2
page
=
urllib2.urlopen(
'url'
).read()
|
这样就可以获得到页面的内容。接下来再用正则匹配去匹配所需要的内容就行了。
但是,真正要做起来,就会有各种各样的细节问题。
这是一个需要登录认证的网站。也不太难,只要导入cookielib和urllib库就行。
1
2
3
|
import
urllib,urllib2,cookielib
cookiejar
=
cookielib.CookieJar()
urlOpener
=
urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
这样就装载进一个cookie,用urlOpener去open登录以后就可以记住信息。
如果只是做到上面的程度,不对open进行包装的话,只要网络状况有些起伏,就直接抛出异常,退出整个程序,是个很不好的程序。这个时候,只要对异常进行处理,多试几次就行了:
1
2
3
4
5
6
7
8
9
|
def
multi_open(opener,
*
arg):
while
True
:
retryTimes
=
20
while
retryTimes>
0
:
try
:
return
opener.
open
(
*
arg)
except
:
print
'.'
,
retryTimes
-
=
1
|
其实正则匹配并不算是一个特别好的方法,因为它的容错性很不好,网页要完全统一。如果有稍微的不统一,就会失败。后来看到说有根据xpath来进行选取的,下次可以尝试一下。
写正则其实是有一定技巧的:
这次的数据是放进Excel的。到后面才意识到如果放进数据库的话,可能就没有那么多事了。但是已经写到一半,难以回头了。
搜索Excel,可以得出几个方案来,一个是用xlrt/xlwt库,这个不管电脑上是否安装了Excel,都可以运行,但只能是xls格式的。还有一个是直接包装了com,需要电脑上安装了软件才行。我采用的是前一种。
基本的读写没有问题。但是数据量一大起来,就有问题了。
结合以上两点,最终采取了这么一个策略,如果行数是1000的倍数,进行一次flush,如果行数超过65536,新开一个sheet,如果超过3个sheet,则新建一个文件。为了方便,把xlwt包装了一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
#coding:utf-8#
import
xlwt
class
XLS:
'''a class wrap the xlwt'''
MAX_ROW
=
65536
MAX_SHEET_NUM
=
3
def
__init__(
self
,name,captionList,typeList,encoding
=
'utf8'
,flushBound
=
1000
):
self
.name
=
name
self
.captionList
=
captionList[:]
self
.typeList
=
typeList[:]
self
.workbookIndex
=
1
self
.encoding
=
encoding
self
.wb
=
xlwt.Workbook(encoding
=
self
.encoding)
self
.sheetIndex
=
1
self
.__addSheet()
self
.flushBound
=
flushBound
def
__addSheet(
self
):
if
self
.sheetIndex !
=
1
:
self
.wb.save(
self
.name
+
str
(
self
.workbookIndex)
+
'.xls'
)
if
self
.sheetIndex>XLS.MAX_SHEET_NUM:
self
.workbookIndex
+
=
1
self
.wb
=
xlwt.Workbook(encoding
=
self
.encoding)
self
.sheetIndex
=
1
self
.sheet
=
self
.wb.add_sheet(
self
.name.encode(
self
.encoding)
+
str
(
self
.sheetIndex))
for
i
in
range
(
len
(
self
.captionList)):
self
.sheet.write(
0
,i,
self
.captionList[i])
self
.row
=
1
def
write(
self
,data):
if
self
.row>
=
XLS.MAX_ROW:
self
.sheetIndex
+
=
1
self
.__addSheet()
for
i
in
range
(
len
(data)):
if
self
.typeList[i]
=
=
"num"
:
try
:
self
.sheet.write(
self
.row,i,
float
(data[i]))
except
ValueError:
pass
else
:
self
.sheet.write(
self
.row,i,data[i])
if
self
.row
%
self
.flushBound
=
=
0
:
self
.sheet.flush_row_data()
self
.row
+
=
1
def
save(
self
):
self
.wb.save(
self
.name
+
str
(
self
.workbookIndex)
+
'.xls'
)
|
由于网页也有自己独特的转义字符,在进行正则匹配的时候就有些麻烦。在官方文档中查到一个用字典替换的方案,私以为不错,拿来做了一些扩充。其中有一些是为保持正则的正确性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
html_escape_table
=
{
"&"
:
"&"
,
'"'
: """,
"'"
:
"'"
,
">"
:
">"
,
"<"
:
"<"
,
u
"·"
:
"·"
,
u
"°"
:
"°"
,
#regular expression
"."
:r
"\."
,
"^"
:r
"\^"
,
"$"
:r
"\$"
,
"{"
:r
"\{"
,
"}"
:r
"\}"
,
"\\":r"
\\",
"|"
:r
"\|"
,
"("
:r
"\("
,
")"
:r
"\)"
,
"+"
:r
"\+"
,
"*"
:r
"\*"
,
"?"
:r
"\?"
,
}
def
html_escape(text):
"""Produce entities within text."""
tmp
=
"".join(html_escape_table.get(c,c)
for
c
in
text)
return
tmp.encode(
"utf-8"
)
|
得出的经验差不多就是这些了。不过最后写出来的程序自已也不忍再看。风格很不好。一开始想着先写着试试。然后试着试着就不想改了。
最终的程序要跑很久,其中网络通信时间占了大部分。是不是可以考虑用多线程重构一下?想想,还是就这样吧。
一、引言
分类算法有很多,不同分分类算法又用很多不同的变种。不同的分类算法有不同的特定,在不同的数据集上表现的效果也不同,我们需要根据特定的任务进行算法的选择,如何选择分类,如何评价一个分类算法的好坏,前面关于决策树的介绍,我们主要用的正确率(accuracy)来评价分类算法。
正确率确实是一个很好很直观的评价指标,但是有时候正确率高并不能代表一个算法就好。比如某个地区某天地震的预测,假设我们有一堆的特征作为地震分类的属性,类别只有两个:0:不发生地震、1:发生地震。一个不加思考的分类器,对每一个测试用例都将类别划分为0,那那么它就可能达到99%的正确率,但真的地震来临时,这个分类器毫无察觉,这个人类带来的损失是巨大的。为什么99%的正确率的分类器却不是我们想要的,因为这里数据分布不均衡,类别1的数据太少,完全错分类别1依然可以达到很高的正确率却忽视了我们关注的东西。接下来详细介绍一下分类算法的评价指标。
二、评价指标
1、几个常用的术语
这里首先介绍几个常见的模型评价术语,现在假设我们的分类目标只有两类,计为正例(positive)和负例(negtive)分别是:
1)True positives(TP): 被正确地划分为正例的个数,即实际为正例且被分类器划分为正例的实例数(样本数);
2)False positives(FP): 被错误地划分为正例的个数,即实际为负例但被分类器划分为正例的实例数;
3)False negatives(FN):被错误地划分为负例的个数,即实际为正例但被分类器划分为负例的实例数;
4)True negatives(TN): 被正确地划分为负例的个数,即实际为负例且被分类器划分为负例的实例数。
实 际 类 别 |
预测类别 |
|||
|
Yes |
No |
总计 |
|
Yes |
TP |
FN |
P(实际为Yes) |
|
No |
FP |
TN |
N(实际为No) |
|
总计 |
P’(被分为Yes) |
N’(被分为No) |
P+N |
上图是这四个术语的混淆矩阵,我只知道FP叫伪阳率,其他的怎么称呼就不详了。注意P=TP+FN表示实际为正例的样本个数,我曾经误以为实际为正例的样本数应该为TP+FP,这里只要记住True、False描述的是分类器是否判断正确,Positive、Negative是分类器的分类结果。如果正例计为1、负例计为-1,即positive=1、negtive=-1,用1表示True,-1表示False,那么实际的类标=TF*PN,TF为true或false,PN为positive或negtive。例如True positives(TP)的实际类标=1*1=1为正例,False positives(FP)的实际类标=(-1)*1=-1为负例,False negatives(FN)的实际类标=(-1)*(-1)=1为正例,True negatives(TN)的实际类标=1*(-1)=-1为负例。
2、评价指标
1)正确率(accuracy)
正确率是我们最常见的评价指标,accuracy = (TP+TN)/(P+N),这个很容易理解,就是被分对的样本数除以所有的样本数,通常来说,正确率越高,分类器越好;
2)错误率(error rate)
错误率则与正确率相反,描述被分类器错分的比例,error rate = (FP+FN)/(P+N),对某一个实例来说,分对与分错是互斥事件,所以accuracy =1 - error rate;
3)灵敏度(sensitive)
sensitive = TP/P,表示的是所有正例中被分对的比例,衡量了分类器对正例的识别能力;
4)特效度(specificity)
specificity = TN/N,表示的是所有负例中被分对的比例,衡量了分类器对负例的识别能力;
5)精度(precision)
精度是精确性的度量,表示被分为正例的示例中实际为正例的比例,precision=TP/(TP+FP);
6)召回率(recall)
召回率是覆盖面的度量,度量有多个正例被分为正例,recall=TP/(TP+FN)=TP/P=sensitive,可以看到召回率与灵敏度是一样的。
7)其他评价指标
对于某个具体的分类器而言,我们不可能同时提高所有上面介绍的指标,当然,如果一个分类器能正确分对所有的实例,那么各项指标都已经达到最优,但这样的分类器往往不存在。比如我们开头说的地震预测,没有谁能准确预测地震的发生,但我们能容忍一定程度的误报,假设1000次预测中,有5次预测为发现地震,其中一次真的发生了地震,而其他4次为误报,那么正确率从原来的999/1000-99.9%下降到996/1000=99.6,但召回率从0/1=0%上升为1/1=100%,这样虽然谎报了几次地震,但真的地震来临时,我们没有错过,这样的分类器才是我们想要的,在一定正确率的前提下,我们要求分类器的召回率尽可能的高。