网络爬虫第三次作业——多线程、scrapy框架

作业①:

1)单/多线程爬取网站图片实验

  要求:指定一个网站,爬取这个网站中的所有的所有图片,例如中国气象网http://www.weather.com.cn。分别使用单线程和多线程的方式爬取。

以下按自己的编码风格复现书本代码

  • 单线程
    程序主要思路:
graph TD A(获取指定网页字符内容) -->B(从中筛选出所有图像url) B --> C(逐一对图像url进行预处理:拼接,去重) C --> D(逐一下载相应的图片到本地images子文件中) D --> E(将下载的Url信息在控制台输出)

代码:

from bs4 import BeautifulSoup
from bs4 import UnicodeDammit
import urllib.request  # 可换用requests
import os
import time
import warnings
warnings.filterwarnings("ignore")  # 在控制台屏蔽警告


class ThreadSpiderImages(object):
    '''单线程爬取指定网站图片类'''
    def __init__(self,start_url):
        '''共同属性是UA头以及url'''
        self.url = start_url
        self.headers = {
            "User-Agent": "Mozilla/5.0"
        }

    def get_html_text(self):
        '''获取指定url的网页字符串'''
        req = urllib.request.Request(self.url, headers=self.headers)
        html = urllib.request.urlopen(req)
        html_text = html.read()
        dammit = UnicodeDammit(html_text, ["utf-8","gbk"])  # 猜测文档编码
        html_text = dammit.unicode_markup
        return html_text

    def extract_imageurls(self,images):
        '''
        从解析出的bs4ResultSet对象中提取图像url信息,
        返回图像urls
        '''
        urls = []
        for image in images:
            try:
                src = image["src"]  # 图像文件的src地址
                url = urllib.request.urljoin(self.url, src)  # 构建完整url
                if url not in urls:  # url去重
                    urls.append(url)
                    print(url)
            except Exception as err:
                print(err)
        return urls

    def download(self,urls):
        '''下载urls中每个url对应的图像'''
        count = 0
        dirname = "images"
        current_path = os.getcwd()
        if not os.path.exists(dirname):  # 如果文件夹不存在则创建
            os.makedirs(dirname)
            print("{}文件夹创建在{}".format(dirname, current_path))

        try:
            for url in urls:
                pos = url[::-1].index(".") + 1  # 找到最后一个.在第几个位置(注意下标从0开始)
                ext = url[-pos:]  # 后缀名

                # 将下载的图像文件存入当前文件夹下的images文件夹

                filename = str(count) + ext
                filepath = dirname + "\\" + filename
                try:
                    urllib.request.urlretrieve(url, filepath)
                except:
                    default_ext = '.jpg'  # 将异常图片格式转化为默认格式.jpg
                    print("将{}图片格式转化为{}图片格式存储".format(ext,default_ext))
                    filename = str(count) + default_ext
                    filepath = dirname + "\\" + filename
                    urllib.request.urlretrieve(url, filepath)

                print("download "+filename)
                count += 1

        except Exception as err:
            print(err)


if __name__ == "__main__":
    start = time.clock()
    start_url = "http://www.weather.com.cn/weather/101280601.shtml"  # 指定网站
    spider = ThreadSpiderImages(start_url)

    try:
        html_text = spider.get_html_text()
        soup = BeautifulSoup(html_text, "html.parser")
        images = soup.select("img")  # 选择图像文件
        urls = spider.extract_imageurls(images)
        spider.download(urls)

    except Exception as err:
        print(err)

    end = time.clock()
    print('Running time: %s Seconds' % (end - start))

运行结果部分截图:


  • 多线程
    程序主要思路基本上同单线程,差别仅在于多线程中为每一个图像url创建线程对象,逐一以download函数为目标启动线程
    代码:
from bs4 import BeautifulSoup
from bs4 import UnicodeDammit
import urllib.request  # 可换用requests
import os
import threading
import time
import warnings
warnings.filterwarnings("ignore")

