web scraping with python 第六章、读取文档

第六章、读取文档

我们很容易想到因特网的主要内容是散布在最流行的web2.0的多媒体内容的文本集合,这几乎忽略了网页抓取的目的。然而,这忽略的互联网最根本的是:一个内容未知媒介来传输文件。

尽管互联网从60年代后期就围绕着某种形式或者另外一种,直到1992年HTML出现。在那时,互联网主要由电子邮件和文件传输组成;我们现在所知的网页的概念在那时候并没有真正的存在。也就是说,互联网不是HTML文件的集合。它是一个有HTML文件作为帧来展示的信息的集合。而不能读多种文件类型,包括文本,PDF,图像,视频,电子邮件等等,我们失去了对现有的很大一部分数据的支持。

本章介绍文件处理,不管你是下载他们到本地或者阅读他们提取数据。我们还将看看处理各种类型的文本编码,它甚至可以使你去读取外语的HTML页面。

文档编码

文档的编码告诉应用程序——无论是电脑操作系统或者你自己的Python代码——如何读它。这种编码通常可以从他的文件扩展名推导出来,虽然此文件的扩展名没有被编码授权。例如,我保存myImage.jpg文件为myImage.txt文件没有什么问题——直到我的文本编辑器试图打开它。幸运的是,这种情况很少见,并且为了正确的读取,所有文档文件的扩展名你通常都是知道的。

在一个根本水平下,所有的文件都是0和1编码。最重要的是,有编码算法定义的东西,比如,“每个字符有多少位”或者“每个像素有多少比特”(在图像文件情况下)。最重要的是,你的PNG文件可能有一个层的压缩,或者一些空间还原算法。

首先虽然处理非HTML文件似乎令人生畏,放心使用正确的库,Python适配处理任何格式的你想要扔掉的信息。在文本文件,视频文件和图像文件之间唯一的差别就是0和1如何解释执行。在这一章中,我们将介绍几种常见的文件类型:text类型,PDF文件,PNG格式和GIF格式。

Text(文本文件)

