python爬取网站数据保存使用的方法


Posted in Python onNovember 20, 2013

编码问题
因为涉及到中文,所以必然地涉及到了编码的问题,这一次借这个机会算是彻底搞清楚了。
问题要从文字的编码讲起。原本的英文编码只有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编码下的'中文',并不相同。可以看如下代码:

>>> 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

#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 改为

#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'

基本方法
其实用python爬取网页很简单,只有简单的几句话

import urllib2
page=urllib2.urlopen('url').read()

这样就可以获得到页面的内容。接下来再用正则匹配去匹配所需要的内容就行了。
但是,真正要做起来,就会有各种各样的细节问题。
登录
这是一个需要登录认证的网站。也不太难,只要导入cookielib和urllib库就行。

import urllib,urllib2,cookielib
cookiejar = cookielib.CookieJar()
urlOpener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))

这样就装载进一个cookie,用urlOpener去open登录以后就可以记住信息。
断线重连
如果只是做到上面的程度,不对open进行包装的话,只要网络状况有些起伏,就直接抛出异常,退出整个程序,是个很不好的程序。这个时候,只要对异常进行处理,多试几次就行了:

def multi_open(opener,*arg):
    while True:
        retryTimes=20
        while retryTimes>0:
            try:
                return opener.open(*arg)
            except:
                print '.',
                retryTimes-=1

正则匹配
其实正则匹配并不算是一个特别好的方法,因为它的容错性很不好,网页要完全统一。如果有稍微的不统一,就会失败。后来看到说有根据xpath来进行选取的,下次可以尝试一下。
写正则其实是有一定技巧的:
非贪婪匹配。比如这样一个标签:<span class='a'>hello</span>,要取出a来,如果写成这样的表达式,就不行了:<span class=.*>hello</span>。因为*进行了贪婪匹配。这是要用.?:<span class=.?>hello</span>。
跨行匹配。实现跨行有一种思路是运用DOTALL标志位,这样.就会匹配到换行。但是这样一来,整个匹配过程就会变得很慢。本来的匹配是以行为单位的。整个过程最多就是O(nc2),n是行数,c是平均列数。现在极有可能变为O((nc)2)。我的实现方案是运用\n来匹配换行,这样可以明确指出匹配最多跨跃多少行。比如:abc\s*\n\s*def,就指出查找的是隔一行的。(.\n)?就可以指定是匹配尽可能少的行。
这里其实还要注意一个点。有的行末是带有\r的。也就是说一行是以\r\n结尾的。当初不知道这一点,正则就调试了很久。现在直接用\s,表示行末空格和\r。
无捕获分组。为了不对捕获的分组造成影响,上面的(.\n)可以改为(?:.\n),这样捕获分组时,就会忽略它。
单括号要进行转义。因为单括号在正则里是用来表示分组的,所以为了匹配单括号就进行转义。正则字符串最好用的是带有r前缀的字符串,如果不是的话,则要对\再进行转义。
快速正则。写了那么多模式,也总结出一规律出来。先把要匹配的字符相关的段落拿出来。要匹配的东西用(.?)代替。把换行\n替换为字符串\s\n\s*,再去掉行首行末的空格。整个过程在vim中可以很快就写好。
Excel操作
这次的数据是放进Excel的。到后面才意识到如果放进数据库的话,可能就没有那么多事了。但是已经写到一半,难以回头了。
搜索Excel,可以得出几个方案来,一个是用xlrt/xlwt库,这个不管电脑上是否安装了Excel,都可以运行,但只能是xls格式的。还有一个是直接包装了com,需要电脑上安装了软件才行。我采用的是前一种。
基本的读写没有问题。但是数据量一大起来,就有问题了。
内存不够。程序一跑起来,内存占用就一点一点往上涨。后面再查了一下,知道要用flush_row_data。但是还是会出错。一看内存占用,没有什么问题,一直很平稳。但最后还是会出现memory error。这真是见鬼了。又是反复地查, 反复地运行。一点结果都没有。要命的是bug只在数据量大起来才出现,而等数据量大起来往往要好几个小时,这debug的成本实在是太高了。一个偶然的机会,突然发现内存占用,虽然总体平稳,但是会规律性的出现小的高涨,而这规律性,会不会和flush_row_data,有关。一直疑惑的是data被flush到了哪里。原来xlwt的作法是很蛋疼的作法。把数据存在内存里,或者flush到一个temp,到save的时候,再一次性写入。而问题正出在这一次性写入,内存猛涨。那我要flush_row_data何用?为什么不一开始就flush进要写入的地方。
行数限制。这个是xls格式本身决定的,最多行数只能是65536。而且数据一大,文件打开也不方便。
结合以上两点,最终采取了这么一个策略,如果行数是1000的倍数,进行一次flush,如果行数超过65536,新开一个sheet,如果超过3个sheet,则新建一个文件。为了方便,把xlwt包装了一下

