Python3 爬虫爬取中国图书网(淘书团) 进阶版

在原版的基础上,添加了进程池,进程锁,以及数据处理分析小实验

原版的链接为:http://www.cnblogs.com/ChrisInsistPy/p/8981820.html

首先分析一下在整个程序的哪个进程中,可以实现多进程提高运行效率,首先爬虫程序会先去拿网站的url,然后对url内的json数据进行处理,之后写入文件

所以在整个过程中,我们可以分别让多个进程去拿url拿response的数据,然后分批处理,写入文件中

这里有个知识点,就是在python中,每个进程都有一个GIL锁,确保同一时间只能有一个线程运行,所以多进程可能会更实用,具体可以google一下

首先找到我们遍历url的方法中,把方法体进一步封装到  scraping_page_data()方法里

创建线程池

def dynamtic_scraping_data(page, headers, fileName):
    pool = multiprocessing.Pool() #进程池
    lock = multiprocessing.Manager().Lock() #加一个写锁,确保每页的内容在一起
    '''
    if code ' lock = multiprocessing.Lock()'报错:
    RuntimeError: Lock objects should only be shared between processes through inheritance
  '''  
    for i in range(page):
        pool.apply(scraping_page_data,(lock, i, headers, fileName))
    pool.close()
    pool.join()

这里有几个知识点,首先,对于进程池,会根据主机的系统资源,去分配到底有多少条进程

其次如果不调用join方法,子进程会在主进程结束后继续执行,因为后面有数据分析的功能所以我们条用了join方法,让主进程去等待其他的进程

在进程池中调用apply方法去实现每个子进程要执行的方法apply(method_name, (parameters....))

由于我们想要确保,每次拿到一条url,希望当前进程写入的数据的时候,不被其他进程打扰,我们给写入操作加上进程锁

在写的过程中,我发现如果按照创建一条进程的方法也创建进程锁,会报错,原因是用了进程池,而进程池中的进程并不是由当前同一个父进程创建的原因。

查了下资料,multiprocessing.Manager()返回的manager对象控制了一个server进程,可用于多进程之间的安全通信。

写入文件加锁

def write_csv_rows(lock, path, headers, rows):
# write one row
    lock.acquire()
    with open(path, 'a', encoding='gb18030', newline='') as f:
        f_csv = csv.DictWriter(f, headers)
         # 如果写入数据为字典,则写入一行,否则写入多行
        if type(rows) == type({}):
            f_csv.writerow(rows)
        else:
            f_csv.writerows(rows)
    lock.release()

这样我们会发现进程池会调用多个进程资源,并执行写入操作,互不干扰,但是这样就遗留下来了问题,总结会提到

接下来拿到数据后,我们读取cvs中的price,分析价格低于200¥的图书的价位分布,为了生成统计图,我们引入matplotlib包

首先处理脏数据:

  这里提到一点在基础版忽视的问题,我们发现在读入的数据中,有一部分的图书名字会加上‘团购:’的无用信息,所以我们要在存入磁盘前删掉它

 for data in result['Data']:
            bookinfo['bookName'] = data['book_name'].replace('团购:','')
            bookinfo['price'] = data['group_price']
            bookinfo['iconLink'] = data['group_image']
            write_csv_rows(lock, fileName,headers,bookinfo)

接下来就是处理数据的部分了:先拿到cvs文件中price的值,也就是栏位为1

然后遍历删掉title => price后,我们发现在price 的数据中,有的值为'39.9-159.9'格式,对于这种格式的数据,我们求平均值来计算

最后我们把价格低于200的数据存入数组,用于生成柱状图

 #matplotlib views, 统计价格小于200的图书价格分布
    prices = []
    price = read_csv_column(csv_filename, 1)
    for i in range(len(price)-1):
        if(price[i+1].find('-') != -1):
            splitprice = price[i+1].split('-')
            average_price = (float(splitprice[0]) + float(splitprice[1]))/ 2
            if(average_price < 200):
                prices.append(average_price)
        elif(float(price[i+1]) < 200):
            prices.append(float(price[i+1]))
    plt.hist(prices, bins=10)
    print(prices)
    plt.show()

调用show()方法后生成统计图

当然matplotlib为我们提供了很多方法去美化,以及生成点状图,线图等等,在这里仅仅是测试功能,自我学习,不多测试了

小实验全部代码:

