基础爬虫案例实战

我们已经学习了多进程、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 urljoin

logging.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.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代码:如果不是,则输出错误日志信息。另外这里实现了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 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_at_pattern, html).group(1) 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': 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 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_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 urljoin

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10


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)


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_url


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_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.findall(categories_pattern, html) else []
    published_at = re.search(published_at_pattern, html).group(1) 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': score}


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)

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等,通过这个案例实战,我们把前面学习到的知识都串联了起来,对于其中的一些实现方法,可以好好思考和体会,也希望这个案例能够让你对爬虫的实现有更实际的了解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/943242.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unittest02|TestSuite、TestRunner、HTMLTestRunner、处理excel表数据、邮件接收测试结果

目录 八、测试套件TestSuite和测试运行器TestRunner 1、基本概念 2、创建和使用测试套件 3、 自动发现测试用例、创建测试套件、运行测试 4、生成html的测试报告&#xff1a;HTMLTestRunner 1️⃣导入HTMLTestRunner模块 2️⃣运行测试用例并生成html文件 九、unittest…

C++-----线性结构

C线性结构模板 概念&#xff1a;线性结构是一种数据元素之间存在一对一线性关系的数据结构&#xff0c;如数组、链表、栈、队列等。C中的模板可以让我们编写通用的代码&#xff0c;适用于不同的数据类型&#xff0c;而不必为每种数据类型都重复编写相同的代码结构。作用&#…

六大基础深度神经网络之CNN

左侧是传统卷积网络输入的是一列像素点&#xff0c;右侧是卷积神经网络&#xff0c;输入的是具有长宽通道数的原始图像 下图为整体架构。卷积层可以认为提取特征&#xff0c;池化层是压缩特征。全连接层是把图像展平然后计算10个类别的概率值 给出一张图像不同区域的特征不同&a…

AIOps平台的功能对比:如何选择适合的解决方案?

定义与概念 AIOps&#xff0c;即人工智能运维&#xff08;Artificial Intelligence for IT Operations&#xff09;&#xff0c;是将人工智能技术应用于 IT 运维领域&#xff0c;以实现自动化、智能化的运维管理。它通过整合大数据、机器学习等先进技术&#xff0c;对海量运维数…

pr基础(3)-lumetri

这篇应该是基础的完结篇了&#xff0c;主要记录调色方面的相关知识。 主要的内容是lumetri的使用 Lumetri的翻译其实来源于两个部分的组合。"Lume"这个词源于拉丁语&#xff0c;意为明亮度或光亮度&#xff0c;是Luminance (Luma) 的缩写&#xff0c;强调了光线的强…

【 thefuck 安装与使用】Linux 终端自动纠错工具:一头GitHub上的“草泥马“ - thefuck,妈妈再也不用担心我打错命令行了!

目录 快速安装使用 . 1.简介 2.安装 3.配置 4.补充 官方盗料参考 快速安装使用 快速安装使用&#xff0c;四步即可&#xff1a; #Ubuntu/Debian系统 sudo apt update sudo apt install python3-dev python3-pip sudo pip3 install thefuck #编辑bashrc配置文件 vim ~/.bashrc…

基于pytorch的深度学习基础3——模型创建与nn.Module

三 模型创建与nn.Module 3.1 nn.Module 模型构建两要素&#xff1a; 构建子模块——__init()__拼接子模块——forward&#xff08;&#xff09; 一个module可以有多个module&#xff1b; 一个module相当于一个运算&#xff0c;都必须实现forward函数&#xff1b; 每一个mod…

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

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

OpenLayers实现渐变透明填充和光效边界

之前在cesium中做过多边形的填充使用渐变透明的效果,那个时候使用的是着色器,利用距离中心点的距离去写shader函数,距离中心越远颜色透明度越高,那么本文我们在openlayers中来实现这一过程。老规矩还是先来看一下效果: 好接下来开始讲述原理,首先关于边界发光的原理我在O…

低代码开发中 DDD 领域驱动的页面权限控制

在低代码开发的领域中&#xff0c;应用安全与灵活性是两大关键考量因素。领域驱动设计&#xff08;DDD&#xff09;作为一种在软件设计领域广泛应用且颇具影响力的方法论&#xff0c;正逐渐在低代码开发的页面权限控制方面展现出其独特的价值与潜力。本文旨在客观地探讨如何借助…

