当前位置: 首页 > news >正文

基础爬虫案例实战

我们已经学习了多进程、requests、正则表达式的基本用法,但还没有完整地实现过一个爬取案例。这一节,我们就来实现一个完整的网站爬虫,把前面学习的知识点串联起来,同时加深对这些知识点的理解。

准备工作

我们需要先做好如下准备工作。

  • 安装好 Python3,最低为 3.6版本,并能成功运行 Python3 程序:
  • 了解 Python 多进程的基本原理。
  • 了解 Python HTTP 请求库 requests 的基本用法。
  • 了解正则表达式的用法和 Python 中正则表达式库re 的基本用法。

爬取目标

本节我们以一个基本的静态网站作为案例进行爬取,需要爬取的链接为 https://ssr1.scrape.center/这个网站里面包含一些电影信息,界面如下图所示。
在这里插入图片描述
网站首页展示了一个由多个电影组成的列表,其中每部电影都包含封面、名称、分类、上映时间评分等内容,同时列表页还支持翻页,单击相应的页码就能进入对应的新列表页。

如果我们点开其中一部电影,会进入该电影的详情页面,例如我们打开第一部电影《霸王别姬》会得到如下图所示的页面。
在这里插入图片描述
这个页面显示的内容更加丰富,包括剧情简介、导演、演员等信息。

我们本节要完成的目标有:

  • 利用requests 爬取这个站点每一页的电影列表,顺着列表再爬取每个电影的详情页;
  • 用正则表达式提取每部电影的名称、封面、类别、上映时间、评分、剧情简介等内容;
  • 把以上爬取的内容保存为JSON 文本文件;
  • 使用多进程实现爬取的加速。

已经做好准备,也明确了目标,那我们现在就开始吧。

爬取列表页

第一步爬取肯定要从列表页人手,我们首先观察一下列表页的结构和翻页规则。在浏览器中访问https://ssr1.scrape.center/,然后打开浏览器开发者工具,如下图所示。
在这里插入图片描述

观察每一个电影信息区块对应的 HTML以及进入到详情页的 URL,可以发现每部电影对应的区块都是一个 div节点,这些节点的 class属性中都有 el-card这个值。每个列表页有 10个这样的 div节点,也就对应着 10部电影的信息。

接下来再分析一下是怎么从列表页进入详情页的,我们选中第一个电影的名称,看下结果,如下图所示。

在这里插入图片描述
可以看到这个名称实际上是一个h2节点,其内部的文字就是电影标题。h2节点的外面包含一个a节点,这个a节点带有 href属性,这就是一个超链接,其中href的值为 /detail/1,这是一个相对网站的根 URL https://ssrl.scrape.center/ 的路径,加上网站的根 URL就构成了 https://ssrl.scrape.centerdetail/1,也就是这部电影的详情页的 URL。这样我们只需要提取这个 href 属性就能构造出详情页的URL 并接着爬取了。

接下来我们分析翻页的逻辑,拉到页面的最下方,可以看到分页页码,如下图所示。
在这里插入图片描述
我们单机第二页,如下图所示:
在这里插入图片描述
可以看到网页的 URL, 变成了 https://ssrl.scrape.center/page/2,相比根 URL多了 /page/2 这部分内容。网页的结构还是和原来一模一样,可以像第1页那样处理。

接着我们查看第3页、第4页等内容,可以发现一个规律,这些页面的 URL最后分别为 /page/3/page/4。所以,/page 后面跟的就是列表页的页码,当然第1页也是一样,我们在根 URL 后面加上/page/1也是能访问这页的,只不过网站做了一下处理,默认的页码是1,所以第一次显示的是第1页内容。

好,分析到这里,逻辑基本清晰了。
于是我们要完成列表页的爬取,可以这么实现:

  • 遍历所有页码,构造10页的索引页URL;
  • 从每个索引页,分析提取出每个电影的详情页 URL。

