- “网络数据采集是为普通大众所喜闻乐见的计算机巫术”。
- bug 是产品生命中的挑战,好产品是不断面对 bug 并战胜 bug 的结果。
有人说编程语言就是宗教,不同语言的设计哲学不同,行为方式各异,“非我族类,其心必异”,但本着美好生活、快乐修行的初衷,我们对所有语言都时刻保持敬畏之心,尊重信仰自由,努力做好自己的功课。对爱好Python的人来说,人生苦短,Python当歌!
学习笔记。ALL FROM:Web Scraping with Python: Collecting Data from the Modern Web
先填坑。
第一部分 创建爬虫
第1章 初见网络爬虫
第2章 复杂HTML解析
第3章 开始采集
第4章 使用API
第5章 存储数据
第6章 读取文档
第二部分 高级数据采集
第7章 数据清洗
第8章 自然语言处理
第9章 穿越网页表单与登录窗口进行采集
第10章 采集JavaScript
第11章 图像识别与文字处理
第12章 避开采集陷阱
第13章 用爬虫测试网站
第14章 远程采集
第一部分 创建爬虫
重点介绍网络数据采集的基本原理。如何用 Python 从网络服务器请求信息,如何对服务器的响应进行基本处理,以及如何以自动化手段与网站进行交互。
第1章 初见网络爬虫
网络链接
|
|
这是一个获取http://pythonscraping.com/pages/page1.html 网页的全部HTML代码的程序。
urllib & urllib2:在 Python 3.x 里,urllib2改名为urllib,被分成一些子模块: urllib.request、urllib.parse 和 你urllib.error。
urllib是Python的标准库(就是说不用额外安装就可以运行这个例子),包含了从网络请求数据,处理 cookie,甚至改变像请求头和用户代理这些元数据的函数。
urlopen:用来打开并读取一个从网络获取的远程对象。因为它是一个非常通用的库(它可以轻松读取HTML文件、图像文件,或其他任何文件流)。
BeautifulSoup简介
BeautifulSoup通过定位HTML标签来格式化和组织复杂的网络信息,用简单易用的Python对象展现XML结构信息。
安装
安装 Python 的包管理器 pip,然后运行:
注意:Python2、Python3的版本问题。
运行Beautifulsoup4
|
|
输出结果:
从网页中提取的h1标签被嵌在 BeautifulSoup 对象 bsObj 结构的第二层(html → body→ h1)。但是,当我们从对象里提取 h1 标签的时候,可以直接调用它:
其实,下面的所有函数调用都可以产生同样的结果:
可靠的网络链接
|
|
这行代码主要可能会发生两种异常:
• 网页在服务器上不存在(或者获取页面的时候出现错误)
• 服务器不存在
第一种异常发生时,程序会返回HTTP错误。HTTP错误可能是“404 Page Not Found”“500Internal Server Error”等。所有类似情形,urlopen函数都会抛出“HTTPError”异常。我们可以用下面的方式处理这种异常:
如果程序返回 HTTP 错误代码,程序就会显示错误内容,不再执行else语句后面的代码。
第2章 复杂HTML解析
面对页面解析难题(Gordian Knot)的时候,不假思索地直接写几行语句来抽取信息是非常直接的做法。但是,像这样鲁莽放纵地使用技术,只会让程序变得难以调试或脆弱不堪,甚至二者兼具。
正确的做法:
• 寻找“打印此页”的链接,或者看看网站有没有 HTML 样式更友好的 移动版(把自己的请求头设置成处于移动设备的状态,然后接收网站移动版。移动版后面还会提到,是一个获取数据非常好的渠道。当有一个网页很难爬去的时候,应该去试试它的移动版。
• 寻找隐藏在 JavaScript 文件里的信息。要实现这一点,你可能需要查看网页加载的JavaScript 文件。
• 虽然网页标题经常会用到,但是这个信息也许可以从网页的URL链接里获取。
• 如果要找的信息只存在于一个网站上, 别处没有,那确实是运气不佳。如果不只限于这个网站,那么可以找找其他数据源。有没有其他网站也显示了同样的数据?网站上显示的数据是不是从其他网站上抓取后攒出来的?
BeautifulSoup
将介绍通过属性查找标签的方法,标签组的使用,以及标签解析树的导航过程。
CSS的发明是网络爬虫的福音。CSS可以让HTML元素呈现出差异化,使那些具有完全相同修饰的元素呈现出不同的样式。
例子:创建一个网络爬虫来抓取http://www.pythonscraping.com/pages/warandpeace.html 这个网页。
创建一个 BeautifulSoup 对象,和第1章里使用的程序类似:
通过BeautifulSoup对象,可以用findAll函数抽取只包含在
标签里的文字,这样就会得到一个人物名称的Python列表(findAll是一个非常灵活的函数,后面会经常用到它):
之前调用 bsObj.tagName 只能获取页面中的第一个指定的标签。现在调用 bsObj.findAll(tagName, tagAttributes)可以获取页面中所有指定的标签,不再只是第一个了。
获取人名列表之后,程序遍历列表中所有的名字,然后打印name.get_text(),就可以把标签中的内容分开显示了。
get_text():.get_text()会把正在处理的HTML文档中所有的标签都清除,然后返回一个只包含文字的字符串。 假如正在处理一个包含许多超链接、段落和标签的大段源代码,那么 .get_text()会把这些超链接、段落和标签都清除掉,只剩下一串不带标签的文字。
用BeautifulSoup对象查找想要的信息,比直接在HTML文本里查找信息要简单得多。 通常在准备打印、存储和操作数据时,应该最后才使用.get_text()。一般情况下,应该尽可能地保留HTML文档的标签结构。后面说不定还会用到的。
BeautifulSoup的find()和findAll()
BeautifulSoup里的find()和findAll()可能是最常用的两个函数。借助它们可以通过标签的不同属性轻松地过滤 HTML页面,查找需要的标签组或单个标签。
这两个函数非常相似,BeautifulSoup文档里两者的定义就是这样:
标签参数tag前面已经介绍过——可以传一个标签的名称或多个标签名称组成的Python列表做标签参数。例如,下面的代码将返回一个包含HTML文档中所有标题标签的列表:
1.findAll({"h1","h2","h3","h4","h5","h6"})属性参数attributes是用一个Python字典封装一个标签的若干属性和对应的属性值。例
如,下面这个函数会返回HTML文档里红色与绿色两种颜色的span标签:1.findAll("span", {"class":{"green", "red"}})递归参数recursive是一个布尔变量。你想抓取HTML文档标签结构里多少层的信息?如果recursive 设置为True,findAll就会根据你的要求去查找标签参数的所有子标签,以及子标签的子标签。如果recursive设置为False,findAll 就只查找文档的一级标签。findAll默认是支持递归查找的(recursive默认值是True);一般情况下这个参数不需要设置,除非你真正了解自己需要哪些信息,而且抓取速度非常重要,那时你可以设置递归参数。
PS:之前感觉都是在瞎胡爬啊,现在再回头看,遇到的问题都是讲过的。书,还是要看三遍才算看过。
- 文本参数text有点不同,它是用标签的文本内容去匹配,而不是用标签的属性。假如想查找前面网页中包含“the prince”内容的标签数量,可以把之前的findAll方法换成下面的代码:12nameList = bsObj.findAll(text="the prince")print(len(nameList))
输出结果为“ 7”。
范围限制参数limit,显然只用于findAll方法。find其实等价于findAll的limit等于1时的情形。如果你只对网页中获取的前x项结果感兴趣,就可以设置它。但是要注意,这个参数设置之后,获得的前几项结果是按照网页上的顺序排序的,未必是你想要的那前几项。
PS:之前的爬虫,也遇到过这种情况。解决的办法是切片的方法。还有一个关键词参数keyword,可以让你选择那些具有指定属性的标签。例如:
12allText = bsObj.findAll(id="text")print(allText[0].get_text())
其他BeautifulSoup对象
BeautifulSoup库里的两种对象。
• BeautifulSoup对象:前面代码示例中的bsObj
• 标签Tag对象:BeautifulSoup对象通过find和findAll,或者直接调用子标签获取的一列对象或单个对象,就像:bsObj.div.h1
• NavigableString对象:用来表示标签里的文字,不是标签(有些函数可以操作和生成NavigableString 对象,而不是标签对象)。
• Comment对象:用来查找HTML文档的注释标签,
导航树
findAll函数通过标签的名称和属性来查找标签。但是如果需要通过标签在文档中的位置来查找标签,该怎么办?这就是导航树(Navigating Trees)的作用。在第1章里,我们看过用单一方向进行BeautifulSoup标签树的导航:
处理子标签和其他后代标签
在 BeautifulSoup库里,孩子(child)和后代(descendant)有显著的不同:和人类的家谱一样,子标签就是一个父标签的下一级,而后代标签是指一个父标签下面所有级别的标签。例如,tr标签是tabel标签的子标签,而tr、th、td、img和span标签都是tabel 标签的后代标签(我们的示例页面中就是如此)。所有的子标签都是后代标签,但不是所有的后代标签都是子标签。
如果你只想找出子标签,可以用 .children 标签:
这段代码会打印giftList表格中所有产品的数据行。如果你用descendants()函数而不是children() 函数,那么就会有二十几个标签打印出来,包括img标签、span标签,以及每个td标签。掌握子标签与后代标签的差别十分重要!
处理兄弟标签
BeautifulSoup 的 next_siblings()函数可以让收集表格数据成为简单的事情,尤其是处理带标题行的表格:
这段代码会打印产品列表里的所有行的产品,第一行表格标题除外。为什么标题行被跳过了呢?有两个理由。首先,对象不能把自己作为兄弟标签。任何时候你获取一个标签的兄弟标签,都不会包含这个标签本身。其次,这个函数只调用后面的兄弟标签。例如,如果我们选择一组标签中位于中间位置的一个标签,然后用next_siblings()函数,那么它就只会返回在它后面的兄弟标签。因此,选择标签行然后调用next_siblings,可以选择表格中除了标题行以外的所有行。
让标签的选择更具体:如果我们选择bsObj.table.tr或直接就用bsObj.tr来获取表格中的第一行,上面的代码也可以获得正确的结果。但是,我们还是采用更长的形式写了一行代码,这可以避免各种意外:
即使页面上只有一个表格(或其他目标标签),只用标签也很容易丢失细节。另外,页面布局总是不断变化的。一个标签这次是在表格中第一行的位置,没准儿哪天就在第二行或第三行了。 如果想让你的爬虫更稳定,最好还是让标签的选择更加具体。如果有属性,就利用标签的属性。和next_siblings一样,如果你很容易找到一组兄弟标签中的最后一个标签,那么previous_siblings函数也会很有用。当然,还有next_sibling和previous_sibling函数,与next_siblings和previous_siblings的作用类似,只是它们返回的是单个标签,而不是一组标签。
父标签处理
偶尔在特殊情况下你也会用到BeautifulSoup 的父标签查找函数, parent 和 parents。例如:
这是如何实现的呢?下面的图形是我们正在处理的HTML页面的部分结构,用数字表示步骤的话:
(1) 选择图片标签 src=”../img/gifts/img1.jpg”;
(2) 选择图片标签的父标签(在示例中是 td 标签);
(3)选择td标签的前一个兄弟标签previous_sibling(在示例中是包含美元价格的td标签);
(4) 选择标签中的文字,“ $15.00”。
正则表达式
如果你有一个问题要用到正则表达式,那么这就成两个问题了。
之所以叫正则表达式,是因为它们可以识别正则字符串(regular string);也就是说,它们可以这么定义:“如果你给我的字符串符合规则,我就返回它”,或者是“如果字符串不符合规则,我就忽略它”。这在要求快速浏览大文档,以查找像电话号码和邮箱地址之类的字符串时是非常方便的。
比如:“aaaabbbbbccccd”“aabbbbbcc”等
正则表达式就是表达这组规则的缩写。这组规则的正则表达式如下所示:
分解之后就会很清楚了。
• aa
a后面跟着的a(读作a星)表示“重复 任意次 a,包括0次”。这样就可以保证字母a至少出现一次。
• bbbbb
这没有什么特别的——就是5次b。
• (cc)
任意偶数个字符都可以编组, 这个规则是用括号两个c,然后后面跟一个星号,表示有
任意次两个c(也可以是0次)。
• (d|)
增加一个竖线( |)在表达式里表示“这个 或* 那个”。本例是表示“增加一个后面跟着空格的d,或者只有一个空格”。这样我们可以保证字符串的结尾最多是一个后面跟着空格的d。
正则表达式在实际中的一个经典应用是识别邮箱地址。虽然不同邮箱服务器的邮箱地址的
具体规则不尽相同, 但是我们还是可以创建几条通用规则.
- 规则:邮箱地址的第一部分至少包括一种内容: 大
写字母、小写字母、数字 0~9、点号(.)、加号(+)或下划线( _)
正则表达式:[A-Za-z0-9._+]+:这个正则表达式简写非常智慧。例如,它用“A-Z”表示“任意A~Z的大写字母”。把所有可能的序列和符号放在中括号(不是小括号)里表示“括号中的符号里任何一个”。要注意后面的加号,它表示“这些符号都可以出现多次,且至少出现1次”
- 规则:邮箱地址会包含一个@符号
正则表达式:@:这个符号很直接。@符号必须出现在中间位置,有且仅有 1 次 - 规则:在符合@之后,邮箱地址还必须至少包含一个大写或小写字母
正则表达式:[A-Za-z]+:可能只在域名的前半部分、符号@后面用字母。而且,至少有一个字。 - 规则:之后跟一个点号( .)
正则表达式:.:在域名前必须有一个点号( .) - 规则:最后邮箱地址用com、org、edu、net结尾(实际上,顶级域名有很多种可能,但是作为示例演示这四个后缀够用了)。
正则表达式:(com|org|edu|net):这样列出了邮箱地址中可能出现在点号之后的字母序列
把上面的规则连接起来,就获得了完整的正则表达式:1[A-Za-z0-9\._+]+@[A-Za-z]+\.(com|org|edu|net)
正则表达式和BeautifulSoup
直接定位那些标签来查找信息。直接通图片的文件路径来查找:
这段代码会打印出图片的相对路径,都是以../img/gifts/img开头,以.jpg结尾,其结果如下所示:
正则表达式可以作为BeautifulSoup语句的任意一个参数,让目标元素查找工作极具灵活性。
获取属性
在网络数据采集时经常不需要查找标签的内容,而是需要查找标签属性。比如标签:a指向的URL链接包含在href属性中,或者img标签的图片文件包含在src属性中,这时获取标签属性就变得非常有用了。对于一个标签对象,可以用下面的代码获取它的全部属性:myTag.attrs要注意这行代码返回的是一个Python字典对象,可以获取和操作这些属性。比如要获取图
片的资源位置 src,可以用下面这行代码:
Lambda表达式
这个在前博客的Python3集合学习中已经提交到了。
Lambda表达式本质上就是一个函数,可以作为其他函数的变量使用;也就是说,一个函数不是定义成 f(x, y),而是定义成 f(g(x), y),或f(g(x),h(x))的形式。
BeautifulSoup 允许我们把特定函数类型当作findAll函数的参数。唯一的限制条件是这些
函数必须把一个标签作为参数且返回结果是布尔类型。BeautifulSoup用这个函数来评估它遇到的每个标签对象,最后把评估结果为“真”的标签保留,把其他标签剔除。例如,下面的代码就是获取有两个属性的标签:
|
|
这行代码会找出下面的标签:
|
|
超越BeautifulSoup
• lxml
这个库( http://lxml.de/) 可以用来解析HTML和XML文档,以非常底层的实现而闻名于世,大部分源代码是用C语言写的。虽然学习它需要花一些时间(其实学习曲线越陡峭,表明你可以越快地学会它),但它在处理绝大多数HTML文档时速度都非常快。
• HTML parser
这是Python自带的解析库( https://docs.python.org/3/library/html.parser.html)。 因为它不用安装(只要装Python就有),所以可以很方便地使用。
第3章 开始采集
遍历单个域名
西电睿思首页:
获取西电睿思首页并提取页面链接的 Python 代码:
forum和home开头的RUL都是主页的链接。
首页爬取的URL:
采集整个网站
爬取一个页面比较简单,但是如果需要系统地把整个网站按目录分类,或者要搜索网站上的每一个页面,怎么办?
那就得采集整个网站,那是一种非常耗费内存资源的过程,尤其是处理大型网站时,最合适的工具就是用一个数据库来储存采集的资源。但是,我们可以掌握这类工具的行为,并不需要通过大规模地运行它们。
遍历整个网站的网络数据采集有许多好处。
- 生成网站地图
收集数据
“ 5 个页面深度,每页 10 个链接”是网站的主流配置。为了避免一个页面被采集两次,链接去重是非常重要的。在代码运行时,把已发现的所有链接都放到一起,并保存在方便查询的列表里(下文示例指Python的集合set类型)。只有“新”链接才会被采集,之后再从页面中搜索其他链接:1234567891011121314151617from urllib.request import urlopenfrom bs4 import BeautifulSoupimport repages = set()def getLinks(pageUrl):global pageshtml = 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("")
采集睿思发现了一个新的公网地址 美滋滋
|
|
存入数据库
|
|
更新
|
|
遇到异常、重复处理
|
|
1218备份
|
|
灌水区and缘聚睿思终极版
灌水区
缘聚睿思