【第四章-1】Python爬虫教程(多线程,多进程,线程池和进程池,多线程爬取广州江南果菜批发市场菜价数据【xpath】)

发布于:2023-01-04 ⋅ 阅读:(344) ⋅ 点赞:(0)

本课程共五个章节,课程地址:

【Python爬虫教程】花9888买的Python爬虫全套教程2021完整版现分享给大家!(已更新项目)——附赠课程与资料_哔哩哔哩_bilibili


第四章

  1. 本章内容梗概
  2. 多线程
  3. 多进程
  4. 线程池和进程池
  5. 抓取广州江南果菜批发市场菜价
  6. 协程
  7. aiohttp模块  多任务异步协程
  8. 扒光一本电子书
  9. 综合训练:抓取一部电影

目录

第四章

(一)本章内容梗概

(二)多线程

先看单线程效果 

那么在python里如何使用多线程呢?

如何创建两个子线程且能区分?(传参)

(三)多进程 

python里怎么创建多进程?

(四)线程池和进程池入门 

在python里如何使用线程池?

进程池

(五)抓取广州江南果菜批发市场菜价

第一步:提取单个页面的数据

提取单页面数据的完整代码

第二步:上线程池,多页面同时抓取

全部代码


(一)本章内容梗概

到目前为止,我们可以解决爬虫的基本抓取流程了,但是抓取效率还是不够高。如何提高抓取效率呢?我们可以选择多线程、多进程、协程等操作完成异步爬虫(多车道同时进行爬取) 

在这里要特殊说明一下,多线程异步爬虫中每一步都可以设立成多线程的,具体操作得实际去分析。当然,也可以像上图这样,每一个url一个单独线程

想抓取一万条数据, 每一次发送请求的时候都要经历这几个过程:

  1. 请求到url
  2. 得到响应
  3. 从响应中提取内容
  4. 存储到数据库或本地

一万条数据就要执行一万次请求,线性过程

异步操作(如一次性跑四个),有很多方法可以实现异步操作:

  • 多线程
  • 多进程
  • 协程

(二)多线程

操作系统每次运行一个程序的时候,都会给这个程序准备一块内存,用来存放这个程序执行过程中产生的变量等。这个内存区域可以叫进程(资源单位)。在这个进程里面会有若干个线程(执行单位)进行工作(每一个进程至少要有一个线程,CPU执行时是找线程)

# 启动每一个程序默认都会有一个主线程

if __name__ == '__main__':
    print("你好啊")

快速敲出下面这条语句的方法:输入main,然后敲回车

if __name__ == '__main__':

先看单线程效果 

# 单线程程序,线性过程,func()执行完了才开始执行main

def func():
    for i in range(1000):
        print("func", i)

if __name__ == '__main__':
    func()
    for i in range(1000):
        print("main",i)

执行过程:程序启动 ——> 加载func() ——> 执行main ——> 调用func() ——> func执行完毕,继续执行main中的内容     整个过程是一条线跑下来的,这就是单线程

误区:单线程是一条线跑下来的,那如果写个if是不是就是两条线了?    非也

 

我们要注意一个细节,不论程序真还是假,它只能选择一条路走,所以还是单线程,并没有异步的效果

 

那么在python里如何使用多线程呢?

借助 Thread类 来完成 

第一套写法: 

# 多线程程序(写法1,适用于小脚本)

from threading import Thread  # 线程类

def func():
    for i in range(1000):
        print("func", i)

if __name__ == '__main__':
    # 创建一个线程类的对象,并给线程安排任务
    t = Thread(target=func)    # func后面不能加(),因为加了()就是调用它
    # 多线程状态为可以开始工作状态, 具体的执行时间由CPU决定
    t.start()
    for i in range(1000):
        print("main",i)

 

第二套写法:

#  多线程程序(写法2,适用于复杂的情况)

from threading import Thread  # 线程类

class MyThread(Thread):    # 写一个类MyThread,继承Thread类
    # 重写run()方法
    def run(self):  # 固定的    -> 当线程被执行的时候, 被执行的就是run()
        for i in range(1000):
            print("子线程", i)


if __name__ == '__main__':
    t = MyThread()   # 造对象
    # t.run()  # 这么写不行,是方法的调用 -> 单线程
    t.start()  # 开启线程

    for i in range(1000):
        print("主线程", i)

 