class ThreadsSpiderImages(object):
    '''多线程爬取指定网站图片类'''
    def __init__(self,start_url):
        '''共同属性是UA头以及url'''
        self.url = start_url
        self.headers = {
            "User-Agent": "Mozilla/5.0"
        }

    def get_html_text(self):
        '''获取指定url的网页字符串'''
        req = urllib.request.Request(self.url, headers=self.headers)
        html = urllib.request.urlopen(req)
        html_text = html.read()
        dammit = UnicodeDammit(html_text, ["utf-8","gbk"])  # 猜测文档编码
        html_text = dammit.unicode_markup
        return html_text

    def extract_imageurls(self,images):
        '''
        从解析出的bs4ResultSet对象中提取图像url信息,
        返回图像urls
        '''
        urls = []
        for image in images:
            try:
                src = image["src"]  # 图像文件的src地址
                url = urllib.request.urljoin(self.url, src)  # 构建完整url
                if url not in urls:  # url去重
                    urls.append(url)
                    print(url)
            except Exception as err:
                print(err)
        return urls

    def threads_download(self,urls):
        '''根据图像urls启用多线程download'''
        global count
        try:
            for url in urls:
                count += 1  # 确保count不重,唯一的count操作
                T = threading.Thread(target=self.download,args=(url,))  # 逐一(每个url)创建线程对象
                T.setDaemon(False)  # True则为守护线程,若设置守护线程,一旦主线程执行完毕,子线程都得结束
                T.start()  # 逐一启动线程
                threads.append(T)
        except Exception as err:
            print(err)

    def download(self,url):
        '''下载指定url对应的图像'''
        dirname = "images"
        if not os.path.exists(dirname):  # 如果文件夹不存在则创建
            os.makedirs(dirname)
            current_path = os.getcwd()
            print("{}文件夹创建在{}".format(dirname, current_path))

        try:
            pos = url[::-1].index(".") + 1  # 找到最后一个.在第几个位置(注意下标从0开始)
            ext = url[-pos:]  # 后缀名

            # 将下载的图像文件存入当前文件夹下的images文件夹

            filename = str(count) + ext
            filepath = dirname + "\\" + filename
            try:
                urllib.request.urlretrieve(url, filepath)
            except:
                default_ext = '.jpg'
                print("将{}图片格式转化为{}图片格式存储".format(ext,default_ext))
                filename = str(count) + default_ext
                filepath = dirname + "\\" + filename
                urllib.request.urlretrieve(url, filepath)

            print("download "+filename)

        except Exception as err:
            print(err)


if __name__ == "__main__":

    start = time.clock()
    start_url = "http://www.weather.com.cn/weather/101280601.shtml"  # 指定网站
    spider = ThreadsSpiderImages(start_url)
    count = 0
    threads = []

    try:
        html_text = spider.get_html_text()
        soup = BeautifulSoup(html_text, "html.parser")
        images = soup.select("img")  # 选择图像文件
        urls = spider.extract_imageurls(images)
        spider.threads_download(urls)
        for t in threads:  # 主线程中等待所有子线程退出
            t.join()

    except Exception as err:
        print(err)

    end = time.clock()
    print('Running time: %s Seconds' % (end - start))

运行结果:
基本与单线程运行结果一致,特殊之处在于运行时间“Running time”的大幅缩短以及由于多线程并发的不确定性导致下载的图片编号乱序



2)心得体会:

  这次实验关键在于多线程在IO密集型任务方面的应用。与单线程执行下载任务对比,可以体会到多线程爬取程序由于可以有多个文件在同时下载,互不干扰,效率和可靠性都明显比单线程爬取程序高。具体实现时主要使用threading模块。

作业②:

1)scrapy框架复现实现

  要求:使用scrapy框架复现作业①。

框架复现主要思路:

graph TD A(在items模块确定要获取的数据项目item) -->B(在spider模块确定入口函数start_requests和回调/解析函数parse) B --> C(在pipelines模块编写数据管道处理类处理item) C --> D(设置配置文件settings.py) D --> E(使用scrpy中专门的命令scrapy crawl执行爬虫程序)

代码:

  • items.py(数据项目类)
import scrapy


class ImageItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    image_url = scrapy.Field()
    pass
  • SpiderImage.py(spiders文件夹下的爬虫程序)
import scrapy
from scrapy.selector import Selector
import warnings
warnings.filterwarnings("ignore")
import sys
sys.path.append("..")  # 为导入上级目录中的模块
from items import ImageItem  # items模块在上级目录中


