基础爬虫实战: 抓取静态网页的信息
实现一个爬虫来爬取一个静态网页的信息。
文章源地址:修能的博客
完整代码请参见本文末尾,或点击此处,或者进入GitHub自取
项目概述
准备工作
- Python3.6及以上,并且环境配置完全。
- 了解Python多进程的基本原理
- 了解PythonHTTP请求库requests的基本用法。
- 了解正则表达式的用法和Python中正则表达式re的基本用法。
爬取目标
需要爬取的链接是:https://ssr1.scrape.center/
,这个网站包含了一些电影信息。
我们点击其中一部电影就可以看到详细的信息。
要完成的目标
- 利用requests库爬取这个站点每一页的电影列表,顺着列表在爬取每个电影的详情页。
- 用正则表达式提取每部电影的名称、封面、类别、上映时间、评分、剧情简介等内容。
- 把以上爬取的内容保存为JSON文件。
- 使用多进程实现爬取的加速。
开始爬取
爬取列表页
第一步: 分析页面
要想爬取一个网站我们就要了解要爬取网站的构成。
使用开发者工具查看页面。
发现每部电影对应的是一个div
节点,而且这些节点的class
属性都有el-card
这个值。
注意到每个列表有十个div
节点,说明一页有十部电影。
选中第一个电影的名称,可以发现这个名称其实是一个h2
节点(其实就是这个节点中的二级标题),在h2节点的外面包含一个a
节点,a节点有一个href
属性,这是一个超链接,其中的href
的值为:/detail/1
,这是一个相对于网站的根URL的地址,可以还原成:https://ssr1.scrape.center/detail/1
,这就是详情页的URL。所以得出结论:只要获取到了href
属性的值,我们就可以获取到电影的详情页URL。,之后我们就可以在详情页中进行爬取信息了。
第二步: 分析翻页的操作逻辑
进行翻页时,我们可以观察到网站的URL从https://ssr1.scrape.center/page/1
转换成了https://ssr1.scrape.center/page/2
,这就是翻页的逻辑,我们也顺利的得到了各页的URL。
代码实现
完成对列表页的爬取的实现,步骤是:
- 遍历所有页码,构造出10页的索引页URL。
- 从每个索引页中分析提取出每个电影的详情页URL。
Code:
###########part_1###########
import requests
import logging
import re
from urllib.parse import urljoin
###########part_2###########
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname) : %(message)s')
BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10
在part_1中,我们引入了requests库
来进行网页的抓取、logging库
来输出信息、re库
来实现正则表达式对页面信息的匹配和urllib.parse.urljoin()方法
来做对URL的拼接。
在part_2中,首先是使用logging.basicConfig()方法
定义日志输出级别和输出格式,BASE_PAGE
为当前爬取站点的根URL,TOTAL_PAGE
设定了需要爬取页面的总数.
Code:
###########part_3###########
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 occured 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 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()
这是一个较为通用的爬取页面方法,sreape_page
,接受一个url
参数,返回页面的HTML代码。如果状态码是不是200
,如果是直接返回页面的HTML代码,如果不是,则输出对应的错误日志信息。
scrape_index(page)
方法就是列表页的爬取方法,按照翻页的逻辑,实现URL的拼接。
parse_index(html)
方法接受一个参数html,级列表页的HTML的代码。
pattern参数
指明了正则表达式的匹配模式,即提取a节点
中参数href
的值,然后通过yield
逐个返回详情页的URL。
最后将上述的函数组合就可以爬取到详情页了。
Result:
2023-07-12 21:18:00,427 - INFO : scraping https://ssr1.scrape.center/page/1...
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/1
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/2
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/3
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/4
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/5
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/6
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/7
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/8
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/9
2023-07-12 21:18:00,593 - INFO : get detail url https://ssr1.scrape.center/detail/10
2023-07-12 21:18:00,593 - 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']
2023-07-12 21:18:00,593 - INFO : scraping https://ssr1.scrape.center/page/2...
......
详情页爬取成功了。
爬取详情页
第一步: 分析页面
通过开发者工具分析发现以下特征:
- 封面:
img节点
,class
属性为cover
,src属性
为封面图片的URL。 - 名称:
h2节点
,其内容是电影名称。 - 类别:
span节点
,其内容是电影的名称。其外层是button节点
,再外层是div节点
,其class属性
是categories
。 - 上映时间:
span节点
,其内容是上映的时间+“上映”,提取时注意去掉“上映”两字。外层是div节点
,其class属性
是m-v-sm info
,正则匹配时匹配info
。 - 评分:
p节点
,其内容是评分,class属性
为score
。 - 剧情简介:
p节点
,其内容是剧情简介。其外层是h3节点
,其内容是“剧情简介”。再外层是属性为drama
的div节点
。
代码实现
Code:
###########part_4###########
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_at_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 None
published_at = re.search(published_at_pattern,html).group(1) if re.search(published_at_pattern,html) else None
drama_at = re.search(drama_at_pattern,html).group(1).strip() if re.search(drama_at_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,
'drmar_at': drama_at,
'score': score
}
定义函数scrape_detail()
,该方法返回详情页的html代码。单独实现函数scrape_detail()
,可以增强程序的灵活性和易读性。
定义函数parse_detail(html)
,对html代码进行匹配,最后返回一个字典对象。
将数据保存
这里采用JSON格式来保存数据,编写以下保存函数以及初始设置。
Code:
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)
运行爬虫
文章源地址:修能的博客
改写main()
:
Code:
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('data saved successfully!^-^')
运行结果:
多线程加速
<待完善>将十页的爬取同时进行,为每一页开一个进程来爬取。而且因为这10个列表页面正好可以提前构造成一个列表,所以选用多进程里面的进程池Pool来实现这个过程。
改写main()
:
Code:
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_PAGE + 1)
pool.map(main, pages)
pool.close()
pool.join()
完整代码
Code:
###########part_1###########
import requests
import logging
import re
import json
from os import makedirs
from os.path import exists
from urllib.parse import urljoin
import multiprocessing
###########part_2###########
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s : %(message)s')
BASE_URL = 'https://ssr1.scrape.center'
TOTAL_PAGE = 10
RESULTS_DIR = 'results'
exists(RESULTS_DIR) or makedirs(RESULTS_DIR)
###########part_3###########
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 occured 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
###########part_4###########
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_at_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 None
published_at = re.search(published_at_pattern, html).group(1) if re.search(published_at_pattern, html) else None
drama_at = re.search(drama_at_pattern, html).group(1).strip() if re.search(drama_at_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,
'drmar_at': drama_at,
'score': score
}
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)
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_PAGE + 1)
pool.map(main, pages)
pool.close()
pool.join()