程序效果:main和func交替执行(如果速度够快,给我们的感觉就是一起执行)

执行过程:加载fun() ——> 执行main ——> 创建子线程t ——> 子线程t启动 ——> 执行func中的内容 |——> 继续执行main

我们成功的让两件事同时发生了,那么想一下,如果我有1000个url准备去下载,那么交给每个func单独去执行就好,主函数该干嘛还干嘛

如何创建两个子线程且能区分?(传参)

from threading import Thread

def func(name):  # 函数传参
    for i in range(1000):
        print(name, i)


if __name__ == '__main__':
    t1 = Thread(target=func, args=("周杰伦",))   # 传递参数必须是元组,所以如果只有一个参数后面必须要有,
    t1.start()

    t2 = Thread(target=func, args=("王力宏",))
    t2.start()

 

对于写法2,即创建类的形式,可以通过定义构造函数的方式往里面传参

class MyThread(Thread):    # 写一个类MyThread,继承Thread类
    def __init__(self):    # 定义构造函数

(三)多进程 

多进程:通过主程序去创建多个进程来完成并行的效果 

相对于多线程而言,多进程更耗资源(要开内存),所以不常用

 

python里怎么创建多进程?

(跟创建多线程代码的逻辑是一样的)

注意,python的作者其实做了一件大好事:本质上多线程和多进程的执行过程是不一样的,python的作者为了让开学人员更舒服,采用了几乎完全相同的API,所以写起来几乎一样

第一套写法: 

from multiprocessing import Process

def func():
    for i in range(100000):
        print("子进程",i)

if __name__ == '__main__':
    p = Process(target=func)    # 创建一个子进程
    p.start()
    for i in range(100000):
        print("主进程",i)

第二套写法:

from multiprocessing import Process

class MyProcess(Process):
    def run(self):
        for i in range(1000):
            print("MyProcess", i)

if __name__ == '__main__':
    t = MyProcess()
    t.start()
    for i in range(1000):
        print("main", i)

 


(四)线程池和进程池入门 

广州江南果菜批发市场 批发市场 最大的水果批发市场 蔬菜批发市场 江南市场

当我们对某些网站内容进行抓取的时候非常容易遇到这样一种情况:看这个网站,我们发现这网站的数据太多了,有一万多页,也就对应着一万多个url,那我们设计多线程的时候,如果每个url对应一个线程就会产生新问题 —— 创建线程本身也是要消耗计算机资源的,那这时我们就可以考虑能不能重复的使用线程呢?答案当然可以,线程池来搞定 

若想爬取所有的数据,且想提高效率,可以把每一个url都放在一个线程里,来完成数据的爬取。但是,对应要使用一万多个线程,此时CPU的资源完全被浪费掉,因为开辟一个线程也是要耗资源的。有没有什么机制能够创建50个线程并反复利用这50个线程来爬取这一万多条的数据?

线程池工作原理:创建一个大池子,存放固定数量的线程/一次性开辟一些线程,用户直接给线程池提交任务,线程任务的调度交给线程池来完成

 

在python里如何使用线程池?

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# ThreadPoolExecutor 线程池    ProcessPoolExecutor 进程池

def fn(name):
    for i in range(1000):
        print(name, i)

if __name__ == '__main__':
    # 创建一个有50个线程的线程池
    with ThreadPoolExecutor(50) as t:
        for i in range(100):   # 有100个任务
            t.submit(fn, name=f"线程{i}")   # f是format  格式化字符串
    # 等待线程池中的任务全部执行完毕. 才继续执行 ——> 守护
    print("123")

进程池

进程池同理,把 ThreadPoolExecutor 更换为 ProcessPoolExecutor 就可以了


(五)抓取广州江南果菜批发市场菜价

思路:

  1. 如何提取单个页面的数据(xpath)
  2. 上线程池,多个页面同时抓取

右键 ——> 检查

 

# xpath的延伸用法

# 场景:table里连同表头和数据都是在<tr>里,现只想要数据,不想要表头
# 两种写法

trs = table.xpath("./tr")[1:]             # 写法1
trs = table.xpath("./tr[position()>1]")   # 写法2

第一步:提取单个页面的数据

import requests
from lxml import etree