class ScrapySpiderImages(scrapy.Spider):
    '''爬取指定网站图像类'''
    name = "threadsSpiderImages"  # 唯一标识这个爬虫
    allowed_domains = ["http://www.weather.com.cn"]  # 允许爬虫的域名列表,若不定义,则不做限制
    start_urls = ["http://www.weather.com.cn/weather/101280601.shtml"]  # 开始爬虫的url列表,可用用start_requests()函数代替
    # 设置settings中的值,优先级高于settings.py
    custom_settings = {
        # 下载延迟的秒数,用来限制访问的频率,默认为0,没有延时
        'DOWNLOAD_DELAY': 0.3,
        #  是否遵守robots协议,默认为True
        'ROBOTSTXT_OBEY': False
    }

    def parse(self,response):

        try:
            html = response.body.decode()  # 返回网页字符内容
            selector = Selector(text=html)  # 装载文档
            images = selector.xpath("//img/@src").extract()  # 获取所有图像文件url,以列表形式返回
            threads = []
            urls = []
            for url in images:
                if url not in urls:
                    urls.append(url)
                    item = ImageItem()
                    item["image_url"] = url
                    yield item  

        except Exception as err:
            print(err)
  • pipelines.py(数据管道处理类)
import os
import urllib

class ThreadSpiderImagePipeline(object):
    # 定义类变量,它的值将在这个类的所有实例之间共享
    count = 0
    dirname = "images"

    def process_item(self, item, spider):

        if not os.path.exists(ThreadSpiderImagePipeline.dirname):  # 如果文件夹不存在则创建
            os.makedirs(ThreadSpiderImagePipeline.dirname)
            current_path = os.getcwd()
            print("{}文件夹创建在{}".format(ThreadSpiderImagePipeline.dirname, current_path))

        ThreadSpiderImagePipeline.count += 1

        try:
            image_url = item["image_url"]
            print(image_url)  # 在控制台输出url信息
            pos = image_url[::-1].index(".") + 1  # 找到最后一个.在第几个位置(注意下标从0开始)
            ext = image_url[-pos:]  # 后缀名

            filename = str(ThreadSpiderImagePipeline.count) + ext
            filepath = ThreadSpiderImagePipeline.dirname + "\\" + filename

            try:
                urllib.request.urlretrieve(image_url, filepath)
            except:
                default_ext = '.jpg'
                print("将{}图片格式转化为{}图片格式存储".format(ext, default_ext))
                filename = str(ThreadSpiderImagePipeline.count) + default_ext
                filepath = ThreadSpiderImagePipeline.dirname + "\\" + filename
                urllib.request.urlretrieve(image_url, filepath)

        except Exception as err:
            print(err)

        return item
  • settings.py(配置文件)
BOT_NAME = 'ThreadSpiderImage'
SPIDER_MODULES = ['ThreadSpiderImage.spiders']
NEWSPIDER_MODULE = 'ThreadSpiderImage.spiders'
ROBOTSTXT_OBEY = True
CONCURRENT_ITEMS = 100
CONCURRENT_REQUESTS = 50  # 由Scrapy下载器执行的并发(即同时)请求的最大数量
DEFAULT_REQUEST_HEADERS = {
   'User-Agent':"Mozilla/5.0",
}
ITEM_PIPELINES = {
   'ThreadSpiderImage.pipelines.ThreadSpiderImagePipeline': 300,  # 数值一般0-1000,数值越小优先级越高
}
  • run.py(执行爬虫程序)
from scrapy import cmdline

command = "scrapy crawl threadsSpiderImages -s LOG_ENABLED=False"
cmdline.execute(command.split())

由于只是用scrapy框架复现作业①代码,所以运行结果基本同作业①(没有打印运行时间和下载的图片名称)

  但试着使用了一下threading模块,发现速度还是提升了不少。
由于scrapy是异步执行框架,自己只编写了spiders文件夹下的ThreadSpider.py文件,其余模块(items,pipelines,settings)采用默认配置,name与上述代码一致,但由于两份代码不在同一项目中所以没有影响。

  • ThreadSpider.py(spiders文件夹下)
