Python+Scrapy爬虫实战

1. Scrapy简介与实例解析

对于一个小型的爬虫项目,一般使用python的requests库+一个解析html文件库(例如lxml,BeautifulSoup等)即可,对于大型的项目我们可以使用专业的爬虫框架,例如Scrapy。
什么是爬虫框架:

  • 爬虫框架是实现爬虫功能的一个软件结构和功能组件集合。
  • 爬虫框架是一个半成品,能够帮助用户实现专业网络爬虫。

Scrapy是一个功能强大的爬虫框架,下图展示了Scrapy爬虫框架的结构(该图来源与中国慕课大学
scrapy框架在这个框架中,大部分功能都已经为我们实现好了,我们只需自定义SPIDERS以及ITEM PIPELINES两个模块即可。
如果需要下载Scrapy直接 pip install scrapy即可

接下来说下本实例的一个大致需求与解决思路:

  • 需求:需要从一个网址上爬取所有的下载文件,该网址还有很多的子目录,子目录下的文件也要下载,下载后要按照原网址的目录进行摆放。下载完后需要定时去检查更新文件。
    爬取网站信息* 思路:首先爬取网址的html文件,解析a标签(实例中的爬取网址信息都存在a标签中)获取所有需要下载的文件链接,包括子文件夹下的文件链接。通过scrapy提供的FilesPipeline下载文件。爬取后使用apscheduler包定时爬取更新(后面定时爬取的部分是使用python调用本地wget工具下载的,因为当时的下载源有几个文件始终下载不下来,但使用wget可以下载)。

存在a标签中的数据

2. 解析html文件中的下载地址

下面我写了一个递归函数来遍历提供url网站中的所有可下载的文件地址(包括子文件夹中的文件)。通过python的request库发起请求获取html文件,然后通过lxml的etree包解析html文件,获取其中所有的a标签的href属性信息(因为下载链接都放在href属性中)。
1)如果a标签href属性为 ../ 说明是返回上一层的标签,无用,舍去
2)排除 ../后, 如果a标签href属性的最后一个元素为 / 说明是下一层目录的标签,此时调用自身开始递归。
3)如果a标签href属性都不在以上情况中,那么说明就是当前文件目录中需要下载的文件,我们存入到file_links列表中。

def get_all_files_link(url):
    html_file = requests.get(url)
    if html_file.status_code != 200:
        print("请求{}失败".format(url))
    html_parse = etree.HTML(html_file.content.decode('utf-8'))
    items = html_parse.xpath("//a/@href")  # 寻找所有的a标签的文本内容
    file_links = []
    for item in items:
        if item == "../":
            continue

        if item[-1] == "/":
            links = get_all_files_link(url+item)
            if len(links) > 0:
                file_links.extend(links)
            continue

        url_path = url + item
        file_links.append(url_path)
    return file_links

3. 对比文件是否需要更新

在上一步获取了所需需要下载的文件链接,接下来对比下远程端与本地端的文件大小是否相同(也可以使用文件的更新时间来判断)。对于远程端,我们直接获取header中的length参数即可,对于本地的文件,先去检查是否存在,如果存在,获取文件的资源大小信息与远程端的进行对比,如果不同说明需要更新。这里建议使用多线程的方式去做,相比单线程能够提速10倍左右。因为刚接触scrapy,对scarpy的中间件不是很了解,所以没用scrapy而是自己写的脚本,如果有知道使用scrapy更新文件的大神请赐教(我之前发现scrapy有一个更新策略是“过期更新”,即下载的文件保存超过一定时间后,才允许去更新,这明显与我的需求不符,而且scrapy在底层判断文件是否过期很慢,没看底层暂不知道为什么)。