import requests
from bs4 import BeautifulSoup
import json
import csv
import multiprocessing
from matplotlib import pyplot as plt
def parse_one_page():
    header = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36',
        'Host': 'tuan.bookschina.com',
        'Referer': 'http://tuan.bookschina.com/',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9'
    }
    url = 'http://tuan.bookschina.com/'
    response = requests.get(url, headers = header) #模仿浏览器登录
    response.encoding = 'utf-8'
    soup = BeautifulSoup(response.text,'html.parser')
    for item in soup.select('div .taoListInner ul li'):
        print(item.select('h2')[0].text) #返回对象为数组
        print(item.select('.salePrice')[0].text)
        print(item.select('img')[0].get('src')) #get方法用来取得tab内部的属性值


def dynamtic_scraping_data(page, headers, fileName):
    pool = multiprocessing.Pool() #进程池
    lock = multiprocessing.Manager().Lock() #加一个写锁,确保每页的内容在一起
    '''
    if code ' lock = multiprocessing.Lock()'报错:
    RuntimeError: Lock objects should only be shared between processes through inheritance
    用了进程池,而进程池中的进程并不是由当前同一个父进程创建的原因。查了下资料,
    multiprocessing.Manager()返回的manager对象控制了一个server进程,可用于多进程之间
    的安全通信。
    '''
    for i in range(page):
        pool.apply(scraping_page_data,(lock, i, headers, fileName))
    pool.close()
    pool.join()


def scraping_page_data(lock, page, headers, fileName):
        url = 'http://tuan.bookschina.com/Home/GroupList?Type=0&Category=0&Price=0&Order=11&Page=' + str(
            page+1) + '&Tyjson=true'
        response = requests.get(url)
        result = json.loads(response.text)
        bookinfo = {}
        for data in result['Data']:
            bookinfo['bookName'] = data['book_name'].replace('团购:','')
            bookinfo['price'] = data['group_price']
            bookinfo['iconLink'] = data['group_image']
            write_csv_rows(lock, fileName,headers,bookinfo)
        print(multiprocessing.current_process().name + ' ' + url)


def write_csv_headers(path, headers):
    '''
    写入表头
    '''
    with open(path, 'a', encoding='gb18030', newline='') as f:
        f_csv = csv.DictWriter(f, headers)
        f_csv.writeheader()


def write_csv_rows(lock, path, headers, rows):
# write one row
    lock.acquire()
    with open(path, 'a', encoding='gb18030', newline='') as f:
        f_csv = csv.DictWriter(f, headers)
         # 如果写入数据为字典,则写入一行,否则写入多行
        if type(rows) == type({}):
            f_csv.writerow(rows)
        else:
            f_csv.writerows(rows)
    lock.release()

def read_csv_column(path, column):
# read one row
    with open(path, 'r', encoding='gb18030', newline='') as f:
        reader = csv.reader(f)
        return [row[column] for row in reader]

def main(page):
    # parse_one_page() #Tip: beautifulSoup test
    csv_filename = "bookInfo.csv"
    headers = ['bookName', 'price', 'iconLink']
    write_csv_headers(csv_filename,headers)
    dynamtic_scraping_data(page, headers, csv_filename)

    #matplotlib views, 统计价格小于200的图书价格分布
    prices = []
    price = read_csv_column(csv_filename, 1)
    for i in range(len(price)-1):
        if(price[i+1].find('-') != -1):
            splitprice = price[i+1].split('-')
            average_price = (float(splitprice[0]) + float(splitprice[1]))/ 2
            if(average_price < 200):
                prices.append(average_price)
        elif(float(price[i+1]) < 200):
            prices.append(float(price[i+1]))
    plt.hist(prices, bins=18)
    print(prices)
    plt.show()
if __name__ == '__main__':
    main(20)

总结:

在测试爬虫程序的过程中,发现了一个非常关键的问题,也就是网络稳定对爬虫程序的影响。如果读取页数相当庞大,在抓取过程中,

会有某个链接出现相应失败的情况,但是本程序默认的操作是跳过继续执行。所以这样就有可能导致数据丢失,以至于分析数据结果

不准确,google后,发现消息队列是一个很好的解决方法,把每次拿到的url存入队列中,每个进程作为消费者去pop队列中的url,

拿数据失败,则从新把url指令存入队列,等待重新执行。有机会试着实现一下

猜你喜欢

转载自www.cnblogs.com/ChrisInsistPy/p/8986930.html