import scrapy
from scrapy.selector import Selector
import urllib
import os
import threading
import time
import warnings
warnings.filterwarnings("ignore")


class ScrapySpiderImages(scrapy.Spider):
    '''多线程爬取指定网站图片'''
    name = "threadsSpiderImages"
    allowed_domains = ["http://www.weather.com.cn"]  # 允许爬虫的域名列表,若不定义,则不做限制
    start_urls = ["http://www.weather.com.cn/weather/101280601.shtml"]  # 开始爬虫的url列表,可用用start_requests()函数代替
    # 设置settings中的值,优先级高于settings.py
    custom_settings = {
        # 下载延迟的秒数,用来限制访问的频率,默认为0,没有延时
        'DOWNLOAD_DELAY': 0.3,
        #  是否遵守robots协议,默认为True
        'ROBOTSTXT_OBEY': False
    }

    def parse(self,response):
        '''回调函数'''
        global count
        global threads
        count = 0
        threads = []
        start = time.clock()

        try:
            html = response.body.decode()  # 返回网页字符内容
            selector = Selector(text=html)  # 装载文档
            images = selector.xpath("//img/@src").extract()  # 获取所有图像文件url,以列表形式返回
            urls = self.pretreat_imageurls(images)
            self.threads_download(urls)
            for t in threads:  # 主线程中等待所有子线程退出
                t.join()
        except Exception as err:
            print(err)

        end = time.clock()
        print('Running time: %s Seconds' % (end - start))

    def pretreat_imageurls(self,images):
        '''从解析出的bs4ResultSet对象中提取图像url信息'''
        urls = []
        for url in images:
            try:
                if url not in urls:  # url去重
                    urls.append(url)
                    print(url)
            except Exception as err:
                print(err)
        return urls

    def threads_download(self,urls):
        '''根据图像urls启用多线程download'''
        global count
        try:
            for url in urls:
                count += 1  # 确保count不重,唯一的count操作
                T = threading.Thread(target=self.download,args=(url,))  # 逐一(每个url)创建线程对象
                T.setDaemon(False)  # True则为守护线程,若设置守护线程,一旦主线程执行完毕,子线程都得结束
                T.start()  # 逐一启动线程
                threads.append(T)
        except Exception as err:
            print(err)

    def download(self, url):
        '''下载指定url对应的图像'''
        dirname = "images"
        if not os.path.exists(dirname):  # 如果文件夹不存在则创建
            os.makedirs(dirname)
            current_path = os.getcwd()
            print("{}文件夹创建在{}".format(dirname, current_path))

        try:
            pos = url[::-1].index(".") + 1  # 找到最后一个.在第几个位置(注意下标从0开始)
            ext = url[-pos:]  # 后缀名

            # 将下载的图像文件存入当前文件夹下的images文件夹

            filename = str(count) + ext
            filepath = dirname + "\\" + filename
            try:
                urllib.request.urlretrieve(url, filepath)
            except:
                default_ext = '.jpg'
                print("将{}图片格式转化为{}图片格式存储".format(ext, default_ext))
                filename = str(count) + default_ext
                filepath = dirname + "\\" + filename
                urllib.request.urlretrieve(url, filepath)

            print("download " + filename)

        except Exception as err:
            print(err)
  • run.py(执行爬虫程序)
from scrapy import cmdline

command = "scrapy crawl threadsSpiderImages -s LOG_ENABLED=False"
cmdline.execute(command.split())

运行结果部分截图:(其余部分与作业①中多线程结果基本相同)

可以看出scapy框架+多线程爬取图片的运行效率比单纯的scapy框架以及不在scapy框架下实现的多线程明显要更高,由此对“scrapy是多线程的,不需要再设置了”的说法表示怀疑。

2)心得体会:

  本次实验重点在于对scrapy整体框架的理解和把握。scrapy是一个快速、功能强大的爬虫框架,而非函数功能库。程序入口为spiders,出口为ItemPipeline。引用一张表格帮助理解scrapy整体框架。