那么我们写代码来实现一下吧。
首先,需要先定义一些基础的变量,并引人一些必要的库,写法如下:

import requests
import logging
import re
from urllib.parse import urljoinlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 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.textlogging.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代码:如果不是,则输出错误日志信息。另外这里实现了reguests 的异常处理,如果出现了爬取异常,就输出对应的错误日志信息。我们将 logging 库中的 error 方法里的 exc info 参数设置为 True,可以打印出 Traceback 错误堆栈信息。

好了,有了 scrape_page 方法之后,我们给这个方法传入一个 ur1,如果情况正常,它就可以返回页面的 HTML 代码了。

在 scrape_page 方法的基础上,我们来定义列表页的爬取方法吧,实现如下:

def scrape_index(page):index_url = f'{BASE_URL}/page/{page}'return scrape_page(index_url)

方法名称叫作 scrape index,这个实现就很简单了,这个方法会接收一个 page参数,即列表页的页码,我们在方法里面实现列表页的URL拼接,然后调用scrape_page方法爬取即可,这样就能得到列表页的 HTML 代码了。

获取了 HTML代码之后,下一步就是解析列表页,并得到每部电影的详情页的URL,实现如下:

def parse_index(html):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,即列表页的 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 就是我们在上文所说的类似 /detai1/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_PAGE + 1):index_html = scrape_index(page)detail_urls = parse_index(index_html)logging.info('detail urls %s', list(detail_urls))if __name__ == '__main__':main()

这里我们定义了 main 方法,以完成对上面所有方法的调用。main 方法中首先使用 range 方法遍历了所有页码,得到的 page 就是 1-10;接着把 page 变量传给scrape_index方法,得到列表页的 HTML;把得到的 HTML 赋值为 index_html 变量。接下来将 index html变量传给 parse_index 方法,得到列表页所有电影的详情页 URL,并赋值为 detail_urls,结果是一个生成器,我们调用list方法就可以将其输出。

运行一下上面的代码,结果如下:

2024-12-22 22:14:49,777 - INFO: scraping https://ssr1.scrape.center/page/1...
2024-12-22 22:14:50,874 - INFO: get detail url https://ssr1.scrape.center/detail/1
2024-12-22 22:14:50,874 - INFO: get detail url https://ssr1.scrape.center/detail/2
2024-12-22 22:14:50,875 - INFO: get detail url https://ssr1.scrape.center/detail/3
2024-12-22 22:14:50,875 - INFO: get detail url https://ssr1.scrape.center/detail/4
2024-12-22 22:14:50,875 - INFO: get detail url https://ssr1.scrape.center/detail/5
2024-12-22 22:14:50,875 - INFO: get detail url https://ssr1.scrape.center/detail/6
2024-12-22 22:14:50,876 - INFO: get detail url https://ssr1.scrape.center/detail/7
2024-12-22 22:14:50,876 - INFO: get detail url https://ssr1.scrape.center/detail/8
2024-12-22 22:14:50,876 - INFO: get detail url https://ssr1.scrape.center/detail/9
2024-12-22 22:14:50,876 - INFO: get detail url https://ssr1.scrape.center/detail/10
2024-12-22 22:14:50,876 - INFO: detail urls ['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']
2024-12-22 22:14:50,877 - INFO: scraping https://ssr1.scrape.center/page/2...
2024-12-22 22:14:51,315 - INFO: get detail url https://ssr1.scrape.center/detail/11
2024-12-22 22:14:51,315 - INFO: get detail url https://ssr1.scrape.center/detail/12
2024-12-22 22:14:51,315 - INFO: get detail url https://ssr1.scrape.center/detail/13
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/14
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/15
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/16
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/17
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/18
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/19
2024-12-22 22:14:51,316 - INFO: get detail url https://ssr1.scrape.center/detail/20
...

输出内容比较多,这里只贴了一部分。

