前言

前面python爬虫(中)–提取,讲的是提取出来的数据保存进一个extracted_data,再保存进extracted_data_,变成一个list包含list的情况,当然你只提取一项,那就没有必要这么做了,可是我的项目中要求可能要提取十几二十项,我为了后面入库方便,所以前面做了这么一个工作。

到提取为止,基本爬虫差保存就完成了,什么是基本爬虫,基本爬虫=请求+提取+保存,而不考虑一些针对反反爬的策略制定等情况。现在我们请求函数有了,提取函数有了,现在就差一个保存函数了。我们这里选择的是保存到MySQL数据库中,当然你也可以保存成为.html等格式的文本,我们保存成数据库是为了后面方便使用考虑.不过在保存之前请认真考虑一下你的数据建模问题.

数据建模不是单独存在的,更多的是为了入库而准备的,根据自己需要的数据有多少种类和每种数据的特征,甚至还要考虑到大数据量存储的问题和未来要提数据的场景,来设计数据库中的表,这里工作很多,很考验数据结构的基本功,换言之是一个很费脑,而且要发挥想象力的步骤,所以做这一步的时候最好不要在昏昏欲睡的时候做,要在头脑清晰最为适宜.


保存到文件

简单提提文件保存,先上个保存成HTML文件为例子:

#! /usr/bin/env python
#encoding:utf-8

from os import makedirs
from os.path import exists
import codecs
import json

def save_As_HTMLFile(directory,extracted_data_):
	
	if  isinstance(extracted_data_,list) :
		print 'the sourceCode you put in save_As_HTMLFile() is a list!!Here will transform list into string\n'
		#json用在列表/字典的转unicode上
		extracted_data_=json.dumps(extracted_data_,encoding="utf-8",ensure_ascii=False)

	if not exists(directory):
		makedirs(directory)
		print 'Finish Making Directory  ',directory

	filename=directory+'extracted_data_from_phantomjs.html'

	f=codecs.open(filename,'wb')#wb是以二进制写模式打开(打开前文件会被清空)更多的看下面注意第四点
	print 'saving data...\n'
	f.write(extracted_data_)#write()里面要放的是str/buffer,不能是JPEG或者其他奇奇怪怪的东西
	print 'Saved.\n'	
	f.close()

	return

要注意一下几点:

if isinstance(extracted_data_,list)这里是为了应付之前extracted_data_传进来是list的情况,如果这个if判断到传进来的extracted_data_ 是个列表,那么就返回True。然后用json模块中的json.dumps()的方法直接转换成字符串格式,unicode编码的文本~

codecs模块的应用,知道python的朋友应该知道其实不用codecs这个模块,把 f=codecs.open(filename,’wb’)换成f=open(filename,’wb’)也是一样的效果,那么为什么要用codecs.open()的方法呢?
答:先来讲讲历史,最早的时候只有open(),可是你也知道python2的编码问题是有多么蛋疼(所以学习python得善用type()方法和chardet.detect()),所以就有了codecs.open()。而io.open(),其实是因为python2的open()是由File模块提供的,而python3的open()是由io模块提供的。然后,python2.6引入python3的特性,为了区别于原来由File模块提供的open(),故叫io.open()。python3建议直接用open(),而python2.*用codecs.open()特别是有中文的情况下,如果希望代码能同时是兼容python2和3,就直接用codecs.open(),这就是为什么我们在上面代码里面用codecs.open()的原因,主要考虑compatible的问题。

codecs.open()和open()[指io.open()]使用上的区别?
Answer : io.open() doesn’t always produce unicode strings,if you pass ‘rb’ as the mode,the file is opened in a binary mode,not text mode,and read() method will return bytes. codecs.open() always open the file in binary mode.

open(),io.open()还是codecs.open()中模式名称用法是一样的,都可以直接参考open()参数mode那一项的设置——详细请见博客

P.s.既然讲到了python的文件保存,那就顺便说说python中获取用户输入的方法——raw_input()和input():

raw_input():任何都可以输入,无论输入什么都会当成字符串 input() : 希望用户的输入的是一个合法python表达式,比如你输入字符串时必须使用引号(单双引号都可以)引起来,不然就报SyntaxError

本质上input()还是调用的是raw_input()实现的,只是调用完raw_input()后再调用eval()

※除非是对input()有需求,不然一般情况推荐raw_input() 注意:Python3里面就只有input()了,一切用户输入皆视为字符串(str),而raw_input()弃用


SQL or NoSQL?

SQL or NoSQL? It’s a problem. 在SQL还是NoSQL选择上我是还没有深究的,初步是用关系型数据库的代表——MySQL来做着,但是关系型型数据库有着它的瓶颈,具体瓶颈在什么地方和到什么程度,我没有亲身去试验过,这里仅仅是说根据网上各种说法所得,貌似NoSQL(Redis/MongoDB为代表)比较适合大剂量的数据存储?至于应该怎么选择,各家优劣比拼,容我先在这里留个坑,等我过段时间折腾过后,再回来填坑。
16.11.19更新:这段时间对各种数据库的调查和接触,发现SQL语言是数据库的基础,无论你是SQL还是NoSQL还是现在HBase等等都离不开SQL(可能语法上会有些不一样,但是都是相似的,毕竟工具都是用熟悉了SQL的工程师开发的)


从Redis中拿数据


保存到MySQL

首先,为了更加具体而且没有那么枯燥,我先放张图——MySQL层次: 这里写图片描述

