Python 多线程、利用request使用代理、利用递归深度抓取电影网页的内容并将电影的介绍和下载链接保存到mysql中

本文仅为学习python过程的一个笔记,其中还有一些bug! 还请各位大佬赐教

有些专业的说法还不是很熟悉,欢迎各位大佬帮忙指出

本人时一个新晋奶爸,而立之年突然想转业,想学习python

先介绍一个大致思路

本文抓取的电影下载网站是我读书期间最近经常下载电影的链接为www.hao6v.tv

分析网页,没有动态加载内容,所以应该算是比较简单。

网页我大致分成两类,一类是类似目录和电影索引页面,暂时称为moviepage(英语不好,也不知道取啥名,后期在代码中也是这样分类的),一类是电影详情及下载页面,暂时称为movieinfo

首先抓取主页内的所有符合条件的url和标题,去重后放到moviepage数据表中,后期利用递归在从moviepage中筛选出未被抓取的页面链接进行爬取,在抓取到电影详情页面时,只获取电影名,介绍,下载链接,并保存到movieinfo中。

以上就是大致的一个思路

目前我自己了解到的几个问题:

1、虽然采用了多线程,而且我也想了办法去限制线程数,但好像线程数量并没有被限制

2、采用了多线程,但因为调用MySQL写入数据,所有采用了几个mutex避免出现数据错乱,但好像非常影响效率。

3、为了方便,同时为了避免多线程获取游标(cur)的问题,所以每次调用MySQL时都会新建连接,在执行完后会关闭MySQL,但这导致一个bug,有时候MySQL会被提前关闭

4、最大的bug,在具体写的时候,我准备了两套写入数据方案,一个是写入MySQL中,一个是直接写到文档中。这个目前还有较多问题未有调试

5、潜在的bug,因为目标网页的子页面非常多,而且很多内容会有重叠,虽然能筛查重复元素,但抓取到后期的效率肯定会越来越低

因为在实际操作过程中需要反复调用数据库操作,所以把数据库操作单独写了一个文件

import os.path
import threading
import time
import json
import requests
from bs4 import BeautifulSoup
import pysql
import re
import random

# 现在的问题就是再使用mysql时,因为我每次调用后默认会关闭,再多线程时就会出现数据库提前被关闭,需要避免这种情况
# 有以下两种思路
# 1、每次调用mysql(pysql)时,给一个新的变量
# 2、直接取消默认关闭,但可能出现链接用户过多,无法链接的情况

# 0、从数据库moviepage表中获取url,如果数据表中没有则从输入的网页链接开始
# 1、登入网页、使用BeautifulSoup解析网页
# 2、分析网页信息,如果是普通页面,执行线路①,如果是电影详情页面执行线路②
    # 线路①
    # 1、获取网页所有超文本连接 获取信息包含(超链接文本,超链接url)
    # 2、比对数据库现有超文本连接,去重后将新的超文本链接存入数据库moviepage表

    # 线路②
    # 1、获取网页电影名称,电影简介,电影下载磁力链接等链接
    # 2、将获取的信息存入数据库movieinfo表

