文章目录

python数据采集5-存储数据

虽然在命令行里显示运行结果很有意思,但是随着数据不断增多,并且需要进行数据分析
时,将数据打印到命令行就不是办法了。为了可以远程使用大部分网络爬虫,你还需要把
采集到的数据存储起来。

本章将介绍三种主要的数据管理方法,对绝大多数应用都适用。如果你准备创建一个网站
的后端服务或者创建自己的 API,那么可能都需要让爬虫把数据写入数据库。如果你需要
一个快速简单的方法收集网上的文档,然后存到你的硬盘里,那么可能需要创建一个文件
流(file stream)来实现。如果还要为偶然事件提个醒儿,或者每天定时收集当天累计的数
据,就给自己发一封邮件吧!

抛开与网络数据采集的关系,大数据存储和与数据交互的能力,在新式的程序开发中也已
经是重中之重了。这一章的内容其实是实现第二部分许多示例的基础。如果你对自动数据
存储相关的知识不太了解,我非常希望你至少能浏览一下。

媒体文件

存储媒体文件有两种主要的方式:只获取文件 URL 链接,或者直接把源文件下载下来。
你可以通过媒体文件所在的 URL 链接直接引用它。这样做的优点如下。

  • 爬虫运行得更快,耗费的流量更少,因为只要链接,不需要下载文件。
  • 可以节省很多存储空间,因为只需要存储 URL 链接就可以。
  • 存储 URL 的代码更容易写,也不需要实现文件下载代码。
  • 不下载文件能够降低目标主机服务器的负载。

不过这么做也有一些缺点。

  • 这些内嵌在你的网站或应用中的外站 URL 链接被称为盗链(hotlinking),使用盗链可
    能会让你麻烦不断,每个网站都会实施防盗链措施。
  • 因为你的链接文件在别人的服务器上,所以你的应用就要跟着别人的节奏运行了。
  • 盗链是很容易改变的。如果你把盗链图片放在博客上,要是被对方服务器发现,很可能
    被恶搞。如果你把 URL 链接存起来准备以后再用,可能用的时候链接已经失效了,或
    者是变成了完全无关的内容。
  • 现实中的网络浏览器不仅可以请求 HTML 页面并切换页面,它们也会下载访问页面上
    所有的资源。下载文件会让你的爬虫看起来更像是人在浏览网站,这样做反而有好处。

如果你还在犹豫究竟是存储文件,还是只存储文件的 URL 链接,可以想想这些文件是要
多次使用,还是放进数据库之后就只是等着“落灰”,再也不会被打开。如果答案是后者,
那么最好还是只存储这些文件的 URL 吧。如果答案是前者,那么就继续往下看!


# -*- coding: utf-8 -*-
# 2018-2-12 16:13:05

from urllib.request import urlretrieve
from urllib.request import urlopen
from bs4 import BeautifulSoup

html = urlopen("http://www.baidu.com")
bsObj = BeautifulSoup(html)
imageLocation = bsObj.find("div", {"id": "lg"}).find("img")["src"]
urlretrieve ("http:"+imageLocation, "logo.jpg")
# 应为这里百度可能是使用的是防盗链src属性 如下
# src="//www.baidu.com/img/bd_logo1.png"
# 所以需要自己手动在前面添加一个"http:"

这段程序从 http://www.baidu.com 下载div的 id为lg(logo) 图片,然后在程序运行的文件夹里保存为
logo.jpg 文件。

如果你只需要下载一个文件,而且知道如何获取它,以及它的文件类型,这么做就可以
了。但是大多数爬虫都不可能一天只下载一个文件。下面的程序会把 http://www.baidu.com 主页上所有 src 属性的文件都下载下来:


import os
from urllib.request import urlretrieve
from urllib.request import urlopen
from bs4 import BeautifulSoup
# 导入相应的库
downloadDirectory = "downloaded"
baseUrl = "http://baidu.com"
# 定义 根目录文件夹 和 爬取的网址
def getAbsoluteURL(baseUrl, source):
    if source.startswith("http://www."):
        url = "http://"+source[11:]
    elif source.startswith("http://"):
        url = source
    elif source.startswith("www."):
        url = source[4:]
        url = "http://"+source
    else:
        url = baseUrl+"/"+source
    if baseUrl not in url:
        return None
    return url
# 对下载URL处理

def getDownloadPath(baseUrl, absoluteUrl, downloadDirectory):
    path = absoluteUrl.replace("www.", "")
    path = path.replace(":", "")
    # 注:windows 替换掉 ":" 不然创建新文件夹
    path = path.replace(baseUrl, "")
    path = downloadDirectory+path
    directory = os.path.dirname(path)

    if not os.path.exists(directory):
        os.makedirs(directory)

    return path