这是不同寻常的以纯文本的在线存储文件,但是它是流行于有大型文本文件资料库的裸机或者“老派”网站。例如,互联网工程任务组(IETF)存储所有公开的文件为HTML,PDF和文本文件(见http://bit.ly/1RCAj2f作为例子)。大多数浏览器会很好的显示这些文件,你应该可以没有问题的抓取他们。

对于大多数的文本文件,例如位于http://www.pythonscraping.com/pages/warandpeace/chapter1.txt的实践文件,你可以使用下面的方法:

from urllib.request import urlopen

textPage = urlopen("http://www.pythonscraping.com/pages/warandpeace/chapter1.txt")

print(textPage.read())

通常情况下,当我们使用urlopen检索页面,我们把它变成一个BeautifulSoup对象来解析HTML。着这种情况下,我们可以直接读取页面。把它变成一个BeautifulSoup对象完全可能只会适得其反——这里没有HTML去解析,所以这个库将是没有用的。一旦文本文件作为一个字符串读取,你只要像分析Python中读取的其他字符串一样。当然,这样的缺点就是你不能使用HTML标签的能力来检索上下文。你真正需要的文本和你不想要的指引着你的方向。当你试图从文本文件中提取确定的信息时,这给了你一个挑战。

文本编码和全球互联网

还记得早前我说过一个文件扩展名需要有正确的读取?好的,奇怪的是,这条规则并不适用于所有的基础文件:.txt文件。

用上述方法读取文件十有八九将工作很好。然而,处理文字在互联网上可以说是一个棘手的业务。接下来,我们将涵盖基础的英语和外国语言的编码,从ASCII码到Unicode再到ISO(光盘镜像文件格式),以及如何处理它们。

编码类型的简要概述

在90年代初期,一个名为Unicode协会的非盈利性组织通过为每一个在文本文档中使用的任何一个语言的字符建立编码,带来的一个通用的文本编辑器。目标是包括一切,这本书使用的来自拉丁语字母表,到西里尔文(кириллица),中国象形图(象形),数学逻辑符号(,>),甚至表情和“杂”符号,如生化危害标志和和平符。

因为你可能已经知道由此产生的编码器,即“通用字符集-8位转换格式”被人戏称为UTF-8,令人混淆的表示。一种常见的误解就是UTF-8存储的所有字符是8位的。然而,“8位”指的是一个字符需要被表示的最小尺寸是8位,而不是最大。(如果每个UTF-8字符真的存放8位,这将仅仅允许256种可能的字符。显然,从中国的象形到一个和平符是没有足够的空间的。)

在现实中,UTF-8的字符开头都有一个指示,说:“只有一个字节用于编码此字符”或者“接下来两个字节编码一个单个字符”四个字节是可能的。由于这4个字节还包含有关多少字节被用来对字符进行编码的信息,这32位(32=4字节×8比特/字节)没有被完全使用,确切的说最多21位被使用,可被使用的2,097,152个字符中,当前有1,114,112个被使用。

虽然Unicode对于很多应用是一个天赐的机会,但是有些习惯很难打破,ASCII依然是很多用户的热门选择。

ASCII,自1960年以来使用的文本编辑标准,每个字符采用7位编码,一共有2^7,或者128个字符。这对于拉丁字母(包括大小写),标点符号,以及普通英式键盘上的所有字符来说足够了。

当然,在1960年,存储文本文件每个字符使用7位还是8位的不同的重要性是存储非常昂贵。计算机科学家每天争论是否需要因为便利性添加额外的位数还是需要更实用的更少的存储空间。最后,7比特的被采纳了。然而,在现代计算机中,每7位的序列被在开头填充一个额外的“0”,①留给我们两个最糟糕的两点——大了14%的文件和仅仅128个字符带来的缺乏灵活性。

当UTF-8被设计出来,创造者决定使用ASCII文档的“填充位”作为它的优势,声明所有以“0”开头的字符表明只有一个字节被用于表示字符,使得这对UTF-8和ASCII两种编码方案是相同的。因此,下面的字符在UTF-8和ASCII中都是有效的:

01000001 - A

01000010 - B

01000011 - C

而下面的字符只适用于UTF-8,文档被解释为ASCII文件是讲显示“非打印字符”:

11000011 10000000 - à

11000011 10011111 - ?

11000011 10100111 - ?

除了UTF-8,还有其他的UTF标准,如UTF-16,UTF-24和UTF-32,但是这些编码格式的文件除了在不寻常的情况下一般是很少碰到的,这超出了本书范围之外。

当然,所有的Unicode标准问题是任何文件用一个单一的外语写出来是远远大于它本身应该的大小的。对于使用特定英语的ASCII情况,虽然你的语言可能只需要使用100个左右的字符,那么你每个字符至少需要16位而不仅仅是8位。至少这使得那些不适用拉丁字符集的外文文本文档至少是英文文档的大约两倍的大小。

ISO解决了为每个语言创建特定编码的问题。像Unicode,它与ASCII有相同的编码,但是需要在每个字符的开始处添加“0”允许它为所需要的语言创建特殊字符。这最适合严重依赖于拉丁字母的欧洲语言(在编码位置在0-127),但也需要一些额外的特殊字符。这使得ISO-8859-1(专为拉丁字母设计)有符号如分数(?),或者版权符号(@)。

其他的ISO字符集,如ISO-8859-9(土耳其),ISO-8859-2(在其他语言中的德语)和ISO-8859-15(在其他语言中的法语)也可以发现有一定的规律性。

虽然近年来ISO编码文档的普及率一直在下降,但互联网上仍旧有9%的网站的编码有一些ISO的味道②,使得在抓取网站之前对它做一个有必要的了解和编码检查。

实际编码

在上一节中,我们使用默认的urlopen读取你可能在互联网上遇到的.txt文档。这个在大多数英文文本时工作很好。然而,第二次你遇到俄语,阿拉伯语,甚至一个像“résumé”(法语概要,摘要的意思)的词语,你可能会遇到一些问题。

例如,使用下面的代码:

from urllib.request import urlopen

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

print(textPage.read())

这个读取战争与和平的第一章(用俄语和法语编写),并且将其打印到屏幕。这个屏幕上的一部分文字:

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


即使对讲俄语的本土人,这可能都有一点困惑。这个问题是,Python尝试把这个文件作为ASCII文件去读取,而浏览器试图把它作为一个ISO-8859-1编码的文档。当然,他们没有一个意识到这是一个UTF-8的文档。

我们可以明确的定义字符串为UTF-8,正确的格式输出西里尔字符:

from urllib.request import urlopen

textPage = urloepn("http://www.pythonscraping.com/pages/warandpeace/chapter1-ru.txt")

print(str(textPage.read()), 'utf-8')

在BeautifulSoup和Python3.x中使用这个概念是这样的:

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版本编码的网站不在这里。不幸的是,在文本文件情况下,它不可能具体确定是哪种编码。有一些库,可以检查文件,并作出最佳猜测(使用一些逻辑来实现“?€D°????DoD°D·?”可能不是一个字),但很多时候是错的。

幸运的是,在HTML页面的情况下,在网站的<head>部分通常有一个标签包含编码。大多数网站,特别是英文网站,有这个标签:

<meta charset="utf-8">

而欧洲计算机制造协会的网站上有这样的标签③:

<meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">

如果你打算做很多网页的抓取,尤其是国际网站,明智的做法是当读取页面内容时寻找这个元标签并使用的推荐的编码方式。

CSV

当网页抓取时你可能会遇到任何一个CSV文件或者一个喜欢这种数据格式的同事。幸运的是,Python有一

个梦幻的库来读和写CSV文件。虽然这个库可以处理多样CSV,我们将主要集中在标准的格式。如果你需要

处理特殊情况,阅读文档!

读取CSV文件

Python的csv库主要面向本地的文件,前提是你需要的CSV数据存储在你的机器上。不幸的是,网页抓取时

并不总是这样。有几种方法解决这个问题:

①手动下载文件到本地然后对Python指出文件位置

②写一个Python脚本来下载文件,读取它和(可选)检索后删除

③作为一个字符串从网页检索,并且封装在一个StringIO对象中,这样就像一个文件。

虽然前两个选项是可行的,当你的文件可以很容易的保存在内存中却采取复杂的硬盘空间时是糟糕的做法

。当做一个字符串可以更好的读取,并把它封装在一个对象中,允许Python像文件一样处理它,而不是以

一个实际保存的文件。下面的脚本检索来自互联网的CSV文件(Monty Python在http://bit.ly/1QjuMv8的

专辑列表),并且在终端中一行一行的打印出来:

from urllib.request import urlopen

import os

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(row)

打印完整的输出太长,但是看起来应该是这样:

['Name', 'Year']

["Monty Python's Flying Circus", '1970']

['Another Monty Python Record', '1971']

["Monty Python's Previous Record", '1972']

...

正如你从示例代码中看到,csv.reader返回的reader对象迭代组成了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.reader函数可自动替你照顾这

一切。进入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.RictReader(dataFile)

print(dictReader.filenames)

for row in dictReader:

print(row)

csv.DictReader返回CSV文件中每行的值作为字典对象而不是列表对象,通过filenames存储在变量

dictReader.filenames中并且作为每个字典对象的键:

['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'}

当然缺点是相对于csvReader它需要稍长的时间创建,处理和打印这些DictReaders,但是便利性和可用性

通常是很值得的开销。

PDF

作为一个Linux用户,我知道被发送一个.docx文件我的非微软软件改动和挣扎试图找到一个解码器来解析

一些新的苹果媒体文件的痛苦。在某些方面,Adobe在1993年革命性的创造了可移植文档格式(Portable

Document Format)。PDF文件允许不同平台上的用户使用完全相同的方式查看图像和文本文档,不管在平

台的查看他们。

尽管在web上存储PDF文件是有点过时了(为什么储存一个静态内容,当你把它写成HTML时是慢装载格式)

,PDF仍然无处不在,尤其是当处理官方的形式和申请时。

2009年,一名名为Nick Innes的英国人搞了一个新闻,在英国的信息自由法下要求白金汉郡市议会的学生

测试得到了有用的信息。经过一些反复的请求和拒绝,他最终收到了他想要的信息——184PDF文档的组成

虽然Innes坚持并最终获得了更多格式正确的数据库,他是一个网络爬虫的专家,他在法院和直接使用PDF

文件节约了自己大量的时间。通过Python众多的一个PDF解析模块。

不幸的是,许多Python2.x中内置的PDF解析库并没有与Python3.x一起推出。但是因为PDF是一个相对简单

和开源的文件格式 ,在Python3.x中也有很多不错的库可以读取它们。

PDFMine3K就是这样一个比较容易使用的库。它是非常灵活的,允许命令行使用或者集成到现有的代码。

它也可以处理各种语言的编码——再次,在一些web上派上用场。

你可以下载这个Python模块,并解压文件夹,运行安装:

$python setup.py install

该文件在文件夹/pdfminer3k-1.3.0/docs/index.html位置提取,虽然目前的文档趋向于面向命令行界面

而不是Python的集成。

这是一个让你阅读本地PDF文件对象的任意一个字符串的基本实现:

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

def readPDF(pdfFile):

rsrcmgr = PDFResourceManager()

retstr = StringIO()

laparams = LAParams()

device = TextConverter(rsrcmgr, resstr, 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.colse()

这个函数的好处是,如果你正在使用本地文件,你可以通过一个常规的本地Python文件对象简单的代替

urlopen的返回。使用下面:

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

这个输出可能不会是完美的,尤其对于PDF文件的图像,奇怪的文本格式或者文字排列表格或者图表。但

是,对于大多数纯文字的PDF文件输出应该和一个PDF的文本文件没有什么不同。

微软的Word和.docx文件

冒着得罪我在微软的朋友的风险:我不喜欢微软的Word。不是因为它未必是一个糟糕的软件,但是由于它

的用户的滥用。它有一个特殊的天赋是把应该简单的文本文件或者PDF转换成大的,速度慢的,难以打开

的野兽,这样失去了一个机器到另一个机器的格式组成,并且无论出于何种原因,可编辑往往意味着内容

是静态的。Word文件从来不意味着频繁的传输。然而,它们在某些网站是无处不在的,包含重要的文件,

甚至图表和多媒体文件;总之一切可以该用HTML创建的东西。

在2008年之前,微软的Office产品使用专有格式.docfile。这个二进制文件是难以阅读并且对其他文字处

理器支持较差。在时代的努力下,采取许多在其他方面软件的标准,微软决定使用开放式的Office XML为

基础的标准,这使得文件和其他开源以及软件兼容。

不幸的是,Python支持这种文件格式,使用谷歌文档,开放式办公和微软Office,仍然不是很大。这里有

python-docx为文档,但这只是给用户创建文档和只读取基础文件数据比如大小和文件的标题,而不是实

际的内容。为了读取微软Office文件的内容,我们推出自己的解决方案。

第一步是从文件中读取XML文件:

from zipfile import ZipFile

from urllib.request import urlopen

from io import BytesIO

wordFile = urlopen().read("http://pythonscraping.com/pages/AWordDocument.docx")

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文件。Word在

http://bit.ly/1Jc8Zql,如图6-2:

读取我的简单的Word文档的Python脚本输出如下:

<!--?xml version="1.0" encoding="UTF-8" standalone="yes"?-->

<w:document mc:ignorable="w14 w15 wp14" xmlns:m="http://schemas.openx

mlformats.org/officeDocument/2006/math" xmlns:mc="http://schemas.open

xmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-micros

oft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/off

iceDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vm

l" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/m

ain" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w14="htt

p://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://

schemas.microsoft.com/office/word/2012/wordml" xmlns:wne="http://sche

mas.microsoft.com/office/word/2006/wordml" xmlns:wp="http://schemas.o

penxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wp14="h

ttp://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" x

mlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessin

gCanvas" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wor

dprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word

/2010/wordprocessingInk" xmlns:wps="http://schemas.microsoft.com/offi

ce/word/2010/wordprocessingShape"><w:body><w:p w:rsidp="00764658" w:r

sidr="00764658" w:rsidrdefault="00764658"><w:ppr><w:pstyle w:val="Tit

le"></w:pstyle></w:ppr><w:r><w:t>A Word Document on a Website</w:t></

w:r><w:bookmarkstart w:id="0" w:name="_GoBack"></w:bookmarkstart><w:b

ookmarkend w:id="0"></w:bookmarkend></w:p><w:p w:rsidp="00764658" w:r

sidr="00764658" w:rsidrdefault="00764658"></w:p><w:p w:rsidp="0076465

8" w:rsidr="00764658" w:rsidrdefault="00764658" w:rsidrpr="00764658">

<w: r> <w:t>This is a Word document, full of content that you want ve

ry much. Unfortunately, it’s difficult to access because I’m putting

it on my website as a .</w:t></w:r><w:prooferr w:type="spellStart"></

w:prooferr><w:r><w:t>docx</w:t></w:r><w:prooferr w:type="spellEnd"></

w:prooferr> <w:r> <w:t xml:space="preserve"> file, rather than just p

ublishing it as HTML</w:t> </w:r> </w:p> <w:sectpr w:rsidr="00764658"

w:rsidrpr="00764658"> <w:pgszw:h="15840" w:w="12240"></w:pgsz><w:pgm

ar w:bottom="1440" w:footer="720" w:gutter="0" w:header="720" w:left=

"1440" w:right="1440" w:top="1440"></w:pgmar> <w:cols w:space="720"><

/w:cols&g; <w:docgrid w:linepitch="360"></w:docgrid> </w:sectpr> </w:

body> </w:document>

有大量的信息在这里,但是被隐藏。幸运的是,所有的文本文件包括顶部的标签包含在<w: t>标签,这使

得它容易抓取:

from zipfile import ZipFile

from urllib.request import urlopen

from io import BytesIO

from bs4 import BeautifulSoup

wordFile = urlopen().read()

wordFile = BytesIO(wordFIle)

wordFile = 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如何分

割文本:

A Word Document on a Website

This is a Word document, full of content that you want very much. Unfortunately,

it’s difficult to access because I’m putting it on my website as a . docx

file, rather than just publishing it as HTML

注意“docx”这个词在它自己的行。在原始的xml文件中,它被标签<w:proofErr w:type="spellStart"/>

包围。这是被红色波浪下划线标记的突出的“docx”,表明它认为它自己的文件格式名称有拼写的错误。

文件的标题优于样式描述标签<w:pstyle w:val="Title">。尽管这不能使我们非常容易确定标题(或其他

样式的文本),因此,使用BeautifulSoup的导航功能是有用的:

textStrings = wordObj.findAll("w:t")

for textElem in textStrings:

closeTag = ""

try:

style = textElem.parent.previousSibling.find("w:style")

if style is not None and style["w:val"] == "Title":

print('<h1>')

closeTag = "</h1>"

except AttributeError:

#No tags to print

pass

print(textElem.text)

print(closeTag)

这个函数可以很容易的扩展到打印标签周围不同的文本样式或者以其他方式标记他们。

①这种“填充”位稍晚将会在ISO标准来困扰我们

②根据使用网络爬虫来收集这类统计的

http://w3techs.com/technologies/history_overview/character_encoding

③Ecma是原来的ISO标准贡献者之一,所以也就不奇怪它的网站编码有ISO的风格。

转载请注明出处http://blog.csdn.net/qq_15297487

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值