2-爬取 deviantart gallery 完整图片


2.爬取 deviantart gallery 完整图片

分为两部分:
1. 第一部分使用 python3 + selenium + chrome 来获取 gallery 页面包含的图片页面地址。
2. 第二部分使用 python3 + requests 爬取第一步获取的图片页面内的高清大图。

2.1 selenium 获取图片页面地址

因为 gallery 页面包含的图片页面地址初始只有24个,要获得所有就需要不断下拉触发ajax以加载更多图片页面地址,而这个网站的ajax参数无法解密(在上篇初步爬取中提到过1-初步爬取 deviantart gallery),所以需要使用 selenium 来控制浏览器完成下拉模拟并解析ajax加载后的页面。

程序最后的结果: imgPageUrl 属性,存储一个个包含高清大图页面地址的列表。

from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
import time
# import requests
import pymongo
import re

db_config = {
    'host': 'localhost',
    'port': 27017,
    'db': 'test_database',  # 保存的目的数据库名
    'col': 'wlopImgPageUrl',  # 保存的目的集合
    'auth': True,  # 我的mongodb打开了登陆认证, 若没有认证: 此参数修改为 False
    'user': 'testuser',  # 自己的认证用户名
    'passwd': 'testpass'  # 自己的认证用户密码
}


class simChrome():
    def __init__(self, artist='wlop'):
        '''
        :param artist: deviantart艺术家 ID。
        '''
        self.base_url = 'https://www.deviantart.com/{}/gallery/'.format(artist)

    def initChrome(self):
        Chrome_setting = {"profile.managed_default_content_settings.images": 2}
        chrome_option = webdriver.ChromeOptions()
        chrome_option.add_experimental_option("prefs", Chrome_setting)
        chrome_option.add_argument('--headless')
        self.browser = webdriver.Chrome(chrome_options=chrome_option)
        self.wait = WebDriverWait(self.browser, 10)

    def initMongo(self, **kw):
        '''
        :param kw: MongoDB的参数。
        '''
        client = pymongo.MongoClient(host=kw['host'], port=kw['port'])
        if kw['auth'] == True:
            db_auth = client.admin
            db_auth.authenticate(kw['user'], kw['passwd'])
        db = client[kw['db']]
        self.collection = db[kw['col']]

    def start(self, **kw):
        '''
        :param kw: MongoDB的参数。
        '''
        try:
            self.initChrome()
            self.initMongo(**kw)
        except Exception as err:
            print(err)

    def indexPage(self):
        try:
            self.browser.get(self.base_url)
            self.wait.until(EC.presence_of_element_located((By.ID, 'gmi-')))
            print('Dom successfully loaded!')
        except TimeoutException:
            print('Selenium timeout, try again.')
            self.indexPage()  # 如果超时那就重新再来一遍

    def simulateScroll(self, scroll_step=3000, max_height=None, fixed_times=3):
        '''
        本程序判断是否爬取所有的依据是比较动作前后 set 的长度,如果三次动作后长度依然没有
        变化则判断没有已经新的数据
        :param scroll_step: 每次下拉的长度,和浏览器窗口大小有关系,需要视实际而定。
            暂时没有想到科学确定的方法。
        :param max_height: 最大距离。如果设置为None,则爬取所有。
        :param fixed_times: fixed_times次动作后长度依然没有变化,判定为结束。有时获取
            不到完整的时可适当增加此次数。
        '''
        # 获取所有图片页面地址的网页元素
        element = set()
        gallery_torpedo = self.browser.find_element_by_id('gmi-')
        height = 0
        no_change = 0  # 对动作后长度没有变化的次数统计
        present_len = 0  # 当前集合的长度
        while True:
            previous_len = present_len  # 动作前集合的长度
            self.browser.execute_script(
                'window.scrollTo(0, {})'.format(height))
            time.sleep(0.5)  # 等待浏览器解析并加载到页面
            items = gallery_torpedo.find_elements_by_css_selector(
                'span[class~=thumb]>a')
            for item in items:
                # 从网页元素中提取url,保存到 set 里
                element.update((item.get_attribute('href'),))
            present_len = len(element)
            if previous_len == present_len:
                no_change += 1
            else:
                no_change = 0
            if no_change == fixed_times or height == max_height:
                # fixed_times次动作后长度依然没有变化或者到达设定的最大值,则停止
                break
            height += scroll_step
        self.browser.close()
        self.imgPageUrl = list(element)  # set 转 list

    def saveMongoDB(self):
        for item in self.imgPageUrl:
            result = {
                'page_name': re.findall('.*/(.*)-\d', item, re.S)[0],
                'url': item
            }
            try:
                if self.collection.insert_one(result):
                    print('{} save to MongoDB successful!'.format(result['page_name']))
            except Exception as err:
                print(err)


