《python网络数据采集》读书笔记

《python网络数据采集》读书笔记

标签(空格分隔): python 爬虫 读书笔记


花了三天时间看了一遍,将我认为值得记下的内容记录了下来。推荐购买。

第一部分 创建爬虫

重点介绍网络数据采集的基本原理。
* 通过网站域名获取HTML数据
* 根据目标信息解析数据
* 存储目标信息
* 如果有必要,移动到另一个网页重复这一过程

第1章 初见网络爬虫

from urllib.request import urlopen,查找python的request模块,只导入一个urlopen(python3)
urllib是python的标准库,包含了从网络请求数据,处理cookie,甚至改变请求头和用户代理这些元数据的函数。
python文档
而urlopen用来打开并读取一个从网络获取的远程对象。

1.2 Beautiful Soup简介

BeautifulSoup可以通过定位HTML标签来格式化和组织复杂的网络信息。

用虚拟环境保存库文件

如果同时负责多个python项目,或者想要轻松打包某个项目及其关联的库文件,或者担心已经安装的库之间可能有冲突,可以安装一个python虚拟环境来分而治之。
使用virtualenv scrapingEnv就可以创建一个虚拟环境
激活并使用

cd scrapingEnv/
source bin/activate

激活后,就会发现环境名称出现在命令行提示符之前,后面你安装的任何库和执行的任何程序都是在这个环境下运行。
当不再使用虚拟环境中的库时,可以通过释放命令退出环境deactivate

1.2.2 运行BeautifulSoup

BeautifulSoup中最常用的就是BeautifulSoup对象
任何HTML文件的任意节点信息都可以被提取出来,只要目标信息的旁边或附近有标记就行。

1.2.3 可靠的网络连接

网络连接代码可能会出现异常,需要进行处理。
html=urlopen("http://www.pythonscraping.com/pages/page1.html")
这行代码主要可能会发生两种异常:
* 网页在服务器上不存在,或者获取页面的时候出现错误
* 服务器不存在
第一种会返回HTTP错误,可能是404 Page Not Fund,或是500 Internal Server Error,这时urlopen函数会抛出HTTPError异常,可以这样处理

try:
    html=urlopen("http://www.pythonscraping.com/pages/page1.html")
except HTTPError as e:
    print (e)
    #返回空值,中断程序,或者执行另一种方案
else:
    #程序继续。如果你已经在上面异常捕捉那一段代码立返回或中断,那么就不需要else语句了,这段代码也不会执行

如果服务器不存在,urlopen会返回一个None对戏那个,可以使用html is None来判断这个返回的html是不是None
即使成功从服务器获取网页,如果网页上的内容并非完全是我们期望的那样,仍然可能会出现异常。因此每当你调用BeautifulSoup对象里的一个标签时,增加一个检查条件保证标签确实存在是一个很聪明的做法。如果标签不存在,BeautifulSoup会返回None对象。如果再调用这个None对象下的子标签,就会发生AttributeError错误。

第2章 复杂HTML解析

2.1 不是一直都要用锤子

假如已经确定了目标内容,如果使用复杂的嵌套查询来获取的话,不仅欠缺美感,而且如果网站管理员对网站稍作修改后,代码就会失效。
* 寻找“打印此页”的链接,或者看看有没有HTML样式更加友好的移动版
* 寻找隐藏在JavaScript文件里的信息。
* 可以尝试从URL链接里获取
* 从其他网站上查询

2.2 再端一碗BeautifulSoup

介绍通过属性查找标签的方法,标签组的使用,以及标签解析树的导航过程。
使用findAll函数来抽取只包含在<span class = "green"></span>标签里的文字
.get_text()会把你正在处理的HTML文档中所有的标签都清除,然后返回一个只包含文字的字符串。通常在准备打印、存储和操作数据时,应该最后才使用.get_text()

2.2.1 BeautifulSoup的find()和findAll()

借助它们,你可以通过标签的不同属性轻松地过滤HTML页面,查找需要的标签组或单个标签
findAll(tag,attributes,recursive,text,limit,keywords)
find(tag,attributes,recursive,text,keywords)
标签参数tag:一个标签的名称或是多个标签名称组成的python列表
属性参数attributes是用一个python字典封装一个标签的若干属性和对应的属性值。
递归参数recursive是一个布尔变量。如果设置为true,findAll会根据你的要求去查找标签参数的所有子标签,以及子标签的子标签。如果设置为false,则只查找文档的一级标签
文本参数text使用标签的文本内容去匹配。比如要查找包含”the prince”内容的标签数量,可以:

nameList = bsObj.findAll(text = "the prince")
print(len(nameList))

输出结果为7。
范围限制参数limit,只用于findAll方法(find方法等价于findAll的limit等于1时的情形)。如果你只对网页中获得的前x项结果感兴趣,就可以设置它。顺序按照网页上的顺序排序。
关键词参数keyword,可以选择具有指定属性的标签:

allText = bsObj.findAll(id="text")
print(allText[0].get_text())

标签参数tag把标签列表传到.findAll()里获取一列标签,实际上是或关系的过滤器。而关键词参数keyword可以让你增加一个与关系的过滤器来简化工作。

2.2 其他BeautifulSoup对象

目前已经介绍了两种对象:
* BeautifulSoup对象
* 标签Tag对象
还有另外两种对象:
* NavigableString对象:用来表示标签里的文字,而不是标签
* Comment对象:用来查找HTML文档的注释标签

2.3 导航树

findAll函数通过标签的名称和属性来查找标签,而导航树则是通过标签在文档中的位置来查找标签。如单一方向的导航bsObj.tag.subTag.anotherSubTag
对标签Tag对象使用children()函数可以获得所有子标签。
使用descendants()函数可以打印所有后代标签。

处理兄弟标签
使用next_siblings()函数可以便捷地处理带标题行的表格。

让标签的选择更加的具体:页面的布局总是不断变化,如果想让爬虫更稳定,最好还是让标签的选择更加具体。如果有属性,就利用标签的属性。
和next_siblings()一样,如果你很容易找到一组兄弟标签中的最后一个标签,那么previous_siblings函数也会很有用。
还有next_sibling和previous_sibling函数,只不过返回的是单个标签。

父标签处理:
使用parent和parents可以查找父标签

2.3 正则表达式

正则表达式是可以识别正则字符串(regular string)的式子。也就是说,它们可以这么定义:“如果你给我的字符串符合规则,我就返回它”,或者是“如果字符串不符合规则,我就忽略它”
可以去RegexPal这类网站上在线测试正则表达式。
正则表达式的一个经典的应用是识别邮箱地址。比如[A-Za-z0-9\._+]+@[A-Za-z]+\.(com|org|edu|net)
正则表达式常用的符号

符号含义
*匹配,0次或多次
+匹配,至少1次
[]匹配任意一个字符(相当于任选一个)
()表达式编组(会优先执行)
{m,n}匹配,从m到n次
[^]匹配任意一个不再中括号内的字符
|匹配任意一个由竖线分割的字符、子表达式
.匹配任意单个字符
^指字符串开始位置的字符或子表达式
|转义字符
$常用在末尾,表示“从字符串的末端匹配”。不然,每个正则表达式默认为”.*”,只会从开头匹配
?!不包含。通常放在字符或正则表达式前面,表示字符不能出现在目标字符串里。如果要在整个字符串全部排除某个字符,就要加上^和$

2.4 正则表达式和BeautifulSoup