# 对本地存储目录进行处理

# 开始爬
html = urlopen("http://www.baidu.com")
bsObj = BeautifulSoup(html, "html.parser")
downloadList = bsObj.findAll(src=True)

for download in downloadList:
    fileUrl = getAbsoluteURL(baseUrl, download["src"])
    if fileUrl is not None:
        print(fileUrl)
        urlretrieve(fileUrl, getDownloadPath(baseUrl, fileUrl, downloadDirectory))

这个程序首先使用 Lambda函数选择首页上所有带 src 属性的标签。然
后对 URL 链接进行清理和标准化,获得文件的绝对路径(而且去掉了外链)。最后,每个
64 | 第 5 章
文件都会下载到程序所在文件夹的 downloaded文件里。

把数据存储到CSV

CSV(Comma-Separated Values,逗号分隔值)是存储表格数据的常用文件格式。Microsoft
Excel 和很多应用都支持 CSV 格式,因为它很简洁。

下面就是一个 CSV 文件的例子:

fruit,cost
apple,1.00
banana,0.30
pear,1.25

和 Python 一样,CSV 里留白(whitespace)也是很重要的:每一行都用一个换行符分隔,
列与列之间用逗号分隔(因此也叫“逗号分隔值”)。CSV 文件还可以用 Tab 字符或其他字
符分隔行,但是不太常见,用得不多。

如果你只想从网页上把 CSV 文件下载到电脑里,不打算做任何解析和修改,那么这节后
面的内容就没必要再看了。只要用上一节里介绍的文件下载方法下载并保存为 CSV 格式
就行了。

Python 的 csv 库可以非常简单地修改 CSV 文件,甚至从零开始创建一个 CSV 文件:

import csv
#from os import open

csvFile = open("../files/test.csv", 'w+', newline='')
try:
    writer = csv.writer(csvFile)
    writer.writerow(('number', 'number plus 2', 'number times 2'))
    for i in range(10):
        writer.writerow( (i, i+2, i*2))
finally:
    csvFile.close()

这里提个醒儿:Python 新建文件的机制考虑得非常周到(bullet-proof)。如果 …/files/test.csv
不存在,Python 会自动创建文件(不会自动创建文件夹)。如果文件已经存在,Python 会
用新的数据覆盖 test.csv 文件。

运行完成后,你会看到一个 CSV 文件:

number,number plus 2,number times 2
0,2,0
1,3,2
2,4,4
3,5,6
4,6,8
5,7,10
6,8,12
7,9,14
8,10,16
9,11,18

import csv
from urllib.request import urlopen
from bs4 import BeautifulSoup


# 定义要爬取的页面
html = urlopen("https://baike.baidu.com/item/Python/407313")
bsObj = BeautifulSoup(html, "html.parser")

#找到要爬取的div-table
table = bsObj.findAll("div",{"class":"basic-info cmn-clearfix"})[0]
# 获取dl
rows = table.findAll("dl")

# 写入到test.csv文件中
csvFile = open("./test.csv", 'wt', newline='', encoding='utf-8')
writer = csv.writer(csvFile)
try:
	for row in rows:
		csvRow = []
		for cell in row.findAll(['dt', 'dd']):
			csvRow.append(cell.get_text())
		writer.writerow(csvRow)
finally:
    csvFile.close()


csv 文件内容如下


外文名,"
Python
",经典教材,"
Head First Python
",发行时间,"
1991年
"
设计者,"
Guido van Rossum
",最新版本,"
3.7.0[3]
",荣    誉,"
2017年度编程语言
"

注:实际工作中写此程序之前的注意事项

如果你有很多 HTML 表格,且每个都要转换成 CSV 文件,或者许多 HTML 表格都要
汇总到一个 CSV 文件,那么把这个程序整合到爬虫里以解决问题非常好。但是,如果
你只需要做一次这种事情,那么更好的办法就是:复制粘贴。选择 HTML 表格内容然
后粘贴到 Excel 文件里,可以另存为 CSV 格式,不需要写代码就能搞定!

Mysql

Python 没有内置的 MySQL 支持工具。不过,有很多开源的库可以用来与 MySQL 做交互,
Python 2.x 和 Python 3.x 版本都支持。最有名的一个库就是 PyMySQL(https://github.com/
PyMySQL/PyMySQL)。
写到这里的时候,PyMySQL 的版本是 0.6.2,你可以用下面的命令下载并安装它