def check_whether_update(self, url_path):
      """
      检查网络文件与本地文件是否一致(通过文件大小判断),
          若与本地文件不同或者本地没有该文件,返回True(需要重新下载)
          若相同返回False(不需要重新下载)
      """
      request_url = request.urlopen(url_path)
      url_file_size = request_url.length   # 获取网络上的资源文件大小

      local_file_path = path.join(FILES_STORE,
                                  url_path.split(self.url_head)[-1])
      if path.exists(local_file_path) is False:
          return True

      local_file_size = stat(local_file_path).st_size  # 获取本地文件资源大小

      if url_file_size == local_file_size:
          print("{}该文件已是最新状态,无需下载".format(url_path))
          return False
      else:
          return True

4. 使用Scarpy爬取文件

经过以上两步,我们知道了哪些文件需要去下载(或更新),那么接下来使用scrapy创建一个爬虫来下载。

创建爬虫

安装好scrapy后进入到你的项目文件夹,打开命令行,输入指令创建一个爬虫项目

scrapy startproject scrapytest

创建项目后,cd进入第一层文件夹,然后输入以下指令进行创建一个爬虫

scrapy genspider testone web_url

其中,testone是创建的爬虫名称,web_url是要爬的网址

创建的整个项目结构如下所示:

└─scrapytest
	├──scrapy.cfg                    部署scrapy爬虫的配置文件
	└──scrapytest                    scrapy框架的用户自定义python代码
			├──_init_.py                初始化脚本
			├──pipelines.py          pipelines代码模板(继承类)
			├──middleware.py     middlewares代码模板(继承类)
			├──items.py               items代码模板(继承类)
			├──setting.py             scrapy爬虫的配置文件
			└──spiders                 spiders代码模板目录(继承类)
					├──_init_.py        初始化文件
					└──testone.py     用户自定义爬虫文件(继承类)
	

settings.py

创建爬虫后,我们首先来配置下settings文件,这里主要是配置了下存储的目录位置,以及下载文件大小的限制,其他的参数均取默认值。

# -*- coding: utf-8 -*-

# Scrapy settings for scrapytest project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     https://docs.scrapy.org/en/latest/topics/settings.html
#     https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#     https://docs.scrapy.org/en/latest/topics/spider-middleware.html
import random

BOT_NAME = 'scrapytest'

SPIDER_MODULES = ['scrapytest.spiders']
NEWSPIDER_MODULE = 'scrapytest.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'scrapytest (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = True

# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 5

# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
#DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16

# Disable cookies (enabled by default)
#COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False

# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}

# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
# SPIDER_MIDDLEWARES = {
#    'scrapytest.middlewares.ScrapytestSpiderMiddleware': 543,
# }

# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
#    'scrapytest.middlewares.ScrapytestDownloaderMiddleware': 543,
#}

# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'scrapytest.pipelines.ScrapytestPipeline': 1,
}

FILES_STORE = 'E:/scrapy_download/'
FILES_EXPIRES = 0               # 设置文件过期时间
DOWNLOAD_WARNSIZE = 1572864000  # 文件过大警告,1.5GB
DOWNLOAD_MAXSIZE = 2097152000  # 下载最大文件不能超过2G

# RETRY_ENABLED = False
DOWNLOAD_TIMEOUT = 36000  # 设置下载超时配置,30分钟

USER_AGENT_LIST = [
    'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
    "Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 (KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
    "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 (KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]
USER_AGENT = random.choice(USER_AGENT_LIST)   # 随机选取一个代理

# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

items.py

接着,我们来创建一个items用来存储在爬虫文件中获取的所有文件下载地址信息。其中files用来保存爬取的文件信息,urls保存文件的下载地址,paths保存文件保存的路径。

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class ScrapytestItem(scrapy.Item):
    # define the fields for your item here like:
    files = scrapy.Field()
    file_urls = scrapy.Field()
    file_paths = scrapy.Field()

testone.py

接着编写我们创建的爬虫文件,这里就是按照之前的思路:
1)遍历所有需要下载的文件url
2)比对每一个远程端与本地文件,看是否需要更新
3)将需要下载的文件url存入自定义item类
4)将自定义item类信息传给pipelines处理