大多数支持字符串参数的函数都可以用正则表达式实现。
例子:从网页中获取商品图片,如果直接获取img标签,可能会有其他图片。但是考虑到商品的图片大多放在同一个文件夹中,如果用下面的程序可以快速获得。

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

html = urlopen("http://www.pythonscraping.com/pages/page3.html")
bsObj = BeautifulSoup(html)
images = bsObj.findAll("img",{"src":re.compile("\.\./img\/gifts/img.*\.jpg")})#显然,图片的相对路径都是以../img/gifts/img开头,以.jpg结尾。

正则表达式可以作为BeautifulSoup语句的任意一个参数,让目标元素查找工作极具灵活性。

2.5 获取属性

有些需要的信息藏在标签属性中,比如\的URL链接,\的图片文件
对于一个标签属性,可以用下面的代码获取它的全部属性
myTag.attrs
它返回的是一个python字典对象,要获取src可以这样获取
myImgTag.attrs["src"]

2.6 Lambda表达式

BeautifulSoup允许把特定函数类型当作findAll函数的参数,唯一的限制条件就是这些函数必须把一个标签作为参数,并且返回结果为布尔类型。BeautifulSoup用这个函数来评估它遇到的每个标签对象,最后把评估结果为真的标签保留,把其他标签剔除。
例如,下面的代码就是获取有两个属性的标签:
soup.findAll(lambda tag: len(tag.attrs)==2)

2.7 超越Beautiful Soup

BeautifulSoup是python里最受欢迎的HTML解析库之一,但它并不是唯一的选择。
* lxml
* HTML parser

第3章 开始采集

3.2 采集整个网站

深网和暗网
深网指的是搜索引擎无法抓取到的那部分网络。
暗网是基于TOR客户端的。

显然,需要使用一个方便查询的列表(例子中使用了set类型)来去重

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

pages = set()
def getLinks(pageUrl):
    global pages
    html = urlopen("http://en.wikipedia.org"+pageUrl)
    bsObj = BeautifulSoup(html)
    for link in bsObj.findAll("a",href = re.compile("^(/wiki/)")):
        if 'href' in link.attrs:
            if link.attrs['href'] not in pages:
                #如果遇到了新的页面
                newPage = link.attrs['href']
                print(newPage)
                pages.add(newPage)
                getLinks(newPage)
getLinks("")
递归的警告

python默认的递归调用限制为1000次。

3.4 用Scrapy采集

Scrapy可以帮你大幅度降低网页链接查找和识别工作复杂度的python库。
创建Scrapy项目:scrapy startproject wikiSpider,这样就创建了名为wikiSpider的新项目。当前目录出现了名为wikiSpider的项目文件夹。
为了创建爬虫,需要在wikiSpider/wikiSpider/spiders文件夹里增加一个articleSpider.py文件。另外,在items.py文件中,需要定义一个Article类。
items.py文件:

#...
from scrapy import Item,Field

class Article(Item):
    title = Field()

Scrapy的每个Item(条目)对象表示网站上的一个页面。下面演示收集每页的title字段

articleSpider.py:

from scrapy.selector import Selector
from scrapy import Spider
from wikiSpider.items import Article

class ArticleSpider(Spider):
    name = "article"
    allowed_domains = ["en.wikipedia.org"]
    start_urls = ["http://en.wikipedia.org/wiki/Main_Page","http://en.wikipedia.org/wiki/Python_%28programming_language%29"]

    def parse(self, response):
        item = Article()
        title = response.xpath('//h1/text()')[0].extract()
        print("Title is: "+ title)
        item['title'] = title
        return item

然后在wikiSpider主目录中使用scrapy crawl article来运行ArticleSpider(由条目名称article来调用爬虫,是ArticleSpider的name=”article”决定的)

Scrapy日志处理

可以在Scrapy的setting.py中设置日志显示层级:LOG_LEVEL = 'ERROR'
也可以通过下面的命令输出到一个独立的文件中:scrapy crawl article -s LOG_FILE = wiki.log

Scrapy用Item对象决定要从它浏览的页面中提取哪些信息。支持不同的输出格式。
Scrapy文档

第4章 使用API

API调用

不同API的调用语法大不相同,但是有几条共同准则。当使用GET请求获取数据时,用URL路径描述你要获取的数据范围,查询参数可以作为过滤器或附加请求使用。

Twitter、Google的API

4.7 解析JSON数据

使用Python标准库中的JSON解析库来进行解析。使用import json来使用它。

4.9 再说一点API