# 将所有步骤封装到一个类中,需要获取的信息有首次爬取的网页链接
class GetMovie(object):
    """本类中可以调用请求头headers,调用代理proxies,调用数据库mysql
    推荐使用数据库保存数据,在修改数据时效率高"""

    def __init__(self,url:str=None,headers_list:list=None,proxies_list:list=None):
        """
        本类中可以调用请求头headers,调用代理proxies,调用数据库mysql

        如果不传入请求头,则使用默认请求头,不传入代理,则默认不使用代理
        代理列表格式为['代理ip:代理port','代理ip:代理port'...]
        如需调用mysql写入数据,则在调用main()之前调用mysql()
        默认是返回文本数据,调用mysql则di
        """
        self.url = url
        self.headers_list= headers_list
        self.proxies_list = proxies_list
        self.headers = {'user-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                                      'AppleWebKit/537.36 (KHTML, like Gecko) Chrom'
                                      'e/86.0.4240.111 Safari/537.36 Edg/86.0.622.61',}
        self.proxies = None
        self.db = None
        self.db_statu = False  # 记录数据库开启情况,默认未开启
        self.url_list = []  # 后期实际爬取网页是从这个列表中获取url
        self.html_num = 0  # 用来记录爬取网页的数量
        self.movie_num = 0  # 用来记录爬取的电影链接数量

        self.deep_num = 0  # 记录爬取深度

        self.mutex_write_page = threading.Lock()  # 写入page数据的锁
        self.mutex_write_info = threading.Lock()  # 写入info数据的锁
        self.mutex_html_num = threading.Lock()  # 记录爬取页面数量的锁
        self.mutex_movie_num = threading.Lock()  # 记录爬取电影数量的锁
        self.mutex_write_is_read = threading.Lock()  # 修改状态的锁
        pass

    # 处理请求头 self.headers将从请求头列表中获得一个随机的请求头
    def __getHeaders(self):
        """责处理请求头,从请求头字典里面随机调出一个请求头,赋值给实例变量self.headers"""
        if self.headers_list:
            l = len(self.headers_list)
            i = random.randrange(l)
            self.headers= self.headers_list[i]
        pass

    # 处理代理  self.proxies将从代理列表中获得一个随机的代理
    def __getProxies(self):
        """处理代理ip和端口,并赋值给实例变量self.proxies"""
        if self.proxies_list:
            self.proxies = {}
            l = len(self.proxies_list)
            while True:
                i = random.randrange(l)
                self.proxies['http'] = self.proxies_list[i]
                # 利用的http://icanhazip.com/返回的IP进行校验,如返回的是代理池的IP,说明代理有效,否则实际代理无效
                try:
                    res = requests.get(url="http://www.hao6v.tv", timeout=2, headers=self.headers,proxies=self.proxies)
                    if res.status_code == 200:
                        with open("proxies_new.txt",'a+',encoding='utf8') as f:
                            f.write(f'{self.proxies}\n')
                        break
                    else:
                        print(f"1{self.proxies}代理IP无效!")
                        self.proxies_list.remove(i)
                        l -=1
                        continue
                except:
                    print(f"2{self.proxies}代理IP无效!")
                    continue
        pass

    # 处理数据库登入信息
    def mysql(self, host: str = '127.0.0.1', user:str = 'root',password: str = None,database: str = None,port:int = 0):
        """默认的数据库地址为本地数据库127.0.0.1,用户名为root,默认的端口为0"""
        self.db = pysql.PySql(host=host,user=user,password=password,database=database,port=port)
        self.db_statu = True
        pass

    # 从数据库或文档中获取爬取的链接列表
    def __urls(self):
        """
        默认使用的数据库工作表为moviepage moviepage表有id,name,link,is_read,is_del信息,
        name为超链接文本,link为超链接
        is_read为判断本条链接是否为已经爬取,默认为int(0),
        is_del为是否删除,默认为int(0)

        默认的moviepage文档是多行文本,每行数据是一个字典,字典包含id name link,is_read,is_del信息
        结果时创建一个爬取时所需要的url列表

        执行后三个结果:
            1、如果没有数据表或文档,则返回输入的url
            2、如果有数据表或文档,则查找符合条件的link
                ①如果没有符合则表示查询完成,返回空的url列表
                ②如果有则采用多线程继续爬取网页数据,返回url列表
        """
        # 1、判断是否启用数据库,启用则调用数据里面的信息获取url,否则从文本。如果文本或者数据没有信息,则为默认链接
        if self.db_statu:
            sql_s = """select id,link,is_read,is_del from moviepage where is_read = '0' and is_del = '0';"""
            db1 = self.db
            select_data = db1.select(sql_s)
            # 如果查询失败,返回None,则创建表并新增第一条数据,同时更新self.url_list的信息,把查询的链接放进去
            if select_data is None:
                print("--正在创建表--")
                sql_c = """create table moviepage(
                id int(11) auto_increment primary key,
                name varchar(100) not null,
                link varchar(500) not null,
                is_read enum('0','1') default '0',
                is_del enum('0','1') default '0');"""
                db1.create(sql_c)
                print("首次查询,查询链接为:%s" % self.url)
                sql_i = f"""INSERT INTO moviepage(name,link) VALUES('首页','{self.url}')"""
                db1.insert(sql_i)
                self.url_list.append(self.url)
            # 如果查询结果为空值,则返回一个空的self.url_list,后期用来判断查询完成
            elif select_data == tuple():
                self.url_list = []
            # 如果不为空,则将返回的元组进行遍历,获得的新元组通过切片获取url,把url添加到self.url_list中,方便再次调用查询
            else:
                for i in select_data:
                    self.url_list.append(i[1])

        else:
            if os.path.exists(r"moviepage.txt"):
                f = open(r'moviepage.txt','r')
                f_data = f.read().split('\n')
                for j in f_data[:-1]:  # 必须要切片,否则最后一个空值也会导入,再loads就会出错
                    i = json.loads(j.replace("\'","\""))
                    i = dict(i)
                    if i['is_read'] == '0' and i['is_del'] == '0':
                        self.url_list.append(i['link'])
                f.close()
            else:
                f = open(r'moviepage.txt','w+',encoding='utf8')
                f.write("{'id':%d,'name':'首页','link':'%s','is_read':'0','is_del':'0'}\n" %(self.html_num,self.url))
                f.close()
                self.url_list.append(self.url)
        pass

    # 定义获取网页的函数
    def __run(self,link):
        """本函数需要link是"""
        # 1、登入网页、使用BeautifulSoup解析网页
        self.mutex_html_num.acquire()
        self.html_num += 1
        self.mutex_html_num.release()
        count_html = requests.get(url=link, headers=self.headers, timeout=10, proxies=self.proxies)
        count_html.encoding = count_html.apparent_encoding
        if count_html.status_code == 200:
            print(f"{link}信息采集成功!")
            # 调用一个函数,用来标记已爬取的网页,将爬取后的网页链接状态标记为1,即is_read = '1'
            self.__write_is_read(link)
        else:
            print(f"{link}请求失败!错误码{count_html.status_code}")
        count = BeautifulSoup(count_html.text, 'lxml')
        # 2、分析网页信息,如果是电影详情页面执行线路①,如果是普通页面,执行线路②
        if "下载地址" in count.text:

            self.__saveMovieInfo(count)
        else:
            self.__saveMoviePage(count)

    # 更改链接爬取状态
    def __write_is_read(self,link):
        # 如果是数据库
        if self.db_statu:
            # 写入锁
            self.mutex_write_is_read.acquire()
            # 修改
            sql_u = f"""update moviepage set is_read = "1" where link = '{link}';"""
            db2 = self.db
            db2.update(sql_u)
            self.mutex_write_is_read.release()
        # 如果是文档
        else:
            # 写入锁
            self.mutex_write_is_read.acquire()
            # 获取文档全部内容
            f = open(r'moviepage.txt','w+',encoding='utf8')
            f_list = f.read().split("\n")
            f.write('')  # 清空文件
            f.close()
            # 遍历所所有行
            for e in f_list[:-1]:
                e = json.loads(e.replace("\'","\""))
                echo = dict(e)
                # 如果是link所在行
                if echo['link'] == link:
                    # 修改对应的is_read
                    echo['is_read'] = '1'
                else:
                    echo = echo
                # 写入新的moviepaga.txt文件中
                n_f = open(r'moviepage.txt','a+',encoding='utf8')
                n_f.write(str(echo)+'\n')  # 写入新的文件,并在结尾换行
                n_f.close()
            self.mutex_write_is_read.release()
        pass

    # 保存电影链接和信息
    def __saveMovieInfo(self,count):
        """movieinfo 有id name info link is_del"""
        # 0、先解析数据内容,将内容筛选获得name,info,link
        # 获取标题,通过soup的find功能查找第一个标签为‘title'
        name = count.find('title').string.split(',')[0]  # 标题
        # 详情介绍需要先定位到“内容介绍:",然后查找后面所有内容,再从下载
        content_regexp = re.compile(u'内容介绍')
        content = count.find(text=content_regexp)
        # 查找在内容介绍后面所有的内容,转成文字
        next_content = content.find_all_next('p')  # find_all和parents获取的内容都是以类似列表返回
        info = ''
        for i in next_content:
            info += (i.text+'\n')
        s = info.find(u'【下载地址】')
        info = info[:s].strip('\n')  # 内容介绍
        download_content = count.find('table')
        downloads = download_content.find_all('a', href=True)
        downloads_dict = {}
        for i in downloads:
            downloads_dict[i.text] = i['href']
        link = str(downloads_dict)  # 链接地址
        # 即将写入数据,加锁并将记录加1
        self.mutex_movie_num.acquire()
        self.movie_num += 1
        self.mutex_movie_num.release()
        # 1、判断数据库是否运行
        if self.db_statu:
            self.mutex_write_info.acquire()
            # 判断数据是否已经存在,已经存在则修改,否则新增
            sql_s = f"""select * from movieinfo where name = '{name}'"""
            db3 = self.db
            result = db3.select(sql_s)
            if result == None:
                print("--原表没有,正在新增--")
                sql_c = """create table movieinfo
                (id mediumint unsigned primary key auto_increment,
                name varchar(50) not null unique, 
                info text,
                link varchar(800) not null unique,
                is_del enum("0","1") not null default '0');"""
                db3.create(sql_c)
            elif result == tuple():
                sql_i = f"""insert into movieinfo(name,info,link) values ('{name}','{info}','{link}');"""
                db3.insert(sql_i)
            else:  # 已存在,升级数据
                sql_u = f"""update movieinfo set link = '{link}' where name = '{name}'"""
                db3.update(sql_u)
            self.mutex_write_info.release()
        # 保存为文档
        else:
            self.mutex_write_info.acquire()
            with open(r'movieinfo.txt','a+',encoding='utf8') as f:
                f.write(f"{'id':{self.movie_num},'name':'{name}','info':'{info}','link':'{link}','is_del':'0'}\n")
            self.mutex_write_info.release()
        pass

    # 保存普通页面的  后续爬取的链接都从这里产生,修改爬取的链接得修改这里
    def __saveMoviePage(self,count):
        # 0、解析网页内容,获取满足条件链接名称和url
        self.mutex_write_page.acquire()
        body = count.find('body')
        url_list = body.find_all('a', href=True)
        link_name_dict = {}
        pattern = '^https://'
        for urls in url_list:
            name = urls.text
            url = urls['href']  # 这里可能会爬取到不属于self.url为主页的链接,在下面会进行排除
            if re.match(pattern,url):
                link = url
                p = f'^{self.url}'
                if re.match(p,link):
                    link_name_dict[link] = name
            else:
                link = self.url+url
                if len(re.findall(":",link)) == 1:
                    link_name_dict[link] = name

        if self.db_statu:
            sql_s = """select name,link from moviepage;"""
            db4 = self.db
            s_r = db4.select(sql_s)
            if s_r is None:
                print("----savemoviepage----")
            elif s_r == tuple():
                print("查询结果为空")
            else:
                for name,link in s_r:
                    if link in link_name_dict.keys():  # 同一个url可能时不同名字,所以这里不能使用url反向指定name
                        link_name_dict.pop(link)

                for link_add,name_add in link_name_dict.items():
                    self.html_num+=1
                    sql_i = f"""insert into moviepage(name,link) values ('{name_add}','{link_add}');"""
                    db4.insert(sql_i)
        else:
            # 默认的moviepage文档是多行文本,每行数据是一个字典,字典包含id name link, is_read, is_del信息
            f = open(r'moviepage.txt','r')
            page_list = f.read().split("\n")
            f.close()
            for p in page_list[:-1]:
                p = json.loads(p.replace("\'","\""))
                page = dict(p)
                if page['link'] in link_name_dict.keys():
                    link_name_dict.pop(page['link'])
            wf = open(r'moviepage.txt','a+',encoding='utf8')
            for link,name in link_name_dict.items():
                self.html_num +=1
                wf.write(f"{'id':{self.html_num},'name':{name},'link':{link},'is_read':'0',is_del='0'}\n")
            wf.close()
        self.mutex_write_page.release()

        pass

    def write_in_mysql(self):
        pass

    def write_in_txt(self):
        pass

    # 主函数,递归调规自己,直至爬取完成
    def main(self,n:int=5):
        """n表示需要调用的线程数,默认为5"""
        if self.deep_num > 1:
            print("查询停止,共爬取了%d个页面,共收集了%d个电影下载链接,爬取深度为:%d轮!" % (self.html_num, self.movie_num,self.deep_num))
        else:
            self.__getHeaders()  # 获取请求头
            self.__getProxies()  # 获取代理
            self.__urls()  # 处理网页链接列表
            l = len(self.url_list)
            # 利用递归批量爬取,设置一个条件,满足条件自动跳出递归
            if l != 0:
                r = l // n
                # 调用__run()会运行爬取网页并保存数据
                for i in range(r + 1):
                    if i * n + n < len(self.url_list):
                        for link in self.url_list[i * n:i * n + n]:
                            threading.Thread(target=self.__run, args=(link,)).start()
                        while True:  # 必须设置主线程等待,否则限制线程数就相当于没用
                            time.sleep(0.5)
                            length = len(threading.enumerate())
                            print("当前子线程数:",length-1)
                            if length <= 1:
                                break
                    else:
                        for link in self.url_list[i * n:l]:
                            threading.Thread(target=self.__run, args=(link,)).start()
                        while True:
                            time.sleep(0.5)
                            length = len(threading.enumerate())
                            print("当前子线程数:",length-1)
                            if length <=1:
                                break
                self.deep_num +=1
                self.main(n=n)
            else:
                print("查询已完成,总共爬取了%d个页面,共收集了%d个电影下载链接,爬取深度为:%d轮!" % (self.html_num, self.movie_num,self.deep_num))