B端UI设计规范是什么?

一、B端UI设计规范是什么&#xff1f; B端UI设计规范是一套针对企业级应用界面设计的全面规则和标准&#xff0c;旨在确保产品界面的一致性、可用性和用户体验。 二、B端UI设计规范要素说明 B端UI设计的基本要素包括设计原则、主题、布局、颜色、字体、图标、按钮和控件、交互…

GitLab 服务变更提醒:中国大陆、澳门和香港用户停止提供服务(GitLab 服务停止)

目录 前言 一. 变更详情 1. 停止服务区域 2. 邮件通知 3. 新的服务提供商 4. 关键日期 5. 行动建议 二. 迁移指南 三. 注意事项 四. 相关推荐 前言 近期&#xff0c;许多位于中国大陆、澳门和香港的 GitLab 用户收到了一封来自 GitLab 官方的重要通知。根据这封邮件…

nginx Rewrite 相关功能

一、Nginx Rewrite 概述 定义 Nginx 的 Rewrite 模块允许对请求的 URI 进行重写操作。它可以基于一定的规则修改请求的 URL 路径&#xff0c;然后将请求定向到新的 URL 地址&#xff0c;这在很多场景下都非常有用&#xff0c;比如实现 URL 美化、网站重构后的 URL 跳转等。主要…

适用于Synology NAS的在线办公套件:ONLYOFFICE安装指南

使用 Synology NAS 上的 ONLYOFFICE 文档&#xff0c;您能在私有云中直接编辑文本文档、电子表格、演示文稿和 PDF&#xff0c;确保工作流程既安全又高效。本指南将分步介绍如何在 Synology 上安装 ONLYOFFICE 文档。 关于 Synology Synology NAS&#xff08;网络附加存储&…

[按键精灵IOS安卓版][脚本基础知识]按键post基本写法

这一期我们来讲按键post的写法&#xff0c;希望通过本期的学习&#xff0c;实现常见的post提交都能编写。 下面开始讲解&#xff1a; 一、使用的命令&#xff1a;url.httppost 选用这个命令的理由是它的参数比较全。 二、post请求都有哪些参数&#xff08;可能用到&#xf…

如何检查交叉编译器gcc工具链里是否有某个库(以zlib库和libpng库为例)

freetype 依赖于 libpng&#xff0c;libpng 又依赖于 zlib&#xff0c;所以我们应该&#xff1a;先编译 安装 zlib&#xff0c;再编译安装 libpng&#xff0c;最后编译安装 freetype。 但是&#xff0c;有些交叉编译器工具链里已经有 zlib库和freetype&#xff0c;所以我们需要…

3D几何建模引擎Parasolid功能解析

一、什么是Parasolid&#xff1f; Parasolid是由Siemens PLM Software开发的高精度精密几何建模引擎。它全面评估CAD&#xff08;计算机辅助设计&#xff09;、CAM&#xff08;计算机辅助制造&#xff09;、CAE&#xff08;计算机辅助工程&#xff09;、PLM&#xff08;产品生…

基于STM32单片机矿井矿工作业安全监测设计

基于STM32单片机矿井矿工作业安全监测设计 目录 项目开发背景设计实现的功能项目硬件模块组成设计思路系统功能总结使用的模块技术详情介绍总结 1. 项目开发背景 随着矿井矿工作业环境的复杂性和危险性逐渐增加&#xff0c;矿井作业安全问题引起了社会各界的广泛关注。传统的…

linux-22 目录管理(二)rmdir命令,删除目录

那接下来我们来看看我们如何去删除目录&#xff1f;那接下来我们来看看我们如何去删除目录&#xff1f;叫remove&#xff0c;remove表示移除的意思&#xff0c;remove directory叫移除目录。所以简写为rmdir&#xff0c;但需要注意&#xff0c;它只能删除空目录&#xff0c;只能…

计算机考研选西电还是成电?

谢邀~先来个总结&#xff1a;电子科技大学计算机综合实力优于西安电子科技大学&#xff0c;但是&#xff0c;二者计算机学硕考研难度没有太大差距&#xff0c;而且考试难度也同属于一个水平&#xff0c;成电性价比更高一些&#xff01;推荐同学优先报考作为985的电子科技大学&a…