优质资源:
Leonard Richardson、Mike Amundsen和Sam Ruby的RESTful Web APIs,为网络API的用法提供了非常全面的理论与实践直到
创建自己的API:Mike Amundsen的精彩视频教学课程Designing APIs for the Web(http://shop.oreilly.com/product/119999125.do)

第5章 存储数据

主要介绍三种数据管理方法:数据库、文件和邮件

5.1 媒体文件

存储媒体文件的两种主要方式:只获取文件URL链接,或者直接把源文件下载下来。
在Python 3.x版本中,urllib.request.urlretrieve可以根据文件的URL下载文件:

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

html = urlopen("http://www.pythonscraping.com")
bsObj = BeautifulSoup(html)
imageLocation = bsObj.find("a",{"id":"logo"}).find("img")["src"]
urlretrieve(imageLocation,"logo.jpg")

上面这个例子从http://pythonscraping.com下载logo图片,然后在程序运行的文件夹里保存为logo.jpg文件。

程序运行注意事项

当你运用管理员程序运行下载程序的时候,你的电脑基本已经处于危险之中。所以应该时刻注意。

5.2 把数据存储到csv

CSV(逗号分隔值)是存储表格数据的常用文件格式。
CSV的每一行都用一个换行符分隔,列与列之间用逗号分隔。还可以用Tab字符或其他字符分隔行,不过用的不多。
如果只需要下载csv文件,使用上面介绍的方法已经足够。下面介绍的是如何下载并修改csv文件,甚至从零创建一个csv文件。
使用python的csv库。

import csv

csvFile = open("../files/test.csv",'w+')
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()

网络数据采集的一个常用功能就是获取HTML表格并写入CSV文件。比如下面就是将维基百科复杂的HTML表格变为CSV文件的例程:

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

html = urlopen("http://en.wikipedia.org/wiki/Comparison_of_text_editors")
bsObj = BeautifulSoup(html)
#主对比表格是当前页面上的第一个表格
table = bsObj.findAll("table",{"class":wikitable})[0]
rows = table.findAll("tr")

csvFile = open("../files/editors.csv",'wt',newline="",encoding='utf-8')
writer = csv.writer(csvFile)
try:
    for row in rows:
    csvRow = []
    for cell in row.findAll(['td','th'])
        csvRow.append(cell.get_text())
        writer.writerow(csvRow)
finally:
    csvFile.close()

5.3 MySQL

使用MySQL前要进行预先安装,然后启动服务器,使用python代码与服务器进行交互

5.3.3 与Python整合

python没有内置的MySQL支持工具,不过有很多开源的库,最著名的是PyMySQL
可以使用下面的命令来安装它

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

需要检查最新版并更新第一行的URL
安装完后就可以执行下面的语句

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

其中有两个对象:连接对象(conn)和光标对象(cur)

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 COLLATE utf8mb4_unicode_ci;

这四行语句改变了数据库、数据表和两个字段的默认编码,从utf8mb4变为了utf8mb4_unicode_ci(utf8mb4严格上来说也属于Unicode,但是对于大多数Unicode的字符的支持都非常不好)
Python的DBAPI标准文档

5.4 Email

邮件通过SMTP(Simple Mail Transfer Protocol)传输的,用Python发邮件很容易,但是需要连接正在运行SMTP协议的服务器。可以使用Amason等云服务器厂商提供的已经设置好的SMTP服务器
下面的代码可以发送邮件,使用远程客户端的话需要修改其中的localhost为指定的IP

imort 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模块里包含了许多使用的邮件格式设置函数,可以用来创建邮件包裹。
smtplib模块用来设置服务器连接的相关信息。

第6章 读取文档

6.2 纯文本

对于大多数简单的纯文本文件,像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进行解析。然而这个页面不是HTML,所以BeautifulSoup库没有用处,应该使用Python字符串方法去解析它。

文本编码和全球互联网

如果想要正确地读取一个文件,只要知道它的扩展名就可以了。但是这并不适用于最基础的文档格式:.txt文件。
互联网常用的英文和非英文编码至少包括ASCII、Unicode和ISO编码
可以显示把字符串转换成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和Python3.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")

处理HTML页面的时候,网站会在部分显示页面所使用的编码格式,建议先看一下meta标签中的内容,用网站推荐的编码方式读取页面内容。

6.3 CSV

Python有一个非常棒的CSV库,但是它主要是面向本地文件。在线文件的话有三种处理方式:
* 手动下载再定位
* 用Python程序下载,读取后删除
* 直接从往上读写字符串,转换成StringIO对象,使其具有文件的属性

下面的程序就是使用第三种方式,读取一个网上的csv,然后把每一行都打印到命令行中。

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

注意csv头与其他数据不相复合,可以选择跳过或处理条,或者使用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里。

6.4 PDF

Python3.x下可以使用PDFMiner3K来进行PDF的处理。可以下载这个模块的源文件,解压并用下面的命令安装:python setup.py install
下面的例子可以把任意PDF读成字符串,然后用StringIO转换成文件对象

from urllib.request import urlopen
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,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()

6.5 微软Word和.docx

Python对于.docx格式的支持还不够好,虽然有一个python-docx库,但是只支持创建新文档和读取一些基本的文件数据,如文件大小和文件标题,不支持正文读取。我们需要自己动手来找方法
第一步是从文件读取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文件读成一个二进制文件对象,再用Python的标准库zipfile解压(所有的.docx文件为了节省空间都进行过压缩),然后读取这个压缩文件,就变成了XML了。
文档的所有正文信息都包含在\标签里面,标题内容也是如此,这样就比较容易处理

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(xmlcontent.decode('utf-8'))
textStrings = wordObj.findAll("w:t")
for textElem in textStrings:
    print(textElem.text)

第二部分 高级数据采集

目前为止,我们创建的网络爬虫比较依赖于网络服务器所提供的信息。如果不能立即提供样式规范的信息,爬虫就不能正确地采集数据,而且只能够采集那些显而易见的信息。
下面就是要帮你分析原始数据,获取隐藏在数据背后的故事——javaScript、登陆表单和网站反抓取措施的背后

第7章 数据清洗

7.1 编写代码清洗数据

使用的函数:正则替换函数regular.sub(pattern,repl,str)
Python字符串处理函数string.strip([char]),用于在字符串前后去掉指定的字符

数据标准化

Python的字典是无序的,但是其collections库里面有一个OrderedDict可以提供有序字典。

7.2 数据存储后再清晰

可以存储起数据,使用第三方工具进行清晰,向OpenRefine,不仅可以快速简单地清晰数据看,还可以让非编程人员轻松地看见和使用你的数据

OpenRefine

OpenRefine是一款开源软件,虽然界面是一个浏览器,但实际上是一个桌面应用,必须进行下载和安装。
使用OpenRefine会看到每一列的标签旁边都有一个箭头。这个箭头提供了一个工具菜单,可以对这一列数据执行筛选、排序、变化或删除操作。
* 筛选:用过滤器(filter)、切片器(facet)实现。
* 清洗:只有当数据一开始就比较干净时,数据筛选才可以直接快速地完成。
OpenRefine的数据变化功能是通过OpenRefine表达式语言GREL来实现的。这个语言通过创建规则简单地lambda函数来实现数据的转化。
详情请阅读Github页面

第8章 自然语言处理

8.1 概括数据

下面用来做数据归纳的文字样本来源与美国第九任总统威廉·亨利·哈里森的就职演说的全文
使用n-gram模型,获取2-gram蓄力,并用Python的operator模块对2-gram序列的频率字典进行排序

from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
import string
import operator

def cleanInput(input):
    input = re.sub('\n+'," ",input).lower()
    input = re.sub('\[[0-9]*\]',"",input)
    input = re.sub(' +'," ",input)
    input = bytes(input,"UTF-8")
    ionput = input.decode("ascii","ignore")
    cleanInput = []
    intput = input.split(' ')
    for item in input:
        item = item.strip(string.punctuation)
        if len(item)>1 or (item.lower() == 'a' or item .lower() == 'i'):
            cleanInput.append(item)
    return cleanInput

def ngrams(input ,n):
    input = cleanInput(input)
    output = {}
    for i in range(len(input)-n+1):
        ngramTemp = " ".join(input[i:i+n])
        if ngramTemp not in output:
            output[ngramTemp] = 0
        output[ngramTemp]+=1
    return output

content = str(urlopen("http://pythonscraping.com/files/inaugurationSpeech.txt").read(),'utf-8')
ngrams = ngrams(content,2)
sortedNGrams = sorted(ngrams.items(),key = operator.itemgetter(1),reverse = True)
print(sortedNGrams)

使用当代美式英语语料库中最常用的5000个单词列表,作为一个基本的过滤器来过滤最常用的2-gram序列。(其实只用前100个单词就可以大幅改善效果)
增加一个isCommon函数

def isCommon(ngram):
    commonWords = ["the","be","and","of","a","in","to","have","it","i","that","for","you","he","with","on","do","say","this","they","is","an","at","but","we","his","from","that","not","by","she","or","as","what","go","their","can","who","get","if","would","her","all","my","make","about","know","will","as","up","one","time","has","been","there","year","so","think","when","which","them","some","me","people","take","out","into","just","see","him","your","come","could","now","than","like","other","how","then","its","our","two","more","these","want","way","look","first","also","new","because","day","more","use","no","man","find","here","thing","give","many","well"]
    for word in ngram:
        if word in commonWords:
            return True
    return False

8.2 马尔可夫模型

马尔可夫文字生成器(Markov text generator),基于一种常用于分析大量随机事件的马尔可夫模型。这些随机事件的特点是一个离散事件发生之后,另一个离散事件将在前一个事件的条件下以一定的概率发生。
Google的pagerank算法也是基于这个模型。
还是使用前面就职演讲的内容,可以通过演讲内容的结构生成任意长度的马尔科夫链组成的句子。

from urllib.request import urlopen
from random import randint

def wordListSum(wordList):
    sum = 0
    for word,value in wordList.items():
        sum+=value
    return sum

def retrieveRandomWord(wordList):
    randIndex = randint(1,wordListSum(wordList))
    for word,value in wordList.items():
        randIndex -= value
        if randIndex <= 0 :
            return word

def buildWordDict(text):
    #剔除换行符和引号
    text = text.replace("\n"," ")
    text = text.replace("\"","")

    #保证每个标点符号都和前面的单词在一起
    #这样不会被剔除,保留在马尔科夫链中
    punctuation = [',','.',';',':']
    for symbol in punctuation:
        text = text.replace(symbol," "+symbol+" ")

    words = text.split(" ")
    #过滤空单词
    words = [word for word in words if word!= ""]

    wordDict = {}
    for i in range(1,len(words)):
        if words[i-1] not in wordDict:
            #为单词创建一个词典
            wordDict[words[i-1]] = {}
        if words[i] not in wordDict[words[i-1]]:
            wordDict[words[i-1]][words[i]] = 0
        wordDict[words[i-1]][words[i]] = wordDict[words[i-1]][words[i]]+1
    return wordDict

text = str(urlopen("http://pythonscraping.com/files/inaugurationSpeech.txt").read(),'utf-8')
wordDict = buildWordDict(text)
#生成链长为100的马尔科夫链
length = 100
chain = ""
currentWord = "I"
for i in range(0,length):
    chain += currentWord + " "
    currentWord = retrieveRandomWord(wordDict[currentWord])
print(chain)

8.3 自然语言工具包

Natural Language Toolkit,NLTK,是一个Python库,用于识别和标记英语文本中各个词的词性。官方网站
模块安装完后,可以使用下面的代码下载NLTK自带的文本库

import nltk
nltk.download()

这两行命令会打开NLTK的下载器,推荐安装所有的包,因为你永远也不知道会用到哪一个。

8.3.2 用NLTK做统计分析

NLTK很擅长生成一些统计信息,包括对一段文字的单词数量、单词频率和单词词性的统计。
用NLTK做统计分析一般是从Text对象开始的。Text对象可以通过下面的方法简单的Python字符串来创建

from nltk import word_tokenize
from nltk import Text

tokens = word_tokenize("Here is some not very interesting text")
text = Text(tokens)

word_tokenize函数的参数可以是任意Python字符串
如果你在NLTK库里内置了几本书,可以使用from nltk.book import *来导入
可以把文本对象放到一个频率分布对象FreqDist中,查看哪些单词是最常用的,以及单词的频率

from nltk import FreqDist
fdist = FreqDst(text6)
fdist.most_common(10)  #返回一个字典对象,按照频率降序排序

使用NLTK创建2-gram模型

from nltk import bigrams
bigrams = bigrams(text6)
bigramsDist = FreqDist(bigrams)

另外还有ngrams模块

用NLTK做词性分析

除了度量语言,NLTK还可以用它超级大词典分析文本内容,帮助人们寻找单词的含义,分析各个词的词性

from nltk.book import *
from nltk import word_tokenize
text = word_tokenize(str)
from nltk import pos_tag
pos_tag(text) #结果是一个元组列表,元组有两个元组,第一个元素为单词,第二个元素为单词在NLTK中的词性标记

NLTK用英语的上下文无关文法识别词性,确定的是一个词性后面可以跟哪些词性。
用途:比如你需要的是做动词的google附近的内容,而不是做名词的google附近的内容。
自然语言中的许多歧义问题都可以用NLTK的pos_tag解决。

其他资源

比较优秀的学习资源来介绍自然语言处理和Python的NLTK,如Natural Language Processing with Python
另外还有Natural Language Annotation for Machine Learning

第9章 穿越网络表单与登录窗口进行采集

页面表单可以看成是一种用户提交POST请求的方式,且这种请求方式是服务器能够理解和使用的。

9.1 Python Requests库

Requests库是一个擅长处理复杂HTTP请求、cookie、header(响应头和请求头)等内容的Python第三方库

9.2 提交一个基本表单

大多数网页表单都是由一些HTML字段、一个提交按钮、一个在表单处理完之后跳转的“执行结果”页面构成。
下面是一个简单地HTML表单:

<form method = "post" action = "processing.php">
First name : <input type = "text" name = "firstname"><br/>
Last name : <input type = "text" name = "lastname"><br/>
<input type = "submit" value = "Submit"/>
</form>

注意两个输入字段的名称,字段的名称决定了表单被确认后传送到服务器上的变量名称。需要确保你的变量名称与字段名称是一一对应的。
还有表单的真实行为是在processing.php中,表单的任何POST请求其实都发生在这个页面上,而非表单本身所在的页面。
用Requests库提交表单只需要四行代码

import requests

params = {'firstname':'Ryan','lastname':'Mitchell'}
r = requests.post("http://pythonscraping.com/files/processing.php",data = params)
print(r.text)

表单被提交后,程序会返回执行页面的源代码。
大多数情况下你只需要关注两件事:
* 你想要提交数据的字段名称
* 表单的action属性,也就是表单提交后网站会显示的页面

9.3 单选按钮、复选框和其他输入

无论表单的字段看起来多么复杂,仍然只有两件事需要关注:字段名称和值。
字段名称可以通过查看源代码寻找name属性轻易获得,而字段的值有时候会比较复杂,可能是在表达提交之前通过js生成的。

9.4 提交文件和图像

比如这么一个上传文件表单

<form action = "processing2.php" method = "post" enctype = "multipart/form-data">
    Submit a jpg, png, or gif: <input type = "file" name = "image" /><br/>
    <input type = "submit" value = "Upload file"/>
</form>

文件上传表单除了有个type属性为file外,看起来和之前看到的文字字段表单没有什么两样。而处理方式也十分的相似。

import requests
files = {'uploadFile':open('../files/Python-logo.png','rb')}
r = requests.post("http://pythonscraping.com/pages/processing2.php",files = files)
print(r.text)

这里提交给uploadFile的值是一个用open打开的Python文件对象。

9.5 处理登录和cookie

大多数新式的网站都使用cookie跟踪用户是否已登录的状态信息。登陆成功后会将cookie缓存在本地以供下次验证使用。
用Requests库跟踪cookie同样很简单:

import requests

params = {'username':'Ryan','password':'password'}
r = requests.post("http://pythonscraping.com/pages/cookies/welcome.php",params)
print("Cookie is set to:")
print(r.cookies.get_dict())
print("---------")
print("Going to profile page")
r = requests.get("http://pythonscraping.com/pages/cookies/profile.php",cookies = r.cookies)
print(r.text)

对简单的访问没有问题,但是如果你面对的网站比较复杂,经常暗自调整cookie,或者如果你从一开始就完全不想要用cookie,该如何处理?

import requests

session = requests.Session()
params = {'username':'username','password':'password'}
s = session.post("http://pythonscraping.com/pages/cookies/welcome.php",params)
print("Cookie is set to:")
print(s.cookies.get_dict())
print("---------")
print("Going to profile page ...")
s = session.get("http://pythonscraping.com/pages/cookies/profile.php")
print(s.text)

在这个例子中,会话对象(session)会持续跟踪会话信息,像cookie、header,甚至包括运行HTTP协议的信息。

HTTP基本接入认证

在发明Cookie之前,处理网站登陆最常用的方法就是用HTTP基本接入认证(HTTP basic access authentication)
Requests库里有一个auth模块专门用来处理HTTP认真:

import requests
from requests.auth import AuthBase
from requests.auth import HTTPBasicAuth

auth = HTTPBasicAuth('ryan','password')
r = requests.post(url = "http://pythonscraping.com/pages/auth/login.php",auth = auth)
print(r.text)

9.6 其他表单问题

关于验证码的问题请查看11章,python的图像处理和文本识别方法。
如果在提交表单的时候遇到了一个莫名其妙的错误,或者服务器一直以陌生的理由拒绝你,请查看12章,关于蜜罐(honey pot)、隐含字段(hidden field),以及其他保护网页表单的安全措施。

第10章 采集JavaScript

常用JavaScript库

  1. jQuery,可以动态地创建HTML内容,需要小心采集
  2. Gogle Analytics,最常用的js库和最受欢迎的用户跟踪工具。如果一个网站使用了Google Analytics或者其他类似的网络分析系统,而你不想让网站知道你在采集数据,就要确保把那些分析工具的cookie或者所有cookie都关掉
  3. Google地图。Google地图的API可以让任何网站都嵌入地图。

10.2 Ajax和动态HTML

目前为止,我们与网站服务器通信的唯一方式就是发出HTTP请求获取新页面。
Ajax是Asynchronous JavaScript and XML(异步JavaScript和XML),网站不需要使用单独的页面请求就可以和网站服务器进行交互。
动态HTML是一系列解决网络问题的技术集合,用客户端语言改变页面的HTML元素。
如果你不能够执行那些让页面产生各种神奇效果的javaScript代码,那么你在浏览器看到的内容就可能与你在网络爬虫上看到的内容有所不同。

在Python中用Selenium执行Javascript

Selenium是一个强大的网络数据采集工具,其最初是为了网站自动化测试而开发的。近几年还被广泛用于获取准确的网站快照。Selenium可以让浏览器自动加载网页,获取需要的数据,甚至页面截屏,或者判断网站上某些动作是否发生。
Selenium自己不带浏览器,需要与第三方浏览器结合使用。可以使用PhantomJS的工具来代替浏览器在后台运行。Selenium库是一个在WebDriver上调用的API。WebDriver有点像可以加载网站的浏览器,但是它也可以像BeautifulSoup对象一样用来查找页面元素,与页面上的元素进行交互等。
PhantomJS是一个“无头”(headless)浏览器,它会把网站加载到内存并执行页面上的javascript,但是不会向用户展示网页的图形界面。把这两个工具结合在一起,就可以运行一个非常强大的网络爬虫了。可以处理cookie、JavaScript、header,以及任何你需要做的事。
目标页面会在两秒之后刷新,用普通的爬虫只能够抓到第一个页面,而无法抓取到刷新后的页面
下面的代码可以获取前面测试页面上Ajax“墙”之后的内容

from selenium import webdriver
import time

driver = webdriver.PhantomJS(executable_path='')
driver.get("http://pythonscraping.com/pages/javascript/ajaxDemo.html")
time.sleep(3)
print(driver.find_element_by_id('content').text)
driver.close()

依据你的PhantomJS安装位置,在创建新的PhantomJS WebDriver的时候,需要在Selenium的WebDriver接入点指明PhantomJS可执行文件的路径。
driver = webdriver.PhantomJS(executable_path="/path/to/download/phantomjs-1.9.8-macosx/bin/phantomjs")

Selenium的选择器

前面用BeautifulSoup的选择器选择页面的元素,比如find和findAll。而Selenium和WebDriver的DOM中使用了全新的选择器,都是十分直截了当的名称。
比如这个例子中使用的find_element_by_id
还有find_element_by_css_selector或find_element_by_tag_name等。
如果想要选择页面上具有同样特征的多个元素,可以用elements来返回一个Python列表
如果还是想用BeautifulSoup来解析网页内容,可以用WebDriver的page_source函数返回页面的源代码字符串

pageSource = driver.page_source
bsObj = BeautifulSoup(pageSource)

虽然上述方法可行,但是效率太低,因为页面的加载时间是不确定的。一种更有效的方法是,让Selenium不断地检查某个元素是否存在,以此确定页面是否已经完全加载。
下面的程序用id是loadedButton的按钮检查页面是否已经完全加载

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.PhantomJS(executable_path='')
driver.get("http://pythonscraping.com/pages/javascript/ajaxDemo.html")
try:
    element = WebDriverWait(driver,10).until(EC.presence_of_element_located((By.ID,"loadedButton")))
finally:
    print(driver.find_element_by_id("content").text)
    driver.close()

WebDriverWait和expected_conditions这两个模块组合起来构成了Selenium的隐式等待(implicit wait)
隐式等待与显式等待的不同之处在于,隐式等待是在DOM中某个状态发生后再继续运行代码(没有明确的等待时间,但是有最大等待时限),而显式等待明确设置了等待时间。
在Selenium库里面元素被触发的期望条件有很多种:
* 弹出一个提示框
* 一个元素被选中
* 页面的标题改变了,或者某个文字显示在页面上或者某个元素里
* 一个元素在DOM中变得可见的,或者一个元素从DOM中消失了
大多数期望条件需要在使用前指定等待的目标元素。元素用定位器(locator)指定,用By对象表示。
下面是By对象的选择策略:
* ID
* CLASS_NAME
* CSS_SELECTOR
* LINK_TEXT(通过链接的文字来进行查询)
* PARTIAL_LINK_TEXT(部分链接文字)
* NAME(通过HTML标签的name属性进行查找)
* TAG_NAME(通过HTML标签的名称进行查找)
* XPATH(用XPATH表达式匹配元素)

XPath

XPath是XML文档中导航和选择元素的查询语言,使用方式通常和CSS选择器一样。
四个重要的概念
1. 根节点和非根节点
* /div 选择div节点,只有当它是文档的根节点时
* //div 选择文档中所有的div节点(包括非根节点)
2. 通过属性选择节点
* //@href 选择带href属性的所有节点
* //a[@href=’http:/google.com’]选择页面中所有指向google的链接。
3. 通过位置选择节点
* //a[3] 选择文档中的第三个链接
* //table[last()] 选择文档中最后一个表
* //a[position()<3] 选择文档中的前三个链接
4. 星号(*)匹配任意字符或节点,可以在不同条件下使用
* //table/tr/* 选择所有表格行tr标签的所有的子节点(比如th和td)
* //div[@*] 选择带任意属性的所有div标签

还有其他很多高级的语法特征,比如布尔类型、函数。可以查看微软的XPath语法页面

10.3 处理重定向

客户端重定向是在服务器将页面内容发送到浏览器之前,由javascript完成的页面跳转。服务器端的重定向可以使用urllib来解决,但是客户端必须使用Selenium。
问题在于怎样识别一个页面已经完成了重定向。可以用一个比较智能的方式,重复调用这个元素直到Selenium抛出StaleElementReferenceException异常,也就是说,元素不在页面的DOM里了,说明这时候网站已经跳转了。

from selenium import webdriver
import time
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import StaleElementReferenceException

def waitForLoad(driver):
    elem = driver.find_element_by_tag_name("html")
    count = 0
    while True:
        count += 1
        if count>20:
            print("Timing out after 10 seconds and returning")
            return
        time.sleep(.5)
        try:
            elem == driver.find_element_by_tag_name("html")
        except StaleElementReferenceException:
            return

driver = webdriver.PhantomJS(executable_path='')
driver.get("http://pythonscraping.com/pages/javascript/redirectDemo1.html")
waitForLoad(driver)
print(driver.page_source)

第11章 图像识别和文字处理

主要是验证码识别的问题。一般将图像翻译成文字称为光学文字识别(Optical Character Recognition,OCR)

11.1 OCR库概述

重点介绍两个库:Pillow和Tesseract

11.1.1 Pillow

和PIL一样,Pillow可以轻松地导入代码,并通过大量的过滤、修饰甚至像素级的变换操作处理图片

from PIL import Image,ImageFilter

kitten = Image.open("kitten.jpg")
blurryKitten = kitten.filter(ImageFilter.GaussianBlur)
blurryKitten.save("kitten_blurred.jpg")
blurryKitten.show()

Pillow文档

11.1.2 Tesseract

Tesseract是目前公认最优秀的、最精确的开源OCR系统
Tesseract是一个Python的命令行工具,而不是库。安装后要用tesseract命令在Python的外面运行

安装和设置

要使用Tesseract的功能,比如训练程序识别字母,需要先在系统中设置一个新的环境变量$TESSDATA_PREFIX,让Tesseract知道训练的数据文件存储在哪里。
大多数系统(linux/Mac OS)可以这么设置:export TESSDATA_PREFIX=/user/local/share

11.1.3 Numpy

Numpy可以用数学方法把图片表示成巨大的像素数组

11.2 处理格式规范的文字

  • 标准字体(不包含手写体、草书)
  • 没有多余的痕迹、污点
  • 排列整齐,没有歪斜
  • 没有超出图片范围或紧贴边缘

通过下面的命令运行Tesseract,读取文件并把结果写在一个文本文件中tesseract text.tif textoutput | cat textoutput.txt
如果对图片进行处理,使得背景色变得渐变,会发现在背景色较浅的地方,文字变得越来越难以识别。可以利用Pillow库,创建一个阈值过滤器来去掉渐变的背景色,只留下文字,从而让图片更加清晰。

from PIL import Image
import subprocess

def cleanFile(filePath,newFilePath):
    image = Image.open(filePath)

    #对图片进行阈值过滤,然后保存
    image = image.point(lambda x: 0 if x<143 else 255)
    image.save(newFilePath)

    #调用系统的tesseract命令对图片进行OCR识别
    subprocess.call(["tesseract",newFilePath,"output"])

    # 打开文件读取结果
    outputFile = open("output.txt",'r')
    print(outputFile.read())
    outputFile.close()

cleanFile("text_2.jpg","text_2_clean.png")

在提交给Tesseract处理之前,那些带标题的、带有大片空白的图片,或者有其他问题的图片,都应该做预处理

从网站图片中抓取文字

下面的程序导航到托尔斯泰的《战争与和平》的大字号印刷版,打开阅读器,收集图片的URL链接,然后下载图片,识别图片,最后打印每个图片的文字。

import time
from urllib.request import urlretrieve
import subprocess
from selenium import webdriver

#创建新的Selenium driver
driver = webdriver.PhantomJS(executable_path = '<path to Phantom JS>')
#有时PhantomJS查找元素会有问题,但是Filefox没有,可以试试Firefox

driver.get("http://www.amazon.com/War-Peace-Leo-Nikolayevich-Tolstoy/dp/1427030200")
time.sleep(2)

#单击图书预览按钮
driver.find_element_by_id("sitbLogoImg").click()
imageList = set()

#等待页面加载完成
time.sleep(5)
#当向右箭头可以点击时,开始翻页
while "pointer" in driver.find_element_by_id("sitbReaderRightPageTurner").get_attribute("style"):
    driver.find_element_by_id("sitbReaderRightPageTurner").click()
    time.sleep(2)
    #获取已加载的新页面(一次可以加载多个页面,但是重复的页面不能加载到集合中)
    pages = driver.find_elements_by_xpath("//div[@class='pageImage']/div/img")
    for page in pages:
        image = page.get_attribute("src")
        imageList.add(image)

driver.quit()

#用Tesseract处理我们收集的图片URL链接
for image in sorted(imageList):
    urlretrieve(image,"page.jpg")
    p = subprocess.Popen(["tesseract","page.jpg","page"],stdout = subprocess.PIPE,stderr = subprocess.PIPE)
    p.wait()
    f = open("page.txt","r")
    print(f.read())

上述代码可以很好地识别出文字,但是对于彩色封面上的文字表现不佳。

11.3 读取验证码和训练Tesseract

CAPTCHA,全自动区分计算机和人类的图灵测试(Completely Automated Public Turing test to tell Computers and Humans Apart)
普通的OCR难以识别验证码

训练Tesseract

要训练Tesseract识别一种文字,你需要向Tesseract提供每个字符不同形式的样本。
第一步:首先讲大量的验证码样本下载到一个文件夹里,下载的样本数量由验证码的复杂程度决定;建议使用验证码的真实结果给每个样本文件命名
第二步:准确告诉Tesseract一张图片的每个字符是什么,以及每个字符的具体位置。这里需要创建一些矩形定位文件(box file),一个验证码图片生成一个矩形定位文件。如下所示:

4 15 26 33 55 0
M 38 13 67 45 0
m 79 15 101 26 0
C 111 33 136 60 0
3 147 17 176 45 0

第一列符号是图片中的每个字符,后面的4个数字分别是包围这个字符的最小矩形的坐标(4个数字分别对应每个字符的左下角x坐标,左下角y坐标,右上角x坐标和右上角y坐标),最后一个数字”0”表示图片样本的编号。
可以使用在线工具Tesseract OCR Chopper,上传图片,如果要添加新的矩形就单击add按钮,最后把新生成的矩形定位文件复制到一个新文件里就可以了。
建议尽量保证足够的训练数据。
自动创建所有必须的训练文件,作者写好了Python版本的解决方案来处理同时包含图片文件和.box文件的数据文件夹。
这个解决方案的主要配置方式和步骤都在main方法里(目前修改为了init方法)和runAll方法里

def __init__(self):
    languageName = "eng"
    fontName = "captchaFont"
    directory = "<path to images>"

def runAll(self):
    self.createFontFile()
    self.cleanImages()
    self.renameFiles()
    self.extractUnicode()
    self.runShapeClustering()
    self.runMfTraining()
    self.runCnTraining()
    self.createTessData()

需要手动设置三个变量:
* languageName:Tesseract用三个字母的语言缩写代码表示识别的语言种类,大多数情况是“eng”(English)
* fontName:表示选择的字体名称,不能包含空格
* directory:表示包含所有图片和.box文件的目录。建议使用绝对路径。

runAll中每个函数的用法
* createFontFile创建一个font_properties文件,让Tesseract知道我们要创建的新字体
* cleanImages首先创建所有样本图片的高对比度版本,然后转换成灰度图,并进行一些清理,让Tesseract更容易读取图片文件。
* renameFiles把所有的图片文件和.box文件的文件名改成Tesseract需要的形式(<languageName>.<fontName>.exp<fileNumber>.box<languageName>.<fontName>.exp<fileNumber>.tiff)
* extractUnicode函数会检查所有已创建的.box文件,确定要训练的字符集范围。
* 之后的三个函数,runShapeClustering、runMfTraining和runCtTraining分别用来创建文件shapetable、pfftable和normproto。它们会生成每个字符的几何和形状信息,也为Tesseract提供计算字符若干可能结果的概率统计信息。
* 最后,Tesseract会用之前设置的语言名称对数据文件夹编译出的每个文件进行重命名,然后把所有的文件编译到最终的训练文件eng.traineddata中。
你唯一需要做的,就是把刚刚创建好的eng.traineddata文件复制到tessdata文件夹里。
Tesseract文档

11.4 获取验证码提交答案

大多数网站生成的验证码图片具有以下属性:
* 它们是服务器端的程序动态生成的图片。验证码图片的src属性可能和普通图片不太一样
* 图片和答案存储在服务器端的数据库里。
* 很多验证码有时间限制,如果太长时间没有解决就会失效。

常用的处理方法就是,首先把验证码图片下载到硬盘里,清理干净,然后用Tesseract处理图片,最后返回符合网站要求的识别结果
下面演示如何用网络机器人破解验证码:

from urllib.request import urlretrieve
from urllib.request import urlopen
from bs4 import BeautifulSoup
import subprocess
import requests
from PIL import Image
from PIL import ImageOps

def cleanImage(imagePath):
    image = Image.open(imagePath)
    image = image.point(lambda x:0 if x<143 lese 255)
    borderImage = ImageOps.expand(image,border=20,fill='white')
    borderImage.save(imagePath)

html = urlopen("http://www.pythonscraping.com/humans-only")
bsObj = BeautifulSoup(html)
#收集需要处理的表单数据(包括验证码和输入字段)
imageLocation = bsObj.find("img",{"title":"Image CAPTCHA"})["src"]
formBuildId = bsObj.find("input",{"name":"form_build_id"})["value"]
captchaSid = bsObj.find("input",{"name":"captcha_sid"})["value"]
captchaToken = bsObj.find("input",{"name":"captcha_token"})["value"]

captchaUrl = "http://pythonscraping.com"+imageLocation
urlretrieve(captchaUrl,"captcha.jpg")
cleanImage("captcha.jpg")
p = subprocess.Popen(["tesseract","captcha.jpg","captcha"],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
p.wait()
f = open("captcha.txt","r")

#清理识别结果中的空格和换行符
captchaResponse = f.read().replace(" ","").replace("\n","")
print("Captcha solution attempt"+captchaResponse)

if(len(captchaResponse)==5)
    params =    {"captcha_token":captchaToken,"captcha_sid":captchaSid,
                "form_id":"comment_node_page_form","form_build_id":formBuildId,
                "captcha_response":captchaResponse,"name":"Ryan Mitchell",
                "subject":"I come to seek the Grail",
                "comment_body[und][0][value]":"...and I am definitely not a bot"}
    r = requests.post("http://www.pythonscraping.com/comment/reply/10",data=params)
    responseObj = BeautifulSoup(r.text)
    if responseObj.find("div",{"class":"messages"}) is not None:
        print(responseObj.find("div",{"class":"messages"}).get_text())
    else:
        print("There was a problem reading the CAPTCHA correctly!")

值得之一的是有两种异常情况会导致程序失败,一种是识别出来的结果不是五个字符,第二种是虽然识别出了五个字符但是服务器不认可。实际上五个字符都对的概率约是30%

第12章 避开采集陷阱

12.1 道德规范

希望你牢记这里演示的许多程序和介绍的技术都不应该在任何一个网站上使用。不仅因为这么做对网站不好,而且你可能会收到一个停止并终止警告信(cease-and-desist letter),甚至还有可能发生更糟糕的事情。

12.2 让网络机器人看起来更像人类用户

12.2.1 修改请求头

requests模块是一个设置请求头的利器。HTTP的请求头是在你每次向网络服务器发送请求时,传递的一组属性和配置信息。HTTP定义了十几种古怪的请求头类型,不过大多数不常用。只有下面的七个字段被大多数浏览器用来初始化所有网络请求(表中的信息是作者自己浏览器的数据)。

属性内容
Hosthttps://www.google.com
Connectionkeep-alive
Accepttext/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
User-AgentMozilla/5.0(Macintosh;Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) CHrome/39.0.2171.95 Safari/537.36
Referrerhttps://www.google.com/
Accept-Encodinggzip,deflate,sdch
Accept-Languageen-US,en;q=0.8

而经典的python爬虫在使用urllib标准库时的请求头如下:

属性内容
Accept-Encodingidentity
User-AgentPython-urllib/3.4

显然,默认的爬虫的请求头很容易被会被网站管理员发现并因此屏蔽。
请求头可以通过requests模块进行自定义。https://www.whatismybrowser.com/网站就是一个非常棒的网站,可以让浏览器测试浏览器属性。下面的程序采集这个网站的信息来验证我们浏览器的cookie设置:

import requests
from bs4 import BeautifulSoup
session = requests.Session()
headers = {'User-Agent':"Mozilla/5.0(Macintosh;Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) CHrome/39.0.2171.95 Safari/537.36",
            "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"}
url = "http://www.whatismybrowser.com/developers/what-http-headers-is-my-browser=sending"
req = session.get(url,headers = headers)
bsObj = BeautifulSoup(req.text)
print(bsObj.find("table",{"class":"table-striped"}).get_text)

虽然网站可能会对HTTP请求头的每个属性进行“是否具有人性”的检查,但是通常真正重要的参数是User-Agent。无论做什么项目,都一定要记得把User-Agent属性设置成不容易引起怀疑的内容,千万不要用Python-urllib/3.4。另外,如果你正在处理一个警觉性比较高的网站,就要注意那些经常用但是很少检查的请求头,比如Accept-Language属性。

请求头会改变你观看网络世界的方式

如果你正在为一个机器学习的研究项目写一个语言翻译器,却没有大量的翻译文本来测试它的效果,只需要简单的修改请求头属性Accept-Language就可以了。一般大型跨国企业都会为不同的语言准备不同的文本内容。请求头还可以改变内容的布局样式,比如User-Agent改为Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53,就会访问到网站的移动端。移动版本一般少有广告、Flash和其他干扰,至少我写读数笔记的时候还是这样子的。

12.2.2 处理cookie

正确处理cookie可以避免很多采集问题。网站会有cookie来跟踪你的访问过程,如果发现了爬虫异常行为就会中断你的访问,比如特别快速地填写表单,或者浏览大量的页面。
如果你在采集一个或者多个目标网站,我建议你检查这些网站生成的cookie,然后想想哪一个cookie是爬虫需要处理的。有一些浏览器插件可以为你显示访问网站和离开网站时cookie是如何设置的,如EditThisCookie就是一款这样的Chrome插件
详情会看9.5节。注意,requests库不能执行javascript,如果因为js导致cookie有所变化,需要使用selenium。
可以对任意网站调用webdriver的get_cookie()方法来查看cookie:

from selenium import webdriver
driver = webdriver.PhantomJS(executable_path='<path to Phantom JS>')
driver.get("http://pythonscraping.com")
driver.implicitly_wait(1)
print(driver.get_cookies())

此外还有delete_cookie()、add_cookie()和delete_all_cookies()方法来处理cookie,还可以保存cookie以备其他网络爬虫使用。

12.2.3 时间就是一切

有一些防护措施完善的网站可能会组织你快速地提交表单,或者快速与网站交互。
应该尽量保证一次加载页面加载且数据请求最小化。如果条件允许,尽量为每个页面访问增加一点时间间隔。
虽然网络数据采集经常会为了获取数据而破坏规则和冲破底线,但是合理控制速度是你不应该破坏的规则。

12.3 常见表单安全措施

12.3.1 隐含输入字段值

在HTML表单中,“隐含”字段可以让字段的值对浏览器可见,但是对用户不可见。在找到另一个最佳用途之前,隐含字段主要用于组织爬虫自动提交表单。
用隐含字段组织网络数据采集的方式主要有两种。第一种是表单页面上的一个字段可以用服务器生成的随机变量表示。如果提交时这个值不再表单处理页面上,服务器就有理由认为这个提交不是在原始表单页面上提交的。绕开这个问题的最佳方法就是,首先采集表单所在页面上生成的随机变量,然后再提交到表单处理页面。
第二种方式是“蜜罐”。如果表单里包含一个具有普通名称的隐含字段,比如username或email address,设计不好的网络机器人往往不管这个字段是不是对用户可见,直接填写这个字段并向服务器提交。

12.3.2 避免蜜罐

虽然在网络数据采集时用CSS属性区分有用信息和无用信息很容易,但有时候也会出问题,比如一个字段通过CSS设置成不可见,显然用户不能填写这个字段。
不仅仅是网页的表单,甚至是链接、图片、文件,以及一些可以被机器人读取,但普通用户在浏览器上却看不到的任何内容上面,只要访问者触及这些“隐含”内容,就会触发服务器脚本封杀这个用户的IP地址。(我在想能不能用CSS样式或Js来实现)
因为Selenium可以获取访问页面的内容,所以它可以区分页面的可见元素和隐藏元素。用is_displayed()可以判断元素在页面上是否可见。

from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement

driver = webdriver.PhantomJS(executable_path = '')
driver.get("http://pythonscraping.com/pages/itsatrap.html")
for link in links:
    if not link.is_displayed():
        print("The link "+link.get_attribute("href")+" is a trap")

fields = driver.find_elements_by_tag_name("input")
for field in fields:
    if not field.is_displayed():
        print("Do not change value of "+field.get_attribute("name"))

12.4 问题检查表

  • 首先,如果你从网络服务器收到的内容是空白的/缺少信息的,或其他遇到不符合预期的情况,可能是网站创建页面时的JavaScript执行有问题。
  • 如果你准备向网站提交表单或发出POST请求,记得检查一下页面的内容,看看每个字段是否都填好,格式也正确。
  • 如果你已经登陆网站却不能保持登陆状态,需要检查cookie。
  • 如果你在客户端上遇到了HTTP错误,尤其是403错误,说明网站可能把你的IP当作机器人了。
  • 确认你的爬虫在网站上的速度不是特别快。
  • 修改请求头
  • 确认没有点击或访问任何人类用户通常不能点击或接入的信息
  • 如果都不行的话,请联系网管,请求他们允许你使用爬虫采集数据。

第13章 用爬虫测试网站

网站前端测试可以使用爬虫来完成。

13.1 测试间接

运行一套测试方法来保证你的代码按照既定的目标运行,不仅可以节约时间,也可以减少你对bug的顾虑,还能让新版本的升级变得更简单

单元测试

测试与单元测试基本是等价的。
* 每个单元测试用于测试一个零件功能的一个方面。一个零件的所有单元测试集成在一个类中
* 每个单元测试可以独立地运行。
* 每个单元测试至少包含一个断言。
* 单元测试与生产代码是分离的。

13.2 Python单元测试

Python的单元测试模块unittest。只要先导入模块然后继承unittest.TestCase类,就可以实现下面的功能:
* 为每个单元测试的开始和结束提供setUp和tearDown函数
* 提供不同的“断言”语句让测试成功或失败
* 把所有以test_开头的函数当作单元测试运行,忽略不带test_的函数

例子:

import unittest

class TesetAddition(unittest.TestCase):
    def setUp(self):
        print("Setting up the test")

    def tearDown(self):
        print("Tearing down the test")

    def test_twoPlusTwo(self):
        total = 2+2
        self.assertEqual(4,total)

if __name'__main__':
    unittest.main()

13.3 Selenium单元测试

在网络中测试JavaScript同样困难,不过可以使用Selenium来进行测试。
Selenium不要求单元测试必须是类的一个函数,它的“断言”页不需要括号,而且测试通过的话不会有提示,只有测试失败了才会有提示。

driver = webdriver.PhantomJS()
driver.get("http://en.wikipedia.org/wiki/Monty_Python")
assert "Monty Python" in driver.title
driver.close()

因此,写Selenium单元测试比Python单元测试更加随意,断言语句甚至可以整合到生产代码中。

与网站进行交互

动作链:为了一次性完成一个元素的多个操作,可以用动作链来存储多个操作,然后在一个程序中执行一次或多次。

### 方法1
firstnameField.send_keys("Ryan")
lastname.send_keys("Mitchell")
submitButton.click()
###方法2
actions = ActionChains(driver).click(firstnameField).send_keys("Ryan")
                                .click(lastnameField).send_keys("Mitchell")
                                .send_keys(Keys.RETURN)
actions.perform()

无论使用第一个方法还是第二个方法,理论上都是一样的。
第一个方法使用的是表单的点击按钮,第二个方法提交表单使用的是回车键

鼠标拖放动作

Selenium可以处理更加复杂的网络表单交互行为,其中就包括鼠标拖放动作。使用它的拖放函数,你需要指定一个被拖放的元素以及拖放的距离,或者元素将被拖放到的目标元素

from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver import ActionChains

driver = webdriver.PhantomJS(executable_path = '')
driver.get("http://pythonscraping.com/pages/javascript/draggableDemo.html")

print(driver.find_element_by_id("message").text)

element = driver.find_element_by_id("draggable")
target = driver.find_element_by_id("div2")
actions = ActionChains(driver)
actions.drag_and_drop(element,target.perform())

print(driver.find_element_by_id("message").text)
截屏

除了普通的测试功能,Seleninum还可以截取屏幕,在单元测试中就可以创建,不需要点击截屏按钮就可以获取

driver = webdriver.PhantomJS()
driver.get('http://www.pythonscraping.com/')
driver.get_screenshot_as_file('tmp/pythonscraping.png')

13.4 Python单元测试与Selenium单元测试

可以将Selenium导入Python的单元测试。

第14章 远程采集

让程序在不同的服务器上运行,可以使用不同的IP地址。

14.1 为什么要用远程服务器

14.1.1 避免IP地址被封杀

建立网络爬虫的第一原则是:所有信息都可以伪造。但是IP地址无法被伪造。

14.1.2 移植性和扩展性

一些任务通过个人电脑将很难完成。使用分布式计算是一个不错的选择。

14.2 Tor代理服务器

洋葱路由网络,常用缩写Tor,是一个IP地址匿名手段。
虽然Tor网络可以让你访问网站时显示的IP是一个不可追踪的IP,但是网站上留给服务器的信息仍然可能暴露身份。
在Python中使用Tor,需要首先安装和运行Tor。

PySocks

PySocks是一个非常简单的Python代理服务器通信模块,可以和Tor配合使用。
模块使用非常简单,运行时,Tor服务必须在9150端口(默认值)

import socks
import socket
from urllib.request import urlopen

socks.set_default_proxy(socks.SOCKS5,"localhost",9150)
socket.socket = socks.socksocket
print(urlopen("http://icanhazip.com").read())

网站http://icanhazip.com/会显示客户端连接的网站服务器的IP地址,可以用来测试Tor是否正常运行。

如果要在Tor里面使用Selenium和PhantomJS,不需要PySocks,只需要保证Tor在运行就可以了:

from selenium import webdriver
service_args = ['--proxy = localhost:9150','--proxy-type=socks5',]
driver = webdriver.PhantomJS(executable_path = '',service_args=service_args)

driver.get("http://icanhazip.com")
print(driver.page_source)
driver.close()

14.3 远程主机

14.3.2 从云主机运行

14.4 其他资源

Google Compute Engine是通过Python和JavaScript使用Google云计算平台的第一手资料。
Python and AWS Cookbook可以让你顺利启动AWS服务。

附录C

C.4 robots.txt和服务协议

大多数网站在每页页脚都有自己的服务协议。TOS不仅包括网络爬虫和自动接入的规则,而且还包括网站收集的信息类型和信息用途,通常还有一条不承担责任的法律声明,提示用户网站提供的服务没有任何费用也不做任何保证。
rebots.txt,机器人排除标准(Robots Exclusion Standard),是一种业内惯用的做法,版本多样,而且并不是常用的约束。
虽然看到一个指明爬虫采集限制范围的文件让人感到憋屈,但是它其实可以成为爬虫开发的指示灯。如果你发现一个robots.txt文件禁止采集网站上某个部分的内容,那么基本可以确定网管同意你采集其他部分的所有内容。

posted @ 2018-07-30 22:21  千灵域  阅读(54)  评论(0)    收藏  举报