simulator = simChrome()
simulator.start(**db_config)
simulator.indexPage()
simulator.simulateScroll()
simulator.saveMongoDB()
url_list = simulator.imgPageUrl
# 列表,存储了有高清大图的页面的地址。
print('len(url_list): ', len(url_list), url_list[:3])

len(url_list):  154 
 ['https://www.deviantart.com/wlop/art/Sarlia-515699512', 'https://www.deviantart.com/wlop/art/The-Lonely-Parade-3d-716468598', 'https://www.deviantart.com/wlop/art/Lean-653039904']

2.2 requests 获取图片页面内高清图

多进程池的一个问题:
由于 Windows 系统不支持 forking,spyder IDE 重定向了 stdout。所以在这两情况下多进程的内部无法 print,所以下面都使用 logging 来保存日志。详细问题见 Simple Python Multiprocessing function in Spyder doesn't output results

访问太多,最好使用代理来爬。但是目前我还不会>_<。

提示:程序运行后由于网络原因,速度会非常慢。我使用校园网半小时才爬完 wlop大大,一共有154张图片。
如果下面程序报错: TypeError: object of type 'NoneType' has no len() ,原因是网络问题,使 get_page() 返回了 None 。在log 文件里查看详细原因。

BUG:
logging 无法获取所有日志。我爬完 154 张图片,应该至少有 154*2 个日志才对,但是我这次爬完只有 60 行日志。

import re
import requests
from requests.exceptions import RequestException
from bs4 import BeautifulSoup
import os
import pymongo
from multiprocessing import Pool
import logging


logging.basicConfig(level=logging.INFO,
                    filename='./log.txt',
                    filemode='w',
                    format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')


client = pymongo.MongoClient(host='localhost', port=27017)
db_auth = client.admin
db_auth.authenticate('testuser', 'testpass')
db = client['test_database']
collection = db['wlopImgUrl']


def get_page(url: str) -> str:
    '''requests获取网页源码'''
    # 设置浏览器标识user-agent
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
        '(KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36'
    }
    try:
        response = requests.get(url, headers=headers, timeout=10)
        if response.status_code == 200:
            return response.text
        return None
    except requests.exceptions.Timeout:
        logging.info('Requests timeout try again')
        get_page(url)
    except RequestException as e:
        logging.error(e)
        return None


def parse_page(text: str) -> dict:
    '''解析图片地址'''
    soup = BeautifulSoup(text, 'html5lib')
    item = soup.select('img.dev-content-full')[0]
    return item.attrs['src']


def download_img(result: dict):
    r = requests.get(result['img_url'], stream=True)  # stream loading
    file_name = './temp/wlop/{}.jpg'.format(result['img_name'])
    with open(file_name, 'wb') as f:
        for chunk in r.iter_content(chunk_size=32):
            f.write(chunk)
    logging.info('%s download successful!'.format(result['img_name']))


def save_to_db(result):
    try:
        if collection.insert_one(result):
            logging.info('{} save to MongoDB successful!'.format(result['img_name']))
    except Exception as err:
        logging.error(err)


def main(url):
    html = get_page(url)
    img_url = parse_page(html)
    result = {
        'img_name': re.findall('.*/(.*?)-', img_url, re.S)[0],
        'img_url': img_url
    }

    save_to_db(result)
    download_img(result)


if __name__ == '__main__':
    os.makedirs('./temp/wlop/', exist_ok=True)
    pool=Pool()
    pool.map(main, url_list)
    print('finished')

3. 高效爬取ajax的想法

理想环境:python3 + selenium + chrome + browsermob-proxy

deviantart gallery每次下拉到底部后ajax请求返回包含 24 张图片的 json 数据,浏览器需要把这些数据解析渲染到页面上后,才能获得对应新的元素,这样导致速度非常慢。
所以我想能不能把这些请求返回的json数据给保存下来,自己直接拿来解析图片真实url,这样省去了页面解析并渲染的过程,然后我就查到了这样一个工具 browsermob-proxy 以及Python的库文件 browsermobproxy,它可以获取浏览器的所有请求以及回应,近期事情太多没有时间和精力搞。

评论
发表评论 说点什么