总的来说自己利用scrapy框架编写爬虫的整体过程是:1、确定起始url作为函数入口。2、确定要编写数据项目类items。3、在spiders下的爬虫程序中导入设计好数据项目类,在parse方法中进行获取。4、编写数据管道处理类,处理爬虫程序推送过来的item对象。5、使用scrapy中专门命令scrapy crawl执行爬虫程序。(注:创建scrapy项目是采用命令“scrapy startproject projectname”的形式)

作业③

1)scrapy框架爬取股票信息实验

  要求:使用scrapy框架爬取股票相关信息。
候选网站:东方财富网:https://www.eastmoney.com/
​ 新浪股票:http://finance.sina.com.cn/stock/
(这里选择东方财富网)

  程序主要思路:

graph TD A(根据设计好的url模板生成指定板块多页的urls列表作为入口) -->B(将存储每页股票信息的列表对象作为要获取的数据项目item) B --> C(设计回调函数parse使其获取并返回上述item) C --> D(编写数据管道处理类中的process_item函数格式化展示item数据并存储) D --> E(配置项目对应的settings.py文件)

代码:

  • items.py(数据项目类)
import scrapy


class StockItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    infos = scrapy.Field()
    pass
  • ScapyStockSpider.py(spiders文件夹下的爬虫程序)
import scrapy
import re
import sys
sys.path.append("..")  # 为导入上级目录中的模块
from items import StockItem  # items模块在上级目录中


class ScrapySpiderImages(scrapy.Spider):
    name = "scapySpiderStock"

    def start_requests(self):
        urls = ["http://5.push2.eastmoney.com/api/qt/clist/get?pn={0:d}&pz=20&po=1&np=1\
        &fltt=2&invt=2&fid=f3&fs={1}&fields=f2,f3,f4,f5,f6,f7,f12,f14,f15,f16,f17,f18"\
                      .format(page,self.get_fs("沪深A股")) for page in range(1,6)]  # 指定爬取板块为沪深A股,循环爬取前五页
        # return [scrapy.Request(url=url,callback=self.parse) for url in urls]
        for url in urls:
            yield scrapy.Request(url=url,callback=self.parse)  # 效率考量

    def parse(self,response):
        html = response.body.decode()
        info = self.extract_infos(html)
        item = StockItem(infos=info)
        yield item

    def get_fs(self,name):
        '''根据板块名称返回板块部分代码号'''
        fs_type = {
            "沪深A股": "m:0+t:6,m:0+t:13,m:0+t:80,m:1+t:2,m:1+t:23",
            "上证A股": "m:1+t:2,m:1+t:23",
            "深证A股": "m:0+t:6,m:0+t:13,m:0+t:80"
        }  # 东方财富网行情中心的板块变化体现在fs字段,通过f12抓包可得以上编码,其余板块依此类推
        return fs_type[name]

    def extract_infos(self,html):
        '''从所有网页字符内容中提取所需信息范围,返回列表类型,元素为字典'''
        pattern = "\[(.*?)\]"
        infos = re.compile(pattern,re.S).findall(html)[0]  # 所有信息一次被匹配,findall方法返回的列表中有且只有一个字符串元素
        return list(eval(infos))  # eval返回该字符串所代表的python对象(元组),为便于后续操作再将元组转化为列表
  • pipelines.py(数据管道处理类)
from itemadapter import ItemAdapter
from prettytable import PrettyTable
from wcwidth import wcswidth
from openpyxl import  Workbook