关于选择MySQL的理由:关系型数据库中最有代表性,而且开源中的战斗机就非MySQL关系型数据库莫属了,很多中小型数据量管理和存储的场合都能见到它的身影,所以网上会有很多支持和帮助,所以我们在SQL关系型数据库中就选择了MySQL入手。

如果要用关系型数据库,那么SQL语句是你首先要过的关,要向你对待Python或其他语言一样对待它,他和编程语言是同等重要的,要很好的操作数据库,必须把SQL弄熟,不介意的话可以参考我的这篇SQL语句基础 :-P

而在Python中则是通过MySQLdb 这个模块完成与数据库链接等操作,然而有些系统并没有装这个模块,那么就要sudo pip install MySQL-python安装之。[吐槽:为什么这里模块名和pip install的名字不一样?]

还有因为MySQLdb是C编写的,因此需要C的编译器,而且有可能在window中安装 MySQLdb时候会出现问题(Error:unable to find vcvarsall.bat),外加python3不支持MySQLdb, 所以你会发现有些项目会用到pymysql这个模块替代MySQLdb模块,因为这个模块是纯种python编写的的mysql链接模块,只要你的python能跑,它就能正常运作,具备对付python2和python3很强的兼容性,虽然效率上会比MySQLdb慢上许多,如果你对效率有需求,在python3的世界中还有几种MySQLdb的替代,比如mysqlclient(A fork of MySQLdb with Python3)或者使用MySQL提供的模块亦可. ORM?SQLAICherry?peewee?

16.10.8更新: pip install MySQL时候可能会出现错误

EnvironmentError: mysql_config not found

Command:"python setup.py egg_info" fail with Error Code 1 /tmp/pip-build-foo/MySQL-python/

解决办法:sudo apt-get install libmysqlclient-dev 少数情况下还要把python-dev装上

这里存入MySQL数据库实现起来其实并不复杂,而且都是套路:

def save_As_Mysql(extracted_data_):

	print 'connecting to mysql....'
	#套路一.链接数据库,别忘记开启MySQL服务
	conn=MySQLdb.connect(

		host='localhost',
		port=3306,
		user='root',
		passwd='填你的Mysql数据库密码',
		db='填你要接到的db名字',
		charset='utf8'
	)#charset指定连接编码格式

	#套路二.创建游标
	cur=conn.cursor()

	#测试用
	# sql_dropTable='drop table if exists '+str(from_city)+'到'+str(to_city)
	# cur.execute(sql_dropTable)
	# print '删除原表成功'

	#套路三.创建表
	sql_newTable='create table if not exists table_name(Field1 varchar(10),Field2 varchar(20))'
	cur.execute(sql_newTable)
	print '新表建立成功'
	
    #套路四.往表中插入数据
	sql_insert='insert into table_name values(%s,%s)'
	cur.executemany(sql_insert,extracted_data_)#extracted_data_列表中列表
	#cur.fetchall()
	conn.commit()
	cur.close()
	conn.close()

更加准确来说其实MySQLdb要用到的只是套路一的链接数据库,套路二的创建游标,套路三的execute(sql,list/tuple)单次执行sql语句,套路四executemany(sql,[list,list,…]/[tuple,tuple,…])多次执行sql语句,conn.commit()把已经有的修改动作真实提交,cur.close()游标关闭,conn.close()链接关闭. 这些东西都是死的,都是一些包装的东西,我们真正起作用的地方是SQL语句的设计编写,MySQLdb只是负责execute或者executemany罢了

说明几点:
①MySQL中的字段类型之字符串
1.char(n):
定长,char不管实际value长度,都会占用固定的几个字符空间,速度快,而且处理字符串尾部空格,超过设置长度n的部分会截断
2.varchar(n):
变长,速度慢,不处理字符串尾部空格(UpperLimit:65535字节),超过设置长度n的部分会截断
3.text:
速度慢,不处理字符串尾部空格,而且会用额外空间去存放数据长度这个量

P.s.char(n)和varchar(n)中的n代表字符个数,并非代表字节个数,当使用中文(utf-8)时,意味着可以插入m个汉字,但是实际上会占用3m个字节,当然如果只是英文的话就是m个字母,实际上占用m个字节
当超过255长度后,varchar和text没有区别,只需要考虑两者类型和特性。

②SQL语句是str格式的

③mysql默认是lantin1编码,因为是由瑞典开发的.要改变的话,在mysql的shell下输入 set names utf8

④还有个花了我很久去解决的Error,我一定要在这里说出来- -#:
ProgrammingError:not all arguments converted during string formatting TypeError:not all arguments converted during string formatting

解决办法:第一,请仔细check一下你的SQL语句有木有写错!第二,cur.execute()和cur.executemany()的区别和用法有没有弄清楚!

很不幸,我就是死在第二点,所以我要特别地在这里说说这两种的区别:

①cur.execute(sql,list/tuple),这里的list不能是那种list包括list的状况,只能单纯的一个list,比如[“16-8-26”,”16年08月30日”]这样的,而不是[[“16-8-26”,”16年08月30日”],[“16-8-26”,”16年08月31日”]]这样的列表嵌套列表,不然以我被它折腾了我一天的经验来看,分分钟给你报not all arguments converted during string formatting 。

②cur.executemany(sql,list-list/list-tuple),所谓list-list就是[[“16-8-26”,”16年08月30日”],[“16-8-26”,”16年08月31日”]]这种了,第一次就会取出list中的第一个list——[“16-8-26”,”16年08月30日”],过来就像execute时候一样,把这个取出的list中每一项取出放到前面的%s(占位符)位置,弄完了就去最外层list的第二个list,以此类推….