if __name__ == "__main__":
    proxy_list = []
    with open(r'proxies.txt',"r") as p:
        p = p.read().split("\n")
        for i in p:
            proxy_list.append(i.strip('\t'))

    headers_list = []
    with open(r'Headers.txt','r') as h:
        h = h.read().split("\n")
        for j in h:
            dic = {}
            dic['user-Agent'] = j.strip(",").strip("'")
            headers_list.append(dic)
    f = GetMovie(url='https://www.hao6v.tv',headers_list=headers_list)
    f.mysql(host='127.0.0.1',user='root',password='xx',database='xx')
    f.main(n=1)

 数据库操作代码,这个代码应该没啥问题,功能比较简单,主要是在上面需要重复调用,所以单独写了一个进行调用

import pymysql


# 设置5个功能
# 1、新增table
# 2、查询表内容
# 3、修改表内容
# 4、增加表内容
# 5、删除表内容

class PySql():
    def __init__(self,host: str = '127.0.0.1', user:str = 'root',password: str = None,database: str = None,port:int = 0):
        self.host = host
        self.user = user
        self.password = password
        self.database = database
        self.port = port
        self.db = None
        self.c_num = 0
        self.i_num = 0
        self.u_num = 0
        self.d_num = 0
        self.s_num = 0
        pass

    def __connect(self):
        try:
            self.db=pymysql.connect(host=self.host, user=self.user,
                                    password=self.password, database=self.database, port=self.port)
        except Exception as ret:
            print("数据库链接失败",ret)
            self.db = None
    
    # 通用操作,获取游标,执行数据库语句,commit
    def __generalOp(self,sql): 
        cur = self.db.cursor()
        cur.execute(sql)
        self.db.commit()
        
    def create(self,sql_c):
        """ sql-c格式为:create table xxx(...);"""
        self.__connect()
        try:
            self.__generalOp(sql_c)
            self.c_num +=1
            print("创建table成功%d" % self.c_num)
        except Exception as ret:
            print(f"创建{sql_c}失败,请检查语句或查看ret",'\nret:',ret)
        finally:
            self.db.close()
        pass

    # 查询不需要commit,但一般需要获取查询内容,所以设置了一个return
    def select(self,sql_s):
        """ sql_s格式为:select * from xxx where xxx and/or xxx;"""
        self.__connect()
        try:
            cur = self.db.cursor()
            cur.execute(sql_s)
            result = cur.fetchall()
            self.s_num +=1
            print("查询成功%d" % self.s_num)
            return result
        except Exception as ret:
            print(f"查询失败{sql_s},请检查语句或查看ret", '\nret:', ret)
            return None
        finally:
            self.db.close()
        pass

    def update(self,sql_u):
        """ sql_u格式为:update xxx set xx = x where yy = y...;"""
        self.__connect()
        try:
            self.__generalOp(sql_u)
            self.u_num+=1
            print("修改成功%d" % self.u_num)
        except Exception as ret:
            print(f"修改{sql_u}失败,请检查语句或查看ret", '\nret:', ret)
        finally:
            self.db.close()
        pass

    def insert(self,sql_i):
        """sql_i格式为:insert into xxx(xx,xx,xx) values (x,x,x),(x,x,x)..."""
        self.__connect()
        try:
            self.__generalOp(sql_i)
            self.i_num +=1
            print("新增成功%d" % self.i_num)
        except Exception as ret:
            print(f"新增{sql_i}失败,请检查语句或查看ret", '\nret:', ret)
        finally:
            self.db.close()
        pass

    def delete(self,sql_d):
        """sql_d格式为:delete from xxx where yy..."""
        self.__connect()
        try:
            self.__generalOp(sql_d)
            self.d_num +=1
            print("删除成功%d" % self.d_num)
        except Exception as ret:
            print(f"删除{sql_d}失败,请检查语句或查看ret", '\nret:', ret)
        finally:
            self.db.close()
        pass

经过自己测试,目前这个代码在子线程只有1的时候,可以正确调用mysql写入数据,但线程变多后就会出现数据库被提前关闭的错误。

直接用文本写入数据的问题目前还没有修复

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值