# -*- coding: utf-8 -*-
import scrapy
from ..items import ScrapytestItem
import requests
from ..settings import FILES_STORE
from os import stat, path
import threading
from lxml import etree


class TestoneSpider(scrapy.Spider):
    name = 'testone'
    # allowed_domains = ['']
    start_urls = ['https://you_want_to_download']

    def parse(self, response):
    	# 遍历获取所有需要下载的文件
        files_link = get_all_files_link(response.url)

        # 使用多线程去检查哪些文件是更新了的,如果没有更新就无需再次下载
        download_files = multi_check_file(files_link)
        print("需要下载/更新的文件包括:")
        for v in download_files:
            print(v)

        items = ScrapytestItem()
        items["file_urls"] = download_files
        yield items

def get_all_files_link(url):
    html_file = requests.get(url)
    if html_file.status_code != 200:
        print("请求{}失败".format(url))
    html_parse = etree.HTML(html_file.content.decode('utf-8'))
    items = html_parse.xpath("//a/@href")  # 寻找所有的a标签的文本内容
    file_links = []
    for item in items:
        if item == "../":
            continue

        if item[-1] == "/":
            links = get_all_files_link(url+item)
            if len(links) > 0:
                file_links.extend(links)
            continue

        url_path = url + item
        file_links.append(url_path)
    return file_links


class MyThread(threading.Thread):
    def __init__(self, func, args=()):
        super(MyThread, self).__init__()
        self.func = func
        self.args = args
        self.result = None

    def run(self):
        self.result = self.func(*self.args)

    def get_result(self):
        try:
            return self.result
        except Exception:
            return None


def multi_check_file(files_link):
    download_list = []
    max_task = 50
    tasks = []
    flag_list = []
    for index, file_link in enumerate(files_link):
        tasks.append(MyThread(check_whether_update, args=(file_link,)))
        # 每创建50个多线程或者已经遍历到最后一个元素,启动多线程
        if (index+1) % max_task == 0 or index+1 == len(files_link):
            [task.start() for task in tasks]  # 启动多线程
            [task.join() for task in tasks]   # 等待多线程结束
            flag_list.extend([task.get_result() for task in tasks])  # 获取多线程结果
            tasks = []  # 清空任务,准备下一批多线程任务

    if len(flag_list) != len(files_link):
        print("multi_check_file failed!")
        exit(2)

    for index in range(len(files_link)):
        if flag_list[index] is True:
            download_list.append(files_link[index])

    return download_list


def check_whether_update(url_path):
    """
    检查网络文件与本地文件是否一致(通过文件大小判断),
        若与本地文件不同或者本地没有该文件,返回True(需要重新下载)
        若相同返回False(不需要重新下载)
    """
    url_head = "https://you_want_to_download"

    response = requests.head(url_path)
    url_file_size = int(response.headers["content-length"])  # 获取网络上的资源文件大小

    local_file_path = path.join(FILES_STORE,
                                url_path.split(url_head)[-1])
    if path.exists(local_file_path) is False:
        return True

    local_file_size = stat(local_file_path).st_size  # 获取本地文件资源大小

    if url_file_size == local_file_size:
        # print("{}该文件已是最新状态,无需下载".format(url_path))
        return False
    else:
        return True

pipelines.py

在上一步中通过yield生成器将自定义item信息传入pipelines,接下来我们来通过scrapy提供的FilesPipeline下载文件,这里我们定义的类需要继承来自于FilesPipeline父类。然后需要实现三个方法file_path, get_media_request, item_completed. 其中file_path使用来指定保存文件的路径(这个路径是相对路径,是在settings中FILES_STORE字段下的目录),get_media_request方法是通过传入的自定义item的urls字段去下载文件,item_completed方法是将file_path解析的相应文件存储路径保存在自定义item中。

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
import os
import scrapy
from scrapy.pipelines.files import FilesPipeline
from scrapy.exceptions import DropItem