可以看到,程序首先爬取了第1页列表页,然后得到了对应详情页的每个 URL,接着再爬第2页第3页,一直到第 10 页,依次输出了每一页的详情页 URL。意味着我们成功获取了所有电影的详情页 URL。

爬取详情页

已经可以成功获取所有详情页 URL了,下一步当然就是解析详情页,并提取我们想要的信息了首先观察一下详情页的 HTML 代码,如下图所示。
在这里插入图片描述
经过分析,我们想要提取的内容和对应的节点信息如下。

  • 封面:是一个 img 节点,其 class 属性为 cover。
  • 名称:是一个h2节点,其内容是电影名称。
  • 类别:是 span节点,其内容是电影类别。span节点的外侧是button节点,再外侧是class为categories的 div 节点。
  • 上映时间:是 span 节点,其内容包含上映时间,外侧是 class为 info 的 div 节点。另外提取结果中还多了“上映”二字,我们可以用正则表达式把日期提取出来。
  • 评分:是一个p节点,其内容便是电影评分。p节点的class属性为score。
  • 剧情简介:是一个p节点,其内容便是剧情简介,其外侧是class 为 drama 的 div 节点。

看着有点复杂吧,不用担心,正则表达式在手,我们都可以轻松搞定,接着实现一下代码吧。
我们已经成功获取了详情页 URL,下面当然是定义一个详情页的爬取方法了,实现如下:

def scrape_detail(url):return scrape_page(url)

这里定义了一个scrape_detail方法,接收一个参数url,并通过调用scrape_page方法获得网页源代码。由于我们刚才已经实现了 scrape_page 方法,所以这里不用再写一遍页面爬取的逻辑,直接调用即可,做到了代码复用。

另外有人会说,这个 scrape_detail 方法里面只调用了 scrape_page 方法,而没有别的功能,那爬取详情页直接用 scrape_page 方法不就好了,还有必要再单独定义 scrape_detail方法吗?有必要单独定义一个 scrape_detail方法在逻辑上会显得更清晰,而且以后如果想对scrape detail方法进行改动,例如添加日志输出、增加预处理,都可以在scrape_detail里实现,而不用改动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 Nonename = re.search(name_pattern, html).group(1).strip() if re.search(name_pattern, html) else Nonecategories = re.findall(categories_pattern, html) if re.findall(categories_pattern, html) else []published_at = re.search(published_at_pattern, html).group(1) if re.search(published_at_pattern, html) else Nonedrama = re.search(drama_pattern, html).group(1).strip() if re.search(drama_pattern, html) else Nonescore = float(re.search(score_pattern, html).group(1).strip()) if re.search(score_pattern, html) else Nonereturn {'cover': cover, 'name': name, 'categories': categories, 'published at': published_at,'drama': drama, 'score': score}

这里我们定义了 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节点的文本即可,由于提取结果是字符串,因此还需要把它转成浮点数,即 float 类型。

上述字段都提取完毕之后,构造一个字典并返回。
这样,我们就成功完成了详情页的提取和分析。
最后,稍微改写一下main方法,增加对scrape_detail方法和 parse_detail方法的调用,改写如下:

def main():for page in range(1, TOTAL_PAGE + 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 并输出。

运行结果如下:

2024-12-22 22:48:12,784 - INFO: scraping https://ssr1.scrape.center/page/1...
2024-12-22 22:48:14,071 - INFO: get detail url https://ssr1.scrape.center/detail/1
2024-12-22 22:48:14,071 - INFO: scraping https://ssr1.scrape.center/detail/1...
2024-12-22 22:48:18,623 - INFO: get detail data {'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'name': '霸王别姬 - Farewell My Concubine', 'categories': ['剧情', '爱情'], 'published at': '1993-07-26', 'drama': '影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝,尤其一出《霸王别姬》,更是誉满京城,为此,两人约定合演一辈子《霸王别姬》。但两人对戏剧与人生关系的理解有本质不同,段小楼深知戏非人生,程蝶衣则是人戏不分。段小楼在认为该成家立业之时迎娶了名妓菊仙(巩俐 饰),致使程蝶衣认定菊仙是可耻的第三者,使段小楼做了叛徒,自此,三人围绕一出《霸王别姬》生出的爱恨情仇战开始随着时代风云的变迁不断升级,终酿成悲剧。', 'score': 9.5}
...

由于内容较多,这里省略了后续内容。
至此,我们已经成功提取出了每部电影的基本信息,包括封面、名称、类别等。

保存数据

成功提取到详情页信息之后,下一步就要把数据保存起来了。由于到现在我们还没有学习数据库的存储,所以临时先将数据保存成文本格式,这里我们可以一个条目定义一个 JSON 文本。

定义一个保存数据的方法如下:

import json
from os import makedirs
from os.path import existsRESULTS_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_PAGE + 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('saving data successfully')

这就是加了对 save data 方法调用的 main 方法,其中还加了一些日志信息。
重新运行,我们看下输出结果:

2024-12-22 22:57:54,570 - INFO: scraping https://ssr1.scrape.center/page/1...
2024-12-22 22:57:55,220 - INFO: get detail url https://ssr1.scrape.center/detail/1
2024-12-22 22:57:55,220 - INFO: scraping https://ssr1.scrape.center/detail/1...
2024-12-22 22:57:55,558 - INFO: get detail data {'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'name': '霸王别姬 - Farewell My Concubine', 'categories': ['剧情', '爱情'], 'published at': '1993-07-26', 'drama': '影片借一出《霸王别姬》的京戏,牵扯出三个人之间一段随时代风云变幻的爱恨情仇。段小楼(张丰毅 饰)与程蝶衣(张国荣 饰)是一对打小一起长大的师兄弟,两人一个演生,一个饰旦,一向配合天衣无缝,尤其一出《霸王别姬》,更是誉满京城,为此,两人约定合演一辈子《霸王别姬》。但两人对戏剧与人生关系的理解有本质不同,段小楼深知戏非人生,程蝶衣则是人戏不分。段小楼在认为该成家立业之时迎娶了名妓菊仙(巩俐 饰),致使程蝶衣认定菊仙是可耻的第三者,使段小楼做了叛徒,自此,三人围绕一出《霸王别姬》生出的爱恨情仇战开始随着时代风云的变迁不断升级,终酿成悲剧。', 'score': 9.5}
2024-12-22 22:57:55,559 - INFO: saving data to json file
2024-12-22 22:57:55,570 - INFO: saving data successfully
...

通过运行结果可以发现,这里成功输出了将数据存储到 JSON 文件的信息。
运行完毕之后,我们可以观察下本地的结果,可以看到 results 文件夹下多了 100个JSON 文件,每部电影数据都是一个JSON文件,文件名就是电影名,如图下所示。
在这里插入图片描述

多进程加速

由于整个爬取是单进程的,而且只能逐条爬取,因此速度稍微有点慢,那有没有方法对整个爬取过程进行加速呢?
前面我们讲了多进程的基本原理和使用方法,下面就来实践一下多进程爬取吧。

由于一共有 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('saving data successfully')if __name__ == '__main__':pool = multiprocessing.Pool()pages = range(1, TOTAL_PAGE + 1)pool.map(main, pages)pool.close()pool.join()

我们首先给 main 方法添加了一个参数 page,用以表示列表页的页码。接着声明了一个进程池。并声明 pages 为所有需要遍历的页码,即 1-10。最后调用 map 方法,其第一个参数就是需要被调用的参数,第二个参数就是 pages,即需要遍历的页码。

这样就会依次遍历 pages 中的内容,把1-10这 10个页码分别传递给 main方法,并把每次的调用分别变成一个进程,加入进程池中,进程池会根据当前运行环境来决定运行多少个进程。例如我的机器的 CPU有8个核,那么进程池的大小就会默认设置为8,这样会有8个进程并行运行。

运行后的输出结果和之前类似,只是可以明显看到,多进程执行之后的爬取速度快了很多。可以清空之前的爬取数据,会发现数据依然可以被正常保存成 JSON 文件。

好了,到现在为止,我们就完成了全站电影数据的爬取,并实现了爬取数据的存储和优化。

完整代码

import requests
import logging
import re
from urllib.parse import urljoinlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10def scrape_page(url):logging.info('scraping %s...', url)try:response = requests.get(url)if response.status_code == 200:return response.textlogging.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)def scrape_index(page):index_url = f'{BASE_URL}/page/{page}'return scrape_page(index_url)def parse_index(html):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_urldef 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_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 Nonename = re.search(name_pattern, html).group(1).strip() if re.search(name_pattern, html) else Nonecategories = re.findall(categories_pattern, html) if re.findall(categories_pattern, html) else []published_at = re.search(published_at_pattern, html).group(1) if re.search(published_at_pattern, html) else Nonedrama = re.search(drama_pattern, html).group(1).strip() if re.search(drama_pattern, html) else Nonescore = float(re.search(score_pattern, html).group(1).strip()) if re.search(score_pattern, html) else Nonereturn {'cover': cover, 'name': name, 'categories': categories, 'published at': published_at,'drama': drama, 'score': score}import json
from os import makedirs
from os.path import existsRESULTS_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)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('saving data successfully')if __name__ == '__main__':pool = multiprocessing.Pool()pages = range(1, TOTAL_PAGE + 1)pool.map(main, pages)pool.close()pool.join()

总结

本篇博客用到的库有 requests、multiprocessing、re、logging等,通过这个案例实战,我们把前面学习到的知识都串联了起来,对于其中的一些实现方法,可以好好思考和体会,也希望这个案例能够让你对爬虫的实现有更实际的了解。

相关文章:

基础爬虫案例实战

我们已经学习了多进程、requests、正则表达式的基本用法,但还没有完整地实现过一个爬取案例。这一节&#xff0c;我们就来实现一个完整的网站爬虫&#xff0c;把前面学习的知识点串联起来&#xff0c;同时加深对这些知识点的理解。 准备工作 我们需要先做好如下准备工作。 安…...

深度学习工作:从追求 SoTA 到揭示新现象

TLDR&#xff1a;主要讨论了从追求模型 SoTA 到揭示新现象的转变。通过几个例子&#xff0c;包括ACNet到RepVGG的发展&#xff0c;RIFE插帧、Film插帧&#xff0c;以及OpenAI的近期工作&#xff0c;阐述了这种转变的重要性。 知乎&#xff1a;黄哲威 hzwer链接&#xff1a;http…...

学习记录—正则表达式-基本语法

正则表达式简介-《菜鸟教程》 正则表达式是一种用于匹配和操作文本的强大工具&#xff0c;它是由一系列字符和特殊字符组成的模式&#xff0c;用于描述要匹配的文本模式。 正则表达式可以在文本中查找、替换、提取和验证特定的模式。 本期内容将介绍普通字符&#xff0c;特殊…...

智慧农业物联网传感器:开启农业新时代

在当今科技飞速发展的时代&#xff0c;农业领域正经历着一场前所未有的变革&#xff0c;而智慧农业物联网传感器无疑是这场变革中的关键利器。它宛如农业的 “智慧大脑”&#xff0c;悄然渗透到农业生产的各个环节&#xff0c;为传统农业注入了全新的活力&#xff0c;让农业生产…...

普通人怎么入门学习并使用AI?

前言 作为普通人看着AI一天一天变革&#xff0c;心急如焚&#xff0c;未来但是就是不知道怎么才算真正进入了AI&#xff0c;使用AI....作为从头至尾追随AI脚步的码农有几点小建议~ 一、&#x1f4bb;使用 AI 网站或软件&#xff0c;解决实际问题 不管用哪种AI&#xff0c;先用…...

Java中处理if-else的几种高级方法

前言 在我看来多写几个if-else没啥大不了的&#xff0c;但是就是看起来没啥逼格&#xff0c;领导嫌弃。我根据开发的经历写几个不同的替代方法 一、枚举法替代 我先前写了一篇文章&#xff0c;可以去看看。 通过枚举替换if-else语句的解决方案_枚举代替if else c语言-CSDN博…...

前端知识补充—CSS

CSS介绍 什么是CSS CSS(Cascading Style Sheet)&#xff0c;层叠样式表, ⽤于控制⻚⾯的样式 CSS 能够对⽹⻚中元素位置的排版进⾏像素级精确控制, 实现美化⻚⾯的效果. 能够做到⻚⾯的样式和结构分离 基本语法规范 选择器 {⼀条/N条声明} 1&#xff09;选择器决定针对谁修改…...

企业架构学习笔记-数字化转型

1. 企业数字化发展阶段 案例1.业务部门“点菜”&#xff0c;IT部门叫苦 随着企业信息化进程的不断推进&#xff0c;IT部门的角色和面临的挑战也在发生显著变化。在信息化建设的初级阶段&#xff0c;确实存在IT部门需要积极引导和说服业务部门重视信息技术价值的情况。当时&am…...

Linux网络——TCP的运用

系列文章目录 文章目录 系列文章目录一、服务端实现1.1 创建套接字socket1.2 指定网络接口并bind2.3 设置监听状态listen2.4 获取新链接accept2.5 接收数据并处理&#xff08;服务&#xff09;2.6 整体代码 二、客户端实现2.1 创建套接字socket2.2 指定网络接口2.3 发起链接con…...

Vue3之状态管理Vuex

Vuex作为Vue.js的官方状态管理库&#xff0c;在大型或复杂的前端项目中扮演着至关重要的角色。本文将从Vuex的原理、特点、应用场景等多个方面进行深入解析&#xff0c;并通过代码示例展示如何在Vuex中实现特定功能。 一、Vuex原理 Vuex是一个专为Vue.js应用程序开发的状态管…...

DPO(Direct Preference Optimization)算法解释:中英双语

中文版 DPO paper: https://arxiv.org/pdf/2305.18290 DPO 算法详解&#xff1a;从理论到实现 1. 什么是 DPO&#xff1f; DPO&#xff08;Direct Preference Optimization&#xff09;是一种直接基于人类偏好进行优化的算法&#xff0c;旨在解决从人类偏好数据中训练出表现…...

Hostapd2.11解析笔记

最近在调试Hostapd,尝试通过配置使能一个支持MLO的AP,不过不知道hostapd conf里面哪些选项开启后可以使能,所以对Hostapd做一个整体解析. 简介 hostapd 是用于接入点和身份验证服务器的用户空间守护程序。它实现 IEEE 802.11 接入点管理、IEEE 802.1X/WPA/WPA2/WPA3/EAP 身份…...

js控制文字溢出显示省略号

.text{display: -webkit-box;overflow: hidden;white-space: normal;text-overflow: ellipsis;word-wrap: break-word;-webkit-line-clamp: 2;-webkit-box-orient: vertical; }本人有个需求就是在一个盒子内有一段文本&#xff0c;然后控制文本显示两行&#xff0c;第二行要显示…...

WPF+MVVM案例实战与特效(四十七)-实现一个路径绘图的自定义按钮控件

文章目录 1、案例效果2、创建自定义 PathButton 控件1、定义 PathButton 类2、设计样式与控件模板3、代码解释3、控件使用4、直接在 XAML 中绑定命令3、源代码获取4、总结1、案例效果 2、创建自定义 PathButton 控件 1、定义 PathButton 类 首先,我们需要创建一个新的类 Pat…...

操作002:HelloWorld

文章目录 操作002&#xff1a;HelloWorld一、目标二、具体操作1、创建Java工程①消息发送端&#xff08;生产者&#xff09;②消息接收端&#xff08;消费者&#xff09;③添加依赖 2、发送消息①Java代码②查看效果 3、接收消息①Java代码②控制台打印③查看后台管理界面 操作…...

odoo中@api.model, @api.depends和@api.onchange 装饰器的区别

文章目录 1. api.model用途特点示例 2. api.depends用途特点示例 3. api.onchange用途特点示例 总结 在 Odoo 中&#xff0c;装饰器&#xff08;decorators&#xff09;用于修饰方法&#xff0c;以指定它们的行为和触发条件。api.model、api.depends 和 api.onchange 是三个常用…...

有哪些精益工具可以帮助企业实现转型?

为了在激烈的市场竞争中立于不败之地&#xff0c;许多企业开始寻求通过精益转型来优化运营、降低成本、提高效率和客户满意度。然而&#xff0c;精益转型并非一蹴而就&#xff0c;而是需要一系列精益工具的辅助&#xff0c;这些工具能够帮助企业识别浪费、优化流程、提升质量&a…...

以太网帧结构

以太网帧结构 目前&#xff0c;我们局域网当中应用最广的技术或者协议啊&#xff0c;就是以太网。我们首先来看一下以太网的真结构。这块内容这里边再系统的来给大家去展开说一下&#xff0c;以太网真格式就如下面这个图。所示前面有八个字节&#xff0c;是用于时钟同步的&…...

QT调用Sqlite数据库

QT设计UI界面&#xff0c;后台访问数据库&#xff0c;实现数据库数据的增删改查。 零售商店系统 数据库表&#xff1a; 分别是顾客表&#xff0c;订单详情表&#xff0c;订单表&#xff0c;商品表 表内字段详情如下&#xff1a; 在QT的Pro文件中添加sql&#xff0c;然后添加头…...

前端

前端页面 Web页面 PC端程序页面 移动端APP页面 ... HTML页面 HTML超文本标记页面 超文本&#xff1a;文本&#xff0c;声音&#xff0c;图片&#xff0c;视频&#xff0c;表格&#xff0c;链接 标记&#xff1a;由许多标签组成 HTML页面运行到浏览器上面 vscode便捷插件使用 vs…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

shell脚本--常见案例

1、自动备份文件或目录 2、批量重命名文件 3、查找并删除指定名称的文件&#xff1a; 4、批量删除文件 5、查找并替换文件内容 6、批量创建文件 7、创建文件夹并移动文件 8、在文件夹中查找文件...

全球首个30米分辨率湿地数据集(2000—2022)

数据简介 今天我们分享的数据是全球30米分辨率湿地数据集&#xff0c;包含8种湿地亚类&#xff0c;该数据以0.5X0.5的瓦片存储&#xff0c;我们整理了所有属于中国的瓦片名称与其对应省份&#xff0c;方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

vue3 字体颜色设置的多种方式

在Vue 3中设置字体颜色可以通过多种方式实现&#xff0c;这取决于你是想在组件内部直接设置&#xff0c;还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法&#xff1a; 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI

前一阵子在百度 AI 开发者大会上&#xff0c;看到基于小智 AI DIY 玩具的演示&#xff0c;感觉有点意思&#xff0c;想着自己也来试试。 如果只是想烧录现成的固件&#xff0c;乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外&#xff0c;还提供了基于网页版的 ESP LA…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序

一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

【Oracle】分区表

个人主页&#xff1a;Guiat 归属专栏&#xff1a;Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

Java多线程实现之Thread类深度解析

Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

jmeter聚合报告中参数详解

sample、average、min、max、90%line、95%line,99%line、Error错误率、吞吐量Thoughput、KB/sec每秒传输的数据量 sample&#xff08;样本数&#xff09; 表示测试中发送的请求数量&#xff0c;即测试执行了多少次请求。 单位&#xff0c;以个或者次数表示。 示例&#xff1a;…...