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()

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