爬虫学习:基础爬虫案例实战
文章目录
- 爬虫学习:基础爬虫案例实战
-
- 一、前言
- 二、案例实战
-
- 任务一:爬取列表页
- 任务二:爬取详细页
- 任务三:保存爬取数据
- 任务四:利用多进程提高效率
- 三、补充一点
- 四、最后我想说
一、前言
前面我们已经学习过了Python爬虫里面的几个基础常用的库,都是分开总结的知识点,想要灵活运用这些知识点,还是需要进行一些实战训练才行,这次我们就来尝试一下基础的爬虫案例。
OK,废话不多说,让我们开始这次的基础爬虫案例实战训练叭。
二、案例实战
这次我们还是使用一个经典的静态网站来作为实战案例进行学习。
网址我就放在这里:https://ssr1.scrape.center/
网站界面没有太多复杂的内容,内容比较清晰明了,结构也比较简单,网站页面如下所示:
下方还有翻页功能:
点击任意一部电影就会进去该电影的详细介绍界面:
网站的大体构造以及内容已经了解差不多了,现在我们需要确定我们要在该网站上爬取自己想要的什么数据,即任务目标。
任务目标可以有如下几个:
- 可以爬取这个站点的每一页的电影列表,获取总共收录了多少电影及他们分别是什么,然后再顺着列表再爬取每个电影的详细介绍
- 可以爬取每部电影的各项内容,比如名称,评分,上映时间等等
- 爬取的内容我们不能直接就不管了,可以将它们转换成JSON文本文件利于保存
- 进阶的利用一下Python的多进程机制来有效提高一下爬取大量信息的效率
大致的基础操作就这些了,现在我们开始一步一步的去实现它们。
任务一:爬取列表页
首先我们需要了解一下网页背后的HTML结构,这样有利于我们的信息爬取,我们在该网页处按下键盘上面的F12键打开浏览器开发者工具,然后点击Elements就可以看见当前网页的HTML的结构了
然后选中我们需要爬取的每个电影对应的区域,然后看下面对应的HTML代码,我们可以发现,每一部电影的信息都被放在了一个div标签内,而且这些节点都有一个相同的属性值class=“el-card”,而且每一满页都只有十个这样的div标签块,证明每一满页都只有十部电影。
研究好一页结构之后,我们来看一下这个网站的翻页逻辑,可以发现他们都是放在li标签里面的,而且每一页的结构和第一页的结构一样,所以我们可以像处理第一页的方式处理后面的每一页。
只是每一页的URL数字不同而已,URL后面的/page/2对应第二页,依次内推即可,我们在提取列表页数据的同时也要提取每一页对应的URL,这样方便后续在每一页内提取每部电影的详细页。
OK,现在我们开始写代码来实现一下我们所定的目标。
首先,我们需要知道我们要用哪些库来实现一个完整的功能,光requests库肯定是不够完善的,所以我们在这里还使用logging库来输出信息,re库来解析爬取的信息,urllib库中的urljoin模块来进行URL的拼接实现每一页的爬取。
import requests import logging import re from urllib.parse import urljoin
logging.basicConfig(level=logging.INFO, format='%(asctime)s – %(levelname)s: %(message)s') # 用来定义日志输出几倍和输出格式
BASE_URL = 'https://ssr1.scrape.center' # URL的根URL TOTAL_PAGE = 10 # 需要爬取的总页码数(按需选择)
接下来实现每一个页面的爬取功能
def scrape_page(url): logging.info('抓取%s中…', url) try: response = requests.get(url) if response.status_code == 200: return response.text logging.error('抓取%s时获取无效的状态码%s', url, response.status_code) except requests.RequestException: logging.error('抓取%s时发生错误', url, exc_info=True)
这里做了一个异常处理来避免遇见某一页加载错误导致爬取停止的问题,只会抛出哪一页出现了问题,并不会影响爬取过程。
这个方法返回的是一个页面的HTML代码,我们还需要爬取每一页的HTML代码,接下来爬取列表页的代码:
def scrape_index(page): index_url = f'{BASE_URL}/page/{page}' return scrape_page(index_url)
在这个方面里面实现URL的拼接,然后再调用前面爬取的方法就可以爬取每一页的HTML代码了。
获取到HTML代码之后,接下来我们就需要对其进行解析。
解析的过程中要注意一个问题,一开始我并没有注意到,结果就报错了,那就是在第三行内要将html通过str()方法转值成为字符串才可以不然会报:TypeError: expected string or bytes-like object的错误信息,因为re.findall方法接受的是一个字符串的参数。
def parse_index(html): pattern = re.compile('<a.*?href="(.*?)".*?class="name">') items = re.findall(pattern, str(html)) # html要转化为字符串 if not items: return [] for item in items: detail_url = urljoin(BASE_URL, item) logging.info('获取详细网址:%s', detail_url) yield detail_url
在这里我们使用了正则表达式里面的非贪婪通用匹配方式来匹配任意字符,同时在href属性的引号之间使用了分组匹配,这样就方便我们获取href的属性值了,我们还需要指定class属性值为name,用来标识这个是电影名称的节点,因为我们获取的href的属性值不是一个完整的URL,所以需要前面拼接URL的方法将获取的值与前面的根URL拼接在一起然后再返回,最后我们只需调用一下每个方法然后执行就可以了。
def main(): for page in range(1, TOTAL_PAGE + 1): index_html = scrape_index(page) detail_urls = parse_index(index_html) logging.info('每一部电影详细页网址:%s', list(detail_urls))
if __name__ == '__main__': main()
整个代码正常运行的结果是:
2022–08–02 21:42:05,907 – INFO: 抓取https://ssr1.scrape.center/page/1中... 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/1 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/2 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/3 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/4 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/5 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/6 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/7 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/8 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/9 2022–08–02 21:42:07,327 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/10 2022–08–02 21:42:07,328 – INFO: 每一部电影详细页网址:['https://ssr1.scrape.center/detail/1', 'https://ssr1.scrape.center/detail/2', 'https://ssr1.scrape.center/detail/3', 'https://ssr1.scrape.center/detail/4', 'https://ssr1.scrape.center/detail/5', 'https://ssr1.scrape.center/detail/6', 'https://ssr1.scrape.center/detail/7', 'https://ssr1.scrape.center/detail/8', 'https://ssr1.scrape.center/detail/9', 'https://ssr1.scrape.center/detail/10'] 2022–08–02 21:42:07,328 – INFO: 抓取https://ssr1.scrape.center/page/2中... 2022–08–02 21:42:07,926 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/11 2022–08–02 21:42:07,926 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/12 2022–08–02 21:42:07,926 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/13 2022–08–02 21:42:07,926 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/14 2022–08–02 21:42:07,926 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/15 ...
输出的结果比较多我就截取一部分,截至现在,我们已经获取了所有电影的详细页URL了,接下来就该进行下一个任务目标的实现了。
任务二:爬取详细页
现在我们首先要去观察一下详细页的HTML代码结构方便我们从中提取我们想要的信息。
我们可以发现我们需要提取的信息都在某一个节点里面然后对应其自己的属性,我们来列个表总结一下这些信息的相对位置:
电影封面 | img节点内,class属性为cover |
电影名称 | 一个h2节点内 |
电影类别 | 一个span节点内,它的外侧是button节点,再外侧是一个class属性为categories的div节点 |
电影上映时间 | 一个span节点内,外侧是一个class属性为info的div节点 |
电影评分 | 一个p节点内,class属性为score |
电影剧情 | 一个p节点内,外侧是一个class属性为drama的div节点 |
知道了这些之后我们开始写对应的代码部分:
def scrape_detail(url): return scrape_page(url)
def parse_detail(html): cover_pattern = re.compile('class="item.*?<img.*?src="(.*?)".*?class="cover">', re.S) name_pattern = re.compile('h2.*?>(.*?)</h2>') categories_pattern = re.compile('<button.*?category.*?<span>(.*?)</span>.*?</button>', re.S) published_pattern = re.compile('(\d{4}-\d{2}-\d{2})\s?上映') drama_pattern = re.compile('<div.*?drama.*?>.*?<p.*?>(.*?)</p>', re.S) score_pattern = re.compile('<p.*?score.*?>(.*?)</p>', re.S)
cover = re.search(cover_pattern, html).group(1).strip() name = re.search(name_pattern, html).group(1).strip() categories = re.findall(categories_pattern, html) published_at = re.search(published_pattern, html).group(1) drama = re.search(drama_pattern, html).group(1).strip() score = float(re.search(score_pattern, html).group(1).strip()) return { '电影封面': cover, '电影名称': name, '电影类别': categories, '电影上映时间': published_at, '电影剧情介绍': drama, '电影评分': score }
在这里定义了一个scrape_detail函数方法是为了获取详细页网址,不再复写避免了代码重复,而且如果后续想对scrape_detail函数方法进行改动更加的方便。
然后我们再定义一个parse_detail函数方法用于解析详细页网址里面的信息,然后利用正则表达式获取我们想要的信息。
最后,我们还需要修改一下main方法,把我们新增的功能加进去调用。
def main(): for page in range(1, TOTAL_PAGE + 1): index_html = scrape_index(page) detail_urls = parse_index(index_html) # logging.info('每一部电影详细页网址:%s' , list(detail_urls)) for detail_url in detail_urls: detail_html = scrape_detail(detail_url) data = parse_detail(detail_html) logging.info('获取详细页的信息:%s', data)
现在代码运行的结果是:
2022–08–03 18:49:50,321 – INFO: 抓取https://ssr1.scrape.center/page/1中... 2022–08–03 18:49:52,530 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/1 2022–08–03 18:49:52,530 – INFO: 抓取https://ssr1.scrape.center/detail/1中... 2022–08–03 18:50:09,717 – INFO: 获取详细页的信息:{'电影封面': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', '电影名称': '霸王别姬 – Farewell My Concubine', '电影类别': ['剧情', '爱情'], '电影上映时间': '1993-07-26', '电影剧情介绍': '影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝,尤其一出《霸王别姬》,更是誉满京城,为此,两人约定合演一辈子《霸王别姬》。但两人对戏剧与人生关系的理解有本质不同,段小楼深知戏非人生,程蝶衣则是人戏不分。段小楼在认为该成家立业之时迎娶了名妓菊仙(巩俐 饰),致使程蝶衣认定菊仙是可耻的第三者,使段小楼做了叛徒,自此,三人围绕一出《霸王别姬》生出的爱恨情仇战开始随着时代风云的变迁不断升级,终酿成悲剧。', '电影评分': 9.5} 2022–08–03 18:50:09,717 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/2 2022–08–03 18:50:09,717 – INFO: 抓取https://ssr1.scrape.center/detail/2中... ...
可以看出已经成功爬取到了我们所需要的电影相关信息了,接下来我们对爬取的内容进行保存即可。
任务三:保存爬取数据
保存数据库的方法有很多中,其中用到最多的方法就是存储在数据库中,但是目前我们先不进行数据库的存储,后续学习到数据库的时候再进行数据库的保存,目前我们就进行数据的json文件的存储。
import json from os import makedirs from os.path import exists
RESULTS_DIR = '电影爬取数据' exists(RESULTS_DIR) or makedirs(RESULTS_DIR)
def save_data(data): name = data.get('电影名称') data_path = f'{RESULTS_DIR}/{name}.json' json.dump(data, open(data_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
导入json库,然后导入os库中的makedirs方法用于递归创建目录和os库中的exists方法用于判断一个路径是否有效,然后定义保存数据的函数方法,这里用到了json库中的dump方法,dump方法里面有两个参数,它们的功能是:
ensure_ascii | 值为False,保证中文字符在文件中能正常显示 |
indent | 值自定义,设置json数据结果有多少行缩进,利于美观 |
接下来继续修改我们的main方法,添加新功能:
def main(): for page in range(1, TOTAL_PAGE + 1): index_html = scrape_index(page) detail_urls = parse_index(index_html) # logging.info('每一部电影详细页网址:%s' , list(detail_urls)) for detail_url in detail_urls: detail_html = scrape_detail(detail_url) data = parse_detail(detail_html) logging.info('获取详细页的信息:%s', data) logging.info('保存爬取数据到json文件中') save_data(data) logging.info('数据保存成功!')
现在代码运行的结果是;
2022–08–03 19:15:39,066 – INFO: 抓取https://ssr1.scrape.center/page/1中... 2022–08–03 19:15:39,433 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/1 2022–08–03 19:15:39,433 – INFO: 抓取https://ssr1.scrape.center/detail/1中... 2022–08–03 19:15:39,984 – INFO: 获取详细页的信息:{'电影封面': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', '电影名称': '霸王别姬 – Farewell My Concubine', '电影类别': ['剧情', '爱情'], '电影上映时间': '1993-07-26', '电影剧情介绍': '影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝,尤其一出《霸王别姬》,更是誉满京城,为此,两人约定合演一辈子《霸王别姬》。但两人对戏剧与人生关系的理解有本质不同,段小楼深知戏非人生,程蝶衣则是人戏不分。段小楼在认为该成家立业之时迎娶了名妓菊仙(巩俐 饰),致使程蝶衣认定菊仙是可耻的第三者,使段小楼做了叛徒,自此,三人围绕一出《霸王别姬》生出的爱恨情仇战开始随着时代风云的变迁不断升级,终酿成悲剧。', '电影评分': 9.5} 2022–08–03 19:15:39,984 – INFO: 保存爬取数据到json文件中 2022–08–03 19:15:39,985 – INFO: 数据保存成功! 2022–08–03 19:15:39,985 – INFO: 获取详细网址:https://ssr1.scrape.center/detail/2 2022–08–03 19:15:39,985 – INFO: 抓取https://ssr1.scrape.center/detail/2中... 2022–08–03 19:15:40,732 – INFO: 获取详细页的信息:{'电影封面': 'https://p1.meituan.net/movie/6bea9af4524dfbd0b668eaa7e187c3df767253.jpg@464w_644h_1e_1c', '电影名称': '这个杀手不太冷 – Léon', '电影类别': ['剧情', '动作', '犯罪'], '电影上映时间': '1994-09-14', '电影剧情介绍': '里昂(让·雷诺 饰)是名孤独的职业杀手,受人雇佣。一天,邻居家小姑娘马蒂尔德(纳塔丽·波特曼 饰)敲开他的房门,要求在他那里暂避杀身之祸。原来邻居家的主人是警方缉毒组的眼线,只因贪污了一小包毒品而遭恶警(加里·奥德曼 饰)杀害全家的惩罚。马蒂尔德 得到里昂的留救,幸免于难,并留在里昂那里。里昂教小女孩使枪,她教里昂法文,两人关系日趋亲密,相处融洽。 女孩想着去报仇,反倒被抓,里昂及时赶到,将女孩救回。混杂着哀怨情仇的正邪之战渐次升级,更大的冲突在所难免……', '电影评分': 9.5} 2022–08–03 19:15:40,732 – INFO: 保存爬取数据到json文件中 2022–08–03 19:15:40,733 – INFO: 数据保存成功! ...
可以看出已经成功保存了我们爬取的数据:
任务四:利用多进程提高效率
整个代码运行时单进程的,爬取效率很低,耗时较长,如果数据更大的话就会花费更多的时间,这个时候就就需要我们利用Python的多进程机制来提高爬取效率。
因为每一页都是十部电影,而且每一页的爬取是相互独立的,所以我们可以每一页对应一个进程进行爬取,可以通过预先设置一个列表然后通过Python的进程池来实现。
那我们现在加入多进程:
import multiprocessing
def main(page): index_html = scrape_index(page) detail_urls = parse_index(index_html) for detail_url in detail_urls: detail_html = scrape_detail(detail_url) data = parse_detail(detail_html) logging.info('获取详细页的信息:%s', data) logging.info('保存爬取数据到json文件中') save_data(data) logging.info('数据保存成功!')
if __name__ == '__main__': # main() pool = multiprocessing.Pool() pages = range(1, TOTAL_PAGE + 1) pool.map(main, pages) pool.close() pool.join()
给main方法添加一个page参数,来表示列表页的页码,然后定义一个进程池,声明pages为所有需要遍历的页码数,然后调用map方法,它的第一个参数是需要被调用的参数,第二个参数是需要遍历的页码。
使用多进程到底快多少呢?还记得我之前的博客有过比较运行时间的方法,具体博客地址我放在下面了,我就不再描述:
https://blog.csdn.net/qq_52417436/article/details/124597734?spm=1001.2014.3001.5501
我的这篇博客里面使用了一种测试运行时间的方法,我们来使用一下最简单的方法测试一下:
首先从horology库中导入timed方法:
from horology import timed
然后在main方法前一行加上@timed,然后运行后就可以看到运行时间:
@timed def main(): ...
首先我们来测试一下单进程的运行时间:
main: 135 s
然后再测试一下多进程的运行时间:
main: 25.8 s
显而易见,多进程节约了大量的运行时间,这样更有利于我们往后爬取数量庞大的数据,所以爬虫的使用更多的也会伴随多进程等一系列提高效率的方法运行,我们不光要学会爬取网页上面的数据,也要学会使用各种方法来提高和优化我们的代码。
三、补充一点
在方法parse_detail中再进行HTML代码的解析时,中间代码部分我们最好写成下面这样,虽然我上面的代码可以成功运行,但是更多可能会导致报错,最好避免这个的发生:
def parse_detail(html): cover_pattern = re.compile('class="item.*?<img.*?src="(.*?)".*?class="cover">', re.S) name_pattern = re.compile('h2.*?>(.*?)</h2>') categories_pattern = re.compile('<button.*?category.*?<span>(.*?)</span>.*?</button>', re.S) published_pattern = re.compile('(\d{4}-\d{2}-\d{2})\s?上映') drama_pattern = re.compile('<div.*?drama.*?>.*?<p.*?>(.*?)</p>', re.S) score_pattern = re.compile('<p.*?score.*?>(.*?)</p>', re.S)
cover = re.search(cover_pattern, html).group(1).strip() if re.search(cover_pattern, html) else None name = re.search(name_pattern, html).group(1).strip() if re.search(name_pattern, html) else None categories = re.findall(categories_pattern, html) if re.findall(categories_pattern, html) else [] published_at = re.search(published_pattern, html).group(1) if re.search(published_pattern, html) else None drama = re.search(drama_pattern, html).group(1).strip() if re.search(drama_pattern, html) else None score = float(re.search(score_pattern, html).group(1).strip()) if re.search(score_pattern, html) else None return { '电影封面': cover, '电影名称': name, '电影类别': categories, '电影上映时间': published_at, '电影剧情介绍': drama, '电影评分': score }
四、最后我想说
这次实战中,我们使用了诸多的Python库,例如:requests、re、logging、multiprocessing等等,就像我上面所说,爬虫的学习会伴随很多其他知识的学习和运用,我们要不断的提高自己的编程能力和思考能力,一方面学会去如何使用这些方法,另一方面也需要我们通过更多的思考去不断完善我们编程思维,总而言之,坚持每天学习编程,可以有效的提高自己在这方面的能力,一定要继续保持,知足,上进,不负野心,这句话不光送给你们,也再时刻提醒着我自己。
感谢能够认真看完的读者们,希望你们能从中学到知识,也希望能得到你们的支持,另外文章如有错误或者不妥之处还请各位帮我指出,谢谢大家!
神龙|纯净稳定代理IP免费测试>>>>>>>>天启|企业级代理IP免费测试>>>>>>>>IPIPGO|全球住宅代理IP免费测试