实验 1 基于多线程的静态网页爬取项目
1. 实验目的
(1) 熟悉网页浏览器开发工具的使用;
(2) 掌握网页爬取 requests 库的使用;
(3) 掌握网页解析技术,例如 Xpath、BeautifulSoup、re 等;
(4) 掌握基本的多线程技术;
(5) 能够根据问题需求,指定网络爬虫方案,并编码实现。
(6) 具备撰写项目实验报告的能力。
2. 实验内容
豆瓣电影TOP250:https://movie.douban.com/top250?start=0&filter=
3. 实验设计及实现
(1) 项目描述
- 收集”豆瓣TOP250“中所有的电影链接;
- 根据链接进一步爬取电影详细内容,并将封面图片进行保存。
(2)项目分析
网页加载方式
右键单击,点击查看网络源代码,Ctrl+f 搜索 肖申克的救赎,如下图所示:
我们可以看到网页是静态加载的。
URL特征
网站每页有25部电影,共10页,需要对URL进行拼接实现翻页处理
第一页链接 https://movie.douban.com/top250?
第二页链接 https://movie.douban.com/top250?start=25&filter=
观察可知,需要对URL进行拼接处理
提取数据内容
进入一个电影,这里以《肖申克的救赎》为例。
提取内容:
- 排名
- 电影名称
- 上映时间
- 封面图片链接(后面根据链接获取图片)
- 导演
- 编剧
- 主演
- 类型
- 制片国家/地区
- 语言
- 片长
- 又名
- IMDb
- 电影评分
- 评分人数
- 电影简介
(3) 爬取方案制定
基于前期的分析,指定爬虫方案,。
通过画图或描述形式,论述爬虫方案,比如多页爬取,爬虫模块、解析模块、翻页的实现,多线程的引用,在项目中如何设置及应用。
具体描述可从以下方面来论述:
a) 网络爬取
URL构成
首先构建10个页面的URL,初始页的URL为:https://movie.douban.com/top250
,也就是https://movie.douban.com/top250?start=0&filter=
,之后的页面只需要修改start后面的数字就可以了,数字的规则为 (页数-1)\*25,利用循环和字符串的拼接就可以实现这一功能,得到一个包含10个URL的列表。
home_url = 'https://movie.douban.com/top250' # 初始网页,起始页
def get_page_url():
page_list = []
for i in range(10):
url = home_url + '?start=' + str(i * 25) + '&filter='
page_list.append(url)
return page_list
反反爬策略
豆瓣网还是比较友好的,但如果短时间内访问次数超过一定限制,那么网页就会对你的IP进行封锁,这时候需要进行登录或者使用代理的方法进行处理,虽然使用代理可以提高爬取效率,但构建代理IP池过程比较复杂,这里使用的方法是进行登录,将user-agent和cookie(登录时的)设为请求头,每爬取一部电影休眠一段时间,休眠时间需要使用random库里的uniform函数随机生成,模拟人访问网页时的情形,如设置为固定休眠时间有可能被网页识别为爬虫。
请求库及方法
这里使用requests库里面的get方法,get里面的参数为 url 和headers。
b) 数据解析
使用Xpath、re、BeautifulSoup 对网页内容解析。
Xpath
对于只有一个的信息,如排名、电影名称、上映时间等,我们可以从网页中复制相应的Xpath路径,减少工作量。
re
使用正则表达式对具有多个数据的属性进行爬取,如编剧、演员等,若使用Xpath会比较复杂。
BeautifulSoup
本案例中只有在获取简介时使用了Beautiful,因为有的电影简介只有一句,可以使用Xpath提取,而有的网页电影简介可能是多句话,每句话之间存在<br>
标签和二进制符号,使用re无法识别。
c) 数据存储
使用 CSV 文件对数据进行存储,对图片以电影名称.jpg
格式进行保存。
d) 多线程的应用
使用threading库,多线程的爬取每页的电影链接,将链接储存到一个列表中。该列表中包含250条url,本来准备对这250条url也使用多线程,但可能会导致IP被封锁,故没有采用。
电影信息采集完成之后,根据电影图片链接获取图片,这里使用多线程进行爬取,加快速度。
e) 其他技术
使用pandas库中的read_csv()和to_csv()函数,对数据进行读写,更加简便。
使用tqdm显示爬取进程,由于在爬取电影详细信息时没有采取多线程,并且每爬取一步电影又要休息一段时间防止被检测为爬虫,使用tqdm显示爬取进度,便于我们安排时间。
(4) 爬虫实现
info.py 获取电影详细信息,
#info.py
import math
import requests
import threading
import re
import time
from time import strftime, gmtime
from lxml import etree
import pandas as pd
import random
from bs4 import BeautifulSoup
from tqdm import tqdm
home_url = 'https://movie.douban.com/top250' # 初始网页,起始页
headers = { # 请求头
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36',
'cookie': 'bid=_exxKmyk-0w; __utmz=30149280.1665989171.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); ll="118088"; __gads=ID=1dac239b9be193ab-22cbfa6f12d70042:T=1665989543:RT=1665989543:S=ALNI_Ma4DYbw_UQn7HhERGSIiQsWOMkaXQ; __gpi=UID=00000b64444f0927:T=1665989543:RT=1666084912:S=ALNI_MY6yUQzRi0x649Vsbqf8s3fW62bSA; ct=y; ap_v=0,6.0; __utma=30149280.373905483.1665989171.1666091846.1666099800.10; __utmb=30149280.0.10.1666099800; dbcl2="263740972:Tq7UHR6Y1kQ"; push_noty_num=0; push_doumail_num=0; _pk_ref.100001.2fad=["","",1666103218,"https://sec.douban.com/"]; _pk_ses.100001.2fad=*; ck=2Hf8; apiKey=; _pk_id.100001.2fad=cc5de779384300aa.1666100208.2.1666103487.1666101026.; login_start_time=1666103716281'
}
def get_html(url):
resp = requests.get(url, headers=headers)
text = resp.text
return text
def get_page_url():
page_list = []
for i in range(10):
url = home_url + '?start=' + str(i * 25) + '&filter='
page_list.append(url)
return page_list
def get_all_url(page_list):
for page in page_list:
t = threading.Thread(target=get_url, args=(page,))
t.start()
t.join()
def get_url(page_url):
text = get_html(page_url)
url = re.findall('<a href="(.*?)" class="">', text) # 返回生成器
for u in url:
url_list.append(u)
def get_info(url):
text = get_html(url)
# 使用xpath提取单个信息
html = etree.HTML(text)
data = html.xpath('//*[@id="content"]')[0]
# 获取排名
rank = data.xpath('./div[1]/span[1]/text()')[0]
# 获取电影名称
name = data.xpath('./h1/span[1]/text()')[0]
# 获取电影上映时间
year = data.xpath('./h1/span[2]/text()')[0]
year = year[1:len(year) - 1] # 去除两端括号
# 获取封面图片
img = data.xpath('//*[@id="mainpic"]/a/img/@src')[0]
# 获取电影评分
score = data.xpath('//*[@id="interest_sectl"]/div[1]/div[2]/strong/text()')[0]
# 获取电影评分人数
num = data.xpath('//*[@id="interest_sectl"]/div[1]/div[2]/div/div[2]/a/span/text()')[0]
# 使用正则表达式提取多个信息
# 导演
dictor = re.search('导演.*?<span.*?>(.*?)</span>', text).group(1)
dictor = re.findall('<a.*?>(.*?)</a>', dictor)
# 编剧
editor = re.search('编剧.*?<span.*?>(.*?)</span>', text)
if editor != None:
editor = re.findall('<a.*?>(.*?)</a>', editor.group(1))
# 主演
actor = re.search('主演.*?<span.*?>(.*?)</span>', text)
if actor != None:
actor = re.findall('<a.*?>(.*?)</a>', actor.group(1))
# 类型
types = re.search('类型:</span>(.*?)<br/>', text).group(1)
types = re.findall('<span.*?>(.*?)</span>', types)
# 制片国家/地区
area = re.search('制片国家/地区:</span>(.*?)<br/>', text).group(1).strip()
# 语言
language = re.search('语言:</span>(.*?)<br/>', text).group(1).strip()
# 片长,只获取一个片长
minutes = re.search('片长:</span> <span.*?>(.*?)</span>', text).group(1)
# 又名
AKA = re.search('又名:</span>(.*?)<br/>', text)
if AKA != None:
AKA = AKA.group(1).strip()
# IMDb
IMDb = re.search('IMDb:</span>(.*?)<br>', text).group(1).strip()
# 提取电影简介
soup = BeautifulSoup(text, 'lxml')
summary = soup.find(id='link-report-intra').find('span').text
summary = summary.strip().replace("\n", "").replace("\u3000", "").replace(" ", "") # 去除换行符、空格和\u3000
info = {
"排名": rank,
"电影名称": name,
"上映时间": year,
"封面图片链接": img,
"导演": dictor,
"编剧": editor,
"主演": actor,
"类型": types,
"制片国家/地区": area,
"语言": language,
"片长": minutes,
"又名": AKA,
"IMDb": IMDb,
"电影评分": score,
"评分人数": num,
"电影简介": summary,
}
return info
def save_info(info_list):
info_list = pd.DataFrame(info_list)
info_list.to_csv('../info/data/movie_info.csv')
def run(url):
info = get_info(url)
info_list.append(info)
if __name__ == "__main__":
start_time = time.perf_counter() # 开始时间
page_list = get_page_url() # 获取每一页的url地址
url_list = []
get_all_url(page_list) # 将url保存到url_list,250个url
# print(len(url_list))
info_list = []
for url in tqdm(url_list):
run(url)
t = random.uniform(1, 3)
time.sleep(t)
save_info(info_list)
end_time = time.perf_counter()
spend_time = math.ceil(end_time - start_time) # 终止时间,向上取整
spend_time = strftime("%H:%M:%S", gmtime(spend_time))
print("运行时间为:{}".format(spend_time))
get_img.py 获取电影封面图片。
import pandas as pd
import requests
import threading
df = pd.read_csv('../info/data/movie_info.csv', index_col=0) # 以第一列为索引
name_list = df['电影名称']
img_url_list = df['封面图片链接']
def get_img(url, name):
r = requests.get(url)
file = '..\\info\\img\\' + name + '.jpg'
with open(file, 'wb') as f: # 打开写入到path路径里-二进制文件,返回的句柄名为f
f.write(r.content) # 往f里写入r对象的二进制文件
for i in range(len(name_list)):
url = img_url_list[i]
name = name_list[i].split(' ', 1)[0]
t = threading.Thread(target=get_img, args=(url, name,))
t.start()
(5) 结果说明
电影信息,250条数据(250*16)如下图所示:
电影封面图片,共250张。
4. 总结与反思
4.1知识总结
4.2总结
通过项目,巩固了 Xpath、re和 BeautifulSoup的使用,对多线程有了更多的认识,对整个项目框架有了更深的理解,提升了自己解决问题的能力。不足之处在于对于网站反爬方法欠缺经验和技术。
4.3出现问题及解决
4.3.1爬取简介问题
在爬取简介的时候,一开始使用Xpath方法,在测试时对《肖申克的救赎》进行爬取,成功获取到了数据。
后面在对所用网页进行爬取时运行,出现错误,查看错误信息,发现线程15在爬取简介时出现了错误,即对《玩具总动员》的简介爬取出现了错误
将测试中的URL换为《玩具总动员》的URL,运行后出现以下错误
查看《玩具总动员》的网页源代码,发现简介里还有<br>
标签,所以这个属性不适合用Xpath,这里我们改用正则表达式
爬取时长问题
在爬取时长的时候,一开始使用Xpath方法,查看出现错误的电影,有的电影只有一个片长,有的却有多个片长,如图所示:
这里只取第一个片长。
爬取又名问题
有的电影具有又名,有的没有,对代码进行修改如果有进行获取,没有就为None
网站反爬
爬取次数过多,网站提示登录
原理:
出现上述现象的原因是网站采取了一些反爬虫措施。例如服务器会检测某个IP在单位时间内的请求次数,如果这个次数超过了指定的阈值,就直接拒绝服务,并返回一些错误信息,这种情况可以称为封IP。这样,网站就成功把我们的爬虫封禁了。
解决方案:
- 先登录网站,找到cookie,将cookie添加到请求头里面;
- 使用random库里面的uniform函数,随机生成休眠时间
文件保存失败
在根据图片链接对图片进行保存时出现错误,有的文件是空白的,且文件名残缺:
发现是因为文件名太长了,超过最大255个字节的限定,这里对电影名称进行分割,使用中文作为电影名称,结果如下: