python数据采集6-读取文档

python数据采集6-读取文档

有种观点认为,互联网基本上就是那些符合新式 Web 2.0 潮流,并且经过多媒体内容点缀
的 HTML 网站构成的集合,这些内容在网络数据采集时几乎都是要被忽略的。但是,这种
观点忽略了互联网最基本的特征:作为不同类型文件的传输媒介。

虽然互联网在 20 世纪 60 年代末期就已经以不同的形式出现,但是 HTML 直到 1992 年才
问世。在此之前,互联网基本上就是收发邮件和传输文件;今天看到的网页的概念那时还
没有。总之,互联网并不是一个 HTML 页面的集合。它是一个信息集合,而 HTML 文件
只是展示信息的一个框架而已。如果我们的爬虫不能读取其他类型的文件,包括纯文本、
PDF、图像、视频、邮件等,我们将会失去很大一部分数据。

本章重点介绍文档处理的相关内容,包括把文件下载到文件夹里,以及读取文档并提取数
据。我们还会介绍文档的不同编码类型,让程序可以读取非英文的 HTML 页面。

文档编码

文档编码是一种告诉程序——无论是计算机的操作系统还是 Python 代码——读取文档的规
则。文档编码的方式通常可以根据文件的扩展名进行判断,虽然文件扩展名并不是由编码
确定的,而是由开发者确定的。例如,如果我把 myImage.jpg 另存为 myImage.txt,不会出
现任何问题,但当我用文本编辑器打开它的时候就有问题了。好在这种情况很少见,如果
要正确地读取一个文档,必须要知道它的扩展名。

从最底层的角度看,所有文档都是由 0 和 1 编码而成的。而在高层(贴近用户的层级),
编码算法会定义“每个字符多少位”或“每个像素的颜色值用多少位”(图像文件里)之类的事情,在那里你会遇到一些数据压缩算法或体积缩减算法,比如 PNG 图像编码格式
(一种无损压缩的位图图形格式)。

虽然第一次处理这些非 HTML 格式的文件时会觉得很没底,但是只要安装了合适的库,
Python 就可以帮你处理任意类型的文档。纯文本文件、视频文件和图像文件的唯一区别,
就是它们的 0 和 1 面向用户的转换方式不同。在本章后面的内容里,我会介绍几种常用的
文档格式:纯文本、PDF、PNG 和 GIF。

纯文本

虽然把文件存储为在线的纯文本格式并不常见,但是一些简易网站,或者拥有大量纯文本
文件的“旧式学术”(old-school)网站经常会这么做。例如,互联网工程任务组(Internet
Engineering Task Force,IETF)网站就存储了 IETF 发表过的所有文档,包含 HTML、PDF
和纯文本格式(例如

https://www.ietf.org/rfc/rfc1149.txt

)。大多数浏览器都可以很好地显示
纯文本文件,采集它们也不会遇到什么问题。
对大多数简单的纯文本文件,像 from urllib.request import urlopen

