Python爬虫抓取纯静态网站及其资源!动态的需要ajax!
遇到的需求 前段时间需要快速做个静态展示页面,要求是响应式和较美观。由于时间较短,自己动手写的话也有点麻烦,所以就打算上网找现成的。 进群:548377875 ? 即可获取数十套PDF哦!需要此案例源码的请单独私信~! 中途找到了几个页面发现不错,然后就开始思考怎么把页面给下载下来。 由于之前还没有了解过爬虫,自然也就没有想到可以用爬虫来抓取网页内容。所以我采取的办法是:
这个办法是我当时能想到的最好办法了。不过这种人为的办法有以下缺点:
然后刚好前段时间接触了一点python,想到可以写个python爬虫来帮我自动抓取静态网站。于是就马上动手,参考相关资料等等。 下面跟大家详细分享一下写爬虫抓取静态网站的全过程。 前置知识储备 在下面的代码实践中,用到了python知识、正则表达式等等,核心技术是正则表达式。 我们来一一了解一下。 Python基础知识 如果你之前有过其他语言的学习经历,相信你可以很快上手python这门语言。具体学习可以上查看python官方文档或者其他教程。 爬虫的概念 爬虫,按照我的理解,其实是一段自动执行的计算机程序,在web领域中,它存在的前提是模拟用户在浏览器中的行为。 它的原理就是模拟用户访问web网页,获取网页内容,然后分析网页内容,找出我们感兴趣的部分,并且最后处理数据。 流程图是: 现在流行的爬虫主流实现形式有以下几种:
正则表达式 概念 正则表达式是由一系列元字符和普通字符组成的字符串,它的作用是根据一定的规则来匹配文本,最终可以对文本做出一系列的处理。 元字符是正则表达式中的保留字符,它有特殊的匹配规则,比如*代表匹配0到无穷多次,普通字符就是普通的abcd等等。 比如在前端中,常见的一个操作就是判断用户的输入是否为空,这时候我们可以先通过正则表达式来进行匹配,先过滤掉用户输入的两边空白值,具体实现如下: function trim(value) { return value.replace(/^s+|s+$/g,'') } // 输出 => "Python爬虫" trim(' Python爬虫 '); 下面我们一起来具体了解一下正则表达式中的元字符。 正则表达式中的元字符 在上面,我们说过元字符是正则表达式中的保留字符,它有特殊的匹配规则,所以我们首先要了解经常出现的元字符。 匹配单个字符的元字符
例子:[^123]可以匹配4/5/6等等,但是不匹配1/2/3 提供计数功能的元字符
提供位置的元字符
其他元字符
环视 我理解的环视是界定当前匹配子表达式的左边文本和右边文本出现的情况,环视本身不会占据匹配的字符,它是当前子表达式的匹配规则但是本身不算进匹配文本。而我们上面说的元字符都代表一定的规则和占据一定的字符。 环视可分为四种:肯定顺序环视、否定顺序环视、肯定逆序环视和否定逆序环视。它们的工作流程如下:
肯定顺序环视 肯定顺序环视匹配成功的条件是当前的子表达式能够匹配右侧文本,它的写法是(?=...),...代表要环视的内容。比如正则表达式(?=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,match='he'> <_sre.SRE_Match object; span=(0,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 否定逆序环视 否定逆序环视匹配成功的条件是当前的子表达式不能匹配左侧文本,它的写法是(? 例子如下: import re reg3 = r'(?<=hello)-python' print(re.search(reg3,match='-python'> None 环视在对字符串插入某些字符很有效,你可以利用它来匹配位置,然后插入对应的字符,而不需要对原来的文本进行替换。 捕获分组 在正则表达式中,分组可以帮助我们提取出想要的特定信息。 指明分组很简单,只需要在想捕获的表达式中两端加上()就可以了。在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,world').group(1)) print(re.search(reg7,python').groups()) print(re.search(reg7,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 world'> 由上可以看到它匹配的是hello world,hello world而不是刚开始的hello world。那如果我们只是想匹配刚开始的hello world,这时候我们可以利用正则表达式的非贪婪模式。 非贪婪匹配正好与贪婪匹配相反,它是指尽可能匹配少的字符,只要匹配到了就结束。要使用贪婪模式,仅需要在量词后面加上一个问号(?)就可以。 还是刚刚那个例子: import re reg5 = r'hello.*world' reg6 = r'hello.*?world' print(re.search(reg5,hello javascript')) print(re.search(reg6,hello world'> <_sre.SRE_Match object; span=(0,11),match='hello world'> 由上可以看到这是我们刚刚想要匹配的效果。 进入开发 有了上面的基础知识,我们就可以进入开发环节了。 我们想实现的最终效果 本次我们的最终目的是写一个简单的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) 以上就是我们的开发全过程。 知识总结 本次开发用到的技术
心得体会 这篇文章也算是我这段时间学习python的一个实践总结,顺便记录下正则表达式的知识。同时我也希望能够帮助到那些想学习正则表达式和爬虫的小伙伴。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |