一、准备工作
上周无意间(真的是无意间)发现了一个奇怪的网站,上面有一些想要的图片,谷歌浏览器上有批量下载图片的插件,但是要把所有页面都打开才能下载,比较麻烦。于是想着能不能写个爬虫程序,刚好自己也一直想学一下这个东西。
秋招面试小红书的时候,二面的面试官问我怎么实现一个分布式爬虫软件,我之前根本不知道爬虫是什么原理,只是听说过而已。所以后来也一直想学一下。
先上网搜索了一下,发现都是python的爬虫。于是周末用了一天时间学了python语法基础,其实只要你学过一门语言,再去学其他语言都会快很多,只要把数据类型,语法格式,函数定义等掌握一下,基本就能看懂代码。
接着找到网上一个下载百度图片的爬虫例子,代码很简单,原理也很好理解。因为我暂时只想下载一些图片,没有必要追求太多。所以就以这个教程作为模板,结合我的目标网站做了相应的修改。
import re
import requests
def dowmloadPic(html, keyword):
pic_url = re.findall('"objURL":"(.*?)",', html, re.S)
i = 1
print('找到关键词:' + keyword + '的图片,现在开始下载图片...')
for each in pic_url:
print('正在下载第' + str(i) + '张图片,图片地址:' + str(each))
try:
pic = requests.get(each, timeout=10)
except requests.exceptions.ConnectionError:
print('【错误】当前图片无法下载')
continue
dir = '../images/' + keyword + '_' + str(i) + '.jpg'
fp = open(dir, 'wb')
fp.write(pic.content)
fp.close()
i += 1
if __name__ == '__main__':
word = input("Input key word: ")
url = 'http://image.baidu.com/search/flip?tn=baiduimage&ie=utf-8&word=' + word + '&ct=201326592&v=flip'
result = requests.get(url)
dowmloadPic(result.text, word)
二、写代码
首先分析了一下我要下载的网站的前端代码,然后基于上面的代码开始写。
- 先利用这个网站的搜索功能,搜索的统一url格式为
公共前缀/关键词
,获得搜索结果页面。 - 搜索结果页面上就有很多相册,点击相册后会进入具体的相册地址,相册的url的统一格式为
公共前缀/相册编号/
- 进入每个相册后,是分页展示的很多图片。而且由于分页页数可能很多,中间一些分页标签用省略号代替了,所以这里需要找到分页的最大页面编号。分页的地址统一格式为
公共前缀/编号.html
。所以匹配出所有分页地址后,把编号的最大值找出来。 - 在每一个分页地址中,可以进行图片下载,图片的地址也是有规律的,
公共前缀/相册编号/a/图片编号
,所以只要找到这些链接然后进行下载就好了。
因为我忘了保存中间代码,所以这部分只说一下思路。下次记得用git保存一下。不过上面这些其实也只是一些简单过程。
这部分花的最多的时间是在写正则表达式,之前都没用过这个知识,这次才发现原来会写正则表达式很省事
三、改进代码
改进的部分主要是用多线程增加下载速度,修改搜索的不足。
- 首先是用多线程增加下载速度。因为我会以文件的格式输入多个关键词,
- 因此在处理关键词的时候,采用多线程形式处理。每个线程处理一个关键词。这里主要用queue.Queue()
的同步队列。
- 然后对每个关键词进行搜索之后会得到很多相册,再设置多线程每个线程负责处理一个相册。刚开始这里也是用了一个queue.Queue()
,后来发现这样的话,不同关键词的相册就混在一起了,于是又改成字典形式{关键词:queue.Queue()}
。 - 另外,我发现网站搜索得到的相册不全。需要进入相册后点击标签上的人名,进入对应的主页才能获得所有相册。相册展示也可能是分页的。这里又做了一步处理。
- 添加了日志输出,关键点的操作输出到控制台和文件中。
完整的代码
import logging
import queue
import threading
from spider_download import spider_download
global_data = threading.local()
threadLock = threading.Lock()
all_albums = set()
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" # 日志格式化输出
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p" # 日期格式
fp = logging.FileHandler('logging.txt', encoding='utf-8', mode='a') # 日志写入文件中
fs = logging.StreamHandler() # 日志写到控制台
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT, handlers=[fp, fs])
class myThread(threading.Thread):
def __init__(self, name, q):
threading.Thread.__init__(self)
self.name = name
self.q = q
def run(self):
logging.info("开启关键字线程:" + self.name)
global_data.num = 0
process_download(self.name, self.q)
logging.info("退出关键字线程:" + self.name)
def process_download(threadName, q):
while not workQueue.empty():
keyword = q.get(True, 3)
logging.info("%s processing %s" % (threadName, keyword))
spider_download(keyword, all_albums)
def read_files():
file_name = "46.txt"
try:
f = open(file_name, "r", encoding="utf-8")
lines = f.readlines()
print(lines)
for line in lines:
line = line.strip('\n')
workQueue.put(line)
logging.info("共添加了%d个关键词" % len(lines))
return len(lines)
finally:
f.close()
workQueue = queue.Queue()
threads = []
if __name__ == "__main__":
"""
读取文件,获取关键词,每个线程负责处理一个关键词
"""
read_files() # 读取文件,填充队列
thread_num = 5 # 也可以根据read_files()返回值进行调整
logging.info("将创建%d个线程" % thread_num)
for num in range(1, thread_num + 1):
thread = myThread("Thread-" + str(num), workQueue)
threads.append(thread) # 添加线程到线程列表
thread.start() # 开启线程
# 等待所有线程完成
for t in threads:
t.join()
logging.info("退出主线程")
import logging
import queue
import re
import threading
from os import mkdir, makedirs, path
import requests
global_data = threading.local()
mapLock = threading.Lock()
workQueueMap = dict() # 相册队列字典,以关键词作为同步队列名
threads = []
all_album = set()
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" # 日志格式化输出
DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p" # 日期格式
fp = logging.FileHandler('logging.txt', encoding='utf-8', mode='a') # 日志写入文件中
fp.setLevel(logging.WARNING)
fs = logging.StreamHandler() # 日志写到控制台
logging.basicConfig(level=logging.INFO, handlers=[fp, fs])
class myThread2(threading.Thread):
def __init__(self, name, q, keyword):
threading.Thread.__init__(self)
self.name = name
self.q = q
self.keyword = keyword
def run(self):
logging.info("开启下载相册线程:" + self.name)
global_data.num = 0
process_download(self.name, self.q, self.keyword)
logging.info("退出下载相册线程:" + self.name)
def process_download(threadName, qmap, keyword):
"""
从队列中取出一个相册地址,进行后续处理,每一个相册对应一个线程
"""
if keyword not in qmap:
# print(qmap)
logging.error("字典为空或关键词不在字典中,线程退出%s--%s" % (threadName, keyword))
return
while not qmap[keyword].empty():
album = qmap[keyword].get(True, 3) # 从队列中取出一个相册地址,完整地址
logging.info("%s processing album %s" % (threadName, album))
nums = re.findall('(\d+)/', album) # 直接从地址中匹配出相册编号
filepath = "images/" + keyword + "/"
if not path.exists(filepath): # 如果目录不存在时,创建目录
makedirs(filepath)
record = "images/" + keyword + "/record.txt" # 记录重复的文件夹,不再重复下载
if nums[0] in all_album:
fp = open(record, 'a+') # 以二进制形式写文件
fp.write(nums[0] + "\n")
fp.close()
continue
else:
all_album.add(nums[0])
filepath = "images/" + keyword + "/" + nums[0] + "/" # 拼接目录
if not path.exists(filepath): # 如果目录不存在时,创建目录
makedirs(filepath)
result = requests.get(album)
reg = album + '\S+"'
page_url = re.findall(reg, result.text) # 根据正则表达式找到分页链接
pages = set(page_url) # 去重
pages.add(album) # 把第一页加进去
# 找到页面数量最大值,进行循环,否则可能存在一些下载不到的图片
max_num = 0
for p in pages:
rst = re.findall('(\d+).html', p)
if rst:
page_num = int(rst[0])
max_num = max(max_num, page_num) # 找到分页编号最大的
global_data.i = 0 # 每个相册重新编号和计数
global_data.failed = 0
global_data.success = 0
logging.info("找到了关于%s的%s号相册的%d个链接" % (keyword, nums[0], max_num))
getimgs(album, nums[0], max_num, keyword, filepath) # 根据每个分页链接获取图片地址
logging.warning("下载完成: %s-%s 相册, 共下载%d张图片,其中%d张成功,%d张失败" % (keyword, str(nums[0]),
global_data.i, global_data.success,
global_data.failed))
def download_img(images, keyword, num, filepath):
"""
下载每个页面上的图片,并且更改名字
"""
logging.info('关键词:' + keyword + '的图片,现在开始下载图片...')
for each in images:
global_data.i += 1
logging.info('正在下载第%d张图片,图片地址:%s' % (global_data.i, each))
try:
pic = requests.get(each, timeout=10) # get请求,超时时间10s
global_data.success += 1
except requests.exceptions.ConnectionError:
logging.info('【错误】当前图片无法下载,地址:%s' % each)
global_data.failed += 1
continue
img_name = filepath + '/' + keyword + '_' + str(num) + '_' + str(global_data.i) + '.jpg' # 给图片重命名
fp = open(img_name, 'wb') # 以二进制形式写文件
fp.write(pic.content)
fp.close()
def getimgs(each, num, max_num, keyword, filepath):
"""
根据每个分页地址,最大分页数,对每个分页访问,获得所有图片地址
"""
for page_num in range(1, max_num + 1):
if page_num == 1:
url = each
else:
url = each + str(page_num) + ".html"
result = requests.get(url)
reg = '"(公共前缀/' + num + '\S+)"'
img_url = re.findall(reg, result.text) # 根据正则表达式找到图片地址链接
images = set(img_url)
logging.info("在第%d页上找到%d张图片" % (page_num, len(images)))
download_img(images, keyword, num, filepath)
def getpages(albums, nums, keyword):
"""
进入每个图册中后,获取分页链接,找到最多分页数目
"""
albums = set(albums)
nums = set(nums)
logging.warning("关键词:%s共找到%d个相册" % (keyword, len(albums)))
if keyword not in workQueueMap:
workQueueMap[keyword] = queue.Queue()
for each in albums:
workQueueMap[keyword].put(each) # 将相册地址加入队列中
# print(workQueueMap)
thread_num = min(5, len(albums)) # 线程太多会导致下载请求超时
# 创建新线程
for num in range(1, thread_num + 1):
thread = myThread2(threading.currentThread().getName() + "-2-" + str(num), workQueueMap, keyword)
threads.append(thread) # 添加线程到线程列表
thread.start() # 开启线程
# 等待所有线程完成
for t in threads:
t.join()
logging.info("退出关键字线程" + threading.currentThread().getName())
def search(keyword):
"""
输入关键词进行搜索得到一个页面并返回给下一步
"""
url = '公共前缀/search/' + keyword
result = requests.get(url)
if result:
logging.info("关于%s的搜索成功" % keyword)
else:
logging.info("关于%s的搜索失败", keyword)
raise Exception("搜索失败")
return result
def getalbum(html, keyword):
"""
需要根据关键词找到的第一个相册获得关键词主页
主页相册可能包括多页,需要进行判断处理
然后返回相册地址集合
"""
album_url = re.findall('(公共前缀/a\S+)"', html)
album_num = []
if album_url:
result = requests.get(album_url[0])
result.encoding = "utf-8"
reg = 'href="(\S+)"\s+target="_blank"\>' + keyword
home_url = re.findall(reg, result.text) # 主页链接
if home_url:
homepage = requests.get(home_url[0])
homepage.encoding = "utf-8"
reg = '已收录<span>(\d+)</span>套写真集'
all_num = re.findall(reg, homepage.text)
logging.warning("%s的主页收录了%s个相册" % (keyword, all_num[0]))
get_allAlbums(keyword, homepage.text, album_url, album_num)
nextpage = home_url[0] + "index_1.html"
hasnext = re.findall(nextpage, homepage.text)
if hasnext:
get_allAlbums(keyword, hasnext.text, album_url, album_num)
else:
logging.error("没有搜索到关键词为%s的主页" % keyword)
else:
logging.error("没有找到关键词%s相关的任何相册" % keyword)
return album_url, album_num
def get_allAlbums(keyword, html, album_url, album_num):
urls = re.findall('(公共前缀/a\S+)"', html)
nums = re.findall('公共前缀/a/(\d+)/', html)
logging.info("搜索页获得%s相册的链接:%s" % (keyword, album_url))
logging.info("搜索页获得%s相册的编号: %s" % (keyword, album_num))
album_url.extend(urls)
album_num.extend(nums)
def spider_download(word, all_albums):
global all_album
all_album = all_albums
result = search(word) # 搜索页面
albums, nums = getalbum(result.text, word) # 获取相册
getpages(albums, nums, word) # 下载相册
四、结束
其实这个程序还是很简单的,并没有用到一些高大上的框架。之后可能研究一些更深入的知识,用来获取一些更有趣的东西。
最后分享几张爬到的图片