目录
机缘:月初的时候接到一个任务:首先给了我一个包含了50714部电影信息的XLS,我们的脚本就根据这个表提供的电影名和日期来执行。 一句话概括:这个表的电影海报链接的字段的内容不合格,需要全部爬取后清洗替换一遍如图就是XLS的前几行:
于是需要写一个脚本,要求脚本的功能如此: 能爬取指定电影中指定上映日期、指定海报链接位置,并把爬取到的链接导出成文件。这段时间里我学习了HTML的基础知识、大量的python脚本工具:requests库、from lxml import etree使用xpath()、文件操作:xlrd、xlsxwriter等,从零到一的一步步实现了一个python脚本,接下里我就总结一下这个过程中的重要知识点以及其中踩到的各种坑。如有错误,请斧正。
一.手动操纵HTML的逻辑:
脚本既然是面向网站的,首先就要搞懂一个网站是怎样组成的:制作网站的三大语言:HTML实现网页的基础架构、CSS用来美化页面、JS用来实现网页的动态性与交互性,三者缺一不可。但我们写此次脚本的只用了解基础的HTML知识足矣。一段完整的HTML文件包含头部和主体两个部分的内容。如以下代码:
<html>
<head>
<title>一个标题</tiele>
</head>
<body>
<h1>holle world!</h1>
<hr size=5px align=center>
</body>
</html>
其中<>称为标记,分为双标记和单标记,如<head>.......</head>
就是一个双标记,包含了HTML的头部文件,<body>......</body>
包含了文本在浏览器中显示的页面内容,<hr size=5px align=center>
是单标记 ,表示在网页中插入一条水平线,其中size和align是此标记的属性,分别限制了这条线的粗细和对齐方式。在爬取网页时我们要的海报链接便放在某个标记的src属性中。为了更深入的了解HTML语言,应在交互性软件上动手试试,如DW下载链接
那么在脚本制作前,我们首先要根据需求手动找到要爬取的网站中一部电影的正确海报链接位置,手动逻辑如下:
1.找到网址:要爬取的网站首页
2.在官网链接后加上:search?query=电影英文名
此操作来代替官网中的搜索指定电影功能,跳转到电影的系列页面。如要搜索XLS中第一个电影《Toy story》
3.右键检查日期找到详情页链接
这个页面列举了此电影的所有系列,包括了续集和不同的上映版本,他们都有特定的上映日期,现在选择指定的电影上映日期条目,如选择上映日期为1995-10-30的条目,只需在日期上右键检查
就能得到此日期的HTML文件位置:
而我们需要点击<span>
的兄弟节点:<a>
的href属性即可跳转到此电影的详情页链接,接下来在详情页中取电影海报的链接。(有同学问此页面也有海报呀为什么不取,要取详情页的海报呢?因为海报的大小不一样,此页面只是小图)
4.跳转到详情页
在醒目的海报上右键检查得到海报的HTML位置取<img >
标签中的data-src属性的内容便是我们所要的电影海报链接了:
在脚本中,此时把内容保存在deque里,随即追加在一个txt文件中,保存在本地,便大功告成了。
脚本执行过程的本质就是重复以上步骤
二.从纷乱的XLS中提取有用信息
上文中我们得到了一个movieinfo的Excel文件,可是其格式极为混乱,原因是在从数据库(mySQL)导出时采用UTF-8编码,逗号分割,使得表格的字段非常混乱,我们借助:
import xlrd #XLS访问库
import xlsxwriter #XLS写入库
可以实现信息的提取,要用的也信息非常少,只有电影名和上映日期即可
经过观察他们分别位于每行的第二个和第三个位置,而且伴有双引号,我们只用按行读取,按双引号split,即可分割出要用的信息。在把电影名和日期提取出后分别放入两个deque中储存:moviename和moviedate_before
import collections #deque工具库
moviename=collections.deque() #电影名
moviedate_before=collections.deque() #日期格式化前
moviedate_after=collections.deque() #日期格式化后
PIC=collections.deque() #电影详情页网址
#操作xls提取电影名和日期函数
def get_exl():
filename= "D:\movieinfo.xls"
workbook = xlrd.open_workbook(filename) # 打开文件
index = workbook.sheet_names()[0] #获取所有sheet表取第一个的索引
sheet = workbook.sheet_by_name(index) #按索引得到sheet对象
#加入列表时两个try语句要分开,否则跳出到except时情况多,不可估计
for i in range(50714):
try:
row=''.join(sheet.row_values(i,0,3)).replace("'","").split('"')#按行读取
moviename.append(row[1]) #取电影名
except:
moviename.append('特殊字段电影名')
for i in range(50714):
try:
row=''.join(sheet.row_values(i,0,3)).replace("'","").split('"')#按行读取
moviedate_before.append(row[3]) #取上映日期,此时格式为:1995-10-30
except:
moviedate_before.append('特殊字段日期')
#print(len(moviename))
#print(len(moviedate_before))
'''---------------------------------------创建另一个XLS实现日期的格式化------------------------------
#创建电影名文件
datebook = xlsxwriter.Workbook('D:\moviedate.xls')
#创建sheet
datesheet = datebook.add_worksheet()
datesheet.write_column("A1",moviedate_before) #按列写入
datebook.close()
#手动在XLS里把日期格式化为:October 30, 1995并保存
-------------------------------------------------------------------------------------------------'''
filedate="D:\moviedate.xls"
datebook = xlrd.open_workbook(filedate)
index2=datebook.sheet_names()[0]
datesheet=datebook.sheet_by_name(index2)
col=datesheet.col_values(0)
for i in range(50714):
moviedate_after.append(col[i]) #在deque写入正确格式的日期
#print('moviedate_afterlen',moviedate_after[i])
'''
with open("D:\movietxt.txt","w")as f:
for i in moviedate_after:
f.write(str(i))'''
值得注意的是,在爬取时对网站里的日期进行保存后得到的是文本英文格式如:October 30, 1995,而文件里提供的格式是:1995-10-30。在之后进行爬取遍历时接口无法匹配,所以把日期在写入一个单独的Excel:moviedate.xls中利用Excel的工具手动更改日期
再读取此文档写入moviedate_after里储存,其元素与moviename一一对应,如此我们的准备工作就OK了。
三.使用xpath()操作网页
脚本其实不会操作网页,它的实现方式是:访问网址,把这个网址下网页中有限的HTML文本储存为etree对象,在内存中读取HTML文本,再用xpath()进行解析的操作。这是本篇的核心算法。
import requests #网站访问
from lxml import etree #xpath()
我们把爬取功能放在一个类picture中,实现封装。
1.对网址进行访问操作:仅需两步
首先是网址的获取:对于五万个不同电影的网页采集问题,我们使用了最淳朴的python字符匹配算法:
url=("https://www.themoviedb.org/search?query=%s"%moviename[index]) #在TMDB中搜索某个电影时的网页
此篇实战所有的从deque到文本的数据传递都用"%s"s
这个平凡而强大的字符格式化语法。
其次需要一个User-Agent(用户代理),在PC端中选择一个对应浏览器的User-Agent复制,粘贴为我们的headers即可
headers={"User-Agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; Tablet PC 2.0; .NET4.0E)"}
在之后的代码中requests()将同时调用这个headers与网址(URL),取得指定网页的HTML文件,提供给xpath()实现解析:
res=requests.get(self.url,headers=self.headers)
res.encoding='utf-8'
html=res.text
htmlform=etree.HTML(html) #格式化为HTML变量
2.核心算法:xpath():
xpath()是一门在HTML文档中查找信息的语言,满足了此次实战中需要的一切功能。它的用法不繁杂,点击这里学习。
我们用到了三次xpath()操作:
一次是在电影的系列网页中使用文本匹配定位到日期的兄弟节点的链接,即找到正确电影详情页的链接brother_node=htmlform.xpath("""//div[@class="title"]/span[text()="%s"]/../div/a/@href"""%moviedate_after[index])
接下来我分开讲讲这句代码的意思:
brother_node=htmlform.xpath()
:遍历htmlform中的HTML文档,将xpath()的查询结果储存在brother_node里
//div[@class="title"]
:在全文中寻找包含属性class且其值为title的节点div
span[text()="%s"]
:在div节点中寻找属性为text()且其值为字符串变量s的节点span。这个值便是电影的上映日期了。
/../div/a/@href
:文本匹配,查找日期节点的父节点(即div)的子节点a的href属性的值,即此电影的详情页链接。a也叫做span的兄弟节点。
%moviedate_after[index]
:对应span[text()="%s"]
中%s的位置的值,即某个电影的日期
第二次是一个特殊情况:网站上没有对应上映日期的电影时的处理,此时选择推荐的第一部电影
brother_node=htmlform.xpath('//div[@class="results flex"]/div[1]/div[@class="wrapper"]/div[@class="image"]/div[@class="poster"]/a/@href')
分段解析:
//div[@class="results flex"]
:在全文中寻找包含属性class且其值为results flex的节点div。
div[1]/div[@class="wrapper"]
:选择上个节点的第一个子字节(即第一部电影)的class属性且值为wrapper的div节点
/div[@class="poster"]/a/@href
:上一个div节点下class属性值为poster的div节点的子节点a的href属性,即此电影详情页链接
第三次是在找到详情页后保存海报链接的时候
pic=htmlform.xpath('//div[@class="image_content backdrop"]/img/@srcset')
有了上面的经验这个操作就极为简单了,找到值为image_content backdrop的class属性的节点div,找此div下的img节点中data-src属性的值,即海报链接。进行保存就ok了。
类的完整代码如下:
class picture:
def __init__(self):
self.headers={"User-Agent":"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; Tablet PC 2.0; .NET4.0E)"}
def get_detailurl(self,index=0):
self.url=("https://www.themoviedb.org/search?query=%s"%moviename[index]) #在TMDB中搜索某个电影时的网页
res=requests.get(self.url,headers=self.headers)
res.encoding='utf-8'
html=res.text
htmlform=etree.HTML(html) #格式化为HTML变量
#date_node=htmlform.xpath('//div[@class="title"]/span/text()')
#此处网站源码的日期导出格式为英式如:October 30, 1995
#直接使用xpath()文本匹配,遍历电影细分网址的日期
try:
brother_node=htmlform.xpath("""//div[@class="title"]/span[text()="%s"]/../div/a/@href"""%moviedate_after[index]) #特别注意:%s的传入会自动去掉双引号导致xpath()格式报错,要写成"%s"
#print('nomal',index+1,brother_node,moviename[index])
#print(moviedate_after[index],moviename[index])
#存在特殊情况:1.原表movieinfo的特殊字段;2.没有匹配日期的电影;3.TMDB库不存储此电影。此时取搜索到的首个电影链接
if brother_node==[]:
brother_node=htmlform.xpath('//div[@class="results flex"]/div[1]/div[@class="wrapper"]/div[@class="image"]/div[@class="poster"]/a/@href')
#print('NULL',index+1,brother_node,moviename[index])
self.detail_url=("https://www.themoviedb.org/%s"%brother_node[0])
#原表格中有极少数无日期信息的电影,此时也爬取搜索电影页推荐的首个电影
except:
#brother_node=htmlform.xpath('//div[@class="results flex"]/div[1]/div[@class="wrapper"]/div[@class="image"]/div[@class="poster"]/a/@href')
#print('error',index+1,brother_node,moviename[index])
self.detail_url='特殊格式'
def get_picture(self,index=0):
with open("D://pictruelink.txt","a")as f:
try:
res=requests.get(self.detail_url,headers=self.headers)
res.encoding='utf-8'
html=res.text
htmlform=etree.HTML(html) #格式化为HTML变量
pic=htmlform.xpath('//div[@class="image_content backdrop"]/img/@srcset') #可用海报的属性名称为data-src
#print(pic,'\n')
print(index+1)
PIC.append(str(index+1)+' '+moviename[index]+'\n')
PIC.append(pic[0]) #无海报从此跳出
PIC.append('\n')
for i in range(3):
print(PIC[i])
f.write(PIC[i])
except:
if self.detail_url=='特殊格式':
'''此时是特殊情况,产生的可能原因:
1.原表此列是特殊字符
2.原表电影名被分为三段以上
3.原表电影名是乱码
4.TMDB没有这部电影
5.爬取HTML时的错误
特殊情况数量极少,导出后根据需要手动校准'''
print(index+1,'特殊情况')
PIC.append(str(index+1)+'\n')
PIC.append('特殊情况')
PIC.append('\n')
for i in range(3):
print(PIC[i])
f.write(PIC[i])
else:
#TMDB网站上此电影并无海报
print(index+1,'无此电影海报')
PIC.append('无此电影海报'+'\n')
PIC.append('\n')
for i in range(2):
print(PIC[i])
f.write(PIC[i])
PIC.clear()
def workon(self):
for i in range(50714):
self.get_detailurl(index=i)
self.get_picture(index=i)
四.写入的技巧
类与函数的调用:
if __name__=='__main__':
get_exl()
a=picture()
a.workon()
'''
lines=collections.deque()
result=collections.deque()
with open("D:\picturelink.txt",'r') as f:
for i in range(101428):
line = f.readline()
lines.append(line)
for i in range(len(lines)):
if i%2 != 0:
result.append(lines[i])
print(len(result))
with open("D:\movietxt.txt","w")as f:
for i in result:
f.write(i)
'''
经过上百次调试、尝试,有了以上代码,我们就可以联网点击运行,等待计算机慢慢的爬取了,在控制台执行一定的输出方便调试。相信当正确的第一次成功运行脚本,看着控制台逐行输出的信息,心中的一定是满满的欣喜与成就感:
接下来,在程序中使用一个deque:PIC储存爬取到电影海报的链接,可是由于数量大,爬取中可能会遇到意外如断网、或是想不到的算法错误时程序结束运行,导致PIC的内容清空,这样等爬取完所有电影再写入txt的效率会很低。所以采用爬取一个写入一个的算法,在遍历中:
with open("D://pictruelink.txt","a")as f:
f.write(PIC)
PIC.clear()
“a”:数据追加入pictruelink.txt的末尾
此时的PIC一次只存储一部电影的海报链接,体现了deque的功能。
经过电脑数小时不竭余力的爬取我们得到了的结果:
最后,经过对movieinfo.xls格式上的调整,将海报链接放回原本的表格
import collections
import xlrd
import xlsxwriter #XLS写入库
movietxt_i=collections.deque()
S='https://www.themoviedb.org'
with open("D://movietxt.txt",'r') as f:
for line in f:
if line[:1]=='无' or line[:1]=='特':
movietxt_i.append(line)
else:movietxt_i.append('"'+S+line.replace('w300_and_h450_bestv2','w185')+'"')
'''
with open("D://test.txt",'w') as f:
for i in movietxt_i:
f.write(i)
'''
file = xlrd.open_workbook("D:\movieinfo.xls")
table = file.sheets()[0]
#nrows = table.nrows
book = xlsxwriter.Workbook('D:\movietest.xls')
#创建sheet
sheet = book.add_worksheet()
for i in range(50714):
#print(table.row_values(i,0))
if movietxt_i[i][:1]=='无' or movietxt_i[i][:1]=='特':
lin=''.join(map(str,table.row_values(i,0))).split(';')
#print(lin)
else:
#print(table.row_values(i,0))
lin=''.join(map(str,table.row_values(i,0))).split(';')
index=[index for index,value in enumerate(lin) if value[0:6]=='"http:']
print(index)
try:lin[index[0]]=movietxt_i[i]
except:print('此电影无海报链接')
#print(lin)
#print([';'.join(lin)])
for j in range(len(lin)):
sheet.write(i,j,lin[j]) #按列写入
print(i)
book.close()
得到了最终结果movietext.xls:
真是一次让人欣喜的尝试