Python爬虫抓取纯静态网站及其资源(开发篇)

203 篇文章 15 订阅

本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

以下文章来源于腾讯云 作者:程序员宝库

( 想要学习Python?Python学习交流群:1039649593,满足你的需求,资料都已经上传群文件流,可以自行下载!还有海量最新2020python学习资料。 )
在这里插入图片描述

进入开发

有了上面的基础知识,我们就可以进入开发环节了。

我们想实现的最终效果

本次我们的最终目的是写一个简单的python爬虫,这个爬虫能够下载一个静态网页,并且在保持网页引用资源的相对路径下下载它的静态资源(如js/css/images)。测试网站为http://www.peersafe.cn/index.html,效果图如下:

在这里插入图片描述
开发流程
我们的总体思路是先获取到网页的内容,然后利用正则表达式来提取我们想要的资源链接,最后就是下载资源。

获取网页内容
我们选用python3自带的urllib.http来发出http请求,或者你可以采用第三方请求库requests。

获取内容的部分代码如下:

url = 'http://www.peersafe.cn/index.html'

# 读取网页内容
webPage = urllib.request.urlopen(url)
data = webPage.read()
content = data.decode('UTF-8')
print('> 网站内容抓取完毕,内容长度:', len(content))

获取到内容之后,我们需要把它保存下来,也就是写到本地磁盘上。我们定义一个SAVE_PATH路径,代表专门放置爬虫下载的文件。

# python-spider-downloads是我们要放置的目录
# 这里推荐使用os模块来获取当前的目录或者拼接路径
# 不推荐直接使用'F://xxx' + '//python-spider-downloads'等方式

SAVE_PATH = os.path.join(os.path.abspath('.'), 'python-spider-downloads')

接下来就是为这个站点创建一个单独的文件夹了。这个站点文件夹的格式是xxxx-xx-xx-domain,比如2018-08-03-www.peersafe.cn。在此之前,我们需要写一个函数来提取出一个url链接的域名、相对路径、请求文件名和请求参数等等,这个在后续在根据资源文件的引用方式创建相对应的文件夹时也会用到。

比如输入http://www.peersafe.cn/index.html,那么将会输出:

{'baseUrl': 'http://www.peersafe.cn', 'fullPath': 'http://www.peersafe.cn/', 'protocol': 'http://', 'domain': 'www.peersafe.cn', 'path': '/', 'fileName': 'index.html', 'ext': 'html', 'params': ''}

部分代码如下:

REG_URL = r'^(https?://|//)?((?:[a-zA-Z0-9-_]+.)+(?:[a-zA-Z0-9-_:]+))((?:/[-_.a-zA-Z0-9]*?)*)((?<=/)[-a-zA-Z0-9]+(?:.([a-zA-Z0-9]+))+)?((?:?[a-zA-Z0-9%&=]*)*)$'

regUrl = re.compile(REG_URL)

# ...

'''
解析URL地址
'''
def parseUrl(url):
    if not url:
        return

    res = regUrl.search(url)
    # 在这里,我们把192.168.1.109:8080的形式也解析成域名domain,实际过程中www.baidu.com等才是域名,192.168.1.109只是IP地址
    # ('http://', '192.168.1.109:8080', '/abc/images/111/', 'index.html', 'html', '?a=1&b=2')
    if res is not None:
        path = res.group(3)
        fullPath = res.group(1) + res.group(2) + res.group(3)

        if not path.endswith('/'):
            path = path + '/'
            fullPath = fullPath + '/'
        return dict(
            baseUrl=res.group(1) + res.group(2),
            fullPath=fullPath,
            protocol=res.group(1),
            domain=res.group(2),
            path=path,
            fileName=res.group(4),
            ext=res.group(5),
            params=res.group(6)
        )

'''
解析路径

eg:
    basePath => F:Programspythonpython-spider-downloads
    resourcePath => /a/b/c/ or a/b/c

    return => F:Programspythonpython-spider-downloadsac
'''
def resolvePath(basePath, resourcePath):
    # 解析资源路径
    res = resourcePath.split('/')
    # 去掉空目录 /a/b/c/ => [a, b, c]
    dirList = list(filter(lambda x: x, res))

    # 目录不为空
    if dirList:
        # 拼接出绝对路径
        resourcePath = reduce(lambda x, y: os.path.join(x, y), dirList)
        dirStr = os.path.join(basePath, resourcePath)
    else:
        dirStr = basePath

    return dirStr

上面的正则表达式REG_URL有点长,这个正则表达式能解析目前我遇到的各种url形式,如果有不能解析的,你可以自行补充,我测试过的url列表可以去我的github中查看。

