基础爬虫案例实战
准备工作#
- Python3,3.6以上的版本
- 了解Python多进程的基本原理
- 了解PythonHTTp请求库requests的基本用法
- 了解正则表达式的用法和正则表达式re的基本用法
爬取目标#
静态网站案例,包含一些电影信息。
电影详情页
需完成的目标:
- requests爬取站点的每一页电影列表,顺着列表再爬取每个列表的详情页
- 用正则表达式提取每页的名称、封面等内容
- 把爬取的内容存为JSON文件
- 使用多线程实现爬取的加速
爬取列表页#
观察列表页的结构和翻页规则
观察每个电影信息区块对应的HTML以及进入到详情页的URL发现每部电影对应的区块都是一个div节点,这些节点的class属性中都有el-card。每个列表都有10个这样的div,表示有10部电影。
查看从列表页进入详情页查看网站源码。
h2节点中对应的是电影标题。h2节点外面包含一个a节点,a节点带有href属性,可以得到电影详情页的URL。提取这个href属性就能构造出详情页的URL进行爬取。
分析翻页的逻辑:拉倒最下方可以看到分页页码。
点击第二页发现URL变成了https://ssr1.scrape.center/page/2,相比根URL多了/page/2
定义一些基础变量,并引入一些基本的库
import requests import logging import re from urllib.parse import urljoin logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') BASE_URL = 'https://ssr1.scrape.center' TOTAL_PAGES = 10
requests库用来爬取页面,logging库用来输出信息,re库用来实现正则表达式解析,urljoin模块来做url的拼接。定义了日志输出级别和输出格式,以及BASE_URL为当前站点的根URL,TOTAL_PAGE为需要爬取的总页码数量。
实现一个页面爬取的方法
def scrape_page(url): logging.info('scraping %s...', url) try: response = requests.get(url) if response.status_code == 200: return response.text logging.error('get invalid status code %s while scraping %s', response.status_code, url) except requests.RequestException: logging.error('error occurred while scraping %s', url, exc_info=True)
考虑不仅要爬取列表页,还要爬取详情页,所以定义一个比较通用的爬取页面的方法,叫做scrape_page,
接收一个参数url,返回页面的HTML代码。上面首先判断状态码是不是200,如果是就直接返回页面的HTML代码;如果不是,则输出错误日志信息。另外这里实现了requests的异常处理,如果出现了爬取异常,就输出对应的错误日志信息。将logging库中的error方法里的exc_info参数设置为True,可以打印出Traceback错误堆栈信息。
有了scrape_page方法,给这个方法传入一个url,如果情况正常,就可以返回页面的HTML代码
def scrape_index(page): index_url = f'{BASE_URL}/page/{page}' return scrape_page(index_url)
scrape_index方法接收一个page参数,即列表页的页码,在方法里面进行了列表页的URL拼接,然后调用scrape_page方法就能获取列表页的HTML代码
def scrape_index(page): index_url = f'{BASE_URL}/page/{page}' return scrape_page(index_url)
获取HTML代码之后,下一步就是解析列表页,并得到每部电影详情页的URL
def parse_html(html): # 提取标题超链接href属性 pattern = re.compile('<a.*?href="(.*?)".*?class="name">') items = re.findall(pattern, html) if not items: return [] for item in items: detail_url = urljoin(BASE_URL, item) logging.info('get detail url %s', detail_url) yield detail_url
定义parse_index方法,接收一个参数html。在parse_index方法里先定义一个提取标题链接的href属性的正则表达式
<a.*?href="(.*?)".*?class="name"
使用非贪婪通用匹配.*?来匹配任意字符,同时在href属性的引号之间使用分组匹配(.*?)正则表达式,这样就能获取href的属性值了,正则表达式后面紧跟class="name'",用来表示<a>节点代表电影名称的节点。
获取列表页所有的href值。使用re库的findall方法,第一个参数传入这个正则表达式构造的pattern对象,第二个参数传入html,这样findall方法便会搜索html中所有能否与该正则表达式相匹配的内容,之后把匹配的结果返回,并赋值为items。
如果items为空,返回空列表;如果items不为空,遍历处理。
遍历items得到的item就是上面所说的类似/detail/1这样的结果。由于不是完整的URL,需要借助urljoin方法吧BASE_URL和href拼接到一起,获得详情页完整的URL,得到的结果就是类似https://ssr1.scrape.center/detail/1这样的完整URL,最后调用yield返回即可。
调用parse_index方法,传入列表页的HTML代码,就可以获得该列表的所有电影的详情页URL。
def main(): for page in range(1, TOTAL_PAGES + 1): index_html = scrape_index(page) detail_urls = parse_index(index_html) logging.info('get detail url %s', list(detail_urls))
定义一个main方法,完成对上面函数的调用。main方法中首先使用range方法遍历了所有页码,得到的page就是1-10;接着把page变量传给scrape_index方法,得到列表页的HTML;把得到的HTML赋值为index_html变量。接下来将index_html变量传给parse_index方法,得到所有电影的详情页URL,并赋值给detail_urls,结果是一个生成器,调用list方法就可以将其输出。
程序首先爬取了第一页列表页,然后得到了对应详情页的每个URL,接着再爬取第2页、第3页,一直到第10页,依次输出了每一页的详情页URL。
爬取详情页#
我们要提取的内容和对应的节点信息如下
封面:img节点,class属性为cover
名称:h2节点,内容是电影名称
类别:span节点,内容是电影类别。span节点的外侧是button节点,外侧是class为categories的div节点
上映时间:span节点,内容包含上映时间,外侧是class为info的节点。
评分:p节点,内容为电影评分。p节点的class的属性为score
剧情简介:p节点,内容是剧情简介,外侧是class为drama的div节点
已经成功获取详情页的URL,下面定义一个详情页的爬取方法:
def scrape_detail(url): return scrape_page(url)
定义一个scrape_detail方法,接收一个参数url,通过调用scrape_page方法获得网页的源代码。上面已经实现scrape_page方法,所以不用再写一遍页面爬取的逻辑,直接调用即可,做到了代码复用。
详情页的爬取已经实现,接下来对详情页解析
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_at_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.search(categories_pattern, html) else None published_at = re.search(published_at_pattern, html).group(1).strip() if re.search(published_at_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': cover, 'name': name, 'categories': categories, 'published_at': published_at, 'drama': drama, 'score': scor }
定义了parse_detail方法,用于解析详情页,接收一个参数为html,解析期中的内容,并以字典的形式返回结果。每个字段的解析情况如下所述。
cover:封面。其值是带有cover这个class的img节点的src属性值,所以src的内容使用(.*?)来表示,在img节点的前面加上一些用来区分位置的标识符,如item。由于结果只有一个,因此写好正则表达式后用search方法提取即可。
name:名称。值为h2节点的文本值,因此可以直接在h2标签的中间使用(.*?)表示。由于结果只有一个,所以正则表达式后面直接用search方法提取即可
categories:类别。每个category的值都是button节点里面的span节点的值,所以写好表示button的正则表达式之后,直接在内部span标签的中间使用(.*?)表示即可。因为结果有多个,使用findall方法提取,结果是一个列表。
published_at:上映时间。每个上映时间信息都包含“上映”两字,日期又都是一个规格的格式,所以对于上映时间的提取,直接使用标准年月日的正则表达式(\d{4}-\d{2}-\d{2})即可。因为结果只有一个,所以直接使用search方法提取即可。
drama:直接提取class为drama的节点内部的p节点的文本即可,用search方法提取。
score:直接提取class为score的p节点的文本即可,由于提取的结果是字符串,还需要转换成浮点数。
上述字段都提取完后,构造一个字典并返回。这样就完成了详情页的提取和分析。
改写main方法,增加对scrape_detail方法和parse_detail方法的调用,改写如下:
def main(): for page in range(1, TOTAL_PAGES + 1): 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('get detail data %s', data)
遍历detail_urls,获取了每个详情页的URL;然后依次调用了scrape_detail和parse_detail方法;最后得到了每个详情页的提取结果,赋值为data并输出。
保存数据#
成功提取详情页信息之后,要把信息保存起来,先暂时存储为文本格式,可以定义一个JSON文本。
import json from os import makedirs from os.path import exists RESULTS_DIR = 'results' exists(RESULTS_DIR) or makedirs(RESULTS_DIR) def save_data(data): name = data.get('name') data_path = f'{RESULTS_DIR}/{name}.json' json.dump(data, open(data_path, 'w', encoding='utf-8'), ensure_ascii=False, indent=2)
首先定义保存数据的文件夹RESULTS_DIR,然后判断这个文件是否存在,如果不存在则创建一个。
然后定义保存数据的方法save_data,先是获取数据的name字段,即电影名称,将其当作JSON文件的名称;然后构造JSON文件的路径,接着用json的dump方法将数据保存成文本格式。dump方法设置两个参数,一个是ensure_ascii,值为False,可以保证中文字符在文件中能以正常的中文文本呈现,而不是unicode字符;另一个是indent,值为2,设置了JSON数据的结果有两行缩进,让JSON数据的格式显得更加美观。
将main()方法改写一下:
def main(): for page in range(1, TOTAL_PAGES + 1): 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('get detail data %s', data) logging.info('saving data to json file') save_data(data) logging.info('data saved successfully')
添加对save_data方法调用的main方法,其中还加了一些日志信息。
多进程加速#
由于整个爬取是单进程,而且只能逐条爬取,因此速度较慢可以使用多进程进行爬取。
由于一共有10页详情页,且这10页内容互不干扰,因此可以一个页面一个进程来爬取,因为这10个列表页页码正好可以提前构成一个列表,所以可以选用多进程里面的进程池Pool来实现这个过程。改写main方法。
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('get detail data %s', data) logging.info('saving data to json file') save_data(data) logging.info('data saved successfully') if __name__ == '__main__': pool = multiprocessing.Pool() pages = range(1, TOTAL_PAGES + 1) pool.map(main, pages) pool.close() pool.join()
首先给main方法添加一个参数page,用以表示列表页的页码。接着声明一个进程池,并声明pages为所有需要遍历的页码,即1-10。最后调用map方法,第一个参数就是需要被调用的参数,第二个参数就是pages,即需要遍历的页码。
这样就会遍历pages中的内容,把1-10这十个页码分别传递给main方法,并把每次的调用分别变成一个进程,加入进程池中,进程池会根据当前运行环境来决定运行多少个进程。例如计算机cpu有8个核,那么进程池的大小就会默认设置为8,这样就会有8个进程并行运行。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署