$ curl -L https://github.com/PyMySQL/PyMySQL/tarball/pymysql-0.6.2 | tar xz
$ cd PyMySQL-PyMySQL-f953785/
$ python setup.py install

如果需要更新,请检查最新版的 PyMySQL,并修改第一行下载链接中的版本号进行更新。

安装完成之后,你就可以使用 PyMySQL 包了。如果你的 MySQL 服务器处于运行状态,
应该就可以成功地执行下面的命令(记得把 root 账户密码加进去):



import pymysql
conn = pymysql.connect(host='127.0.0.1', unix_socket='/tmp/mysql.sock',
                       user='root', passwd=None, db='mysql')
cur = conn.cursor()
cur.execute("USE scraping")
cur.execute("SELECT * FROM pages WHERE id=1")
print(cur.fetchone())
cur.close()
conn.close()

这段程序有两个对象:连接对象( conn )和光标对象( cur )。

连接 / 光标模式是数据库编程中常用的模式,不过刚刚接触数据库的时候,有些用户很难
区分两种模式的不同。连接模式除了要连接数据库之外,还要发送数据库信息,处理回滚
操作(当一个查询或一组查询被中断时,数据库需要回到初始状态,一般用事务控制手段
实现状态回滚),创建新的光标对象,等等。

而一个连接可以有很多个光标。一个光标跟踪一种状态(state)信息,比如跟踪数据库的
使用状态。如果你有多个数据库,且需要向所有数据库写内容,就需要多个光标来处理。

光标还会包含最后一次查询执行的结果。通过调用光标函数,比如 cur.fetchone() ,可以
获取查询结果。

用完光标和连接之后,千万记得把它们关闭。如果不关闭就会导致连接泄漏(connection
leak),造成一种未关闭连接现象,即连接已经不再使用,但是数据库却不能关闭,因为数
据库不能确定你还要不要继续使用它。这种现象会一直耗费数据库的资源,所以用完数据
库之后记得关闭连接!

刚开始的时候,你最想做的事情可能就是把采集的结果保存到数据库里。让我们用前面维
基百科爬虫的例子来演示一下如何实现数据存储。

在进行网络数据采集时,处理 Unicode 字符串是很痛苦的事情。默认情况下,MySQL 也
不支持 Unicode 字符处理。不过你可以设置这个功能(这么做会增加数据库的占用空间)。
因为在维基百科上我们难免会遇到各种各样的字符,所以最好一开始就让你的数据库支持


Unicode:
ALTER DATABASE scraping CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
ALTER TABLE pages CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE pages CHANGE title title VARCHAR(200) CHARACTER SET utf8mb4 COLLATE
utf8mb4_unicode_ci;
ALTER TABLE pages CHANGE content content VARCHAR(10000) CHARACTER SET utf8mb4 CO
LLATE utf8mb4_unicode_ci;

这四行语句改变的内容有:数据库、数据表,以及两个字段的默认编码都从 utf8mb4
(严格说来也属于 Unicode,但是对大多数 Unicode字符的支持都非常不好)转变成了
utf8mb4_unicode_ci

你可以在 title 或 content 字段中插入一些德语变音符(umlauts)或汉语字符,如果没有
错误就表示转换成功了。
现在数据库已经准备好接收维基百科的各种信息了,你可以用下面的程序来存储数据:


from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import datetime
import random
import pymysql

conn = pymysql.connect(host='127.0.0.1', unix_socket='/tmp/mysql.sock', user='root', passwd=None, db='mysql', charset='utf8')
cur = conn.cursor()
cur.execute("USE scraping")

random.seed(datetime.datetime.now())

def store(title, content):
    cur.execute("INSERT INTO pages (title, content) VALUES (\"%s\",\"%s\")", (title, content))
    cur.connection.commit()

def getLinks(articleUrl):
    html = urlopen("http://en.wikipedia.org"+articleUrl)
    bsObj = BeautifulSoup(html, "html.parser")
    title = bsObj.find("h1").get_text()
    content = bsObj.find("div", {"id":"mw-content-text"}).find("p").get_text()
    store(title, content)
    return bsObj.find("div", {"id":"bodyContent"}).findAll("a", href=re.compile("^(/wiki/)((?!:).)*$"))

links = getLinks("/wiki/Kevin_Bacon")
try:
    while len(links) > 0:
         newArticle = links[random.randint(0, len(links)-1)].attrs["href"]
         print(newArticle)
         links = getLinks(newArticle)
finally:
    cur.close()
    conn.close()


