Python 网络数据采集(三):采集整个网站

 作者:高玉涵
 时间:2022.5.30 15:35
 博客:blog.csdn.net/cg_i

心急吃不了热豆腐。

1.采集整个网站

上一节中,实现了在示例网站以遍历方式从一个链接跳到另一个链接。但是,如果你需要要系统地把整个网站按目录分类,或者要搜索网站上的每一个页面,怎么办?那就得采集整个网站,那是一种非常耗费内存资源的过程,尤其是处理大型网站时,最合适的工具就是用一个数据库来储存采集的资源。但是,我暂时并不打算大规模地运行它们。关于数据库使用的相关知识,将放在后面适当的章节里讨论。

 一个常用的费时的网站采集方法就是从顶级页面开始(比如主页),然后搜索页面上的所有链接,形成列表。再去采集这些链接的每一个页面,然后把在每个页面上找到的链接形成新的列表,重复执行这一轮采集。

 为了避免一个页面被采集两次,链接去重是非常重要的。在代码运行时,把已发现的所有链接都放在一起,并保存在方便查询的列表里(下文示例指 Python 的集合 set 类型)。只有“新”链接才会被采集,之后再从页面中搜索其他链接:

from urllib.request import urlopen
from urllib.error import HTTPError
from bs4 import BeautifulSoup

pages = set()

def getLinks(pageUrl):
	global pages
	html = urlopen(pageUrl)
	bsObj = BeautifulSoup(html.read(), 'html.parser')
	for link in bsObj.findAll('a'):
		if 'href' in link.attrs:
			if link.attrs['href'] not in pages:
				newPage = link.attrs['href']
				print(newPage)
				pages.add(newPage)
				getLinks(url)
				
getLinks('https://free.kuaidaili.com')

 为了全面地展示这个网络数据采集示例是如何工作的,我降低了在前面例子里使用的“只寻找(/free/inha)路径“的标准。不再限制爬虫采集的页面范围,只要遇到页面就查找所有 <a>的标签。

 一开始,用 getLinks 处理主页。然后,遍历首页上每个链接,并检查是否已经在全局变量集合 pages 里面了(已经采集的页面集合)。如果不在,就打印到屏幕上,并把链接加入 pages 集合,再用 getLinks 递归地处理这个链接。

 输出结果:

/
/free/
/pricing/
/dps
/tps
/kps
/ops
/cart?p=day&t=dps&c=10&num=2500
/usercenter/dps/
/dps/fetch/
/dps/genapiurl/
/doc/dev/dps/#api
/doc/dev/dps/
/cart?p=month&t=tps&ct=dynamic
/usercenter/tps/
/tps/genapiurl/
/doc/dev/tps/#api
/doc/dev/tps/
/cart?p=month&t=kps_std&num=1
/usercenter/kps/
/kps/fetch/
/kps/genapiurl/
/doc/dev/kps/#api
/doc/dev/kps/
/cart?p=month&t=vip
/usercenter/ops/
/fetch/
/genapiurl/
/doc/api/getproxy/
/dist/
/helpcenter/
/doc/dev/quickstart/
/doc/dev/dps/#_3
/doc/dev/dps/#httpresponse
/doc/dev/dps/#_5
/doc/dev/sdk/
/doc/dev/sdk_http/
/doc/dev/tps/#_3
/doc/dev/tps/#httpresponse
/doc/dev/tps/#_5
/doc/dev/sdk_tps_http/
/doc/dev/kps/#_3
/doc/dev/kps/#httpresponse
/doc/dev/kps/#_5
/doc/dev/sdk_api_proxy/#_3
/doc/dev/ops/#_3
/doc/api/#2-api
/doc/dev/sdk_api_proxy/#_1
/doc/faq/buy/
/doc/faq/product/
/doc/faq/recharge/
/doc/faq/invoice/
javascript:void(0)
/support/
/entcustomer/
/changelog/
/tool/fetchua/
/cps/
/usercenter/
/login/
......(省略)

关于递归的警告

Python 默认的递归限制(程序递归地自我调用次数)是 1000 次。当程序达到递归限制后会停止(报错,RecursionError: maximum recursion depth exceeded in comparison),除非你设置一个较大的递归计数器,或者其他手段不让它停止。

解决办法:

可以将递归的深度修改的大一些,但还是不建议这样做。

import sys
sys.setrecursionlimit(100000) #例如这里设置为十万 
2. 完整代码
'''
	作者:高玉涵
	时间:2022.5.27 13:20
	说明:抓取所有网页(爬虫版)
'''
import socket
import re
from tkinter.messagebox import NO
from types import coroutine
from typing import Counter
from urllib.request import urlopen
from urllib.parse import urljoin
from urllib.error import HTTPError
from bs4 import BeautifulSoup


def getTable(bsObj:BeautifulSoup):
	try:
		table = bsObj.table
	except AttributeError as e:
		return None
	return table

