本文仅为学习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写入数据,但线程变多后就会出现数据库被提前关闭的错误。
直接用文本写入数据的问题目前还没有修复