textPage = urlopen(“http://txt.bookben.com/c_down/2018/02/136356/太平洋战争中的坦克战:1941-1945年(书本网www.bookben.com).txt”)
print(textPage.read())
txt 这个练习文件,你可以用下面的方法读取:


from urllib.request import urlopen

textPage = urlopen("http://txt.bookben.com/c_down/2018/02/136356/%E5%A4%AA%E5%B9%B3%E6%B4%8B%E6%88%98%E4%BA%89%E4%B8%AD%E7%9A%84%E5%9D%A6%E5%85%8B%E6%88%98%EF%BC%9A1941-1945%E5%B9%B4(%E4%B9%A6%E6%9C%AC%E7%BD%91www.bookben.com).txt")
print(textPage.read())

返回结果如下


b'CHAPTER I\n\n"Well, Prince, so Genoa and Lucca are now just family estates of theBuonapartes. But I warn you, if you don\'t tell me that this means war,if you still try to defend the infamies and horrors perpetrated bythat Antichrist- I really believe he is Antichrist- I will havenothing more to do with you and you are no longer my friend, no longermy \'faithful slave,\' as you call yourself! But how do you do? I seeI have frightened you- sit down and tell me all the news."\n\nIt was in July, 1805, and the speaker was the well-known AnnaPavlovna Scherer, maid of honor and favorite of the Empress MaryaFedorovna. With these words she greeted Prince Vasili Kuragin, a manof high rank and importance, who was the first to arrive at herreception. Anna Pavlovna had had a cough for some days. She was, asshe said, suffering from la grippe; grippe being then a new word inSt. Petersburg, used only by the elite.\n\nAll her invitations without exception, written in French, anddelivered by a

....省略

通常,当用 urlopen 获取了网页之后,我们会把它转变成 BeautifulSoup 对象,方便后面
对 HTML 进行分析。在这段代码中,我们直接读取页面内容。你可能觉得,如果把它转变
成 BeautifulSoup 对象应该也不错,但那样做其实适得其反——这个页面不是 HTML,所以
BeautifulSoup 库就没用了。一旦纯文本文件被读成字符串,你就只能用普通 Python 字符串
的方法分析它了。当然,这么做有个缺点,就是你不能对字符串使用 HTML 标签,去定位
那些你真正需要的文字,避开那些你不需要的文字。如果现在你想从纯文本文件中抽取某
些信息,还是有些难度的。

注解

大多数时候用前面的方法读取纯文本文件都没问题。但是,互联网上的文本文件会比较复
杂。下面介绍一些英文和非英文编码的基础知识,包括 ASCII、Unicode 和 ISO 编码,以
及对应的处理方法。

  1. 编码类型简介
    20 世纪 90 年代初,一个叫 Unicode 联盟(The Unicode Consortium)的非营利组织尝
    试将地球上所有用于书写的符号进行统一编码。其目标包括拉丁字母、斯拉夫字母
    (кириллица)、中国象形文字(象形)、数学和逻辑符号( , ≥),甚至表情和
    “杂项”(miscellaneous)符号,如生化危机标记( )和和平符号( )等。

编码的结果就是你熟知的 UTF-8,全称是“Universal Character Set - Transformation Format
8 bit”,即“统一字符集 - 转换格式 8 位”。一个常见的误解是 UTF-8 把所有字符都存储成
8 位。其实“8 位”只是显示一个字符需要的最小位数,而不是最大位数。(如果 UTF-8 的
每个字符都是 8 位,那一共也只能存储 2 8 个,即 256 个字符。这对中文字符和其他符号来
说显然不够。)
真实情况是,UTF-8 的每个字符开头有一个标记表示“这个字符只用一个字节”或“那个
字符需要用两个字节”,一个字符最多可以是四个字节。由于这四个字节里还包含一部分
设置信息,用来决定多少字节用做字符编码,所以全部的 32 位(32 位 =4 字节 ×8 位 / 字
节)并不会都用,其实最多使用 21 位,也就是总共 2 097 152 种可能里面可以有 1 114 112
个字符。

虽然对很多程序来说,Unicode 都是上帝的礼物(godsend),但是有些习惯很难改变,
ASCII 依然是许多英文用户的不二选择。
ASCII 是 20 世纪 60 年代开始使用的文字编码标准,每个字符 7 位,一共 2 7 ,即 128 个字
符。这对于拉丁字母(包括大小写)、标点符号和英文键盘上的所有符号,都是够用的。

在 20 世纪 60 年代,存储的文件用 7 位编码和用 8 位编码之间的差异是巨大的,因为内存
非常昂贵。当时,计算机科学家们为了是需要增加一位来获得一个漂亮的二进制数(用 8
位),还是让文件用更少的位数(用 7 位)费尽心机。最终,7 位编码胜利了。但是,在新
式的计算方式中,每个 7 位码的前面都补充(pad)了一个“0” 1 ,留给我们两个最坏的结果
是,文件大了 14%(编码由 7 位变成 8 位,体积增加了 14%),并且由于只有 128 个字符,
缺乏灵活性。

在 UTF-8 设计过程中,设计师决定利用 ASCII 文档里的“填充位”,让所有以“0”开头的
字节表示这个字符只用 1 个字节,从而把 ASCII 和 UTF-8 编码完美地结合在一起。因此,
下面的字符在 ASCII 和 UTF-8 两种编码方式中都是有效的:

01000001 - A
01000010 - B
01000011 - C

注 1: padding(填充)位在稍后介绍的 ISO 编码标准里还会介绍。
读取文档 | 83
而下面的字符只在 UTF-8 编码里有效,如果文档用 ASCII 编码,那么就会被看成是“无法
打印”:

11000011 10000000 - À
11000011 10011111 - ß
11000011 10100111 - ç

除了 UTF-8,还有其他 UTF 标准,像 UTF-16、UTF-24、UTF-32,不过很少用这些编码标
准对文件进行编码,只在一些超出本书介绍范围的环境里使用。
显然,Unicode 标准也有问题,就是任何一种非英文语言文档的体积都比 ASCII 编码的体
积大。虽然你的语言可能只需要用大约 100 个字符,像英文的 ASCII 编码,8 位就够了,
但是因为是用 UTF-8 编码,所以你还是得用至少 16 位表示每个字符。这会让非英文的纯
文本文档体积差不多达到英文文档的两倍,对那些不用拉丁字符集的语言来说都是如此。

ISO 标准解决这个问题的办法是为每种语言创建一种编码。和 Unicode 不同,它使用了与
ASCII 相同的编码,但是在每个字符的开头用 0 作“填充位”,这样就可以让语言在需要
的时候创建特殊字符。这种做法对欧洲那些依赖拉丁文字母的语言(编码还是按照 0-127
一一对应)非常合适,只不过需要增加一些特殊字符。这使得 ISO-8859-1(为拉丁文字母
设计的)标准里有了分数符号(如 ½)和版权标记符号(©)。

还有一些 ISO 字符集,像 ISO-8859-9(土耳其语)、ISO-8859-2(德语等语言)、ISO-8859-
15(法语等语言)也是用类似的规律做出来的。
虽然这些年 ISO 编码标准的使用率一直在下降,但是目前仍有约 9% 的网站使用 ISO 编
码,所以有必要做基本的了解,并在采集网站之前需要检查是否使用了这种编码方法。

  1. 编码进行时
    在上一节里,我们用默认设置的 urlopen 读取了网上的 .txt 文档。这么做对英文文档没有
    任何问题。但是,如果你遇到的是俄语、阿拉伯语文档,或者文档里有一个像“résumé”
    这样的单词,就可能出问题。

看看下面的代码:

from urllib.request import urlopen
textPage = urlopen(
"http://www.pythonscraping.com/pages/warandpeace/chapter1-ru.txt")

print(textPage.read())
这段代码会把《战争与和平》原著(托尔斯泰用俄语和法语写的)的第 1 章打印到屏幕
上。打印结果一开头是这样:
注 2: 数据源自 http://w3techs.com/technologies/history_overview/character_encoding,通过网络爬虫收集。

b"\xd0\xa7\xd0\x90\xd0\xa1\xd0\xa2\xd0\xac \xd0\x9f\xd0\x95\xd0\xa0\xd0\x92\xd0\
x90\xd0\xaf\n\nI\n\n\xe2\x80\x94 Eh bien, mon prince.

另外,在大多数浏览器里访问页面也会呈现乱码(如图 6-1)。
图 6-1:法语和斯拉夫语用浏览器常用的文本编码格式 ISO-8859-1 编码的效果
就算让懂俄语的人来看,这些乱码也难以辨认。这个问题是因为 Python 默认把文本读成
ASCII 编码格式,而浏览器把文本读成 ISO-8859-1 编码格式。其实都不对,应该用 UTF-8
编码格式。
我们可以把字符串显示转换成 UTF-8 格式,这样就可以正确显示斯拉夫文字了:

from urllib.request import urlopen
textPage = urlopen(
"http://www.pythonscraping.com/pages/warandpeace/chapter1-ru.txt")
print(str(textPage.read(), 'utf-8'))

用 BeautifulSoup 和 Python 3.x 对文档进行 UTF-8 编码,如下所示:

html = urlopen("http://en.wikipedia.org/wiki/Python_(programming_language)")
bsObj = BeautifulSoup(html)
content = bsObj.find("div", {"id":"mw-content-text"}).get_text()
content = bytes(content, "UTF-8")
content = content.decode("UTF-8")

你可能打算以后用网络爬虫的时候全部采用 UTF-8 编码读取内容,毕竟 UTF-8 也可以完
美地处理 ASCII 编码。但是,要记住还有 9% 的网站使用 ISO 编码格式。所以在处理纯文
本文档时,想用一种编码搞定所有的文档依旧不可能。有一些库可以检查文档的编码,或
是对文档编码进行估计(用一些逻辑判断“Ñ € аÑÑказє不是单词),不过效果并
不是很好。

处理 HTML 页面的时候,网站其实会在 <head>部分显示页面使用的编码格式。大多数网
站,尤其是英文网站,都会带这样的标签:
<meta charset="utf-8" />
而 ECMA(European Computer Manufacturers Association,欧洲计算机制造商协会,http://
www.ecma-international.org/)网站的标签是这样

<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
如果你要做很多网络数据采集工作,尤其是面对国际网站时,建议你先看看 meta 标签的内
容,用网站推荐的编码方式读取页面内容。

CSV

进行网页采集的时候,你可能会遇到 CSV 文件,也可能有同事希望将数据保存为 CSV
格式。Python 有一个超赞的标准库(https://docs.python.org/3.4/library/csv.html)可以读写
CSV 文件。虽然这个库可以处理各种 CSV 文件,但是这里我重点介绍标准 CSV 格式。如
果你在处理 CSV 时有特殊需求,请查看文档!

Python 的 csv 库主要是面向本地文件,就是说你的 CSV 文件得存储在你的电脑上。而进
行网络数据采集的时候,很多文件都是在线的。不过有一些方法可以解决这个问题:

  • 手动把 CSV 文件下载到本机,然后用 Python 定位文件位置;
  • 写 Python 程序下载文件,读取之后再把源文件删除;
  • 从网上直接把文件读成一个字符串,然后转换成一个 StringIO 对象,使它具有文件的
    属性。

虽然前两个方法也可以用,但是既然你可以轻易地把 CSV 文件保存在内存里,就不要
再下载到本地占硬盘空间了。直接把文件读成字符串,然后封装成 StringIO 对象,让
Python 把它当作文件来处理,就不需要先保存成文件了。下面的程序就是从网上获取一个
CSV 文件(这里用的是 http://pythonscraping.com/files/MontyPythonAlbums.csv 里的 Monty
Python 乐团的专辑列表),然后把每一行都打印到命令行里:


from urllib.request import urlopen
from io import StringIO
import csv

data = urlopen("http://pythonscraping.com/files/MontyPythonAlbums.csv").read().decode('ascii', 'ignore')
dataFile = StringIO(data)
csvReader = csv.reader(dataFile)

for row in csvReader:
	print("The album \""+row[0]+"\" was released in "+str(row[1]))

显示结果很长,开始部分是这样:



['Name', 'Year']
["Monty Python's Flying Circus", '1970']
['Another Monty Python Record', '1971']
["Monty Python's Previous Record", '1972']
...

从代码中你会发现 csv.reader 返回的 csvReader 对象是可迭代的,而且由 Python 的列表对
象构成。因此, csvReader 对象可以用下面的方式接入:

for row in csvReader:
print("The album \""+row[0]+"\" was released in "+str(row[1]))




输出结果是:




The album "Name" was released in Year
The album "Monty Python's Flying Circus" was released in 1970
The album "Another Monty Python Record" was released in 1971
The album "Monty Python's Previous Record" was released in 1972
...

注意看第一行的内容, The album “Name” was released in Year 。虽然写示例代码的时候,
这行内容是否显示都无所谓,但是工作中你肯定不希望将这行信息保留在数据里。有些程
序员可能会简单地跳过 csvReader 对象的第一行,或者写一个简单的条件把第一行处理掉。
不过,还有一个函数可以很好地处理这个问题,那就是 csv.DictReader :


from urllib.request import urlopen
from io import StringIO
import csv

data = urlopen("http://pythonscraping.com/files/MontyPythonAlbums.csv").read().decode('ascii', 'ignore')
dataFile = StringIO(data)
dictReader = csv.DictReader(dataFile)

print(dictReader.fieldnames)

for row in dictReader:
    print(row)

csv.DictReader 会返回把 CSV 文件每一行转换成 Python 的字典对象返回,而不是列表对
象,并把字段列表保存在变量 dictReader.fieldnames 里,字段列表同时作为字典对象的键:


['Name', 'Year']
{'Name': "Monty Python's Flying Circus", 'Year': '1970'}
{'Name': 'Another Monty Python Record', 'Year': '1971'}
{'Name': "Monty Python's Previous Record", 'Year': '1972'}

虽然用 DictReaders 创建、处理和打印 CSV 信息,比 csvReaders 要多写一点儿代码,但是
考虑到它的便利性和实用性,多写那点儿代码还是值得的。

PDF

目前很多 PDF 解析库都是用 Python 2.x 版本建立的,还没有迁移到 Python 3.x 版本。

但是,因为 PDF 比较简单,而且是开源的文档格式,所以有一些给力的 Python 库可以读
取 PDF 文件,而且支持 Python 3.x 版本。
PDFMiner3K 就是一个非常好用的库(是 PDFMiner 的 Python 3.x 移植版)。它非常灵活,
可以通过命令行使用,也可以整合到代码中。它还可以处理不同的语言编码,而且对网络
文件的处理也非常方便。

你可以下载这个模块的源文件(https://pypi.python.org/pypi/pdfminer3k),解压并用下面命
令安装:



$python setup.py install


文档位于源文件解压文件夹的 /pdfminer3k-1.3.0/docs/index.html 里,这个文档更多是在介

绍命令行接口,而不是 Python 代码整合。

下面的例子可以把任意 PDF 读成字符串,然后用 StringIO 转换成文件对象



from pdfminer.pdfinterp import PDFResourceManager, process_pdf
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from io import StringIO
from io import open
from urllib.request import urlopen

def readPDF(pdfFile):
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, laparams=laparams)

    process_pdf(rsrcmgr, device, pdfFile)
    device.close()

    content = retstr.getvalue()
    retstr.close()
    return content

pdfFile = urlopen("http://pythonscraping.com/pages/warandpeace/chapter1.pdf")
outputString = readPDF(pdfFile)
print(outputString)
pdfFile.close()



readPDF 函数最大的好处是,如果你的 PDF 文件在电脑里,你就可以直接把 urlopen返回
的对象 pdfFile 替换成普通的 open()文件对象:

pdfFile = open("../pages/warandpeace/chapter1.pdf", 'rb')

输出结果可能不是很完美,尤其是当 PDF 里有图片、各种各样的文本格式,或者带有表格
和数据图的时候。但是,对大多数只包含纯文本内容的 PDF 而言,其输出结果与纯文本格
式基本没什么区别。

微软Word和.docx

大约在 2008 年以前,微软 Office 产品中 Word 用 .doc 文件格式。这种二进制格式很难读
取,而且能够读取 word 格式的软件很少。为了跟上时代,让自己的软件能够符合主流软
件的标准,微软决定使用 Open Office 的类 XML 格式标准,此后新版 Word 文件才与其他
文字处理软件兼容,这个格式就是 .docx。

不过,Python 对这种 Google Docs、Open Office 和 Microsoft Office 都在使用的 .docx 格
式的支持还不够好。虽然有一个 python-docx 库(http://python-docx.readthedocs.org/en/
latest/),但是只支持创建新文档和读取一些基本的文件数据,如文件大小和文件标题,不
支持正文读取。如果想读取 Microsoft Office 文件的正文内容,我们需要自己动手找方法。

第一步是从文件读取 XML:



from zipfile import ZipFile
from urllib.request import urlopen
from io import BytesIO
wordFile = urlopen("http://pythonscraping.com/pages/AWordDocument.docx").read()
wordFile = BytesIO(wordFile)
document = ZipFile(wordFile)
xml_content = document.read('word/document.xml')
print(xml_content.decode('utf-8')


这段代码把一个远程 Word 文档读成一个二进制文件对象( BytesIO 与本章之前用的
StringIO 类似),再用 Python 的标准库 zipfile 解压(所有的 .docx 文件为了节省空间都
进行过压缩),然后读取这个解压文件,就变成 XML 了。

但是发现大量信息,但是被隐藏在 XML 里面。好在文档的所有正文内容都包含在 <w:t>
标签里面,标题内容也是如此,这样就容易处理了。


from zipfile import ZipFile
from urllib.request import urlopen
from io import BytesIO
from bs4 import BeautifulSoup
wordFile = urlopen("http://pythonscraping.com/pages/AWordDocument.docx").read()
wordFile = BytesIO(wordFile)
document = ZipFile(wordFile)
xml_content = document.read('word/document.xml')
wordObj = BeautifulSoup(xml_content.decode('utf-8'))
textStrings = wordObj.findAll("w:t")
for textElem in textStrings:
    print(textElem.text)

这段代码的结果并不完美,但是已经差不多了,一行打印一个 <w:t> 标签,可以看到 Word
是如何对文字进行断行处理的:


你会看到这里“docx”是单独一行,这是因为在原始的 XML 里,它是由 <w:proofErr
w:type="spellStart"/> 标签包围的。这是 Word 用红色波浪线高亮显示“docx”的方式,
提示这个词可能有拼写错误。

文档的标题是由样式定义标签 <w:pStyle w:val=“Title”/> 处理的。虽然不能非常简单地定
位标题(或其他带样式的文本),但是用 BeautifulSoup 的导航功能还是可以帮助我们解决
问题的:

textStrings = wordObj.findAll("w:t")
for textElem in textStrings:
closeTag = ""
    try:
        style = textElem.parent.previousSibling.find("w:pstyle")
        if style is not None and style["w:val"] == "Title":
        print("<h1>")
        closeTag = "</h1>"
except AttributeError:
#不打印标签
pass
print(textElem.text)
print(closeTag)

这段代码很容易进行扩展,打印不同文本样式的标签,或者把它们标记成其他形式。

posted @ 2018-11-21 22:24  孙中明  阅读(278)  评论(0编辑  收藏  举报