首先一个最复杂的url链接(比如’http://192.168.1.109:8080/abc/images/111/index.html?a=1&b=2’)来说,我们想分别提取出http://, 192.168.1.109:8080, /abc/images/111/, index.html, ?a=1&b=2。提取出/abc/images/111/的目的是为以后创建目录做准备,index.html是写入网页内容的名字。

有需要的可以深入研究一下REG_URL的写法,如果有更好的或者看不懂的,我们可以一起探讨。

有了parseUrl函数之后,我们就可以把刚刚获取网页内容和写入文件联系起来了,代码如下:

# 首先创建这个站点的文件夹
urlDict = parseUrl(url)
print('分析的域名:', urlDict)
domain = urlDict['domain']

filePath = time.strftime('%Y-%m-%d', time.localtime()) + '-' + domain
# 如果是192.168.1.1:8000等形式,变成192.168.1.1-8000,:不可以出现在文件名中
filePath = re.sub(r':', '-', filePath)
SAVE_PATH = os.path.join(SAVE_PATH, filePath)

# 读取网页内容
webPage = urllib.request.urlopen(url)
data = webPage.read()
content = data.decode('UTF-8')
print('> 网站内容抓取完毕,内容长度:', len(content))

# 把网站的内容写下来
pageName = ''
if urlDict['fileName'] is None:
    pageName = 'index.html'
else:
    pageName = urlDict['fileName']

pageIndexDir = resolvePath(SAVE_PATH, urlDict['path'])
if not os.path.exists(pageIndexDir):
    os.makedirs(pageIndexDir)

pageIndexPath = os.path.join(pageIndexDir, pageName)
print('主页的地址:', pageIndexPath)
f = open(pageIndexPath, 'wb')
f.write(data)
f.close()

提取有用的资源链接

我们想要的资源是图片资源,js文件、css文件和字体文件。如果我们要对网页内容一一进行解析,利用分组,来捕获出我们想要的链接形式,比如images/1.png和scripts/lib/jquery.min.js。

代码如下:

REG_RESOURCE_TYPE = r'(?:href|src|data-original|src)=["'](.+?.(?:js|css|jpg|jpeg|png|gif|svg|ico|ttf|woff2))[a-zA-Z0-9?=.]*["']'

# re.S代表开启多行匹配模式
regResouce = re.compile(REG_RESOURCE_TYPE, re.S)

# ...

# 解析网页内容,获取有效的链接
# content是上一步读取到的网页内容
contentList = re.split(r's+', content)
resourceList = []
for line in contentList:
    resList = regResouce.findall(line)
    if resList is not None:
        resourceList = resourceList + resList

下载资源

在解析出资源链接后,我们要针对每一个资源链接进行检查,把它变成符合http请求的url格式,比如把images/1.png加上http头和刚刚的domain,也就是http://domain/images/1.png。

下面是对资源链接进行处理的代码:

# ./static/js/index.js
# /static/js/index.js
# static/js/index.js
# //abc.cc/static/js
# http://www.baidu/com/static/index.js
if resourceUrl.startswith('./'):
    resourceUrl = urlDict['fullPath'] + resourceUrl[1:]
elif resourceUrl.startswith('//'):
    resourceUrl = 'https:' + resourceUrl
elif resourceUrl.startswith('/'):
    resourceUrl = urlDict['baseUrl'] + resourceUrl
elif resourceUrl.startswith('http') or resourceUrl.startswith('https'):
    # 不处理,这是我们想要的url格式
    pass
elif not (resourceUrl.startswith('http') or resourceUrl.startswith('https')):
    # static/js/index.js这种情况
    resourceUrl = urlDict['fullPath'] + resourceUrl
else:
    print('> 未知resource url: %s' % resourceUrl)

接着就是对每个规范的资源链接进行解析(parseUrl),提取出它要存放的目录和文件名等等,然后创建对应的目录。

在这里,我也处理了引用的其他网站的资源。

# 解析文件,查看文件路径
resourceUrlDict = parseUrl(resourceUrl)
if resourceUrlDict is None:
    print('> 解析文件出错:%s' % resourceUrl)
    continue

resourceDomain = resourceUrlDict['domain']
resourcePath = resourceUrlDict['path']
resourceName = resourceUrlDict['fileName']

if resourceDomain != domain:
    print('> 该资源不是本网站的,也下载:', resourceDomain)
    # 如果下载的话,根目录就要变了
    # 再创建一个目录,用于保存其他地方的资源
    resourceDomain =  re.sub(r':', '-', resourceDomain)
    savePath = os.path.join(SAVE_PATH, resourceDomain)
    if not os.path.exists(SAVE_PATH):
        print('> 目标目录不存在,创建:', savePath)
        os.makedirs(savePath)
    # continue
else:
    savePath = SAVE_PATH

# 解析资源路径
dirStr = resolvePath(savePath, resourcePath)

if not os.path.exists(dirStr):
    print('> 目标目录不存在,创建:', dirStr)
    os.makedirs(dirStr)

# 写入文件
downloadFile(resourceUrl, os.path.join(dirStr, resourceName))

下载的函数downloadFile的代码是:

'''
下载文件
'''
def downloadFile(srcPath, distPath):
    global downloadedList

    if distPath in downloadedList:
        return
    try:
        response = urllib.request.urlopen(srcPath)
        if response is None or response.status != 200:
            return print('> 请求异常:', srcPath)
        data = response.read()

        f = open(distPath, 'wb')
        f.write(data)
        f.close()

        downloadedList.append(distPath)
        # print('>>>: ' + srcPath + ':下载成功')

    except Exception as e:
        print('报错了:', e)

以上就是我们的开发全过程。

知识总结

本次开发用到的技术
  1. 利用urllib.http来发网络请求
  2. 利用正则表达式来解析资源链接
  3. 利用os系统模块来处理文件路径问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值