class ScrapytestPipeline(FilesPipeline):
    url_head = 'https://you_want_to_download'

    def file_path(self, request, response=None, info=None):
        return request.url.split(self.url_head)[-1]

    def get_media_requests(self, item, info):
        for file_url in item['file_urls']:
            yield scrapy.Request(file_url)

    def item_completed(self, results, item, info):
        file_paths = [x['path'] for ok, x in results if ok]
        if not file_paths:
            raise DropItem("Item contains no files")
        item['file_paths'] = file_paths
        return item

启动爬虫

通过以上步骤,我们的爬虫项目就简单的完成了,接下来,启动爬虫爬取文件。这里提供两种方法:
1)通过命令行窗口输入scrapy指令启动(在scrapytest的第一级目录下)

scrapy crawl testone

2)通过python调用本地指令启动(这个好处在于可以使用ide调试)创建一个run.py脚本(在scrapytest的第一级目录下),内容如下:

from scrapy import cmdline

cmdline.execute("scrapy crawl testone".split())

5. 使用Pyhton+wget下载文件

对于一般爬虫项目,使用以上scrapy脚本基本就能满足需求了,但是之前爬取的资源中,有些文件始终下载不下来(下载下来的文件与远程端的文件大小不一致,无法打开),我有尝试过使用python的urllib、requests包、本地的curl工具等都无法下载。但是使用chrome浏览器以及wget工具是可以下载的。所以这里我就针对某些下载不了的资源使用pyhton+wget的方式下载。

使用wget下载新文件

首先确保本机下载了wget工具,下面是使用python3.6的subprocess库调用本地wget工具进行下载的,注意使用subprocess时command的编码方式,Linux和Windows是不同的,Linux直接使用utf-8编码即可,但windows必须使用gbk编码(坑)。

file_dir = path.join(FILES_STORE, "/".join(url.split(start_url)[-1].split("/")[:-1]))
command = "wget -P {} {} --no-check-certificate".format(file_dir, url).encode('gbk')

try:
    # 注意sub.run方法在linux中和windows中的编码方式不同,linux是utf-8,windows是gbk
    res = sub.run(command.decode('gbk'), shell=True, stdout=sub.PIPE,
                  stderr=sub.PIPE, timeout=3600, encoding='gbk')

    # 检查文件是否与下载源提供的大小相同
    response = requests.head(url)
    url_file_size = int(response.headers["content-length"])  # 获取网络上的资源文件大小
    local_file_size = stat(file_dir + url.split("/")[-1]).st_size  # 获取本地文件资源大小
    if url_file_size == local_file_size:
        logging.debug("successful download: ", url.split(start_url)[-1])
    else:
        logging.error("error: 下载的文件大小与下载源提供的content-length不等")
        os.remove(file_dir + url.split("/")[-1])  # 删除该文件
        continue
except Exception as e:
    logging.error(e)

使用wget更新文件

其实这里与上面的下载方式几乎相同,不同的点在于:
1)使用的wget指令有些不同,这里是 -O
2)由于是更新文件,为了不影响原先下载文件,这里是先下载在后缀为.cache的文件中,等下载完成后验证没问题再将原来的文件替换掉。

file_path = path.join(FILES_STORE, url.split(start_url)[-1])
# 下载到.cache文件,等下载完后在替换
command = "wget -O {} {} --no-check-certificate".format(file_path + ".cache", url).encode('gbk')

try:
    # 注意sub.run方法在linux中和windows中的编码方式不同,linux是utf-8,windows是gbk
    res = sub.run(command.decode('gbk'), shell=True, stdout=sub.PIPE,
                  stderr=sub.PIPE, timeout=3600, encoding='gbk')

    # 检查文件是否与下载源提供的大小相同
    response = requests.head(url)
    url_file_size = int(response.headers["content-length"])  # 获取网络上的资源文件大小
    local_file_size = stat(file_path + ".cache").st_size  # 获取本地文件资源大小
    if url_file_size == local_file_size:
        logging.info("successful update: ", url.split(start_url)[-1])
    else:
        logging.error("error: 下载的文件大小与下载源提供的content-length不等")
        os.remove(file_path + ".cache")
        continue

    # 下载完成,先删除旧文件,在重命名
    os.remove(file_path)  
    os.rename(file_path + ".cache", file_path)