class StockPipeline:
    num = 1  # num为序号
    dic = {
        "股票代码": 'f12', "股票名称": 'f14', "最新报价": 'f2', "涨跌幅": 'f3',
        "涨跌额": 'f4', "成交量": 'f5', "成交额": 'f6', "振幅": 'f7',
        "最高": 'f15', "最低": 'f16', "今开": 'f17', "昨收": 'f18'
    }  # json网页中的字典键值对应含义
    titles = ["序号", "股票代码", "股票名称", "最新报价", "涨跌幅", "涨跌额",
              "成交量", "成交额", "振幅", "最高", "最低", "今开", "昨收"]
    wb = Workbook()
    ws = wb.active
    save_path = 'stock.xlsx'

    def process_item(self, item, spider):
        infos = item['infos']
        datas = self.get_datas(infos)
        for data in datas:  # datas是generator的可循环对象
            self.show_data(data)  # 格式化展示数据
            if data[0] == 1:
                StockPipeline.ws.append(StockPipeline.titles)
            StockPipeline.ws.append(data)
        StockPipeline.wb.save(StockPipeline.save_path)
        return item

    def get_datas(self,infos):
        '''从单页所需信息范围中提取数据存为列表'''
        for info in infos:  # infos为列表,info为字典
            info = self.format_data(info)
            data = [StockPipeline.num]  # 初始化为序号
            StockPipeline.num += 1  # 序号递增
            for value in StockPipeline.dic.values():  # 顺序存入列表
                data.append(info[value])
            yield data  # 等待调用循环抽取

    def format_data(self,info):
        '''将不符合题目格式的原始量格式化'''
        # pending_format = ["涨跌幅","成交量","成交额"]
        try:  # 实际运行过程可能会出现数据缺失的情况
            info[StockPipeline.dic["涨跌幅"]] = str(info[StockPipeline.dic["涨跌幅"]]) + "%"  # 更改为百分数形式
            info[StockPipeline.dic["成交量"]] = str('%.2f' %(info[StockPipeline.dic["成交量"]]/1e4))+ "万"  # 保留两位小数+单位
            info[StockPipeline.dic["成交额"]] = str('%.1f' %(info[StockPipeline.dic["成交额"]]/1e8))+ "亿"  # 保留一位小数+单位
        except:
            print("相应数据缺失")
        finally:
            return info

    # def show_data(self,data,shownum=20):
    #     '''展现结果,格式化输出所提取的数据'''
    #     StockPipeline.pt.add_row(data)
    #     if StockPipeline.num == shownum:
    #         print(StockPipeline.pt)

    def show_data(self,data):
        if data[0] == 1:  # 第一次打印标题
            pat = "{0:^2}\t{1:^7}\t{2:^7}\t{3:^7}\t{4:^7}\t{5:^7}\t{6:^7}\t{7:^7}\t{8:^7}\t{9:^7}\t{10:^7}\t{11:^7}\t{12:^7}"
            titles = StockPipeline.titles
            print(pat.format(titles[0],titles[1],titles[2],titles[3],titles[4],titles[5],\
                             titles[6],titles[7],titles[8],titles[9],titles[10],titles[11],titles[12]))

        pat = "{0:^3}\t{1:^8}\t{2:^8}\t{3:^8}\t{4:^8}\t{5:^8}\t{6:^8}\t{7:^8}\t{8:^8}\t{9:^8}\t{10:^8}\t{11:^8}\t{12:^8}"
        print(pat.format(data[0], data[1], data[2], data[3], data[4], data[5], \
                         data[6], data[7], data[8], data[9], data[10], data[11], data[12]))
  • settings.py(配置文件)
BOT_NAME = 'stock'
SPIDER_MODULES = ['stock.spiders']
NEWSPIDER_MODULE = 'stock.spiders'
ROBOTSTXT_OBEY = False
ITEM_PIPELINES = {
   'stock.pipelines.StockPipeline': 300,
}
DEFAULT_REQUEST_HEADERS = {
   'User-Agent':"Mozilla/5.0",
}
  • run.py(执行爬虫程序)
from scrapy import cmdline

command = "scrapy crawl scapySpiderStock -s LOG_ENABLED=False"
cmdline.execute(command.split())

运行结果部分截图:

控制台输出

excel保存

2)心得体会:

  这次实验重点依然是scrapy框架的使用。我学会了如何编写scrapy框架的各个模块使其相互配合来完成任务。以及如何在pipelines模块中进行规范的数据展示以及数据存储(excel存储可利用openpyxl中的Workbook)。
但在使用scrapy框架的过程中其实也有以下疑惑:
1、scrapy框架中的parse_item方法和process_item方法都可以用来处理数据,那么应该选取什么样的策略在两个方法中分配数据处理的工作量?是单纯让parse_items尽可能少做数据处理还是说尽可能均衡工作量?
2、scrapy框架中多线程到底是如何实现的?我在作业②中实现多线程的方法是否规范?

posted @ 2020-10-20 22:31  奔跑着看风景  阅读(574)  评论(0编辑  收藏  举报