#coding:utf-8#
import xlwtclass 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')

转换网页特殊字符
由于网页也有自己独特的转义字符,在进行正则匹配的时候就有些麻烦。在官方文档中查到一个用字典替换的方案,私以为不错,拿来做了一些扩充。其中有一些是为保持正则的正确性。

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")


得出的经验差不多就是这些了。不过最后写出来的程序自已也不忍再看。风格很不好。一开始想着先写着试试。然后试着试着就不想改了。
最终的程序要跑很久,其中网络通信时间占了大部分。是不是可以考虑用多线程重构一下?想想,还是就这样吧。

Python 相关文章推荐
python使用内存zipfile对象在内存中打包文件示例
Apr 30 Python
python实现的解析crontab配置文件代码
Jun 30 Python
使用python3.5仿微软记事本notepad
Jun 15 Python
Python I/O与进程的详细讲解
Mar 08 Python
python 求1-100之间的奇数或者偶数之和的实例
Jun 11 Python
python print出共轭复数的方法详解
Jun 25 Python
利用Python代码实现一键抠背景功能
Dec 29 Python
tensorflow 只恢复部分模型参数的实例
Jan 06 Python
Keras使用tensorboard显示训练过程的实例
Feb 15 Python
解决Python图形界面中设置尺寸的问题
Mar 05 Python
Python中的None与 NULL(即空字符)的区别详解
Sep 24 Python
python库sklearn常用操作
Aug 23 Python
使用python搭建Django应用程序步骤及版本冲突问题解决
Nov 19 #Python
python解析json实例方法
Nov 19 #Python
python聊天程序实例代码分享
Nov 18 #Python
python实现系统状态监测和故障转移实例方法
Nov 18 #Python
python线程池的实现实例
Nov 18 #Python
python批量导出导入MySQL用户的方法
Nov 15 #Python
python连接MySQL、MongoDB、Redis、memcache等数据库的方法
Nov 15 #Python
You might like
基于PHP+MySQL的聊天室设计
2006/10/09 PHP
php部分常见问题总结
2008/03/27 PHP
PHP基础陷阱题(变量赋值)
2012/09/12 PHP
解析linux下安装memcacheq(mcq)全过程笔记
2013/06/27 PHP
ThinkPHP实例化模型的四种方法概述
2014/08/22 PHP
php版微信公众平台接口参数调试实现判断用户行为的方法
2016/09/23 PHP
JavaScript 事件查询综合
2009/07/13 Javascript
jquery入门——事件机制之事件中的冒泡现象示例解释
2020/09/12 Javascript
js修改原型的属性使用介绍
2014/01/26 Javascript
在jQuery中使用$而避免跟其它库产生冲突的方法
2015/08/13 Javascript
JavaScript中的定时器之Item23的合理使用
2015/10/30 Javascript
json的使用小结
2016/06/08 Javascript
一句jQuery代码实现返回顶部效果(简单实用)
2016/12/28 Javascript
原生JS实现几个常用DOM操作API实例
2017/01/19 Javascript
jquery easyui dataGrid动态改变排序字段名的方法
2017/03/02 Javascript
详解react如何在组件中获取路由参数
2017/06/15 Javascript
实例讲解v-if和v-show的区别
2019/01/31 Javascript
微信小程序使用字体图标的方法
2019/05/23 Javascript
[02:20]DOTA2英雄基础教程 黑暗贤者
2013/12/19 DOTA
[03:22]DSPL第一期精彩集锦:酷炫到底!
2014/11/07 DOTA
Python open读写文件实现脚本
2008/09/06 Python
Python程序设计入门(5)类的使用简介
2014/06/16 Python
详解Python中的分组函数groupby和itertools)
2018/07/11 Python
详解Python 爬取13个旅游城市,告诉你五一大家最爱去哪玩?
2019/05/07 Python
Python Django实现layui风格+django分页功能的例子
2019/08/29 Python
python批量处理txt文件的实例代码
2020/01/13 Python
解决Tensorflow sess.run导致的内存溢出问题
2020/02/05 Python
HTML5 form标签之解放表单验证、增加文件上传、集成拖放的使用方法
2013/04/24 HTML / CSS
石油工程专业毕业生求职信
2014/04/13 职场文书
大学生英语演讲稿
2014/04/24 职场文书
绿色环保演讲稿
2014/05/10 职场文书
公司领导班子对照检查材料
2014/09/24 职场文书
考试作弊被抓检讨书
2014/10/02 职场文书
党的群众路线教育实践活动领导班子整改措施
2014/10/28 职场文书
详解MySql中InnoDB存储引擎中的各种锁
2022/02/12 MySQL
前端vue+express实现文件的上传下载示例
2022/02/18 Vue.js