这里有几点需要注意:首先, charset=‘utf8’ 要增加到连接字符串里。这是让连接 conn 把
所有发送到数据库的信息都当成 UTF-8 编码格式(当然,前提是数据库默认编码已经设置
成 UTF-8)。

然后要注意的是 store 函数。它有两个参数: title 和 content ,并把这两个参数加到了一
个 INSERT 语句中并用光标执行,然后用光标进行连接确认。这是一个让光标与连接操作分
离的好例子;当光标里存储了一些数据库与数据库上下文(context)的信息时,需要通过
连接的确认操作先将信息传进数据库,再将信息插入数据库。

最后要注意的是, finally 语句是在程序主循环的外面,代码的最底下。这样做可以保证,
无论程序执行过程中如何发生中断或抛出异常(当然,因为网络很复杂,你得随时准备遭
遇异常),光标和连接都会在程序结束前立即关闭。无论你是在采集网络,还是处理一个
打开连接的数据库,用 try…finally 都是一个好主意。

虽然 PyMySQL 规模并不大,但是里面有一些非常实用的函数本书并没有介绍。具体请参
考 Python 的 DBAPI 标准文档(http://legacy.python.org/dev/peps/pep-0249/)。

Email

与网页通过 HTTP 协议传输一样,邮件是通过 SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)传输的。而且,和你用网络服务器的客户端(浏览器)处理那些通过
HTTP 协议传输的网页一样,Email 服务器也有客户端,像 Sendmail、Postfix 和 Mailman
等,都可以收发邮件。

虽然用 Python 发邮件很容易,但是需要你连接那些正在运行 SMTP 协议的服务器。在服
务器或本地机器上设置 SMTP 客户端有点儿复杂,也超出了本书的介绍范围,但是有很多
资料可以帮你解决问题,如果你用的是 Linux 或 Mac OS X 系统,参考资料会更丰富。

下面的代码运行的前提是你的电脑已经可以正常地运行一个 SMTP 客户端。(如果要调整
代码用于远程 SMTP 客户端,请把 localhost 改成远程服务器地址。)
用 Python 发一封邮件只要 9 行代码

import smtplib
from email.mime.text import MIMEText
msg = MIMEText("The body of the email is here")
msg['Subject'] = "An Email Alert"
msg['From'] = "ryan@pythonscraping.com"
msg['To'] = "webmaster@pythonscraping.com"
s = smtplib.SMTP('localhost')
s.send_message(msg)
s.quit()

Python 有两个包可以发送邮件: smtplib 和 email 。

Python 的 email 模块里包含了许多实用的邮件格式设置函数,可以用来创建邮件“包
裹”。下面的示例中使用的 MIMEText 对象,为底层的 MIME(Multipurpose Internet Mail
Extensions,多用途互联网邮件扩展类型)协议传输创建了一封空邮件,最后通过高层的
SMTP 协议发送出去。 MIMEText 对象 msg 包括收发邮箱地址、邮件正文和主题,Python 通
过它就可以创建一封格式正确的邮件。

smtplib 模块用来设置服务器连接的相关信息。就像 MySQL 服务器的连接一样,这个连接
必须在用完之后及时关闭,以避免同时创建太多连接而浪费资源。
把这个简单的邮件程序封装成函数后,可以更方便地扩展和使用:


import smtplib
from email.mime.text import MIMEText
from bs4 import BeautifulSoup
from urllib.request import urlopen
import time

def sendMail(subject, body):
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = "christmas_alerts@pythonscraping.com"
    msg['To'] = "ryan@pythonscraping.com"

	s = smtplib.SMTP('localhost')
	s.send_message(msg)
	s.quit()

bsObj = BeautifulSoup(urlopen("https://isitchristmas.com/"))
while(bsObj.find("a", {"id":"answer"}).attrs['title'] == "NO"):
    print("It is not Christmas yet.")
    time.sleep(3600)
    bsObj = BeautifulSoup(urlopen("https://isitchristmas.com/"))
sendMail("It's Christmas!", "According to http://itischristmas.com, it is Christmas!")



这个程序每小时检查一次 https://isitchristmas.com/ 网站(根据日期判断当天是不是圣诞
节)。如果页面上的信息不是“NO”(中国用户在网站页面上看到的“NO”在源代码里是
<noscript>不是 </noscript>),就会给你发一封邮件,告诉你圣诞节到了。
虽然这个程序看起来并没有墙上的挂历有用,但是稍作修改就可以做很多有用的事情。它
可以发送网站访问失败、应用测试失败的异常情况,也可以在 Amazon 网站上出现了一款
卖到断货的畅销品时通知你——这些都是挂历做不到的事情。

10-04 12:26