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

107 阅读8分钟

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

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

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

遇到的需求

前段时间需要快速做个静态展示页面,要求是响应式和较美观。由于时间较短,自己动手写的话也有点麻烦,所以就打算上网找现成的。

中途找到了几个页面发现不错,然后就开始思考怎么把页面给下载下来。

由于之前还没有了解过爬虫,自然也就没有想到可以用爬虫来抓取网页内容。所以我采取的办法是:

  • 打开chrome的控制台,进入Application选项
  • 找到Frames选项,找到html文件,再右键Save As...
  • 手动创建本地的js/css/images目录
  • 依次打开Frames选项下的Images/Scripts/Stylesheets,一个文件就要右键Save As...

这个办法是我当时能想到的最好办法了。不过这种人为的办法有以下缺点:

  • 手工操作,麻烦费时
  • 一不小心就忘记保存哪个文件
  • 难以处理路径之间的关系,比如一张图片a.jpg, 它在html中的引用方式是images/banner/a.jpg,这样我们以后还要手动去解决路径依赖关系然后刚好前段时间接触了一点python,想到可以写个python爬虫来帮我自动抓取静态网站。于是就马上动手,参考相关资料等等。

下面跟大家详细分享一下写爬虫抓取静态网站的全过程。

前置知识储备 在下面的代码实践中,用到了python知识、正则表达式等等,核心技术是正则表达式。

我们来一一了解一下。

Python基础知识

如果你之前有过其他语言的学习经历,相信你可以很快上手python这门语言。具体学习可以上查看python官方文档或者其他教程。

爬虫的概念

爬虫,按照我的理解,其实是一段自动执行的计算机程序,在web领域中,它存在的前提是模拟用户在浏览器中的行为。

它的原理就是模拟用户访问web网页,获取网页内容,然后分析网页内容,找出我们感兴趣的部分,并且最后处理数据。

流程图是:在这里插入图片描述

现在流行的爬虫主流实现形式有以下几种:

1.自己抓取网页内容,然后自己实现分析过程 2.用别人写好的爬虫框架,比如Scrapy

正则表达式

概念

正则表达式是由一系列元字符和普通字符组成的字符串,它的作用是根据一定的规则来匹配文本,最终可以对文本做出一系列的处理。

元字符是正则表达式中的保留字符,它有特殊的匹配规则,比如*代表匹配0到无穷多次,普通字符就是普通的abcd等等。

比如在前端中,常见的一个操作就是判断用户的输入是否为空,这时候我们可以先通过正则表达式来进行匹配,先过滤掉用户输入的两边空白值,具体实现如下:

function trim(value) {
    return value.replace(/^s+|s+$/g, '')
}

// 输出 => "Python爬虫"
trim(' Python爬虫 ');

下面我们一起来具体了解一下正则表达式中的元字符。

正则表达式中的元字符

在上面,我们说过元字符是正则表达式中的保留字符,它有特殊的匹配规则,所以我们首先要了解经常出现的元字符。

匹配单个字符的元字符

  • 代表匹配一个任意字符,除了(换行符),比如可以匹配任意的字母数字等等
  • [...]表示字符组,里面可以有任意字符,它只会匹配当中的任意一个,比如[abc]可以匹配a或b或c,这里值得注意的是,字符组里面的元字符有时候会被当成是普通字符,比如[-?]等等,它代表的仅仅是-或或?,而不是-代表区间,*代表0到无穷次匹配,?代表0或1次匹配。
  • [^...]跟[...]的含义相反,它的意思是匹配一个不属于[...]里面的字符,而不是不匹配[...]里面的字符,这两种说法虽然细微但是有很大差别,前者规定一定要匹配一个字符,这个切记。

例子:[^123]可以匹配4/5/6等等,但是不匹配1/2/3

提供计数功能的元字符

  • *代表匹配0次到无穷次,可以不匹配任何字符
  • +代表匹配1次到无穷次,至少匹配1次
  • ?代表匹配0次或1次
  • {min, max}代表匹配min次到max次,如a{3, 5}表示a至少匹配3-5次

提供位置的元字符

  • ^ 代表匹配字符串开头,如^a表示a要出现在字符串开头,bcd则不匹配
  • 代表匹配字符串结尾,A 代表匹配字符串结尾, 如A表示A要出现在字符串结尾,ABAB则不匹配

其他元字符

  • |代表一个范围,可以匹配任意的子表达式,比如abc|def可以匹配abc或者def,不匹配abd
  • (...)代表分组,它的作用有界定子表达式的范围和与提供功能的元字符相结合,比如(abc|def)+代表可以匹配1次或1次以上的abc或者defdef,如abcabcabc,def
  • i代表反向引用,i可以为1/2/3等整数,它的含义是指向上一个()里面匹配的内容。比如匹配(abc)+(12)*,如果匹配成功的话,的内容是abc,的内容是12或者空。反向引用通常用在匹配""或者''中

环视

我理解的环视是界定当前匹配子表达式的左边文本和右边文本出现的情况,环视本身不会占据匹配的字符,它是当前子表达式的匹配规则但是本身不算进匹配文本。而我们上面说的元字符都代表一定的规则和占据一定的字符。