def getAgentData(table:BeautifulSoup):
	# 抓取到代理数据
	agent_data = []
	# 表头
	theads = []
	# 获取表头
	theads = getThead(table)

	try:
		# 获取所有行
		rows = table.findAll('tr')
	except AttributeError as e:
		print("TR 标签未找到!")
		return None
	else:
		for row in rows:
			# 存放代理信息
			agent = {}
			for t in theads:
				# 逐行查找与列对应的数据
				text = row.find('td', {'data-title':t})
				if text is not None:
					agent.setdefault(t, text.get_text())
			if len(agent) != 0:
				agent_data.append(agent)
		return agent_data
	

def getThead(table:BeautifulSoup):
	# 存放获取的表头值
	theads = []
	
	try:
		# 遍历表格头子标签
		for h in table.thead.tr.children:
			# 提取标签内的值去掉前后空格
			text = h.get_text().replace(" ","")
			# 忽略不可见的换行符
			if text != '\n':
				theads.append(text)
	except AttributeError as e:
		print("TR 标签未找到!")
		return None
	else:
		return theads


def getUrl(url:str):
	'''
		获取 URL 
		:param url 地址
		:return 返回 bs 对象
	'''
	try:
		html = urlopen(url, timeout=1)
	except HTTPError as e:
		return None
	except socket.error as e:
		return None
	
	try:
		bsObj = BeautifulSoup(html.read(), 'html.parser')
	except AttributeError as e:
		return None
	except ValueError:
		return None
	return bsObj


def getMorePages(base_url:str, relative_url:str=""):
	'''
		获取更多页面
		:param base_url 基本 URL 地址
		:param relative_url 相对路径 URL
		:return None 失败
	'''
	global pages 			# 采集过的页面
	global agent_list 		# 保存采集到的代理数据
	global pages_error_count# 访问出错页面计数器
	agents = {}				# 代理结构
	
	# 拼接 URL
	url = urljoin(base_url, relative_url)
	bsObj = getUrl(url)
	if bsObj == None:
		pages_error_count += 1 # 失败页面计数
		return None

	for link in bsObj.findAll('a'):
		if 'href' in link.attrs:
			# 是新的链接
			if link.attrs['href'] not in pages:
				getMorePages(url, link.attrs['href'])
				# 打开链接指向的页面
				newUrl = urljoin(base_url, link.attrs['href'])
				newObj = getUrl(newUrl)
				if newObj == None:
					pages.add(link.attrs['href'])	# 保存处理过的页面
					pages_error_count += 1  # 失败页面计数
					continue				# 返回到循环开始处
				
				# 在打开的新页面里查找符合的数据
				table = getTable(newObj)
				if table == None:
					pages.add(link.attrs['href'])	# 保存处理过的页面
					pages_error_count += 1	# 失败页面计数
					continue				# 返回到循环开始处
				
				# 保存采集的数据
				agents = getAgentData(table)
				agent_list.extend(agents)
				pages.add(link.attrs['href'])	# 保存处理过的页面
				print(f"页面 {newUrl} 成功采集 {len(agent_list)} 条数据,忽略 {pages_error_count} 个页面。")
				
if __name__ == '__main__':
	pages = set()			# 处理过的页面
	agent_list = []			# 保存采集到的代理数据
	pages_error_count = 0	# 访问出错页面计数器

	base_url = 'https://free.kuaidaili.com/free/inha'
	print(f"目标:{base_url}")
	user_choice = input('是否继续(y/n):')
	if user_choice not in('y','Y'): 
		exit(0)
	getMorePages(base_url)

 输出结果:

目标:https://free.kuaidaili.com/free/inha
是否继续(y/n):y
页面 https://free.kuaidaili.com/free/ 成功采集 15 条数据,忽略 3 个页面。
页面 https://free.kuaidaili.com/free/inha/ 成功采集 30 条数据,忽略 154 个页面。
页面 https://free.kuaidaili.com/free/intr/ 成功采集 45 条数据,忽略 155 个页面。
页面 https://free.kuaidaili.com/free/inha/1/ 成功采集 60 条数据,忽略 158 个页面。
页面 https://free.kuaidaili.com/free/inha/2/ 成功采集 75 条数据,忽略 159 个页面。
页面 https://free.kuaidaili.com/free/inha/3/ 成功采集 90 条数据,忽略 160 个页面。
页面 https://free.kuaidaili.com/free/inha/4/ 成功采集 105 条数据,忽略 161 个页面。
页面 https://free.kuaidaili.com/free/inha/5/ 成功采集 120 条数据,忽略 162 个页面。
......(省略)
3. 下一节,继续优化这个“爬虫”程序

 这个 for 循环内调用递归的"爬虫"效率低的惊人且极易出错,你可能还发现偶而会丢失一点儿数据(或重复采集),下一节中我们将针对这些问题展开讨论。

参见

Python 采集网络 数据(一):BeautifulSoup

Python 网络数据采集(二):抓取所有网页

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值