参考:《python3网络爬虫开发实战第二版》——2.5基础爬虫案例实战
正则表达式、request
技巧
看源代码
源代码有和显示的代码有出入
<img data-src="https://p0.pipi.cn/mmdb/d2dad59253751bd236338fa5bd5a27c710413.jpg?imageView2/1/w/160/h/220" alt="我不是药神" class="board-img" />
使用CSS选择器
cover_pattern = re.compile('<img.*?class="board-img".*?src="(.*?)">') # 封面
$(’.board-img’)有多个元素,所以上述的正则表达式需要再精确范围
电影网站
正则表达式 + 文本 + 多进程版
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 # 总共有10页
def scrape_page(url):
"""
获取页面html代码
:param url: 链接
:return: 页面的HTML代码
"""
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):
"""
构造链接
:param page: 列表页的页数
:return: 完整的列表页链接
"""
index_url = f'{BASE_URL}/page/{page}'
return scrape_page(index_url)
def parse_index(html):
"""
获取详情页的链接
:param html:列表页的html
:return: 详情页的url
"""
pattern = re.compile('<a.*?href="(.*?)".*?class="name">') # 获取相对路径 如 /detail/1
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):
"""
获取详情页的html
:param url: 详情页的url
:return: 详情页的html
"""
return scrape_page(url)
def parse_detail(html):
"""
解析详情页数据
:param html: 详情页的html
:return: 数据字典
"""
# 正则表达式 引号外单内双
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}).*?上映') # 上映时间
drama_pattern = re.compile('<div.*?class="drama">.*?<p.*?>(.*?)</p></div>', re.S) # 剧情简介
score_pattern = re.compile('<p.*?score.*?>(.*?)</p>', re.S) # 评分
# 如果没找到就返回None
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)
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))
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('save data to json file')
save_data(data)
logging.info('data saved successfully')
if __name__ == '__main__':
main()
日志
2021-12-24 17:29:03,587 - INFO: get detail url https://ssr1.scrape.center/detail/99
2021-12-24 17:29:03,587 - INFO: scraping https://ssr1.scrape.center/detail/99...
2021-12-24 17:29:04,876 - INFO: get detail data {'cover': '/static/img/logo.png', 'name': '教父2 - The Godfather: Part Ⅱ', 'categories': ['剧情', '犯罪'], 'published_at': '1974-12-12', 'drama': '影片主要讲述第二代教父麦克·柯里昂(阿尔·帕西诺 饰)的奋斗历程,同时回忆了第一代教父维多·柯里昂(罗伯特·德尼罗 饰)创业的艰辛,反映了不同历史时期,两代教父的事业、家庭生活。麦克为儿子托尼举行圣餐仪式和庆祝活动的当夜,麦克在家中遭到袭击,凶手被人灭口,面临接管家族事业以来的重重危机,麦克回忆起了父亲维多·柯里昂年轻时在美国的创业历程。麦克一边调查袭击的真相,一边继续开展赌博、酒店等生意,和另一个黑帮人物海门罗斯斗智斗勇,不断扩大势力。终于,麦克的不法行为引起了政府的关注,麦克受到一系列的指控;同时,麦克的家庭也遇到了危机,夫妻感情濒临破裂;而最让麦克痛心的,却是家族中,亲人的背叛。和第一代教父其乐融融的家庭生活比起来,麦克无疑很失败。麦克怎么样面对事业、家庭的双重危机?为什么两代教父会有截然不同的家庭生活?让我们自己在影片中寻找答案。', 'score': 9.0}
2021-12-24 17:29:04,876 - INFO: save data to json file
2021-12-24 17:29:04,877 - INFO: data saved successfully
2021-12-24 17:29:04,877 - INFO: get detail url https://ssr1.scrape.center/detail/90
2021-12-24 17:29:04,877 - INFO: scraping https://ssr1.scrape.center/detail/90...
2021-12-24 17:29:05,649 - INFO: get detail data {'cover': '/static/img/logo.png', 'name': '辛德勒的名单 - Schindler's List', 'categories': ['剧情', '历史', '战争'], 'published_at': '1993-11-30', 'drama': '1939年,波兰在纳粹德国的统治下,党卫军对犹太人进行了隔离统治。德国商人奥斯卡·辛德勒(连姆·尼森 饰)来到德军统治下的克拉科夫,开设了一间搪瓷厂,生产军需用品。凭着出众的社交能力和大量的金钱,辛德勒和德军建立了良好的关系,他的工厂雇用犹太人工作,大发战争财。1943年,克拉科夫的犹太人遭到了惨绝人寰的大屠杀,辛德勒目睹这一切,受到了极大的震撼,他贿赂军官,让自己的工厂成为集中营的附属劳役营,在那些疯狂屠杀的日子里,他的工厂也成为了犹太人的避难所。1944年,德国战败前夕,屠杀犹太人的行动越发疯狂,辛德勒向德军军官开出了1200人的名单,倾家荡产买下了这些犹太人的生命。在那些暗无天日的岁月里,拯救一个人,就是拯救全世界。', 'score': 9.5}
2021-12-24 17:29:05,650 - INFO: save data to json file
2021-12-24 17:29:05,651 - INFO: data saved successfully
多线程
# 多线程
import multiprocessing
def main(page):
index_html = scrape_index(page)
detail_urls = parse_index(index_html)
# logging.info('detail urls %s',list(detail_urls))
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('save data to json file')
save_data(data)
logging.info('data saved successfully')
if __name__ == '__main__':
pool = multiprocessing.Pool()
pages = range(1, TOTAL_PAGE + 1)
# map()函数会将第二个参数(可迭代的列表)的元素一个个的传入第一个参数的函数中
pool.map(main, pages) # 每个main开启一个线程
pool.close()
pool.join()
PyQuery + MongoDB + 多进程版
待定
猫眼top100
注意:这个网站的显示内容与源代码有出入,请看源代码选择元素
正则
参考: 《python3网络爬虫开发实战第一版》——3.4
爬取速度过快会有滑动验证码
只需在浏览器访问该网页手动验证一下,然后就可以继续爬
import requests
import json
import re
import time
class Spider:
def __init__(self):
self.url_temp = 'https://maoyan.com/board/4?offset={}'
self.headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'
}
# 1.根据地址的规律构造url_list
def get_url_list(self):
url_list = [self.url_temp.format(i) for i in range(0, 91, 10)]
return url_list
# 2.发送请求,获取响应
def req_url_resp(self, url):
print('start:' + url)
response = requests.get(url, headers=self.headers)
# print(response.text)
return response.content.decode()
# 3.提取数据
def get_content_list(self, resp_content):
content_list = [] # 用列表存储信息
pattern = re.compile(
'<dd>.*?board-index.*?>(\d+)</i>.*?<img.*?data-src="(.*?)".*?name".*?><a.*?>(.*?)</a>.*?star">(.*?)</p>'
'.*?releasetime">(.*?)</p>.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)
contents = re.findall(pattern, resp_content)
for content in contents:
item = {}
item["id"] = content[0]
item["img_url"] = content[1]
item["name"] = content[2]
item["star"] = content[3].strip()[3:]
item["releasetime"] = content[4][5:]
item["grade"] = content[5] + content[6] + "分"
# print(id,name,star,releasetime,grade,img_url)
content_list.append(item)
return content_list
# 4.保存数据
def save_content(self, content_list):
with open("猫眼Top100.txt", 'a', encoding='utf-8') as f:
for content in content_list:
f.write(json.dumps(content, ensure_ascii=False))
f.write('\n')
print('保存成功!')
def run(self):
# 1.根据地址的规律构造url_list
url_list = self.get_url_list()
# 2.发送请求,获取响应
for url in url_list:
time.sleep(1)
resp_content = self.req_url_resp(url)
# 3.提取数据
content_list = self.get_content_list(resp_content)
# 4.保存数据
self.save_content(content_list)
print("爬取结束")
if __name__ == '__main__':
spider = Spider()
spider.run()
结果
{"id": "1", "img_url": "https://p0.pipi.cn/mmdb/d2dad59253751bd236338fa5bd5a27c710413.jpg?imageView2/1/w/160/h/220", "name": "我不是药神", "star": "徐峥,周一围,王传君", "releasetime": "2018-07-05", "grade": "9.6分"}
{"id": "2", "img_url": "https://p0.pipi.cn/mmdb/d2dad592c7e7e1d236f2aa1811a8a64794b33.jpg?imageView2/1/w/160/h/220", "name": "肖申克的救赎", "star": "蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿", "releasetime": "1994-09-10(加拿大)", "grade": "9.5分"}
{"id": "3", "img_url": "https://p0.pipi.cn/mmdb/d2dad59253751b230f21f0818a5bfd4d8679c.jpg?imageView2/1/w/160/h/220", "name": "绿皮书", "star": "维果·莫腾森,马赫沙拉·阿里,琳达·卡德里尼", "releasetime": "2019-03-01", "grade": "9.5分"}
{"id": "4", "img_url": "https://p0.pipi.cn/mmdb/d2dad592c7e7e1d2365bf1b63cd25951b722b.jpg?imageView2/1/w/160/h/220", "name": "海上钢琴师", "star": "蒂姆·罗斯,比尔·努恩 ,克兰伦斯·威廉姆斯三世", "releasetime": "2019-11-15", "grade": "9.3分"}
{"id": "5", "img_url": "https://p0.pipi.cn/mmdb/d2dad592537923f0ee07acada3ac59b9f3ffb.jpg?imageView2/1/w/160/h/220", "name": "哪吒之魔童降世", "star": "吕艳婷,囧森瑟夫,瀚墨", "releasetime": "2019-07-26", "grade": "9.6分"}
Beautiful Soup
css选择器(推荐)
select() 返回符合css选择器的节点组成的列表
,列表中的元素也是Tag类型
只要是Tag类型,就可以嵌套选择
获取属性 node[‘id’] 或 node.attrs['id] 有s
获取文本 node.string 或 node.get_text()
from bs4 import BeautifulSoup
# 3.提取数据
def get_content_list(self, resp_content):
soup=BeautifulSoup(resp_content,'lxml')
content_list = [] # 用列表存储信息
# css 选择器
for dd in soup.select('.board-wrapper dd'):
item = {}
# print(type(dd)) # <class 'bs4.element.Tag'> 可以嵌套选择
# print(dd.select('.board-index')) # [<i class="board-index board-index-1">1</i>] 是一个列表
item["id"] = dd.select('.board-index')[0].get_text() # string也可以
# print(dd.select('img')) # 列表有两个元素
# print(type(dd.select('img')[1])) # 列表中的元素依然是<class 'bs4.element.Tag'>
item["img_url"] = dd.select('img')[1].attrs['data-src'] # dd.select('img')[1]['data-src']也可以
item['name']=dd.select('.name')[0].get_text()
item["star"] = dd.select('.star')[0].get_text()
item["releasetime"] = dd.select('.releasetime')[0].get_text()[5:]
item["grade"] = dd.select('.integer')[0].string+dd.select('.fraction')[0].string + "分"
# print(id,name,star,releasetime,grade,img_url)
content_list.append(item)
return content_list
方法选择器
find_all(name,attrs,recursive,text,**kwargs) 返回一个符合条件的节点组成的列表
,列表中的元素也是Tag类型
find() 返回第一个匹配的节点
# 3.提取数据
def get_content_list(self, resp_content):
soup = BeautifulSoup(resp_content, 'lxml')
content_list = [] # 用列表存储信息
# 方法选择器
# print(soup.find_all(attrs={'class': 'board-wrapper'}))
dl=soup.find_all(attrs={'class': 'board-wrapper'}) # 返回一个列表
for dd in dl[0].find_all(name='dd'):
item = {}
# print(type(dd)) # <class 'bs4.element.Tag'> 可以嵌套选择
# print(dd.find(class_='board-index')) # <i class="board-index board-index-1">1</i> 是一个节点元素
item["id"] = dd.find(class_='board-index').get_text() # dd.find(attr={'class':'board-index'})也可以
# print(dd.find(name='img',class_='board-img'))
item["img_url"] = dd.find(name='img',class_='board-img').attrs['data-src']
item['name'] = dd.find(class_='name').get_text()
item["star"] = dd.find(class_='star').get_text()
item["releasetime"] = dd.find(class_='releasetime').get_text()[5:]
item["grade"] = dd.find(class_='integer').string + dd.find(class_='fraction').string + "分"
content_list.append(item)
return content_list
pyquery
和jQuery api一样的 常用
css选择器 伪类选择器 find() parent() remove() 通通都可以用
获取属性
.attr(‘id’) 没有s 返回第一个节点的属性
.attr.id
选择结果可能是多个节点,也可能是单个节点,类型都是PyQuery
多个节点遍历获取请用item()
from pyquery import PyQuery as pq
# 3.提取数据
def get_content_list(self, resp_content):
doc = pq(resp_content)
content_list = [] # 用列表存储信息
dd_list=doc('.board-wrapper dd').items() # 生成器 变成一个列表 里面的元素都是PyQuery类型
print(dd_list) # <generator object PyQuery.items at 0x000001BB98B3AE40>
print(type(dd_list)) # <class 'generator'>
for dd in dd_list:
item = {}
print(type(dd)) # <class 'pyquery.pyquery.PyQuery'>
print(dd('.board-index')) # <i class="board-index board-index-1">1</i> 是一个节点元素
item["id"] = dd('.board-index').text()
item["img_url"] = dd('img .board-img').attr['data-src']
item['name'] = dd('.name').text()
item["star"] = dd('.star').text()
item["releasetime"] = dd('.releasetime').text()[5:]
item["grade"] = dd('.integer').text() + dd('.fraction').text() + "分"
content_list.append(item)
return content_list