用Python爬取妹子图——基于BS4+多线程的处理

浏览: 1537

我有一个朋友,喜欢在一个图站看图(xie)片(zhen),光看就算了,他还有收集癖,想把网站的所有图片都下载下来,于是找我帮忙。
本业余玩家经过【好久的】研究,终于实现,写成本教程。本人经济学专业,编程纯属玩票,不足之处请指出,勿喷,谢谢。
本文分两部分:第一部分是基础方法,也就是单线程下爬图片的流程;第二部分是使用了多线程的功能,大大提高了爬取的效率。

前言

本次爬取基于的是BeautifulSoup+urllib/urllib2模块,Python另一个高效的爬虫模块叫Scrapy,但是我至今没研究懂,因此暂时不用。

基础流程

说明

此次爬取,在输入端仅需要一个初始网址(为避免彼网站找我麻烦,就以URL代替),以及文件保存路径(为保护我隐私,以PATH代替),大家在阅读代码时敬请注意。
从该网站下载图片以及文件处理有如下几步:【我要是会画流程图就好了】
1.打开网站首页,获得总页数,获得每个专辑的链接;
2.点进某专辑,获得专辑的标题作为保存的文件夹名,并获得该专辑的页数;
3.获取每个图片的链接
4.下载图片,以网站上图片的文件名保存至本地,同时对应第2步的文件夹。

代码和解释

# -*- coding: utf-8 -*-
"""
@author: Adam
"""


import urllib2, urllib, os
from bs4 import BeautifulSoup

root = PATH
url = URL
req = urllib2.Request(url)
content = urllib2.urlopen(req).read()
soup = BeautifulSoup(content, "lxml")
page = soup.find_all('a')
pagenum1 = page[-3].get_text() #注1

for i in range(0, int(pagenum1) + 1):
if i == 0:
url1 = URL
else:
url1 = URL + str(i+1) + ".html" #注2
req1 = urllib2.Request(url1)
#
#print url
content1 = urllib2.urlopen(req1).read()
soup1 = BeautifulSoup(content1, "lxml")
table = soup1.find_all('td')
title = soup1.find_all('div', class_ = 'title') #注3

#print title
for j in range(1, 19):
folder = title[j-1].get_text()
folder = folder.replace('\n', '') #注4
curl=table[j].a['href'] #注5
purl = URL+curl
#Second Page
preq = urllib2.Request(purl)
pcontent = urllib2.urlopen(preq).read()
psoup = BeautifulSoup(pcontent, "lxml")
page2 = psoup.find_all('a')
pagenum2 = page2[-4].get_text()
if not os.path.exists(root + folder):
os.mkdir(root + folder)
else:
os.chdir(root + folder)
#print folder
for t in range(1, int(pagenum2) + 1):
if t == 1:
purl1 = purl
else:
purl1 = purl[:-5] + '-' + str(t) + '.html'
preq2 = urllib2.Request(purl1)
pcontent2 = urllib2.urlopen(preq2).read()
psoup2 = BeautifulSoup(pcontent2, "lxml")
picbox = psoup2.find_all('div', class_ = 'pic_box') #注6
for k in range(1,7):
filename = root + folder + "/" + str(k+6*(t-1)) + ".jpg"
if not os.path.exists(filename):
try:
pic = picbox[k].find('img')
piclink = pic.get('src') #注7
urllib.urlretrieve(piclink, filename)
except:
continue

注1:获取页码的方法,因为页码的HTML源码为
<a href="/albums/XiuRen-27.html">27</a>
注2:因为我发现该网站翻页后的网址即为首页网址后加页码数字
注3:专辑标题的HTML源码为
<div class="title"><span class="name">专辑标题</span></div>
注4:将专辑标题命名为文件夹名,这里要给title字符串做些处理,下问讲
注5:放每个专辑自己链接的HTML源码为
<td><a href="/photos/XiuRen-5541.html" target="_blank"></a></td>
注6、7:放图片的HTML源码为
<div class="pic_box"><i.m.g(为防系统认为有图片) src=" " alt=" "></div>
通过find_all('div', class_ = 'pic_box')找到放图的区块,然后用find('img')找到图片的标签,再用get('src')的方法获取图片链接

通过以上的代码,下载所有图片并保存到对应的文件夹的流程就笨拙地完成了。该方法效率极低,首先是单线程操作,其次用了N次嵌套循环,因此我想到了借助多线程提高效率的方式。

多线程方法

介绍

Python多线程的方法在网上有很多文章介绍,但是都好(是)像(我)很(水)复(平)杂(低),后来我发现了一个模块,寥寥几行就实现了功能。

from multiprocessing.dummy import Pool as ThreadPool
import urllib2

url = "http://www.cnblogs.com"
urls = [url] * 50
pool = ThreadPool(4)
results = pool.map(urllib2.urlopen, urls)
pool.close()
pool.join()

其中,urls是一个列表,该模块正是用map(func, list)的方法将list的元素从前到后送入至func运算。results传出的是一个列表类型

在本案例中应用

# -*- coding: utf-8 -*-
"""
@author: Adam
"""


import os
import urllib2
import urllib
from bs4 import BeautifulSoup
import re
import inspect

from multiprocessing.dummy import Pool as ThreadPool
from multiprocessing import cpu_count as cpu #进程池个数等于CPU个数

def read_url(url):
req = urllib2.Request(url)
fails = 0
while fails < 5:
try:
content = urllib2.urlopen(req, timeout=20).read()
break
except:
fails += 1
print inspect.stack()[1][3] + ' occused error' #注1
raise #注2
soup = BeautifulSoup(content, "lxml")
return soup


def get_links_first(url):
soup = read_url(url)
page = soup.find_all('a')
pagenum = page[-3].get_text()
link = [url[:-5] + '-' + str(i) + '.html' for i in range(2, int(pagenum)+1)]
link.insert(0, url) #注3
return link


def get_links_second(url):
purlist = []
soup = read_url(url)
table = soup.find_all('td')
for j in range(1,19):
try:
curl = table[j].a['href']
purl = URL + curl
if purl not in purlist:
# print purl
purlist.append(purl)
except:
continue
return purlist


def get_links_third(url):
# print url
download = {}
soup = read_url(url)
page = soup.find_all('a')
pagenum = page[-4].get_text()
link = [url[:-5] + '-' + str(i) + '.html' for i in range(2, int(pagenum)+1)]
link.insert(0, url)
title = soup.find_all('div', class_ = 'inline')[0].get_text() #注4
title = re.findall('\S', title)
folder = ''.join(title) #注5
# print folder
download[folder] = link #注6
return download


def get_picture((url, folder)): #注7
soup = read_url(url)
picbox = soup.find_all('div', class_ = 'pic_box')
for k in range(1, 7):
try:
pic = picbox[k].find('img')
piclink = pic.get('src')
filename = folder + '/' + os.path.basename(piclink)
print folder
if not os.path.exists(filename):
urllib.urlretrieve(piclink, filename)
# print filename
except:
# raise
continue



def multi_get_sec_link(url):
pool = ThreadPool(cpu())
linkset = pool.map(get_links_second, url)
pool.close()
pool.join()
return linkset


def multi_get_third_link(url):
pool = ThreadPool(4)
linkset = pool.map(get_links_third, url)
pool.close()
pool.join()
return linkset


def multi_get_picture(download, root=PATH):
pool = ThreadPool(4)
for i in range(len(download)):
picurlset = download[download.keys()[i]]
folder = root + '/' + download.keys()[i] #注8
# print folder
if not os.path.exists(folder):
os.mkdir(folder)
# else:
# os.chdir(folder)
pool.map(get_picture, zip(picurlset, folder)) #注9
pool.close()
pool.join()

if __name__ == '__main__':
url = URL
linkset = multi_get_sec_link(get_links_first(url))
linkset = [j for i in linkset for j in i] #注10
linkset2 = multi_get_third_link(linkset)
finalset = {}
for dic in linkset2:
finalset.update(dic) #注11
multi_get_picture(finalset)

总注:
get_links_firstget_links_secondget_links_thirdget_picture分别为上文中的1、2、3步和下载图片的最后一步。
multi_get_sec_linkmulti_get_third_linkmulti_get_picture即为多线程处理这些步骤的过程。
注1:inspect.stack()[1][3]是返回当前运行着的函数名称
注3:列表解析的方式生成列表是效率最高的方式,方法是 list = [i for in in xxx]link.insert(index, par)是将元素par插入列表的方法,index代表插入的位置
注2:设置timeout=20来避免由于网络不通导致网页请求停滞,同时加入重试5次,一直都不通则通过raise报异常
注4:在第一种方法中,获得专辑标题是在网站首页完成的,但由于此方法中将每一步切分成了单独的函数来做,所以在每个专辑各自的页面中获得标题,其HTML源码为<div class="inline">标题</div>
注5:获取的title中有大量空格以及\n字符,因此通过正则表达式匹配\S,即匹配任何非空白字符。通过re.findall处理过的字符会被切分成一个个字符串的列表,于是要用.join()函数重新组合成字符串
注6:生成字典变量downloadfolder为键,link是值
注8:前文将专辑名与相对应的图片链接列表存了字典变量,这里要重新取出来。download[download.keys()[i]]取的是值,download.keys()[i]取的是键
注7、注9:由于map(func, list)函数中的list,所以fund只能接一个参数,而用了zip(par1, par2, ...)后,配合函数设定为def func((par1, par2, ...)),即可实现多参传递,且若函数中有已设默认值参数,则为def func((par1, par2, ...), par3='')
注10:由于由于上文所讲的多线程的特性,输出的是多个列表组成的列表,使用语句[j for i in linkset for j in i]将此列表展开
注11:linkset2所使用的函数生成的是专辑+链接的字典,输出的是一个个字典对组成的列表,如[{'a': [1,2,3]}, {'b': [2,3,4]}]因此为了把此列表展开,重新变成{'a': [1,2,3], 'b': [4,5,6]}, 使用一个循环。字典类型的添加元素用dict.update(dic)的方法

附加

由于我处的网络环境实在是太奇葩了,即使网页打得开、视频流畅,在用urlopen时却各种timeout,因此我添加了以下代码,将最后的链接字典保存成一个文件。

f = open(FILENAME,"r")
f.write(str(finalset))
f.close()

之后直接读取该文件。同时为了让程序在无数次蛋疼的timeout报错中自己不断重试,又进一步增加了循环语句。

f = open("/Users/Adam/Documents/Python_Scripts/Photos/links.txt","r")
finalset = eval(f.read())
while True:
try:
multi_get_picture(finalist) #转成字符串的字典再转回来
break
except:
continue

写到这里,本程序就写完了,接下来就是跑起来然后看着一个个文件夹在电脑里冒出来,然后一个个图片如雨后春笋般出现吧。

推荐 1
本文由 八公水鸭几 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册