环视可分为四种:肯定顺序环视、否定顺序环视、肯定逆序环视和否定逆序环视。它们的工作流程如下:

  • 肯定顺序环视:先找到环视中的文本在右侧出现的初始位置,然后从匹配到的右侧文本的最左的位置开始匹配字符
  • 否定顺序环视:先找到环视中的文本在右侧没有出现的初始位置,然后从匹配到的右侧文本的最左的位置开始匹配字符
  • 肯定逆序环视:先找到环视中的文本在左侧出现的初始位置,然后从匹配到的左侧文本的最右的位置开始匹配字符
  • 否定逆序环视:先找到环视中的文本在左侧没有出现的初始位置,然后从匹配到的左侧文本的最右的位置开始匹配字符

肯定顺序环视

肯定顺序环视匹配成功的条件是当前的子表达式能够匹配右侧文本,它的写法是(?=...),...代表要环视的内容。比如正则表达式(?=hello)he的意思是匹配包含hello的文本,它只匹配位置,不匹配具体字符,匹配到位置之后,才真正匹配要占用的字符是he,所以后面可以具体匹配llo等。

对于(?=hello)he而言,hello world可以匹配成功,而hell world则匹配失败。具体代码如下:

import re

reg1 = r'(?=hello)he'

print(re.search(reg1, 'hello world'))
print(re.search(reg1, 'hell world hello'))
print(re.search(reg1, 'hell world'))

# 输出结果
<_sre.SRE_Match object; span=(0, 2), match='he'>
<_sre.SRE_Match object; span=(11, 13), match='he'>
None

否定顺序环视

否定顺序环视匹配成功的条件是当前的子表达式不能匹配右侧文本,它的写法是(?!...),...代表要环视的内容,还是上面的例子,比如正则表达式(?!hello)he的意思是匹配不是hello的文本,找到位置,然后匹配he。

例子如下:

import re

reg2 = r'(?!hello)he'

print(re.search(reg2, 'hello world'))
print(re.search(reg2, 'hell world hello'))
print(re.search(reg2, 'hell world'))

# 输出结果
None
<_sre.SRE_Match object; span=(0, 2), match='he'>
<_sre.SRE_Match object; span=(0, 2), match='he'>

肯定逆序环视 肯定逆序环视匹配成功的条件是当前的子表达式能够匹配左侧文本,它的写法是(?<=...),...代表要环视的内容,比如正则表达式(?<=hello)-python的意思是匹配包含-python的子表达式,并且它的左侧必须出现hello,hello只匹配位置,不匹配具体字符,真正占用的字符是后面的-python。

例子如下:

import re

reg3 = r'(?<=hello)-python'
print(re.search(reg3, 'hello-python'))
print(re.search(reg3, 'hell-python hello-python'))
print(re.search(reg3, 'hell-python'))

# 输出结果
<_sre.SRE_Match object; span=(5, 12), match='-python'>
<_sre.SRE_Match object; span=(17, 24), match='-python'>
None

否定逆序环视

否定逆序环视匹配成功的条件是当前的子表达式不能匹配左侧文本,它的写法是(?<!...),...代表要环视的内容,比如正则表达式(?<!hello)-python的意思是匹配包含-python的子表达式,并且它的左侧必须不能出现hello。

例子如下:

import re

reg3 = r'(?<=hello)-python'
print(re.search(reg3, 'hello-python'))
print(re.search(reg3, 'hell-python hello-python'))
print(re.search(reg3, 'hell-python'))

# 输出结果
<_sre.SRE_Match object; span=(5, 12), match='-python'>
<_sre.SRE_Match object; span=(17, 24), match='-python'>

环视在对字符串插入某些字符很有效,你可以利用它来匹配位置,然后插入对应的字符,而不需要对原来的文本进行替换。

捕获分组

在正则表达式中,分组可以帮助我们提取出想要的特定信息。

指明分组很简单,只需要在想捕获的表达式中两端加上()就可以了。在python中,我们可以用re.search(reg, xx).groups()来获取到所有的分组。 默认的()中都指明了一个分组,分组序号为i,i从1开始,分别用re.search(reg, xx).group(i)来获取。

如果不想捕获分组可以使用(?:...)来指明。

具体例子如下:

import re

reg7 = r'hello,([a-zA-Z0-9]+)'
print(re.search(reg7, 'hello,world').groups())
print(re.search(reg7, 'hello,world').group(1))
print(re.search(reg7, 'hello,python').groups())
print(re.search(reg7, 'hello,python').group(1))

# 输出结果
('world',)
world
('python',)
python

贪婪匹配

贪婪匹配是指正则表达式尽可能匹配多的字符,也就是趋于最大长度匹配。

正则表达式默认是贪婪模式。

例子如下:

import re

reg5 = r'hello.*world'
print(re.search(reg5, 'hello world,hello python,hello world,hello javascript'))

# 输出结果
<_sre.SRE_Match object; span=(0, 36), match='hello world,hello python,hello world'>

由上可以看到它匹配的是hello world,hello python,hello world而不是刚开始的hello world。那如果我们只是想匹配刚开始的hello world,这时候我们可以利用正则表达式的非贪婪模式。

非贪婪匹配正好与贪婪匹配相反,它是指尽可能匹配少的字符,只要匹配到了就结束。要使用贪婪模式,仅需要在量词后面加上一个问号(?)就可以。

还是刚刚那个例子:

import re

reg5 = r'hello.*world'
reg6 = r'hello.*?world'
print(re.search(reg5, 'hello world,hello python,hello world,hello javascript'))
print(re.search(reg6, 'hello world,hello python,hello world,hello javascript'))

# 输出结果
<_sre.SRE_Match object; span=(0, 36), match='hello world,hello python,hello world'>
<_sre.SRE_Match object; span=(0, 11), match='hello world'>

由上可以看到这是我们刚刚想要匹配的效果。