def download_one_page(url):
    # 拿到页面源代码
    resp = requests.get(url)
    html = etree.HTML(resp.text)
    # print(resp.text)

    # 从页面源代码里提取我们想要的内容
    table = html.xpath("/html/body/div/div[4]/div/div[2]/div[2]/table")[0]
    # print(table)
    trs = table.xpath("./tbody/tr")   # 相对查找 剔除表头
    # print(len(trs))
    for tr in trs:    # 拿到每个tr
        txt = tr.xpath("./td/text()")
        print(txt)

if __name__ == '__main__':
    download_one_page("http://www.jnmarket.net/import/list-1_1.html")

我们想去掉如 “江苏/河南/山东/云南” 中的 “/”,对代码稍作修改:

        # 对数据做简单的处理:去掉/
        txt = (item.replace("/","") for item in txt)   # 生成器
        print(list(txt))

将数据写进csv文件:

import csv

f = open("data_new.csv",mode="w",newline="",encoding="utf-8")
csvwriter = csv.writer(f)

        # 把数据存放在文件中
        csvwriter.writerow(txt)
    print(url,"提取完毕!")

提取单页面数据的完整代码:

import requests
from lxml import etree
import csv

f = open("data_new.csv",mode="w",newline="",encoding="utf-8")
csvwriter = csv.writer(f)

def download_one_page(url):
    # 拿到页面源代码
    resp = requests.get(url)
    html = etree.HTML(resp.text)
    # print(resp.text)

    # 从页面源代码里提取我们想要的内容
    table = html.xpath("/html/body/div/div[4]/div/div[2]/div[2]/table")[0]
    # print(table)
    trs = table.xpath("./tbody/tr")   # 相对查找 剔除表头
    # print(len(trs))
    for tr in trs:    # 拿到每个tr
        txt = tr.xpath("./td/text()")
        # 对数据做简单的处理:去掉/
        txt = (item.replace("/","") for item in txt)   # 生成器
        # print(list(txt))
        # 把数据存放在文件中
        csvwriter.writerow(txt)
    print(url,"提取完毕!")

if __name__ == '__main__':
    download_one_page("http://www.jnmarket.net/import/list-1_1.html")

 

第二步:上线程池,多页面同时抓取

from concurrent.futures import ThreadPoolExecutor

if __name__ == '__main__':
    # for i in range(1,10548):   # 效率极其低下
        # download_one_page(f"http://www.jnmarket.net/import/list-1_{i}.html")
    
    # 创建线程池
    with ThreadPoolExecutor(50) as t:    # 准备一个50个线程的线程池
        for i in range(1,10548):
            # 把下载任务提交给线程池
            t.submit(download_one_page, f"http://www.jnmarket.net/import/list-1_{i}.html")
    print("全部下载完毕!")

 

全部代码:

import requests
from lxml import etree
import csv
from concurrent.futures import ThreadPoolExecutor

f = open("data_new.csv",mode="w",newline="",encoding="utf-8")
csvwriter = csv.writer(f)

def download_one_page(url):
    # 拿到页面源代码
    resp = requests.get(url)
    html = etree.HTML(resp.text)
    # print(resp.text)

    # 从页面源代码里提取我们想要的内容
    table = html.xpath("/html/body/div/div[4]/div/div[2]/div[2]/table")[0]
    # print(table)
    trs = table.xpath("./tbody/tr")   # 相对查找 剔除表头
    # print(len(trs))
    for tr in trs:    # 拿到每个tr
        txt = tr.xpath("./td/text()")
        # 对数据做简单的处理:去掉/
        txt = (item.replace("/","") for item in txt)   # 生成器
        # print(list(txt))
        # 把数据存放在文件中
        csvwriter.writerow(txt)
    print(url,"提取完毕!")

if __name__ == '__main__':
    # for i in range(1,10548):   # 效率极其低下
        # download_one_page(f"http://www.jnmarket.net/import/list-1_{i}.html")
    
    # 创建线程池
    with ThreadPoolExecutor(50) as t:    # 准备一个50个线程的线程池
        for i in range(1,10548):
            # 把下载任务提交给线程池
            t.submit(download_one_page, f"http://www.jnmarket.net/import/list-1_{i}.html")
    print("全部下载完毕!")
本文含有隐藏内容,请 开通VIP 后查看