except Exception as e:
    logging.error(e)

6. 使用apscheduler定时更新文件

关于定时更新文件,直接使用apscheduler库即可,可以定时去调用之前讲的scrapy爬虫服务或者使用python+wget的方式,都可以。下面就是已python+wget的方式为例,代码和之前的一样,只不过添加了apscheduler服务而以。

import requests
from os import stat, path
import os
from scrapytest.settings import FILES_STORE
import threading
from lxml import etree
import subprocess as sub
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
import logging


# 配置log信息
logging.basicConfig(level=logging.INFO,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                    datefmt='%Y-%m-%d %H:%M:%S',
                    filename='update_androidSDK_log.txt',
                    filemode='a')


def get_all_files_link(url):
    """
    遍历url下所有需要下载的文件地址
    """
    html_file = requests.get(url)
    if html_file.status_code != 200:
        logging.warning("请求{}失败".format(url))
    html_parse = etree.HTML(html_file.content.decode('utf-8'))
    items = html_parse.xpath("//a/@href")  # 寻找所有的a标签的文本内容
    file_links = []
    for item in items:
        if item == "../":
            continue

        if item[-1] == "/":
            links = get_all_files_link(url + item)
            if len(links) > 0:
                file_links.extend(links)
            continue

        url_path = url + item
        file_links.append(url_path)
    return file_links


class MyThread(threading.Thread):
    """
    自定义多线程类,方便获取每个线程的返回结果
    """
    def __init__(self, func, args=()):
        super(MyThread, self).__init__()
        self.func = func
        self.args = args
        self.result = None

    def run(self):
        self.result = self.func(*self.args)

    def get_result(self):
        try:
            return self.result
        except Exception:
            return None


def multi_check_file(files_link):
    """
    使用多线程的方式,遍历所有下载源的header信息,
    检查是否有需要更新的文件,或者需要下载的新文件
    """
    download_list = []
    update_list = []
    max_task = 50
    tasks = []
    flag_list = []
    for index, file_link in enumerate(files_link):
        tasks.append(MyThread(check_whether_update, args=(file_link,)))
        # 每创建50个多线程或者已经遍历到最后一个元素,启动多线程
        if (index + 1) % max_task == 0 or index + 1 == len(files_link):
            [task.start() for task in tasks]  # 启动多线程
            [task.join() for task in tasks]  # 等待多线程结束
            flag_list.extend([task.get_result() for task in tasks])  # 获取多线程结果
            tasks = []  # 清空任务,准备下一批多线程任务

    if len(flag_list) != len(files_link):
        logging.error("multi_check_file failed!")
        exit(2)

    for index in range(len(files_link)):
        if flag_list[index] is 1:    # 文件不存在需要下载
            download_list.append(files_link[index])
        elif flag_list[index] is 2:  # 文件已过期,需要重新下载
            update_list.append(files_link[index])

    return download_list, update_list


def check_whether_update(url_path):
    """
    检查网络文件与本地文件是否一致(通过文件大小判断),
        若与本地文件不同或者本地没有该文件,返回True(需要重新下载)
        若相同返回False(不需要重新下载)

    return:
        code:
            0  文件已是最新,无需重新下载
            1  文件不存在需要下载
            2  文件已过期,需要重新下载
    """
    url_head = "https://you_want_to_download"

    response = requests.head(url_path)
    url_file_size = int(response.headers["content-length"])  # 获取网络上的资源文件大小

    local_file_path = path.join(FILES_STORE,
                                url_path.split(url_head)[-1])
    if path.exists(local_file_path) is False:
        return 1  # 文件不存在需要下载

    local_file_size = stat(local_file_path).st_size  # 获取本地文件资源大小

    if url_file_size == local_file_size:
        logging.info("{}该文件已是最新状态,无需下载".format(url_path))
        return 0  # 文件已是最新,无需重新下载
    else:
        return 2  # 文件已过期,需要重新下载


