????????从这章开始,我们就要真刀真枪上战场(互联网)了。前面那些都只是铺垫,所举的的例子也都是皮毛,这章开始往后都是实战。
开始前,先解决一下大家的疑惑:啥是“网络爬虫”?
因为它们可以在 Web 上爬行。它们本质上就是一种递归方式。它们必须首先获取一个 URL 对应的网页内容,然后检查这个页面,寻找另一个 URL,再获取该 URL 对应的网页内容,并不断循环这一过程。
不过要注意的是:你可以抓取网页,并不意味着你总是应该这么做。当你需要的所有数据 都在一个页面上时,前面例子中的爬虫就足以解决问题了。使用网络爬虫的时候,必须非 常谨慎地考虑需要消耗多少带宽,还要尽力思考能不能让抓取目标的服务器负载更低一些。
3.1 遍历单个域名
即 使 你 没 听 说 过 “ 维 基 百 科 六 度 分 隔 理 论 ”, 也 很 可 能 听 过 “ 凯 文 ? 贝肯(Kevin Bacon)的 六 度 分 隔 值 游 戏 ”。
这里,我想对作者说句抱歉,这两个我真都没听过……但这丝毫不影响我学习爬虫的热情!
?原文中是以“维基百科”为例子,因为这是一个国外的网站,一般情况下很难登入,所以我们不用这个网站,用自家的“百度百科”也是非常强大的!
百度百科:百度百科_全球领先的中文百科全书 (baidu.com)
?接下来,我们写一段代码,获取该网站上的部分链接:
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen('https://baike.baidu.com/')
bs = BeautifulSoup(html,'html.parser')
for link in bs.find_all('a'):
print(link.attrs['href'])
?输出结果:
如果我们还想找的更精确一点,可以加上正则表达式:
假如,我们要抓这部分内容:
那就可以这样写:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
html = urlopen('https://baike.baidu.com/')
bs = BeautifulSoup(html,'html.parser')
for link in bs.find_all('a',href = re.compile('baike\.baidu\.com\S')):
if'href' in link.attrs:
print(link.attrs['href'])
?结果:
动态抓取代码如下:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import datetime
import random
import re
random.seed(datetime.datetime.now())
def getLinks(arrticleUrl):
html = urlopen('https://baike.baidu.com/')
bs = BeautifulSoup(html,'html.parser')
return bs.find_all('a',href = re.compile('baike\.baidu\.com\S'))
links = getLinks('/tashuo/')
while len(links) > 0:
newArticle = links[random.randint(0,len(links)-1)].attrs['href']
print(newArticle)
links = getLinks(newArticle)
?
3.2 抓取整个网站
我们实现了在一个网站上随机地从一个链接跳到另一个链接。但是,如果你需要系统地为网站编目录,或者要搜索网站上的每一个页面,该怎么办?
那么,什么时候抓取整个网站是有用的,什么时候又是有害无益的呢?遍历整个网站的网 络爬虫有许多好处。
生成网站地图 几年前,我曾经遇到过一个问题:一位重要的客户想对网站的一个重新设计方案进行效 果评估,但是不想让我们公司进入他们的网站内容管理系统(CMS), 也 没 有 一 个 公 开 可用的网站地图。我就用爬虫抓取了整个网站,收集了所有的内链,然后把页面整理成 他们网站实际使用的目录结构。这样,我很快找出了网站中我以前不曾留意的部分,并 准确地计算出了需要重新设计多少网页,以及需要移动多少内容。 收集数据 我的另一位客户为了给一个专业的搜索平台创建一个工作原型,想收集一些文章(故 事 、 博 文 、 新 闻 文 章 等 )。 虽 然 这 些 网 站 的 抓 取 不 需 要 全 面 彻 底 , 但 是 需 要 广 泛 ( 我 们?有 意 收 集 数 据 的 网 站 不 多 )。 于 是 我 就 创 建 了 一 个 爬 虫 来 递 归 地 遍 历 每 个 网 站 , 并 且 只 收集那些文章页面上的数据。 全面彻底地抓取网站的常用方法是从一个顶级页面(比如主页)开始,然后搜索该页面上 的所有内链,形成列表。之后,抓取这些链接跳转到的每一个页面,再把在每个页面上找 到的链接形成新的列表,接着执行下一轮抓取。 很明显,这是一种复杂度迅速增加的情形。假如每个页面有 10 个内链,网站的深度是 5 个 页 面 ( 中 等 规 模 网 站 的 常 见 深 度 ), 那 么 如 果 你 要 抓 取 整 个 网 站 , 一 共 得 抓 取 的 网 页 数 量就是 105, 即 100 000 个页面。不过,虽然“5 个页面的深度,每个页面 10 个内链”是 网站的主流配置,但其实很少有网站真的有 100 000 个或更多的页面。这是因为大部分内 链都是重复的。 为了避免一个页面被抓取两次,链接去重是非常重要的。在代码运行时,要把已发现的所 有链接都放到一起,并保存在方便查询的集合(set)里。集合与列表类似,但是集合中的 元素没有特定的顺序,集合只存储唯一的元素,这正是我们需要的功能。只有“新”链接 才应被抓取,并从其页面中搜索其他链接:
为了避免一个页面被抓取两次,链接去重是非常重要的。在代码运行时,要把已发现的所 有链接都放到一起,并保存在方便查询的集合(set)里。集合与列表类似,但是集合中的 元素没有特定的顺序,集合只存储唯一的元素,这正是我们需要的功能。只有“新”链接 才应被抓取,并从其页面中搜索其他链接:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages = set()
def getLinks(pageUrl):
global pages
html = urlopen('https://baike.baidu.com/'.format(pageUrl))
bs = BeautifulSoup(html,'html.parser')
for link in bs.find_all('a',href=re.compile('^https:')):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
newPage = link.attrs['href']
print(newPage)
pages.add(newPage)
getLinks(newPage)
getLinks(' ')
一开始,用 getLinks 处理一个空 URL,其实就是维基百科的主页,因为在函数里空 URL就是百度百科_全球领先的中文百科全书。然后,遍历首页上的每个链接,并检查它是否已经在全局变量集合 pages(已经抓取的页面集合)里面了。如果不在,就添加到集合中,并打印到 屏幕上,再用 getLinks 递归地处理这个链接。?
例如上述代码中set里面就记录了很多以https:开头的链接:
收集整个网站数据:
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re
pages = set()
def getLinks(pageUrl):
global pages
html = urlopen('https://baike.baidu.com/'.format(pageUrl))
bs = BeautifulSoup(html,'html.parser')
try:
print(bs.h1.get_text())
print(bs.find(id='mw-content-text').find_all('p')[0])
print(bs.find(id='ca-edit').find('span').find('a').attrs['href'])
except AttributeError:
print("页面缺少一些属性,没事")
for link in bs.find_all('a',href=re.compile('^https:/')):
if 'href' in link.attrs:
if link.attrs['href'] not in pages:
newPage=link.attrs['href']
print('-'*20)
print(newPage)
pages.add(newPage)
getLinks(newPage)
getLinks(" ")
输出:
这个程序中的 for 循环和原来的抓取程序中基本上是一样的(除了打印一条虚线来分离不 同的页面内容之外)。 因为我们不可能确保每个页面上都有所有类型的数据,所以每个打印语句都是按照数据在 页面上出现的可能性从高到低排列的。也就是说,<h1> 标题标签会出现在每个页面上,所 以 我 们 首 先 试 着 获 取 该 数 据 。 文 本 内 容 会 出 现 在 大 多 数 页 面 上 ( 除 了 文 件 页 面 ), 因 此 是 第 二 个 获 取 的 数 据 。“ 编 辑 ” 按 钮 只 出 现 在 标 题 和 文 本 内 容 都 已 存 在 的 页 面 上 , 但 不 是 所 有这类页面上都有“编辑”按钮,所以我们最后打印这类数据。
3.3 在互联网上抓取
创建一个网站需要什么?
????????首先你要一个数据仓库,其次你还需要一个网络爬虫。
????????谷歌在 1994 年成立的时候,就是两名斯坦福大学毕业生使用一台陈旧的服务器和一个 Python 网络爬虫。
????????网络爬虫驱动着许多现代 Web 技术,你不一定需要一个大型数据仓库来使用它们。要实现任何跨站的数据分析,你确实需要构建出可以解析并存储互联网上无数网页中的数据的爬虫。
????????就像前一个例子一样,你将创建的网络爬虫也是顺着链接从一个页面跳到另一个页面,绘 制出一张 Web 地图。但是这一次,它们不再忽略外链,而是跟着外链跳转。
在你编写爬虫跟随外链跳转之前,请问自己几个问题。 ?? 我要收集哪些数据?数据收集可以通过抓取几个预定义的网站(永远是最简单的做法) 完成吗?或者我的爬虫需要能够发现那些我可能不知道的网站吗? ?? 当我的爬虫到达某个网站,它是立即顺着下一个出站链接跳到一个新网站,还是在网站 上停留一会儿,深入抓取网站的内容? ?? 有没有我不想抓取的一类网站?我对非英文网站的内容感兴趣吗? ?? 如果我的网络爬虫引起了某个网站管理员的怀疑,我如何避免承担法律责任?(关于这 个问题的更多信息,请参考第 18 章。) 将几个灵活的 Python 函数组合起来就可以实现不同类型的网络爬虫,用不超过 60 行代码 就可轻松地写出来:
将几个灵活的 Python 函数组合起来就可以实现不同类型的网络爬虫,用不超过 60 行代码 就可轻松地写出来:
#编写网络爬虫
from urllib.request import urlopen
from urllib.parse import urlparse
from bs4 import BeautifulSoup
import re
import datetime
import random
pages = set()
random.seed(datetime.datetime.now())
# 获取页面中所有内链的列表
def getInternalLinks(bs, includeUrl):
includeUrl = '{}://{}'.format(urlparse(includeUrl).scheme,
urlparse(includeUrl).netloc)
internalLinks = []
# 找出所有以"/"开头的链接
for link in bs.find_all('a',
href=re.compile('^(/|.*'+includeUrl+')')):
if link.attrs['href'] is not None:
if link.attrs['href'] not in internalLinks:
if(link.attrs['href'].startswith('/')):
internalLinks.append(
includeUrl+link.attrs['href'])
else:
internalLinks.append(link.attrs['href'])
return internalLinks
# 获取页面中所有外链的列表
def getExternalLinks(bs, excludeUrl):
externalLinks = []
# 找出所有以"http"或"www"开头且不包含当前URL的链接
for link in bs.find_all('a',
href=re.compile('^(http|www)((?!'+excludeUrl+').)*$')):
if link.attrs['href'] is not None:
if link.attrs['href'] not in externalLinks:
externalLinks.append(link.attrs['href'])
return externalLinks
def getRandomExternalLink(startingPage):
html = urlopen(startingPage)
bs = BeautifulSoup(html, 'html.parser')
externalLinks = getExternalLinks(bs,
urlparse(startingPage).netloc)
if len(externalLinks) == 0:
print('No external links, looking around the site for one')
domain = '{}://{}'.format(urlparse(startingPage).scheme,
urlparse(startingPage).netloc)
internalLinks = getInternalLinks(bs, domain)
return getRandomExternalLink(internalLinks[random.randint(0,
len(internalLinks)-1)])
else:
return externalLinks[random.randint(0, len(externalLinks)-1)]
def followExternalOnly(startingSite):
externalLink = getRandomExternalLink(startingSite)
print('Random external link is: {}'.format(externalLink))
followExternalOnly(externalLink)
followExternalOnly('http://oreilly.com')
上面这个程序从 http://oreilly.com 开始,随机地从一个外链跳到另一个外链。输出的结果如 下所示: http://igniteshow.com/ http://feeds.feedburner.com/oreilly/news http://hire.jobvite.com/CompanyJobs/Careers.aspx?c=q319 http://makerfaire.com/ 在网站首页上并不总是能发现外链。这时,为了找到外链,就需要用一种类似于前面例子 中使用的抓取方法的方法,递归地深入一个网站,直到找到一个外链为止。 图 3-1 把程序操作可视化成了一个流程图。 获取页面上的所有外链返回一个随机外链还有外链吗?是否进入页面上的一个内链
图 3-1:从互联网的网站上抓取外链的程序流程图?
把任务分解成像“获取页面上所有外链”这样的小函数的好处是,以后可以方便地重构代 码,以满足另一个抓取任务的需求。例如,如果你的目标是抓取一个网站中所有的外链并 且逐一记录下来,你可以增加下面的函数:
#收集在网站上发现的所有外链列表
allExtLinks = set()
allIntLinks = set()
def getAllExternalLinks(siteUrl):
?html = urlopen(siteUrl)
?domain = '{}://{}'.format(urlparse(siteUrl).scheme,
?urlparse(siteUrl).netloc)
?bs = BeautifulSoup(html, 'html.parser')
?internalLinks = getInternalLinks(bs, domain)
?externalLinks = getExternalLinks(bs, domain)
?for link in externalLinks:
?if link not in allExtLinks:
?allExtLinks.add(link)
?print(link)
?for link in internalLinks:
?if link not in allIntLinks:
?allIntLinks.add(link)
?getAllExternalLinks(link)
allIntLinks.add('http://oreilly.com')
getAllExternalLinks('http://oreilly.com')
可以将这段代码视为共同协作的两个循环,一个是收集内链,一个是收集外链。程序的流 程如图 3-2 所示。
|