def task():
    """
    定期执行的任务
    """
    start_url = 'https://you_want_to_download'
    files_link = get_all_files_link(start_url)  # 获取所有需要下载的文件url
    
    # 使用多线程去检查哪些文件是更新了的,哪些是新文件,如果没有更新就无需再次下载
    download_files, update_files = multi_check_file(files_link)
    
    logging.info("需要下载的文件有{}项:".format(len(download_files)))
    for v in download_files:
        logging.info(v)
    
    logging.info("需要更新的文件有{}项:".format(len(update_files)))
    for v in update_files:
        logging.info(v)
    
    # 下载新文件
    for url in download_files:
        file_dir = path.join(FILES_STORE, "/".join(url.split(start_url)[-1].split("/")[:-1]))
        command = "wget -P {} {} --no-check-certificate".format(file_dir, url).encode('gbk')
    
        try:
            # 注意sub.run方法在linux中和windows中的编码方式不同,linux是utf-8,windows是gbk
            res = sub.run(command.decode('gbk'), shell=True, stdout=sub.PIPE,
                          stderr=sub.PIPE, timeout=3600, encoding='gbk')
    
            # 检查文件是否与下载源提供的大小相同
            response = requests.head(url)
            url_file_size = int(response.headers["content-length"])  # 获取网络上的资源文件大小
            local_file_size = stat(file_dir + url.split("/")[-1]).st_size  # 获取本地文件资源大小
            if url_file_size == local_file_size:
                logging.debug("successful download: ", url.split(start_url)[-1])
            else:
                logging.error("error: 下载的文件大小与下载源提供的content-length不等")
                os.remove(file_dir + url.split("/")[-1])  # 删除该文件
                continue
        except Exception as e:
            logging.error(e)
    
    # 更新文件
    for url in update_files:
        file_path = path.join(FILES_STORE, url.split(start_url)[-1])
        # 下载到.cache文件,等下载完后在替换
        command = "wget -O {} {} --no-check-certificate".format(file_path + ".cache", url).encode('gbk')
    
        try:
            # 注意sub.run方法在linux中和windows中的编码方式不同,linux是utf-8,windows是gbk
            res = sub.run(command.decode('gbk'), shell=True, stdout=sub.PIPE,
                          stderr=sub.PIPE, timeout=3600, encoding='gbk')
    
            # 检查文件是否与下载源提供的大小相同
            response = requests.head(url)
            url_file_size = int(response.headers["content-length"])  # 获取网络上的资源文件大小
            local_file_size = stat(file_path + ".cache").st_size  # 获取本地文件资源大小
            if url_file_size == local_file_size:
                logging.info("successful update: ", url.split(start_url)[-1])
            else:
                logging.error("error: 下载的文件大小与下载源提供的content-length不等")
                os.remove(file_path + ".cache")
                continue
    
            # 下载完成,先删除旧文件,在重命名
            os.remove(file_path)  
            os.rename(file_path + ".cache", file_path)
        except Exception as e:
            logging.error(e)


def my_listener(event):
    if event.exception:
        logging.error('error')
    else:
        logging.info('-------successful-------')


if __name__ == "__main__":
    # By default, only one instance of each job is allowed to be run at the same time.
    # This means that if the job is about to be run but the previous run hasn’t finished yet,
    # then the latest run is considered a misfire.
    # 当前一个任务没有完成,下一个任务会跳过
    scheduler = BlockingScheduler(timezone="Asia/Shanghai")
    # 每天的指定时间开始更新数据
    scheduler.add_job(func=task, id='day_job', trigger='cron',
                      year="*", month="*", day="*",
                      hour="1", minute="0", second="0")
    scheduler.add_listener(my_listener, EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
    scheduler._logger = logging
    # 启动
    scheduler.start()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章