文章采集api

文章采集api

数据治理之数字画像

采集交流优采云 发表了文章 • 0 个评论 • 160 次浏览 • 2022-07-10 02:00 • 来自相关话题

  数据治理之数字画像
  00引言
  随着全网步入大数据时代,企业的目光日益聚焦在利用大数据服务精细化营销、精细化运营上,各类客户画像、员工画像理论如雨后春笋般兴起,而数据应用的底层——数据治理,却鲜有整体的理论体系。如何避免治理工作自身“无的放矢”,如何量化数据基础建设的贡献,我们需要为数据治理工作描绘一张“数字画像”。这个命题的内涵外延非常丰富,在此我们选取用户体验、架构质量两个角度进行讨论。
  01用户体验的数字画像
  基于不同的感知角度,将用户分为外部客户、内部用户、管理层、技术人员四类,针对特定的业务场景刻画四类使用者所体会到的“科技赋能”。
  1、外部客户
  功能体验指标:功能体验指标用于衡量操作平台的易用性及直观程度。可以通过各类埋点,对用户的点击行为、页面停留时间、页面浏览深度进行跟踪。从而挖掘用户常使用的功能,探究的实际需要,对于常用功能可以开展功能的改版优化,进行同业产品比较、用户反馈调查等,重点关注主要功能的流畅度、实用性。
  平台服务指标:
  (1)服务平台一般利用API接口向外提供数据,因此,通过计算API调用率可以计算出其向外输出服务的活跃程度。
  (2)由数据服务带来的产品升值也是需要衡量的一大重要指标。营销、运营等商业活动价值提升以一定的比例分配给其相应的数据治理工作,从业务部门有感的角度评估数据治理工作对营销等活动的赋能。
  2、内部用户
  
  便利性:过去业务部门向数据管理部门申请使用数据,通常使用邮件或行政流程的方式,这种方法无法实时跟踪申请进度,也无法在统一的页面集中管理,导致相关工作人员在查询与沟通上花费了大量精力。建立自动化、规范化流程以及线上运营工具,将极大地便利流程,因此,手工提取工单压降比率可以作为度量内部用户程度的指标。
  时效性:线上化数据治理意味着资产地图、标准架构即存放在用户指尖,关键作业的全链路交付时间是触达内部用户的另一直观感受。该指标可以通过统计各节点的流转时间,计算相应平均值获得。
  贡献度:不仅是底层的数据管理,数据的应用输出同样能够为用户带来实际业务价值。BI工具的使用、模型提供数量等指标标志着用户对于应用类数据成果的满意度。
  3、管理层
  质量提升:对于管理层而言,保障数据仓库、数据湖的“清澈”是他们关心的问题。由于监管报送结果是银保监对银行的重点考核指标,报送规定的达标率成为对于管理层数据治理成效最直观的反映。基于DQC的一系列指标同样可作为面向管理层的数据清洁度体现。
  效率提升:除监管要求之外,数据运营成本对于全行管理也是至关重要的。只有建立规范和高效的数据架构,压降数据报表,降低储存、运维成本,才能实现精细化营运,维持高效率盈利。
  4、技术人员
  数据字典评分:当企业实施开发过程强管控时,数据字典的角色可看作是法律之于社会,其整体逻辑必须经得起反复推敲。在数据字典的查询页面设立评分反馈是一种简单但行之有效的方法。页面上有计划的引导,反映设计者关注开发人员的使用体验,从而让“吐槽”变成建议,优化和解决使用数据字典时遇到的问题。
  02架构质量的数字画像
  全行统一的数据架构应在追求高效率的同时降低成本,根据《华为数据之道》中信息架构的经典四范式,我们将从模型、分布、标准、资产四个角度对架构赋能能力进行度量。
  1、模型
  公共层加工频率:公共层中存放有事实数据、维表数据等等,它们支撑着指标体系中的一级指标层。在建立指标时,将规范化、集约化,提高公共指标的复用性,减少重复加工的操作,故公共层数据模型的复用率可作为公共层架构评估的指数之一。
  
  应用层引用频率:类似于人际关系网络拓扑结构中的核心人物算法,该指数直接衡量应用层中数据的系统性重要程度,引导资产盘点的目标。数据血缘关系是一种有向的、无权值、无自环的网络图。被引用频率高的资产一般来源于关键业务实体中最准确和最及时的业务记录。这一些资产被跨部门、跨业务领域调用的概率最大,需要实现所有部门可访问并且访问到相同的数据。该指数还能够有效地筛选出“孤儿表”、临时表,减少资源投入和储存成本。
  2、分布
  数据覆盖:对于大型银行而言,数以百计的系统,数以万计的库表在全国范围内分布式储存。采集是资产盘点的第一步,测量采集数在全量系统的覆盖率帮助我们明确当前采集的进度,定位未采集的数据来源。
  数据冗余:数据冗余指同层数据的冗余,具体可分为两个来源。第一,多个物理位置中存储了相同意义的数据;第二,架构模型本身在设计上有较多的重复交叉项。
  数据容量:数据容量是对数据中台的整体描述,它包括当前中台所囊括的整体数据体量的绝对值,也包含该体量随时间的增长比例。数据容量并非越高或者越低更理想,它需要结合银行的现状辩证性地看待。
  3、标准
  标准稳定性:数据标准规范化了数据含义、结构等等,应当满足内容统一、不交叉定义等条件,避免数据标准内部发生“数据打架”。
  标准落标率:在标准的技术规范完备,主题齐全,标准已权威发布的前提下,标准落标率反映了数据标准“最后一公里”的执行情况。借助自动化工具,能够计算出各类分层、切片后的数据落标率,智能化地发现落标潜在问题。
  4、资产
  技术元数据统计:技术元数据打通了源数据和,记录了数据从产生到消亡的过程。我们从中挑选出系统覆盖率、系统内表级覆盖率、表名以及字段名的有效率、枚举值的有效率等统计指标表示数据架构中技术类资产的产出效益。
  企业活动命中率:数据资产是从业务流程、业务模型中抽取出来的数字化描述。标签资产对业务行为的命中率、指标资产对报表统计的命中率、报表资产的用户访问量等数值越高,代表着资产内容映射企业活动的准确度越高。
  03结语
  伴随着企业数字化转型不断深入,“数据治理的数字画像”从方法论到实践都将趋于完善,内容价值、安全性能、用户体验也会随之提高。如何动态地衡量数据治理工作成效,建立适合自身企业的“北极星指标”,是每一家处于智慧转型阶段的公司所必须研究的,它的成功将创造出不可估量的商业价值。 查看全部

  数据治理之数字画像
  00引言
  随着全网步入大数据时代,企业的目光日益聚焦在利用大数据服务精细化营销、精细化运营上,各类客户画像、员工画像理论如雨后春笋般兴起,而数据应用的底层——数据治理,却鲜有整体的理论体系。如何避免治理工作自身“无的放矢”,如何量化数据基础建设的贡献,我们需要为数据治理工作描绘一张“数字画像”。这个命题的内涵外延非常丰富,在此我们选取用户体验、架构质量两个角度进行讨论。
  01用户体验的数字画像
  基于不同的感知角度,将用户分为外部客户、内部用户、管理层、技术人员四类,针对特定的业务场景刻画四类使用者所体会到的“科技赋能”。
  1、外部客户
  功能体验指标:功能体验指标用于衡量操作平台的易用性及直观程度。可以通过各类埋点,对用户的点击行为、页面停留时间、页面浏览深度进行跟踪。从而挖掘用户常使用的功能,探究的实际需要,对于常用功能可以开展功能的改版优化,进行同业产品比较、用户反馈调查等,重点关注主要功能的流畅度、实用性。
  平台服务指标:
  (1)服务平台一般利用API接口向外提供数据,因此,通过计算API调用率可以计算出其向外输出服务的活跃程度。
  (2)由数据服务带来的产品升值也是需要衡量的一大重要指标。营销、运营等商业活动价值提升以一定的比例分配给其相应的数据治理工作,从业务部门有感的角度评估数据治理工作对营销等活动的赋能。
  2、内部用户
  
  便利性:过去业务部门向数据管理部门申请使用数据,通常使用邮件或行政流程的方式,这种方法无法实时跟踪申请进度,也无法在统一的页面集中管理,导致相关工作人员在查询与沟通上花费了大量精力。建立自动化、规范化流程以及线上运营工具,将极大地便利流程,因此,手工提取工单压降比率可以作为度量内部用户程度的指标。
  时效性:线上化数据治理意味着资产地图、标准架构即存放在用户指尖,关键作业的全链路交付时间是触达内部用户的另一直观感受。该指标可以通过统计各节点的流转时间,计算相应平均值获得。
  贡献度:不仅是底层的数据管理,数据的应用输出同样能够为用户带来实际业务价值。BI工具的使用、模型提供数量等指标标志着用户对于应用类数据成果的满意度。
  3、管理层
  质量提升:对于管理层而言,保障数据仓库、数据湖的“清澈”是他们关心的问题。由于监管报送结果是银保监对银行的重点考核指标,报送规定的达标率成为对于管理层数据治理成效最直观的反映。基于DQC的一系列指标同样可作为面向管理层的数据清洁度体现。
  效率提升:除监管要求之外,数据运营成本对于全行管理也是至关重要的。只有建立规范和高效的数据架构,压降数据报表,降低储存、运维成本,才能实现精细化营运,维持高效率盈利。
  4、技术人员
  数据字典评分:当企业实施开发过程强管控时,数据字典的角色可看作是法律之于社会,其整体逻辑必须经得起反复推敲。在数据字典的查询页面设立评分反馈是一种简单但行之有效的方法。页面上有计划的引导,反映设计者关注开发人员的使用体验,从而让“吐槽”变成建议,优化和解决使用数据字典时遇到的问题。
  02架构质量的数字画像
  全行统一的数据架构应在追求高效率的同时降低成本,根据《华为数据之道》中信息架构的经典四范式,我们将从模型、分布、标准、资产四个角度对架构赋能能力进行度量。
  1、模型
  公共层加工频率:公共层中存放有事实数据、维表数据等等,它们支撑着指标体系中的一级指标层。在建立指标时,将规范化、集约化,提高公共指标的复用性,减少重复加工的操作,故公共层数据模型的复用率可作为公共层架构评估的指数之一。
  
  应用层引用频率:类似于人际关系网络拓扑结构中的核心人物算法,该指数直接衡量应用层中数据的系统性重要程度,引导资产盘点的目标。数据血缘关系是一种有向的、无权值、无自环的网络图。被引用频率高的资产一般来源于关键业务实体中最准确和最及时的业务记录。这一些资产被跨部门、跨业务领域调用的概率最大,需要实现所有部门可访问并且访问到相同的数据。该指数还能够有效地筛选出“孤儿表”、临时表,减少资源投入和储存成本。
  2、分布
  数据覆盖:对于大型银行而言,数以百计的系统,数以万计的库表在全国范围内分布式储存。采集是资产盘点的第一步,测量采集数在全量系统的覆盖率帮助我们明确当前采集的进度,定位未采集的数据来源。
  数据冗余:数据冗余指同层数据的冗余,具体可分为两个来源。第一,多个物理位置中存储了相同意义的数据;第二,架构模型本身在设计上有较多的重复交叉项。
  数据容量:数据容量是对数据中台的整体描述,它包括当前中台所囊括的整体数据体量的绝对值,也包含该体量随时间的增长比例。数据容量并非越高或者越低更理想,它需要结合银行的现状辩证性地看待。
  3、标准
  标准稳定性:数据标准规范化了数据含义、结构等等,应当满足内容统一、不交叉定义等条件,避免数据标准内部发生“数据打架”。
  标准落标率:在标准的技术规范完备,主题齐全,标准已权威发布的前提下,标准落标率反映了数据标准“最后一公里”的执行情况。借助自动化工具,能够计算出各类分层、切片后的数据落标率,智能化地发现落标潜在问题。
  4、资产
  技术元数据统计:技术元数据打通了源数据和,记录了数据从产生到消亡的过程。我们从中挑选出系统覆盖率、系统内表级覆盖率、表名以及字段名的有效率、枚举值的有效率等统计指标表示数据架构中技术类资产的产出效益。
  企业活动命中率:数据资产是从业务流程、业务模型中抽取出来的数字化描述。标签资产对业务行为的命中率、指标资产对报表统计的命中率、报表资产的用户访问量等数值越高,代表着资产内容映射企业活动的准确度越高。
  03结语
  伴随着企业数字化转型不断深入,“数据治理的数字画像”从方法论到实践都将趋于完善,内容价值、安全性能、用户体验也会随之提高。如何动态地衡量数据治理工作成效,建立适合自身企业的“北极星指标”,是每一家处于智慧转型阶段的公司所必须研究的,它的成功将创造出不可估量的商业价值。

文章采集api Zabbix 任性,文末送书 X 5

采集交流优采云 发表了文章 • 0 个评论 • 117 次浏览 • 2022-07-02 05:59 • 来自相关话题

  文章采集api Zabbix 任性,文末送书 X 5
  Zabbix版本不断升级,以满足日益增长的用户需求,支持高可用HA,k8s、指标topN、机器学习、定制前端品牌logo等!
  旧版本需要脚本才实现的功能,升级至最新版本可轻松解决!Zabbix6.0为业务服务提供商、DevOps和ITOps团队提供了附加值,优化了整体监控工作流程,并在许多不同层面提供了新见解。
  目录
  业务服务监控达到全新高度
  高阶业务服务SLA计算逻辑
  通过根因分析增强业务服务监控能力
  开箱即用的Zabbix server高可用群集
  机器学习
  Kubernetes监控
  详细高效的Zabbix审计日志模式
  可视化数据的新方法
  Zabbix性能优化
  提升Zabbix Agent2模块化,新的Zabbix Agent 监控项和功能
  原生TLS/SSL网站证书监控
  通用性改进
  通过自定义密码复杂程度要求来保护您的Zabbix登录
  支持定制前端展示品牌logo
  新增模板和集成
  其它新功能和优化
  01
  BMS业务服务监控达到全新高度
  优化Services部分,显示业务服务的状态和当前SLA级别
  通过对现有Services页面和功能的重大改进和优化,业务服务监控提升到了一个新高度。业务服务监控功能(BSM)非常适合多组件服务场景,例如服务器群集、负载平衡器和其它具有冗余组件的服务。
  Zabbix 6.0提供多种功能自定义业务服务树实现BMS业务服务监控:
  • 重新设计 Zabbix 6.0 Services页面和功能
  • 支持单个Zabbix实例监控超过10万个业务服务
  • 支持新的灵活服务状态计算逻辑
  • 能够自定义业务服务的访问权限
  • 能够为特定业务服务自定义只读和读写权限
  • 业务服务权限既可以基于显式服务列表,也可以基于服务标签的访问限制
  • 导出和导入业务服务树
  • 新的Service动作类型能让用户接收告警并对业务服务状态更改作出反应
  02
  高阶业务服务SLA计算逻辑
  提供大量可供选择的服务状态计算规则,能支持灵活的服务定义
  业务服务状态计算逻辑在Zabbix 6.0中得到了极大扩展,增加了许多新功能,例如:
  • 能够为每项业务服务分配权重
  • 仅当N个子服务都处于X严重级别的问题状态时才更改状态
  • 对处于问题状态下的子服务的权重进行分析并作出反应
  • 仅当特定百分比的子服务处于问题状态时才作出反应
  • 其它计算规则
  用户还可以自定义和访问指定服务的SLA报告。
  03
  通过根因分析增强业务服务监控能力
  根因问题会立即显示在service下
  对业务服务执行根因分析。利用根因分析功能找出可能导致业务服务SLA下降的潜在问题列表:
  • 在Zabbix前端Services页面查看根因问题列表
  • 接收告警中的根因问题列表
  • 通过Zabbix API收集根因问题信息
  04
  开箱即用的Zabbix server高可用群集
  在系统信息组件中跟踪集群集节点状态
  Zabbix server高可用防止硬件故障或计划维护期的停机:
  • 原生选择加入HA群集配置
  • 定义一个或多个备用节点
  
  •实时监控Zabbix server群集节点的状态
  • 不需要外部工具即可将Zabbix server配置为HA群集模式
  05
  机器学习
  使用新函数对意外异常率或与指标基准的偏差做出反应
  新的基线监控和异常检测趋势功能以动态方式检测问题,而不是静态阈值方式:
  •新的趋势函数-baselinewma and baselinedev ,能计算指标基线和偏离值
  •新的趋势函数-trendstl,能检测异常指标行为
  •能够指定异常检测偏差算法及季节性
  06
  Kubernetes监控
  Zabbix 6.0 LTS添加了多个新模板,用于监控不同的Kubernetes组件
  Zabbix 6.0 LTS新增Kubernetes监控功能,可以在Kubernetes系统从多个维度采集指标:
  •Kubernetes节点和pods的自动发现和监控
  •无代理方式采集Kubernetes pods和节点的信息
  •获取Kubernetes节点主机高水平信息
  Kubernetes监控还能够监控Kubernetes组件,例如
  •kube-controller-manager
  •kube-proxy
  •kube-apiserver
  •kube-scheduler
  •kubelet
  07
  详细高效的Zabbix审计日志模式
  重新设计的审计日志能提供全新的详细信息,并优化筛选功能。
  新的审计日志模式允许用户对Zabbix前端、Zabbix API和Zabbix server记录执行详细审计。通过修改审计日志,对Zabbix实例执行的所有更改都将记录在审计日志中:
  •创建、修改或删除新对象
  •通过LLD发现新实体
  •API命令
  •定期登录/退出
  •Zabbix实例中发生的所有其它事情
  新的审计日志模式在设计时考虑了最佳性能,因此扩展的功能不会影响Zabbix实例的性能。审计日志模式的工作是一项持续的工作,会在后续Zabbix发布周期中持续进行。
  08
  可视化数据的新方法
  主机排序组件可显示按监控项值排序的前N个或后N个主机的列表
  Zabbix 6.0新增的构件提供了展示信息的许多新方法。
  •地理地图构件能在地图上显示主机和问题
  •数据表构件能创建有关主机指标状态的摘要视图
  •数据表构件的前N和后N函数能展示最高或最低的监控项值
  •单一监控项构件能展示单个指标的值
  •对现有矢量图的许多改进,例如新的矢量图类型、引用单一监控项等
  •SLA构件能显示特定业务服务的当前SLA
  09
  Zabbix性能优化
  针对不同的Zabbix组件进行多项性能优化:
  •提升链接模板时的性能
  •提升Zabbix proxy性能和内存使用率
  历史数据表使用主键,这有多种好处,例如:
  •提高Zabbix server和Zabbix前端的性能
  •减少历史数据表的大小
  10
  提升Zabbix Agent2模块化,
  新的Zabbix Agent 监控项和功能
  优化的Zabbix agent现在能够开箱即用监控一组指标
  Zabbix 6.0为Zabbix Agent和Agent2提供了一套新的监控项。支持以下功能:
  •获取额外文件信息,如文件所有者和文件权限
  •采集agent主机元数据作为指标
  
  •计数匹配的TCP/UDP sockets
  某些已有的监控项支持新的功能:
  •vfs.fs.discovery-在Windows上添加了对{#FSLABEL}宏的支持
  •vfs.fs.get-在Windows上添加了对{#FSLABEL}宏的支持
  • vfs.file.size-添加了一个新的模式参数。设置以字节数或行数为单位
  Zabbix Agent2现在支持加载独立插件,而无需重新编译Agent2。
  11
  原生TLS/SSL网站证书监控
  使用新的Zabbix agent2 监控项监控SSL/TLS证书
  支持使用新的Zabbix agent 2监控项来监控SSL/TLS证书。监控项可用于验证TLS/SSL证书,并提供其它证书详细信息。
  12
  通用性改进
  通过优化的创建主机UI,使创建新主机从未如此简单
  Zabbix 6.0使Zabbix配置工作流程更精简!Zabbix用户现在可直接在Monitoring页面创建主机和监控项:
  •直接从Monitoring -Hosts页面创建主机
  •直接从Monitoring -Latest data页面创建监控项
  •删除了Monitoring -Overview页面。为了改善用户体验,现在只能通过仪表盘构件访问触发器和数据概览功能。
  现在将根据监控项的键值自动选择监控项的默认信息类型。
  拓扑图标签和图形名称中的简单宏已替换为表达式宏,以确保与新的触发器表达式语法一致。
  13
  通过自定义密码复杂程度要求
  来保护您的Zabbix登录
  设置密码复杂程度确保前端登录安全
  Zabbix超级管理员现在能够定义密码复杂程度要求。现在可以:
  •设置最小密码长度
  •定义密码字符要求
  •通过禁止使用最常见的密码字符串来降低字典攻击的风险。
  14
  支持定制前端展示品牌logo
  定制Zabbix实例代表您的公司。将现有的Zabbix品牌和帮助页面URL替换为您自己的公司品牌和自定义网站URL。
  改名功能不会违反Zabbix许可协议-可以自由更换Zabbix品牌!
  15
  新增模板和集成
  Zabbix 6.0为最受欢迎的供应商提供了许多新模板:
  •f5 BIG-IP
  •Cisco ASAv
  •HPE ProLiant servers
  •Cloudflare
  •InfluxDB
  •Travis CI
  •Dell PowerEdge
  Zabbix 6.0还带来了一个新的Github webhook集成,能基于Zabbix问题或恢复事件生成Github问题!
  所有官方的Zabbix模板现在都是独立的,不需要依赖导入其他模板。
  请查看当前可用集成的完整列表。
  16
  其它新功能和优化
  更多改进功能(部分):
  •使用新聚合函数计数返回值或匹配监控项的数量-count和item_count函数
  •在未配置交换空间的情况下提升system.swap监控项行为
  •使用新的单调历史函数检测连续增加或减少的值
  •支持两个新的Prometheus预处理标签匹配运算符!= 及 !~
  •当从构件链接导航到列表样式页面时,构件显示能更可靠地转换为不同的筛选器选项
  •使用新配置参数ListenBacklog为Zabbix server、Zabbix proxy、Zabbix agent配置TCP队列中挂起连接的最大数量
  •文档页面字体和可读性的改进
  •调整许多现有模板和修复小bug
  •新增utf8mb4作为受支持的MySQL字符集和校对集
  •新增对Webhook的额外HTTP方法的支持
  •对Zabbix命令行工具的超时设置
  Zabbix官方首本工具书《Zabbix监控系统之深度解析和实践》现已出版,欢迎阅读。 查看全部

  文章采集api Zabbix 任性,文末送书 X 5
  Zabbix版本不断升级,以满足日益增长的用户需求,支持高可用HA,k8s、指标topN、机器学习、定制前端品牌logo等!
  旧版本需要脚本才实现的功能,升级至最新版本可轻松解决!Zabbix6.0为业务服务提供商、DevOps和ITOps团队提供了附加值,优化了整体监控工作流程,并在许多不同层面提供了新见解。
  目录
  业务服务监控达到全新高度
  高阶业务服务SLA计算逻辑
  通过根因分析增强业务服务监控能力
  开箱即用的Zabbix server高可用群集
  机器学习
  Kubernetes监控
  详细高效的Zabbix审计日志模式
  可视化数据的新方法
  Zabbix性能优化
  提升Zabbix Agent2模块化,新的Zabbix Agent 监控项和功能
  原生TLS/SSL网站证书监控
  通用性改进
  通过自定义密码复杂程度要求来保护您的Zabbix登录
  支持定制前端展示品牌logo
  新增模板和集成
  其它新功能和优化
  01
  BMS业务服务监控达到全新高度
  优化Services部分,显示业务服务的状态和当前SLA级别
  通过对现有Services页面和功能的重大改进和优化,业务服务监控提升到了一个新高度。业务服务监控功能(BSM)非常适合多组件服务场景,例如服务器群集、负载平衡器和其它具有冗余组件的服务。
  Zabbix 6.0提供多种功能自定义业务服务树实现BMS业务服务监控:
  • 重新设计 Zabbix 6.0 Services页面和功能
  • 支持单个Zabbix实例监控超过10万个业务服务
  • 支持新的灵活服务状态计算逻辑
  • 能够自定义业务服务的访问权限
  • 能够为特定业务服务自定义只读和读写权限
  • 业务服务权限既可以基于显式服务列表,也可以基于服务标签的访问限制
  • 导出和导入业务服务树
  • 新的Service动作类型能让用户接收告警并对业务服务状态更改作出反应
  02
  高阶业务服务SLA计算逻辑
  提供大量可供选择的服务状态计算规则,能支持灵活的服务定义
  业务服务状态计算逻辑在Zabbix 6.0中得到了极大扩展,增加了许多新功能,例如:
  • 能够为每项业务服务分配权重
  • 仅当N个子服务都处于X严重级别的问题状态时才更改状态
  • 对处于问题状态下的子服务的权重进行分析并作出反应
  • 仅当特定百分比的子服务处于问题状态时才作出反应
  • 其它计算规则
  用户还可以自定义和访问指定服务的SLA报告。
  03
  通过根因分析增强业务服务监控能力
  根因问题会立即显示在service下
  对业务服务执行根因分析。利用根因分析功能找出可能导致业务服务SLA下降的潜在问题列表:
  • 在Zabbix前端Services页面查看根因问题列表
  • 接收告警中的根因问题列表
  • 通过Zabbix API收集根因问题信息
  04
  开箱即用的Zabbix server高可用群集
  在系统信息组件中跟踪集群集节点状态
  Zabbix server高可用防止硬件故障或计划维护期的停机:
  • 原生选择加入HA群集配置
  • 定义一个或多个备用节点
  
  •实时监控Zabbix server群集节点的状态
  • 不需要外部工具即可将Zabbix server配置为HA群集模式
  05
  机器学习
  使用新函数对意外异常率或与指标基准的偏差做出反应
  新的基线监控和异常检测趋势功能以动态方式检测问题,而不是静态阈值方式:
  •新的趋势函数-baselinewma and baselinedev ,能计算指标基线和偏离值
  •新的趋势函数-trendstl,能检测异常指标行为
  •能够指定异常检测偏差算法及季节性
  06
  Kubernetes监控
  Zabbix 6.0 LTS添加了多个新模板,用于监控不同的Kubernetes组件
  Zabbix 6.0 LTS新增Kubernetes监控功能,可以在Kubernetes系统从多个维度采集指标:
  •Kubernetes节点和pods的自动发现和监控
  •无代理方式采集Kubernetes pods和节点的信息
  •获取Kubernetes节点主机高水平信息
  Kubernetes监控还能够监控Kubernetes组件,例如
  •kube-controller-manager
  •kube-proxy
  •kube-apiserver
  •kube-scheduler
  •kubelet
  07
  详细高效的Zabbix审计日志模式
  重新设计的审计日志能提供全新的详细信息,并优化筛选功能。
  新的审计日志模式允许用户对Zabbix前端、Zabbix API和Zabbix server记录执行详细审计。通过修改审计日志,对Zabbix实例执行的所有更改都将记录在审计日志中:
  •创建、修改或删除新对象
  •通过LLD发现新实体
  •API命令
  •定期登录/退出
  •Zabbix实例中发生的所有其它事情
  新的审计日志模式在设计时考虑了最佳性能,因此扩展的功能不会影响Zabbix实例的性能。审计日志模式的工作是一项持续的工作,会在后续Zabbix发布周期中持续进行。
  08
  可视化数据的新方法
  主机排序组件可显示按监控项值排序的前N个或后N个主机的列表
  Zabbix 6.0新增的构件提供了展示信息的许多新方法。
  •地理地图构件能在地图上显示主机和问题
  •数据表构件能创建有关主机指标状态的摘要视图
  •数据表构件的前N和后N函数能展示最高或最低的监控项值
  •单一监控项构件能展示单个指标的值
  •对现有矢量图的许多改进,例如新的矢量图类型、引用单一监控项等
  •SLA构件能显示特定业务服务的当前SLA
  09
  Zabbix性能优化
  针对不同的Zabbix组件进行多项性能优化:
  •提升链接模板时的性能
  •提升Zabbix proxy性能和内存使用率
  历史数据表使用主键,这有多种好处,例如:
  •提高Zabbix server和Zabbix前端的性能
  •减少历史数据表的大小
  10
  提升Zabbix Agent2模块化,
  新的Zabbix Agent 监控项和功能
  优化的Zabbix agent现在能够开箱即用监控一组指标
  Zabbix 6.0为Zabbix Agent和Agent2提供了一套新的监控项。支持以下功能:
  •获取额外文件信息,如文件所有者和文件权限
  •采集agent主机元数据作为指标
  
  •计数匹配的TCP/UDP sockets
  某些已有的监控项支持新的功能:
  •vfs.fs.discovery-在Windows上添加了对{#FSLABEL}宏的支持
  •vfs.fs.get-在Windows上添加了对{#FSLABEL}宏的支持
  • vfs.file.size-添加了一个新的模式参数。设置以字节数或行数为单位
  Zabbix Agent2现在支持加载独立插件,而无需重新编译Agent2。
  11
  原生TLS/SSL网站证书监控
  使用新的Zabbix agent2 监控项监控SSL/TLS证书
  支持使用新的Zabbix agent 2监控项来监控SSL/TLS证书。监控项可用于验证TLS/SSL证书,并提供其它证书详细信息。
  12
  通用性改进
  通过优化的创建主机UI,使创建新主机从未如此简单
  Zabbix 6.0使Zabbix配置工作流程更精简!Zabbix用户现在可直接在Monitoring页面创建主机和监控项:
  •直接从Monitoring -Hosts页面创建主机
  •直接从Monitoring -Latest data页面创建监控项
  •删除了Monitoring -Overview页面。为了改善用户体验,现在只能通过仪表盘构件访问触发器和数据概览功能。
  现在将根据监控项的键值自动选择监控项的默认信息类型。
  拓扑图标签和图形名称中的简单宏已替换为表达式宏,以确保与新的触发器表达式语法一致。
  13
  通过自定义密码复杂程度要求
  来保护您的Zabbix登录
  设置密码复杂程度确保前端登录安全
  Zabbix超级管理员现在能够定义密码复杂程度要求。现在可以:
  •设置最小密码长度
  •定义密码字符要求
  •通过禁止使用最常见的密码字符串来降低字典攻击的风险。
  14
  支持定制前端展示品牌logo
  定制Zabbix实例代表您的公司。将现有的Zabbix品牌和帮助页面URL替换为您自己的公司品牌和自定义网站URL。
  改名功能不会违反Zabbix许可协议-可以自由更换Zabbix品牌!
  15
  新增模板和集成
  Zabbix 6.0为最受欢迎的供应商提供了许多新模板:
  •f5 BIG-IP
  •Cisco ASAv
  •HPE ProLiant servers
  •Cloudflare
  •InfluxDB
  •Travis CI
  •Dell PowerEdge
  Zabbix 6.0还带来了一个新的Github webhook集成,能基于Zabbix问题或恢复事件生成Github问题!
  所有官方的Zabbix模板现在都是独立的,不需要依赖导入其他模板。
  请查看当前可用集成的完整列表。
  16
  其它新功能和优化
  更多改进功能(部分):
  •使用新聚合函数计数返回值或匹配监控项的数量-count和item_count函数
  •在未配置交换空间的情况下提升system.swap监控项行为
  •使用新的单调历史函数检测连续增加或减少的值
  •支持两个新的Prometheus预处理标签匹配运算符!= 及 !~
  •当从构件链接导航到列表样式页面时,构件显示能更可靠地转换为不同的筛选器选项
  •使用新配置参数ListenBacklog为Zabbix server、Zabbix proxy、Zabbix agent配置TCP队列中挂起连接的最大数量
  •文档页面字体和可读性的改进
  •调整许多现有模板和修复小bug
  •新增utf8mb4作为受支持的MySQL字符集和校对集
  •新增对Webhook的额外HTTP方法的支持
  •对Zabbix命令行工具的超时设置
  Zabbix官方首本工具书《Zabbix监控系统之深度解析和实践》现已出版,欢迎阅读。

腾讯3面:说说前端监控平台/监控SDK的架构设计和难点亮点?

采集交流优采云 发表了文章 • 0 个评论 • 53 次浏览 • 2022-06-28 05:55 • 来自相关话题

  腾讯3面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在 腾讯三轮面试 的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计 ;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的 整体架构 和 可以做的亮点优化 ;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读; 传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过 削峰限流 和 数据加工 后,将原始日志存储于 ES 中,再经过 数据清洗 、数据聚合 后,将 issue(聚合的数据) 持久化存储 于 MySQL ,最后提供 RESTful API 提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是 内核+插件 的插件式设计;每个 SDK 首先继承于平台无关的 Core 层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个 web环境 下的数据,包括 Nodejs、微信小程序、Electron 等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个 SDK 可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护 ;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改 build 的脚本,对 packages 文件夹下的每个平台都单独打一个包,并且分开上传到 npm 平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK 也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件 的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加 Monitor 插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br />class WebSdk extends Core {<br />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br />  public performanceInstance: WebVitals;<br /><br />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br />  public userInstance: UserVitals;<br /><br />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br />  public errorInstance: ErrorVitals;<br /><br />  // 上报实例,这里面封装上报方法<br />  public transportInstance: TransportInstance;<br /><br />  // 数据格式化实例<br />  public builderInstance: BuilderInstance;<br /><br />  // 维度实例,用以初始化 uid、sid等信息<br />  public dimensionInstance: DimensionInstance;<br /><br />  // 参数初始化实例<br />  public configInstance: ConfigInstance;<br /><br />  private options: initOptions;<br /><br />  constructor(options: initOptions) {<br />    super();<br />    this.configInstance = new ConfigInstance(this, options);<br />    // 各种初始化......<br />  }<br />}<br /><br />export default WebSdk;<br />
  看上面的代码,我在初始化每个插件的时候,都将 this 传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的 内核+插件 设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有 单元测试 的来保证业务的稳定性;
  我们可以引入单元测试,并对 每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个 SDK 都用 try catch 包裹起来,那么这样子即使出现了错误,也会被拦截在我们的 catch 里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过 JS 调用 new Date() 获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说 API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头 上有一个字段 Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的 Date 值后计算 Diff差值 存在本地;
  这样子就可以提供一个 公共API,来提供一个时间校对的服务,让本地的时间 比较趋近于 服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br />export const diffTime = (date: string) => {<br />  const serverDate = new Date(date);<br />  const inDiff = Date.now() - serverDate.getTime();<br />  if (diff === 0 || diff > inDiff) {<br />    diff = inDiff;<br />  }<br />};<br /><br />export const getTime = () => {<br />  return new Date(Date.now() - diff);<br />};<br />
  当然,这里还可以做的更精确一点,我们可以让后端服务在返回的时候,带上 API 请求在后端服务执行完毕所消耗的时间 server-timing,放在响应头里;我们取到数据后,将 ttfb 耗时 减去返回的 server-timing 再除以 2;就是单程传输的耗时;那这样我们上文的计算中差的 单程传输耗时的误差 就可以补上了;
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成 错误mid 的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br />export const getErrorUid = (input: string) => {<br />  return window.btoa(unescape(encodeURIComponent(input)));<br />};<br />
  
  所以说我们传入的参数,是 错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的 错误mid 都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br />errorSendHandler = (data: ExceptionMetrics) => {<br />  // 统一加上 用户行为追踪 和 页面基本信息<br />  const submitParams = {<br />    ...data,<br />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br />  } as ExceptionMetrics;<br />  // 判断同一个错误在本次页面访问中是否已经发生过;<br />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br />  // 检查一下错误在本次页面访问中,是否已经产生过<br />  if (hasSubmitStatus) return;<br />  this.submitErrorUids.push(submitParams.errorUid);<br />  // 记录后清除 breadcrumbs<br />  this.engineInstance.userInstance.breadcrumbs.clear();<br />  // 一般来说,有报错就立刻上报;<br />  this.engineInstance.transportInstance.kernelTransportHandler(<br />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br />  );<br />};<br />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先 Beacon API[6] 是一个较新的 API
  然后 Ajax 请求方式就不用我多说了,大家应该平常用的最多的异步请求就是 Ajax;
  最后来说一下 Image 上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用 sendBeacon + xmlHttpRequest 降级上报的方式,当浏览器不支持 sendBeacon 或者 传输的数据量超过了 sendBeacon 的限制,我们就降级采用 xmlHttpRequest 进行上报数据;
  优先选用 Beacon API 的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载 的传输方式。
  而降级使用 XMLHttpRequest 的原因是, Beacon API 现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且 sendbeacon 不能传输大数据量的信息,这个时候还是得回到 Ajax 来;
  看到了这里,有的同学可能会问:为什么不用 Image 呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br />  // PV访问数据<br />  PV = 'pv',<br />  // 性能数据<br />  PERF = 'perf',<br />  // api 请求数据<br />  API = 'api',<br />  // 报错数据<br />  ERROR = 'error',<br />  // 自定义行为<br />  CUS = 'custom',<br />}<br /><br />export interface DimensionStructure {<br />  // 用户id,存储于cookie<br />  uid: string;<br />  // 会话id,存储于cookiestorage<br />  sid: string;<br />  // 应用id,使用方传入<br />  pid: string;<br />  // 应用版本号<br />  release: string;<br />  // 应用环境<br />  environment: string;<br />}<br /><br />export interface TransportStructure {<br />  // 上报类别<br />  category: transportCategory;<br />  // 上报的维度信息<br />  dimension: DimensionStructure;<br />  // 上报对象(正文)<br />  context?: Object;<br />  // 上报对象数组<br />  contexts?: Array;<br />  // 捕获的sdk版本信息,版本号等...<br />  sdk: Object;<br />}<br /><br />export default class TransportInstance {<br />  private engineInstance: EngineInstance;<br /><br />  public kernelTransportHandler: Function;<br /><br />  private options: TransportParams;<br /><br />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br />    this.engineInstance = engineInstance;<br />    this.options = options;<br />    this.kernelTransportHandler = this.initTransportHandler();<br />  }<br /><br />  // 格式化数据,传入部分为 category 和 context \ contexts<br />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br />    const transportStructure = {<br />      category,<br />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br />      sdk: getSdkVersion(),<br />    } as TransportStructure;<br />    if (data instanceof Array) {<br />      transportStructure.contexts = data;<br />    } else {<br />      transportStructure.context = data;<br />    }<br />    return transportStructure;<br />  };<br /><br />  // 初始化上报方法<br />  initTransportHandler = () => {<br />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br />  };<br /><br />  // beacon 形式上报<br />  beaconTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br />      if (!status) this.xmlTransport().apply(this, data);<br />    };<br />    return handler;<br />  };<br /><br />  // XMLHttpRequest 形式上报<br />  xmlTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const xhr = new (window as any).oXMLHttpRequest();<br />      xhr.open('POST', this.options.transportUrl, true);<br />      xhr.send(JSON.stringify(data));<br />    };<br />    return handler;<br />  };<br />}<br />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是 Math.random() ,我们让用户传入一个采样率,
<p>if(Math.random() 查看全部

  腾讯3面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在 腾讯三轮面试 的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计 ;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的 整体架构 和 可以做的亮点优化 ;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读; 传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过 削峰限流 和 数据加工 后,将原始日志存储于 ES 中,再经过 数据清洗 、数据聚合 后,将 issue(聚合的数据) 持久化存储 于 MySQL ,最后提供 RESTful API 提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是 内核+插件 的插件式设计;每个 SDK 首先继承于平台无关的 Core 层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个 web环境 下的数据,包括 Nodejs、微信小程序、Electron 等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个 SDK 可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护 ;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改 build 的脚本,对 packages 文件夹下的每个平台都单独打一个包,并且分开上传到 npm 平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK 也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件 的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加 Monitor 插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br />class WebSdk extends Core {<br />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br />  public performanceInstance: WebVitals;<br /><br />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br />  public userInstance: UserVitals;<br /><br />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br />  public errorInstance: ErrorVitals;<br /><br />  // 上报实例,这里面封装上报方法<br />  public transportInstance: TransportInstance;<br /><br />  // 数据格式化实例<br />  public builderInstance: BuilderInstance;<br /><br />  // 维度实例,用以初始化 uid、sid等信息<br />  public dimensionInstance: DimensionInstance;<br /><br />  // 参数初始化实例<br />  public configInstance: ConfigInstance;<br /><br />  private options: initOptions;<br /><br />  constructor(options: initOptions) {<br />    super();<br />    this.configInstance = new ConfigInstance(this, options);<br />    // 各种初始化......<br />  }<br />}<br /><br />export default WebSdk;<br />
  看上面的代码,我在初始化每个插件的时候,都将 this 传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的 内核+插件 设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有 单元测试 的来保证业务的稳定性;
  我们可以引入单元测试,并对 每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个 SDK 都用 try catch 包裹起来,那么这样子即使出现了错误,也会被拦截在我们的 catch 里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过 JS 调用 new Date() 获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说 API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头 上有一个字段 Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的 Date 值后计算 Diff差值 存在本地;
  这样子就可以提供一个 公共API,来提供一个时间校对的服务,让本地的时间 比较趋近于 服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br />export const diffTime = (date: string) => {<br />  const serverDate = new Date(date);<br />  const inDiff = Date.now() - serverDate.getTime();<br />  if (diff === 0 || diff > inDiff) {<br />    diff = inDiff;<br />  }<br />};<br /><br />export const getTime = () => {<br />  return new Date(Date.now() - diff);<br />};<br />
  当然,这里还可以做的更精确一点,我们可以让后端服务在返回的时候,带上 API 请求在后端服务执行完毕所消耗的时间 server-timing,放在响应头里;我们取到数据后,将 ttfb 耗时 减去返回的 server-timing 再除以 2;就是单程传输的耗时;那这样我们上文的计算中差的 单程传输耗时的误差 就可以补上了;
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成 错误mid 的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br />export const getErrorUid = (input: string) => {<br />  return window.btoa(unescape(encodeURIComponent(input)));<br />};<br />
  
  所以说我们传入的参数,是 错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的 错误mid 都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br />errorSendHandler = (data: ExceptionMetrics) => {<br />  // 统一加上 用户行为追踪 和 页面基本信息<br />  const submitParams = {<br />    ...data,<br />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br />  } as ExceptionMetrics;<br />  // 判断同一个错误在本次页面访问中是否已经发生过;<br />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br />  // 检查一下错误在本次页面访问中,是否已经产生过<br />  if (hasSubmitStatus) return;<br />  this.submitErrorUids.push(submitParams.errorUid);<br />  // 记录后清除 breadcrumbs<br />  this.engineInstance.userInstance.breadcrumbs.clear();<br />  // 一般来说,有报错就立刻上报;<br />  this.engineInstance.transportInstance.kernelTransportHandler(<br />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br />  );<br />};<br />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先 Beacon API[6] 是一个较新的 API
  然后 Ajax 请求方式就不用我多说了,大家应该平常用的最多的异步请求就是 Ajax;
  最后来说一下 Image 上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用 sendBeacon + xmlHttpRequest 降级上报的方式,当浏览器不支持 sendBeacon 或者 传输的数据量超过了 sendBeacon 的限制,我们就降级采用 xmlHttpRequest 进行上报数据;
  优先选用 Beacon API 的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载 的传输方式。
  而降级使用 XMLHttpRequest 的原因是, Beacon API 现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且 sendbeacon 不能传输大数据量的信息,这个时候还是得回到 Ajax 来;
  看到了这里,有的同学可能会问:为什么不用 Image 呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br />  // PV访问数据<br />  PV = 'pv',<br />  // 性能数据<br />  PERF = 'perf',<br />  // api 请求数据<br />  API = 'api',<br />  // 报错数据<br />  ERROR = 'error',<br />  // 自定义行为<br />  CUS = 'custom',<br />}<br /><br />export interface DimensionStructure {<br />  // 用户id,存储于cookie<br />  uid: string;<br />  // 会话id,存储于cookiestorage<br />  sid: string;<br />  // 应用id,使用方传入<br />  pid: string;<br />  // 应用版本号<br />  release: string;<br />  // 应用环境<br />  environment: string;<br />}<br /><br />export interface TransportStructure {<br />  // 上报类别<br />  category: transportCategory;<br />  // 上报的维度信息<br />  dimension: DimensionStructure;<br />  // 上报对象(正文)<br />  context?: Object;<br />  // 上报对象数组<br />  contexts?: Array;<br />  // 捕获的sdk版本信息,版本号等...<br />  sdk: Object;<br />}<br /><br />export default class TransportInstance {<br />  private engineInstance: EngineInstance;<br /><br />  public kernelTransportHandler: Function;<br /><br />  private options: TransportParams;<br /><br />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br />    this.engineInstance = engineInstance;<br />    this.options = options;<br />    this.kernelTransportHandler = this.initTransportHandler();<br />  }<br /><br />  // 格式化数据,传入部分为 category 和 context \ contexts<br />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br />    const transportStructure = {<br />      category,<br />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br />      sdk: getSdkVersion(),<br />    } as TransportStructure;<br />    if (data instanceof Array) {<br />      transportStructure.contexts = data;<br />    } else {<br />      transportStructure.context = data;<br />    }<br />    return transportStructure;<br />  };<br /><br />  // 初始化上报方法<br />  initTransportHandler = () => {<br />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br />  };<br /><br />  // beacon 形式上报<br />  beaconTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br />      if (!status) this.xmlTransport().apply(this, data);<br />    };<br />    return handler;<br />  };<br /><br />  // XMLHttpRequest 形式上报<br />  xmlTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const xhr = new (window as any).oXMLHttpRequest();<br />      xhr.open('POST', this.options.transportUrl, true);<br />      xhr.send(JSON.stringify(data));<br />    };<br />    return handler;<br />  };<br />}<br />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是 Math.random() ,我们让用户传入一个采样率,
<p>if(Math.random()

腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?

采集交流优采云 发表了文章 • 0 个评论 • 93 次浏览 • 2022-06-27 21:05 • 来自相关话题

  腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在腾讯三轮面试的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的整体架构和可以做的亮点优化;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读;传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过削峰限流和数据加工后,将原始日志存储于ES中,再经过数据清洗、数据聚合后,将issue(聚合的数据)持久化存储于MySQL,最后提供RESTful API提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是内核+插件的插件式设计;每个SDK首先继承于平台无关的Core层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个web环境下的数据,包括Nodejs、微信小程序、Electron等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个SDK可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改build的脚本,对packages文件夹下的每个平台都单独打一个包,并且分开上传到npm平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加Monitor插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />class WebSdk extends Core {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public performanceInstance: WebVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public userInstance: UserVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public errorInstance: ErrorVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报实例,这里面封装上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public transportInstance: TransportInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 数据格式化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public builderInstance: BuilderInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 维度实例,用以初始化 uid、sid等信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public dimensionInstance: DimensionInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 参数初始化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public configInstance: ConfigInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: initOptions;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(options: initOptions) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    super();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.configInstance = new ConfigInstance(this, options);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    // 各种初始化......<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default WebSdk;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  看上面的代码,我在初始化每个插件的时候,都将this传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的内核+插件设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有单元测试的来保证业务的稳定性;
  我们可以引入单元测试,并对每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个SDK都用try catch包裹起来,那么这样子即使出现了错误,也会被拦截在我们的catch里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过JS调用new Date()获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头上有一个字段Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的Date值后计算Diff差值存在本地;
  这样子就可以提供一个公共API,来提供一个时间校对的服务,让本地的时间比较趋近于服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const diffTime = (date: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const serverDate = new Date(date);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const inDiff = Date.now() - serverDate.getTime();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (diff === 0 || diff > inDiff) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    diff = inDiff;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getTime = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return new Date(Date.now() - diff);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成错误mid的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getErrorUid = (input: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return window.btoa(unescape(encodeURIComponent(input)));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  所以说我们传入的参数,是错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的错误mid都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />errorSendHandler = (data: ExceptionMetrics) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 统一加上 用户行为追踪 和 页面基本信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const submitParams = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    ...data,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  } as ExceptionMetrics;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 判断同一个错误在本次页面访问中是否已经发生过;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 检查一下错误在本次页面访问中,是否已经产生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (hasSubmitStatus) return;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.submitErrorUids.push(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 记录后清除 breadcrumbs<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.userInstance.breadcrumbs.clear();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 一般来说,有报错就立刻上报;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.transportInstance.kernelTransportHandler(<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  );<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先Beacon API[6]是一个较新的 API
  然后Ajax请求方式就不用我多说了,大家应该平常用的最多的异步请求就是Ajax;
  最后来说一下Image上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用sendBeacon+xmlHttpRequest降级上报的方式,当浏览器不支持sendBeacon或者传输的数据量超过了sendBeacon的限制,我们就降级采用xmlHttpRequest进行上报数据;
  优先选用Beacon API的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载的传输方式。
  而降级使用XMLHttpRequest的原因是,Beacon API现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且sendbeacon不能传输大数据量的信息,这个时候还是得回到Ajax来;
  看到了这里,有的同学可能会问:为什么不用Image呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // PV访问数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PV = 'pv',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PERF = 'perf',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // api 请求数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  API = 'api',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 报错数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  ERROR = 'error',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 自定义行为<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  CUS = 'custom',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface DimensionStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 用户id,存储于cookie<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  uid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 会话id,存储于cookiestorage<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用id,使用方传入<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  pid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用版本号<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  release: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用环境<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  environment: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface TransportStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报类别<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  category: transportCategory;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报的维度信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  dimension: DimensionStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象(正文)<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  context?: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象数组<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  contexts?: Array;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 捕获的sdk版本信息,版本号等...<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sdk: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default class TransportInstance {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private engineInstance: EngineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public kernelTransportHandler: Function;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: TransportParams;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance = engineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.options = options;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.kernelTransportHandler = this.initTransportHandler();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 格式化数据,传入部分为 category 和 context \ contexts<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const transportStructure = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      category,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      sdk: getSdkVersion(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } as TransportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    if (data instanceof Array) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.contexts = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } else {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.context = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return transportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 初始化上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  initTransportHandler = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // beacon 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  beaconTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      if (!status) this.xmlTransport().apply(this, data);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // XMLHttpRequest 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  xmlTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const xhr = new (window as any).oXMLHttpRequest();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.open('POST', this.options.transportUrl, true);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.send(JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是Math.random(),我们让用户传入一个采样率,
<p>if(Math.random() 查看全部

  腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在腾讯三轮面试的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的整体架构和可以做的亮点优化;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读;传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过削峰限流和数据加工后,将原始日志存储于ES中,再经过数据清洗、数据聚合后,将issue(聚合的数据)持久化存储于MySQL,最后提供RESTful API提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是内核+插件的插件式设计;每个SDK首先继承于平台无关的Core层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个web环境下的数据,包括Nodejs、微信小程序、Electron等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个SDK可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改build的脚本,对packages文件夹下的每个平台都单独打一个包,并且分开上传到npm平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加Monitor插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />class WebSdk extends Core {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public performanceInstance: WebVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public userInstance: UserVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public errorInstance: ErrorVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报实例,这里面封装上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public transportInstance: TransportInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 数据格式化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public builderInstance: BuilderInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 维度实例,用以初始化 uid、sid等信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public dimensionInstance: DimensionInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 参数初始化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public configInstance: ConfigInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: initOptions;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(options: initOptions) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    super();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.configInstance = new ConfigInstance(this, options);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    // 各种初始化......<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default WebSdk;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  看上面的代码,我在初始化每个插件的时候,都将this传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的内核+插件设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有单元测试的来保证业务的稳定性;
  我们可以引入单元测试,并对每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个SDK都用try catch包裹起来,那么这样子即使出现了错误,也会被拦截在我们的catch里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过JS调用new Date()获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头上有一个字段Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的Date值后计算Diff差值存在本地;
  这样子就可以提供一个公共API,来提供一个时间校对的服务,让本地的时间比较趋近于服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const diffTime = (date: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const serverDate = new Date(date);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const inDiff = Date.now() - serverDate.getTime();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (diff === 0 || diff > inDiff) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    diff = inDiff;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getTime = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return new Date(Date.now() - diff);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成错误mid的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getErrorUid = (input: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return window.btoa(unescape(encodeURIComponent(input)));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  所以说我们传入的参数,是错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的错误mid都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />errorSendHandler = (data: ExceptionMetrics) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 统一加上 用户行为追踪 和 页面基本信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const submitParams = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    ...data,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  } as ExceptionMetrics;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 判断同一个错误在本次页面访问中是否已经发生过;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 检查一下错误在本次页面访问中,是否已经产生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (hasSubmitStatus) return;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.submitErrorUids.push(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 记录后清除 breadcrumbs<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.userInstance.breadcrumbs.clear();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 一般来说,有报错就立刻上报;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.transportInstance.kernelTransportHandler(<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  );<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先Beacon API[6]是一个较新的 API
  然后Ajax请求方式就不用我多说了,大家应该平常用的最多的异步请求就是Ajax;
  最后来说一下Image上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用sendBeacon+xmlHttpRequest降级上报的方式,当浏览器不支持sendBeacon或者传输的数据量超过了sendBeacon的限制,我们就降级采用xmlHttpRequest进行上报数据;
  优先选用Beacon API的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载的传输方式。
  而降级使用XMLHttpRequest的原因是,Beacon API现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且sendbeacon不能传输大数据量的信息,这个时候还是得回到Ajax来;
  看到了这里,有的同学可能会问:为什么不用Image呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // PV访问数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PV = 'pv',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PERF = 'perf',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // api 请求数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  API = 'api',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 报错数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  ERROR = 'error',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 自定义行为<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  CUS = 'custom',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface DimensionStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 用户id,存储于cookie<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  uid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 会话id,存储于cookiestorage<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用id,使用方传入<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  pid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用版本号<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  release: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用环境<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  environment: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface TransportStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报类别<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  category: transportCategory;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报的维度信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  dimension: DimensionStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象(正文)<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  context?: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象数组<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  contexts?: Array;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 捕获的sdk版本信息,版本号等...<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sdk: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default class TransportInstance {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private engineInstance: EngineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public kernelTransportHandler: Function;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: TransportParams;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance = engineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.options = options;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.kernelTransportHandler = this.initTransportHandler();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 格式化数据,传入部分为 category 和 context \ contexts<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const transportStructure = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      category,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      sdk: getSdkVersion(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } as TransportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    if (data instanceof Array) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.contexts = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } else {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.context = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return transportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 初始化上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  initTransportHandler = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // beacon 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  beaconTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      if (!status) this.xmlTransport().apply(this, data);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // XMLHttpRequest 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  xmlTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const xhr = new (window as any).oXMLHttpRequest();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.open('POST', this.options.transportUrl, true);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.send(JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是Math.random(),我们让用户传入一个采样率,
<p>if(Math.random()

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 127 次浏览 • 2022-06-26 01:10 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。 查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。

信息收集组合拳之从废弃接口中寻找漏洞

采集交流优采云 发表了文章 • 0 个评论 • 59 次浏览 • 2022-06-21 06:53 • 来自相关话题

  信息收集组合拳之从废弃接口中寻找漏洞
  使用OneForAll工具通过域名收集网址和ip
  工具地址:GitHub - shmilylty/OneForAll: OneForAll是一款功能强大的子域收集工具
  常用命令:python oneforall.py --targets targets.txt run
  targets.txt中放入需要扫描的域名,运行后会在results文件夹下生成扫描结果
  在运行结果中我们可以看到有url、子域名、ip
  其中运行结果中ip是一行多个、还有重复的,我们需要提取ip转换成每行一个ip且没有重复的txt文本中,方便其他工具扫描
  脚本:删除重复ip
  #!/usr/bin/env python# conding:utf-8<br />##把同一行的ip换行,然后写进result.txt的文件里with open('ip.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): with open('result.txt','a',encoding='utf-8') as writelist: b = dirs.replace(",", '\n') writelist.write(b)<br />#去除重复ip,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://blog.csdn.net/qq_22764 ... s%3D1
  提取成这样单行一个ip且不重复的文本,我们就可以放到goby、fscan、小米范等工具中扫描
  
  fscan工具扫描ip
  工具地址:GitHub - shadow1ng/fscan: 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
  这款工具主要是用于内网扫描,发现资产以及进行漏洞扫描与弱口令爆破,运行速度很快,用于外网探测发现一些web资产也是不错的选择
  常用命令:全端口扫描 fscan64.exe -hf ip.txt -p 1-65535 -o result.txt
  ip.txt中放入需要扫描的ip地址,result.txt为运行结果
  
  小米范
  工具地址:我的小工具 - 标签 - 范世强 - 博客园
  (找不到这个版本的地址了,就贴个作者的博客地址吧)
  JSFinder扫描js及url
  工具地址:GitHub - Threezh1/JSFinder: JSFinder is a tool for quickly extracting URLs and subdomains from JS files on a website.
  常用命令:python JSFinder.py -f targets.txt -d -ou JSurl.txt -os JSdomain.txt
  targets.txt中放入需要扫描的url,运行结束后生会成两个txt文本, JSurl.txt为URL,JSdomain.txt为子域名
  上面这些工具的扫描结果中含有很多的url,我们需要效率高一些的话我们可以优先从参数下手,于是需要筛选含有参数的url
  脚本:提取含有参数的url
  #!/usr/bin/env python# conding:utf-8<br />#字符串中有“?”且不在字符串的结尾的就写入result.txt中with open('JSurl.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): # re_result=re.search(r"'?'",dirs) # re_result=str(re_result) if "?" in dirs :#判断字符中是否有“?”,如果有则返回该字符串的位置,是从坐标0开始算的 re = dirs.find("?") # a=len(dirs)-2是为了判断“?”是不是在最后一个字符,len()与find()不同是从一开始算字符串的长度的,在加上每行字符中\n换行符也占了一个字符,所以要减2 a=len(dirs)-2#判断字符串中“?”是不是在字符的最后 if re < a : with open('result.txt','a',encoding='utf-8') as writelist: writelist.write(dirs)<br />#去除result.txt中的重复字符串,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://www.cnblogs.com/luguankun/p/11846401.html(判断一个字符是否在一个字符串中)
  <br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  运行完脚本生成的含有参数的url如下所示
  从废弃接口中寻找漏洞
  有些网站经过了多轮渗透,正常业务都测烂的,连逻辑漏洞都找不到,经历了上面一番信息收集后,一般能收集到网站的历史业务中的url。
  然后我们将url使用脚本处理筛选之后的结果可以批量的放到sqlmap中跑,还有一些敏感接口可以尝试寻找越权、信息泄露等。
  sqlmap批量扫描
  常用命令:python sqlmap.py -m urls.txt --batch
  在urls.txt文件内放入我们使用“提取含有参数的url”这个脚本筛选后的url
  除了参数以外也可以用同样的思路把脚本修改一下去找敏感接口与url跳转参数等
  常见敏感接口
  常见跳转参数
  toUrl=
  login_url=
  register_url
  redirect_url=
  load_url=
  proxy_url=
  file_url=
  jump_url=
  某次项目中客户都疑惑我怎么找到的接口
  
  转载于:
  推荐↓↓↓ 查看全部

  信息收集组合拳之从废弃接口中寻找漏洞
  使用OneForAll工具通过域名收集网址和ip
  工具地址:GitHub - shmilylty/OneForAll: OneForAll是一款功能强大的子域收集工具
  常用命令:python oneforall.py --targets targets.txt run
  targets.txt中放入需要扫描的域名,运行后会在results文件夹下生成扫描结果
  在运行结果中我们可以看到有url、子域名、ip
  其中运行结果中ip是一行多个、还有重复的,我们需要提取ip转换成每行一个ip且没有重复的txt文本中,方便其他工具扫描
  脚本:删除重复ip
  #!/usr/bin/env python# conding:utf-8<br />##把同一行的ip换行,然后写进result.txt的文件里with open('ip.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): with open('result.txt','a',encoding='utf-8') as writelist: b = dirs.replace(",", '\n') writelist.write(b)<br />#去除重复ip,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://blog.csdn.net/qq_22764 ... s%3D1
  提取成这样单行一个ip且不重复的文本,我们就可以放到goby、fscan、小米范等工具中扫描
  
  fscan工具扫描ip
  工具地址:GitHub - shadow1ng/fscan: 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
  这款工具主要是用于内网扫描,发现资产以及进行漏洞扫描与弱口令爆破,运行速度很快,用于外网探测发现一些web资产也是不错的选择
  常用命令:全端口扫描 fscan64.exe -hf ip.txt -p 1-65535 -o result.txt
  ip.txt中放入需要扫描的ip地址,result.txt为运行结果
  
  小米范
  工具地址:我的小工具 - 标签 - 范世强 - 博客园
  (找不到这个版本的地址了,就贴个作者的博客地址吧)
  JSFinder扫描js及url
  工具地址:GitHub - Threezh1/JSFinder: JSFinder is a tool for quickly extracting URLs and subdomains from JS files on a website.
  常用命令:python JSFinder.py -f targets.txt -d -ou JSurl.txt -os JSdomain.txt
  targets.txt中放入需要扫描的url,运行结束后生会成两个txt文本, JSurl.txt为URL,JSdomain.txt为子域名
  上面这些工具的扫描结果中含有很多的url,我们需要效率高一些的话我们可以优先从参数下手,于是需要筛选含有参数的url
  脚本:提取含有参数的url
  #!/usr/bin/env python# conding:utf-8<br />#字符串中有“?”且不在字符串的结尾的就写入result.txt中with open('JSurl.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): # re_result=re.search(r"'?'",dirs) # re_result=str(re_result) if "?" in dirs :#判断字符中是否有“?”,如果有则返回该字符串的位置,是从坐标0开始算的 re = dirs.find("?") # a=len(dirs)-2是为了判断“?”是不是在最后一个字符,len()与find()不同是从一开始算字符串的长度的,在加上每行字符中\n换行符也占了一个字符,所以要减2 a=len(dirs)-2#判断字符串中“?”是不是在字符的最后 if re < a : with open('result.txt','a',encoding='utf-8') as writelist: writelist.write(dirs)<br />#去除result.txt中的重复字符串,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://www.cnblogs.com/luguankun/p/11846401.html(判断一个字符是否在一个字符串中)
  <br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  运行完脚本生成的含有参数的url如下所示
  从废弃接口中寻找漏洞
  有些网站经过了多轮渗透,正常业务都测烂的,连逻辑漏洞都找不到,经历了上面一番信息收集后,一般能收集到网站的历史业务中的url。
  然后我们将url使用脚本处理筛选之后的结果可以批量的放到sqlmap中跑,还有一些敏感接口可以尝试寻找越权、信息泄露等。
  sqlmap批量扫描
  常用命令:python sqlmap.py -m urls.txt --batch
  在urls.txt文件内放入我们使用“提取含有参数的url”这个脚本筛选后的url
  除了参数以外也可以用同样的思路把脚本修改一下去找敏感接口与url跳转参数等
  常见敏感接口
  常见跳转参数
  toUrl=
  login_url=
  register_url
  redirect_url=
  load_url=
  proxy_url=
  file_url=
  jump_url=
  某次项目中客户都疑惑我怎么找到的接口
  
  转载于:
  推荐↓↓↓

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 103 次浏览 • 2022-06-19 21:31 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。 查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。

图数据库无缝集成Tushare接口

采集交流优采云 发表了文章 • 0 个评论 • 66 次浏览 • 2022-06-11 03:29 • 来自相关话题

  图数据库无缝集成Tushare接口
  
  @TOC[1]Here's the table of contents:
  图数据库无缝集成Tushare接口
  使用第三方API,有助于我们快速集成数据,构建业务分析需要的知识图谱数据。这篇文章主要介绍如何将Tushare HTTP接口集成到图数据库,并使用Cypher构建知识图谱。
  在开始集成前,请确保你的图数据库安装了APOC组件,并保证apoc.load.jsonParams过程可以正常使用。APOC同时还支持JSON Path,即以特定模式搜索JSON文档中的数据项并返回其内容,其概念类似应用于XML的XPath和应用于HTML的jQuery。更多使用方式可以查看Neo4j社区技术专家俞博士的文章Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据[2]。
  一、Tushare介绍
  Tushare[3]是一个免费、开源的python财经数据接口包。主要实现对股票等金融数据从数据采集、清洗加工到数据存储的过程,能够为金融分析人员提供快速、整洁、和多样的便于分析的数据,为他们在数据获取方面极大地减轻工作量,使他们更加专注于策略和模型的研究与实现上。
  
  Tushare官网二、集成Tushare接口
  在开始集成之前请确保你有一个Tushare的访问账号。
  在图数据库的安装目录下找到conf文件夹,并在neo4j.conf文件中为Tushare HTTP API的URL定义别名,新增一行配置即可。修改配置后,重启数据库服务即可。
  apoc.json.tushare.url=http://api.tushare.pro
  三、使用接口数据
  现在我们可以编写Cypher代码很方便地从Tushare获取数据了。下面我将演示一个申万成分股图谱构建的案例。请注意在使用Cypher脚本时请设置私有token。
  下面的代码通过分批循环调用首先从stock_basic接口获取到股票代码,然后再使用股票代码获取申万成分股时间序列数据。每个股票代码调用index_member接口之前,设置了执行四百万次加法运算表示进行延时1~2秒,这个操作的目的是为了保证HTTP接口调用时不要超过接口频率限制。
  //循环获取所有股票代码<br />WITH RANGE(1,10) AS list,1000 AS limit<br />UNWIND list AS num<br />WITH num*limit AS offset,limit<br />//分批获取股票代码<br />WITH <br />    '{"api_name":"stock_basic","token":"xxxxxxxxxxx","params":{"limit":"'+limit+'","offset":"'+offset+'"},"fields":"ts_code"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.has_more AS has_more,value.data.items AS stocks<br />WHERE has_more<br />//获取申万成分数据<br />WITH has_more,stocks<br />UNWIND stocks AS stock<br />WITH stock<br />//延时执行【HTTP API对调用频率有限制,让函数执行四百万次加法,耗时大概1~2秒】<br />WITH RANGE(1,2000) AS l,stock UNWIND l AS a UNWIND l AS b WITH SUM(a+b) AS delay,stock<br />WITH <br />    '{"api_name":"index_member","token":"xxxxxxxxxxx","params":{"ts_code":"'+stock[0]+'"},"fields":"index_code,index_name,con_code,con_name,in_date,out_date,is_new"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.items AS items<br />//构建申万行业成分股图谱<br />UNWIND items AS item<br />WITH item<br />MERGE (stk:股票 {code:item[2]}) SET stk+={name:item[3]+'('+item[2]+')'}<br />MERGE (hy:申万行业 {code:item[0]}) SET hy+={name:item[1]+'('+item[0]+')'}<br />CREATE (stk)-[r:属于]->(hy) SET r+={in_date:item[4],out_date:item[5],is_new:item[6]}
  
  申万行业成分股时序图谱引用链接
  [1]TOC:图数据库无缝集成Tushare接口
  [2]Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据:
  [3]Tushare:
   查看全部

  图数据库无缝集成Tushare接口
  
  @TOC[1]Here's the table of contents:
  图数据库无缝集成Tushare接口
  使用第三方API,有助于我们快速集成数据,构建业务分析需要的知识图谱数据。这篇文章主要介绍如何将Tushare HTTP接口集成到图数据库,并使用Cypher构建知识图谱。
  在开始集成前,请确保你的图数据库安装了APOC组件,并保证apoc.load.jsonParams过程可以正常使用。APOC同时还支持JSON Path,即以特定模式搜索JSON文档中的数据项并返回其内容,其概念类似应用于XML的XPath和应用于HTML的jQuery。更多使用方式可以查看Neo4j社区技术专家俞博士的文章Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据[2]。
  一、Tushare介绍
  Tushare[3]是一个免费、开源的python财经数据接口包。主要实现对股票等金融数据从数据采集、清洗加工到数据存储的过程,能够为金融分析人员提供快速、整洁、和多样的便于分析的数据,为他们在数据获取方面极大地减轻工作量,使他们更加专注于策略和模型的研究与实现上。
  
  Tushare官网二、集成Tushare接口
  在开始集成之前请确保你有一个Tushare的访问账号。
  在图数据库的安装目录下找到conf文件夹,并在neo4j.conf文件中为Tushare HTTP API的URL定义别名,新增一行配置即可。修改配置后,重启数据库服务即可。
  apoc.json.tushare.url=http://api.tushare.pro
  三、使用接口数据
  现在我们可以编写Cypher代码很方便地从Tushare获取数据了。下面我将演示一个申万成分股图谱构建的案例。请注意在使用Cypher脚本时请设置私有token。
  下面的代码通过分批循环调用首先从stock_basic接口获取到股票代码,然后再使用股票代码获取申万成分股时间序列数据。每个股票代码调用index_member接口之前,设置了执行四百万次加法运算表示进行延时1~2秒,这个操作的目的是为了保证HTTP接口调用时不要超过接口频率限制。
  //循环获取所有股票代码<br />WITH RANGE(1,10) AS list,1000 AS limit<br />UNWIND list AS num<br />WITH num*limit AS offset,limit<br />//分批获取股票代码<br />WITH <br />    '{"api_name":"stock_basic","token":"xxxxxxxxxxx","params":{"limit":"'+limit+'","offset":"'+offset+'"},"fields":"ts_code"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.has_more AS has_more,value.data.items AS stocks<br />WHERE has_more<br />//获取申万成分数据<br />WITH has_more,stocks<br />UNWIND stocks AS stock<br />WITH stock<br />//延时执行【HTTP API对调用频率有限制,让函数执行四百万次加法,耗时大概1~2秒】<br />WITH RANGE(1,2000) AS l,stock UNWIND l AS a UNWIND l AS b WITH SUM(a+b) AS delay,stock<br />WITH <br />    '{"api_name":"index_member","token":"xxxxxxxxxxx","params":{"ts_code":"'+stock[0]+'"},"fields":"index_code,index_name,con_code,con_name,in_date,out_date,is_new"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.items AS items<br />//构建申万行业成分股图谱<br />UNWIND items AS item<br />WITH item<br />MERGE (stk:股票 {code:item[2]}) SET stk+={name:item[3]+'('+item[2]+')'}<br />MERGE (hy:申万行业 {code:item[0]}) SET hy+={name:item[1]+'('+item[0]+')'}<br />CREATE (stk)-[r:属于]->(hy) SET r+={in_date:item[4],out_date:item[5],is_new:item[6]}
  
  申万行业成分股时序图谱引用链接
  [1]TOC:图数据库无缝集成Tushare接口
  [2]Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据:
  [3]Tushare:
  

【干货】一篇文章讲透数据挖掘

采集交流优采云 发表了文章 • 0 个评论 • 108 次浏览 • 2022-06-10 03:56 • 来自相关话题

  【干货】一篇文章讲透数据挖掘
  (1)数据挖掘流程
  数据挖掘一般是指从大量的数据中通过算法搜索隐藏于其中信息的过程。它通常与计算机科学有关,并通过统计、在线分析处理、情报检索、机器学习、专家系统(依靠过去的经验法则)和模式识别等诸多方法来实现上述目标。它的分析方法包括:分类、估计、预测、相关性分组或关联规则、聚类和复杂数据类型挖掘。
  
  1)数据的采集
  首先得有数据,数据的收集有两个方式,第一个方式是拿,专业点的说法叫抓取或者爬取,例如搜索引擎就是这么做的,它把网上的所有的信息都下载到它的数据中心,然后你一搜才能搜出来。
  2)数据的传输
  一般会通过队列方式进行,因为数据量实在是太大了,数据必须经过处理才会有用,可是系统处理不过来,只好排好队,慢慢的处理。
  3)数据的存储
  现在数据就是金钱,掌握了数据就相当于掌握了钱。要不然网站怎么知道你想买什么呢?就是因为它有你历史的交易的数据,这个信息可不能给别人,十分宝贵,所以需要存储下来。
  4)数据的清洗和分析
  上面存储的数据是原始数据,原始数据多是杂乱无章的,有很多垃圾数据在里面,因而需要清洗和过滤,得到一些高质量的数据。对于高质量的数据,就可以进行分析,从而对数据进行分类,或者发现数据之间的相互关系,得到知识。
  注:第三与第四个步骤,现存后清洗和先清洗再存,在真是的业务场景中可以适当互换。
  5)数据的检索和挖掘
  检索就是搜索,所谓外事问google,内事问百度。挖掘,仅仅搜索出来已经不能满足人们的要求了,还需要从信息中挖掘出相互的关系。
  6)数据的加载与应用
  怎么友好的展示与传递给用户为数据挖掘工作做好闭环。
  (2)数据挖掘工具类1)数据采集工具
  1、针对日志文件类
  工具
  定义
  Logstash
  Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash可以动态地将来自不同数据源的数据统一起来,并将数据标准化到所选择的目的地。
  Filebeat
  Filebeat 作为一个轻量级的日志传输工具可以将日志推送到中心 Logstash。
  Fluentd
  Fluentd 创建的初衷主要是尽可能的使用 JSON 作为日志输出,所以传输工具及其下游的传输线不需要猜测子字符串里面各个字段的类型。这样,它为几乎所有的语言都提供库,即可以将它插入到自定义的程序中。
  Logagent
  Logagent 是 Sematext 提供的传输工具,它用来将日志传输到 Logsene(一个基于 SaaS 平台的 Elasticsearch API)。
  Rsylog
  绝大多数 Linux 发布版本默认的守护进程,rsyslog 读取并写入 /var/log/messages 。它可以提取文件、解析、缓冲(磁盘和内存)以及将它们传输到多个目的地,包括 Elasticsearch 。可以从此处找到如何处理 Apache 以及系统日志。
  Logtail
  阿里云日志服务的生产者,目前在阿里集团内部机器上运行,经过3年多时间的考验,目前为阿里公有云用户提供日志收集服务。
  2、针对爬虫类
  页面下载 --> 页面解析 --> 数据存储
  (1)页面下载器
  对于下载器而言,python的库requests能满足大部分测试+抓取需求,进阶工程化scrapy,动态网页优先找API接口,如果有简单加密就破解,实在困难就使用splash渲染。
  (2)页面解析器
  ①BeautifulSoup(入门级):Python爬虫入门BeautifulSoup模块
  ②pyquery(类似jQuery):Python爬虫:pyquery模块解析网页
  ③lxml:Python爬虫:使用lxml解析网页内容
  ④parsel:Extract text using CSS or XPath selectors
  ⑤scrapy的Selector (强烈推荐, 比较高级的封装,基于parsel)
  ⑥选择器(Selectors):python爬虫:scrapy框架xpath和css选择器语法
  ---------------------
  总结:
  解析器直接使用scrapy的Selector 就行,简单、直接、高效。
  (3)数据存储
  ①txt文本:Python全栈之路:文件file常用操作
  ②csv文件:python读取写入csv文件
  ③sqlite3 (python自带):Python编程:使用数据库sqlite3
  ④MySQL:SQL:pymysql模块读写mysql数据
  ⑤MongoDB:Python编程:mongodb的基本增删改查操作
  ---------------------
  总结:
  数据存储没有什么可深究的,按照业务需求来就行,一般快速测试使用MongoDB,业务使用MySQL
  (4)其他工具
  ①execjs :执行js
  Python爬虫:execjs在python中运行javascript代码
  ②pyv8: 执行js
  mac安装pyv8模块-JavaScript翻译成python
  ③html5lib
  Python爬虫:scrapy利用html5lib解析不规范的html文本
  2)数据清洗工具
  1、DataWrangler
  基于网络的服务是斯坦福大学的可视化组设计来清洗和重排数据的.文本编辑非常简单。例如,当我选择大标题为“Reported crime in Alabama”的样本数据的某行的“Alabama”,然后选择另一组数据的“Alaska”,它会建议提取每州的名字。把鼠标停留在建议上,就可以看到用红色突出显示的行。
  2、Google Refine
  它可以导入导出多种格式的数据,如标签或逗号分隔的文本文件、Excel、XML和JSON文件。Refine设有内置算法,可以发现一些拼写不一样但实际上应分为一组的文本。导入你的数据后,选择编辑单元格->聚类,编辑,然后选择要用的算法。数据选项,提供快速简单的数据分布概貌。这个功能可以揭示那些可能由于输入错误导致的异常——例如,工资记录不是80,000美元而竟然是800,000美元;或指出不一致的地方——例如薪酬数据记录之间的差异,有的是计时工资,有的是每周支付,有的是年薪。除了数据管家功能,Google Refine还提供了一些有用的分析工具,例如排序和筛选。
  3、Logstash
  Logstash 是一款强大的数据处理工具,它可以实现数据传输,格式处理,格式化输出,还有强大的插件功能,常用于日志处理。
  3)数据存储工具
  数据存储主要分为结构化数据的存储和非结构化数据的存储。
  1、结构化数据
  (1)定义
  一般指存储在数据库中,具有一定逻辑结构和物理结构的数据,最为常见的是存储在关系数据库中的数据;非结构化数据:一般指结构化数据以外的数据,这些数据不存储在数据库中,而是以各种类型的文本形式存放,其中Web上的一些数据(内嵌于HTML或XML标记中)又具有一定的逻辑结构和物理结构,被称为半结构数据。
  (2)存储系统
  目前比较成熟的结构化存储系统有Oracle、MySQL、Hadoop等。
  2、非结构化数据
  (1)定义
  非结构化数据是数据结构不规则或不完整,没有预定义的数据模型,不方便用数据库二维逻辑表来表现的数据。包括所有格式的办公文档、文本、图片、XML, HTML、各类报表、图像和音频/视频信息等等。
  (2)存储方式
  1)使用文件系统存储文件,而在数据库中存储访问路径。这种方式的优点是实现简单,不需要DBMS的高级功能,但是这种方式无法实现文件的事务性访问,不便于数据备份和恢复,不便于数据迁移等;
  2)使用阿里云OSS的文件存储功能。
  4)数据计算工具
  数据计算分为实时计算、在线计算、离线计算。
  1、数据实时计算
  ApacheStorm
  2、数据在线计算
  Elasticsearch
  MySQL
  3、数据离线计算
  HaDoop Hive
  5)数据分析工具
  1、对数据矩阵科学计算:Python的numpy库
  2、对数据切片等常规处理:强大的pandas库
  3、对数据建模处理:sklearn库
  6)数据加载工具
  1、数据的可视化处理:Python中的matplotlib和seaborn库
  2、常用的BI可视化工具:Tableu和帆软
  3、ECharts
  ——————————————
  阅读推荐 查看全部

  【干货】一篇文章讲透数据挖掘
  (1)数据挖掘流程
  数据挖掘一般是指从大量的数据中通过算法搜索隐藏于其中信息的过程。它通常与计算机科学有关,并通过统计、在线分析处理、情报检索、机器学习、专家系统(依靠过去的经验法则)和模式识别等诸多方法来实现上述目标。它的分析方法包括:分类、估计、预测、相关性分组或关联规则、聚类和复杂数据类型挖掘。
  
  1)数据的采集
  首先得有数据,数据的收集有两个方式,第一个方式是拿,专业点的说法叫抓取或者爬取,例如搜索引擎就是这么做的,它把网上的所有的信息都下载到它的数据中心,然后你一搜才能搜出来。
  2)数据的传输
  一般会通过队列方式进行,因为数据量实在是太大了,数据必须经过处理才会有用,可是系统处理不过来,只好排好队,慢慢的处理。
  3)数据的存储
  现在数据就是金钱,掌握了数据就相当于掌握了钱。要不然网站怎么知道你想买什么呢?就是因为它有你历史的交易的数据,这个信息可不能给别人,十分宝贵,所以需要存储下来。
  4)数据的清洗和分析
  上面存储的数据是原始数据,原始数据多是杂乱无章的,有很多垃圾数据在里面,因而需要清洗和过滤,得到一些高质量的数据。对于高质量的数据,就可以进行分析,从而对数据进行分类,或者发现数据之间的相互关系,得到知识。
  注:第三与第四个步骤,现存后清洗和先清洗再存,在真是的业务场景中可以适当互换。
  5)数据的检索和挖掘
  检索就是搜索,所谓外事问google,内事问百度。挖掘,仅仅搜索出来已经不能满足人们的要求了,还需要从信息中挖掘出相互的关系。
  6)数据的加载与应用
  怎么友好的展示与传递给用户为数据挖掘工作做好闭环。
  (2)数据挖掘工具类1)数据采集工具
  1、针对日志文件类
  工具
  定义
  Logstash
  Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash可以动态地将来自不同数据源的数据统一起来,并将数据标准化到所选择的目的地。
  Filebeat
  Filebeat 作为一个轻量级的日志传输工具可以将日志推送到中心 Logstash。
  Fluentd
  Fluentd 创建的初衷主要是尽可能的使用 JSON 作为日志输出,所以传输工具及其下游的传输线不需要猜测子字符串里面各个字段的类型。这样,它为几乎所有的语言都提供库,即可以将它插入到自定义的程序中。
  Logagent
  Logagent 是 Sematext 提供的传输工具,它用来将日志传输到 Logsene(一个基于 SaaS 平台的 Elasticsearch API)。
  Rsylog
  绝大多数 Linux 发布版本默认的守护进程,rsyslog 读取并写入 /var/log/messages 。它可以提取文件、解析、缓冲(磁盘和内存)以及将它们传输到多个目的地,包括 Elasticsearch 。可以从此处找到如何处理 Apache 以及系统日志。
  Logtail
  阿里云日志服务的生产者,目前在阿里集团内部机器上运行,经过3年多时间的考验,目前为阿里公有云用户提供日志收集服务。
  2、针对爬虫类
  页面下载 --> 页面解析 --> 数据存储
  (1)页面下载器
  对于下载器而言,python的库requests能满足大部分测试+抓取需求,进阶工程化scrapy,动态网页优先找API接口,如果有简单加密就破解,实在困难就使用splash渲染。
  (2)页面解析器
  ①BeautifulSoup(入门级):Python爬虫入门BeautifulSoup模块
  ②pyquery(类似jQuery):Python爬虫:pyquery模块解析网页
  ③lxml:Python爬虫:使用lxml解析网页内容
  ④parsel:Extract text using CSS or XPath selectors
  ⑤scrapy的Selector (强烈推荐, 比较高级的封装,基于parsel)
  ⑥选择器(Selectors):python爬虫:scrapy框架xpath和css选择器语法
  ---------------------
  总结:
  解析器直接使用scrapy的Selector 就行,简单、直接、高效。
  (3)数据存储
  ①txt文本:Python全栈之路:文件file常用操作
  ②csv文件:python读取写入csv文件
  ③sqlite3 (python自带):Python编程:使用数据库sqlite3
  ④MySQL:SQL:pymysql模块读写mysql数据
  ⑤MongoDB:Python编程:mongodb的基本增删改查操作
  ---------------------
  总结:
  数据存储没有什么可深究的,按照业务需求来就行,一般快速测试使用MongoDB,业务使用MySQL
  (4)其他工具
  ①execjs :执行js
  Python爬虫:execjs在python中运行javascript代码
  ②pyv8: 执行js
  mac安装pyv8模块-JavaScript翻译成python
  ③html5lib
  Python爬虫:scrapy利用html5lib解析不规范的html文本
  2)数据清洗工具
  1、DataWrangler
  基于网络的服务是斯坦福大学的可视化组设计来清洗和重排数据的.文本编辑非常简单。例如,当我选择大标题为“Reported crime in Alabama”的样本数据的某行的“Alabama”,然后选择另一组数据的“Alaska”,它会建议提取每州的名字。把鼠标停留在建议上,就可以看到用红色突出显示的行。
  2、Google Refine
  它可以导入导出多种格式的数据,如标签或逗号分隔的文本文件、Excel、XML和JSON文件。Refine设有内置算法,可以发现一些拼写不一样但实际上应分为一组的文本。导入你的数据后,选择编辑单元格->聚类,编辑,然后选择要用的算法。数据选项,提供快速简单的数据分布概貌。这个功能可以揭示那些可能由于输入错误导致的异常——例如,工资记录不是80,000美元而竟然是800,000美元;或指出不一致的地方——例如薪酬数据记录之间的差异,有的是计时工资,有的是每周支付,有的是年薪。除了数据管家功能,Google Refine还提供了一些有用的分析工具,例如排序和筛选。
  3、Logstash
  Logstash 是一款强大的数据处理工具,它可以实现数据传输,格式处理,格式化输出,还有强大的插件功能,常用于日志处理。
  3)数据存储工具
  数据存储主要分为结构化数据的存储和非结构化数据的存储。
  1、结构化数据
  (1)定义
  一般指存储在数据库中,具有一定逻辑结构和物理结构的数据,最为常见的是存储在关系数据库中的数据;非结构化数据:一般指结构化数据以外的数据,这些数据不存储在数据库中,而是以各种类型的文本形式存放,其中Web上的一些数据(内嵌于HTML或XML标记中)又具有一定的逻辑结构和物理结构,被称为半结构数据。
  (2)存储系统
  目前比较成熟的结构化存储系统有Oracle、MySQL、Hadoop等。
  2、非结构化数据
  (1)定义
  非结构化数据是数据结构不规则或不完整,没有预定义的数据模型,不方便用数据库二维逻辑表来表现的数据。包括所有格式的办公文档、文本、图片、XML, HTML、各类报表、图像和音频/视频信息等等。
  (2)存储方式
  1)使用文件系统存储文件,而在数据库中存储访问路径。这种方式的优点是实现简单,不需要DBMS的高级功能,但是这种方式无法实现文件的事务性访问,不便于数据备份和恢复,不便于数据迁移等;
  2)使用阿里云OSS的文件存储功能。
  4)数据计算工具
  数据计算分为实时计算、在线计算、离线计算。
  1、数据实时计算
  ApacheStorm
  2、数据在线计算
  Elasticsearch
  MySQL
  3、数据离线计算
  HaDoop Hive
  5)数据分析工具
  1、对数据矩阵科学计算:Python的numpy库
  2、对数据切片等常规处理:强大的pandas库
  3、对数据建模处理:sklearn库
  6)数据加载工具
  1、数据的可视化处理:Python中的matplotlib和seaborn库
  2、常用的BI可视化工具:Tableu和帆软
  3、ECharts
  ——————————————
  阅读推荐

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 208 次浏览 • 2022-06-07 07:15 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 132 次浏览 • 2022-06-06 08:26 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

深入浅出云原生环境信息收集技术(一)

采集交流优采云 发表了文章 • 0 个评论 • 41 次浏览 • 2022-06-05 10:27 • 来自相关话题

  深入浅出云原生环境信息收集技术(一)
  前言
  信息收集在攻击和防御两端都是非常重要的一环。从宏观的角度来说,大多数信息相关的工作都可以看作信息收集和信息处理交替进行的循环。优质的信息收集成果是后续工作顺利展开的首要条件。《孙子兵法》有云:故善战人之势,如转圆石于千仞之山者,势也。在掌握了充足信息后,攻防工作将“如转圆石于千仞之山”。
  然而,信息的琐碎性和云原生本身的复杂组成为云原生环境下的信息收集工作带来了一定挑战。有些朋友也许会说,这有何难?比如,执行uname -a命令,就能收集到内核信息了。没错,信息收集确实是一步步进行、一项项完成的。但是,如果只是想当然地进行,收集到的信息难免陷于凌乱琐碎,也很可能不全面。
  对此,笔者结合在攻、防两端积累的经验,希望与大家探讨四个问题:
  1. 站在攻击者视角,云原生环境下的信息收集方式有哪些?
  2. 站在攻击者视角,云原生环境下的信息分类维度有哪些?
  3. 站在攻击者视角,收集到的云原生环境信息有什么价值?
  4. 站在攻击者视角,有没有可能阻碍或影响防守者收集信息?
  就“信息收集”这个话题而言,毫无疑问,防守者是占尽天时地利的,无论是能够收集到的信息种类、规模,还是信息收集开始的时间、收集信息所需的权限,都远远在攻击者之上。防守者更需要关注的是如何使用、分析收集到的信息。因此,我们从攻击者的角度出发进行探讨。这并不意味着防守的同学不需要关注。相反,只有对攻击者的技术了然于胸,才能更好地识别攻击行为、判定攻击意图。
  作为本系列的第一篇文章,本文将展开讨论第一个问题:站在攻击者视角,云原生环境下的收集信息方式有哪些?
  注:文中案例相关操作均在实验环境中进行,相关技术仅供研究交流,请勿应用于未授权的渗透测试。
  站在攻击者视角,云原生环境下的信息收集方式有哪些?
  思路重在“有章可循”。先有一个点,再进行发散。信息收集方式通常与攻击场景紧密相关。在云原生环境下,攻击场景通常有三种:
  1.攻击从远程发起。远程发起的攻击十分常见,例如,通过存在未授权访问漏洞的KubernetesAPI Server或DockerDaemon执行命令。
  2.攻击从容器内发起。容器内发起的攻击通常属于一次渗透测试的后渗透阶段——它的前提是获得了容器内某种权限的Shell,或者是Containeras a Service(后文简称CaaS)的场景——攻击者本身就是容器服务的“客户”。
  3.依托于镜像的软件供应链攻击。包括“镜像漏洞利用”和“镜像投毒”等,《云原生安全:攻防实践与体系构建》第三章对此进行了详细介绍。
  相应地,信息收集方式主要也有这三种,与攻击场景相伴而生。让我们来一起看一下。
  1通过远程交互收集信息
  综合来看,云原生环境中开放的远程服务主要有两类:云原生控制面服务和容器化业务服务。远程交互,顾名思义,网络可达就行,别无限制。收集到的信息数量和价值主要取决于目标的访问控制机制。
  (1)从云原生控制面服务收集信息
  如前所述,如果遇到存在未授权访问漏洞的Kubernetes API Server,不费吹灰之力即可控制整个云原生集群;如果目标设置了合理的访问控制机制,则获取到的有价值信息将大大减少,但也并非毫无所得。例如,许多Kubernetes API Server允许匿名用户访问部分API endpoints。在下面的示例中,攻击者通过访问/version,获得了目标Kubernetes的版本号:
  rambo@t-matrix:~$ curl -k https://1.1.1.1:6443/version{ "major": "1", "minor": "16", "gitVersion": "v1.16.2", "gitCommit": "c97fe5036ef3df2967d086711e6c0c405941e14b", "gitTreeState": "clean", "buildDate": "2019-10-15T19:09:08Z", "goVersion": "go1.12.10", "compiler": "gc", "platform": "linux/amd64"}
  通过版本匹配,攻击者能够判断目标Kubernetes可能存在哪些漏洞,从而进一步利用,这便是版本信息的价值。
  即使目标设置了非常严格的访问控制,攻击者通常也能够获得一些信息。例如,即使访问/version失败,根据失败信息我们能够得知目标是一个Kubernetes集群,从而利用Kubernetes的背景知识,去探测其他Kubernetes控制面服务端口(如kubelet、etcd等)。
  (2)从容器化业务服务收集信息
  大多数情况下,就信息收集而言,容器化与非容器化业务服务没有显著不同之处,收集到的信息均与业务服务(如Web服务、数据库服务等)本身强相关。关于这些信息的收集方法,安全社区已经有很多总结,本文不再展开讲述。
  然而,许多业务在云原生化的过程中,其自身架构或部署形态也会发生变化,引入微服务治理(如服务网格)、API治理(如API网关)的特征。这些特征有时是有价值的信息,提供了承载业务的云原生环境的一些线索,同样值得收集。例如,如果我们发现与服务交互的HTTP返回头中包含了x-envoy-开头的项,可以推测该服务处于一个由Istio/Envoy进行服务网格管理的云原生环境中。其中,x-envoy-peer-metadata-id更是包含了服务所在的Pod、Pod的内部IP和Kubernetes命名空间等重要信息[1]:
  rambo@t-matrix:~$ curl -k https://1.1.1.1 -vv 2>&1 | grepx-envoy-peer-metadata-id< x-envoy-peer-metadata-id:sidecar~2.2.2.2~PodName.Namespace~Namespace.svc.cluster.local
  事实上,网络空间测绘的关键步骤之一就是通过远程交互收集信息。我们通过测绘发现,Istio、Envoy和Kong等云原生治理程序都会给被治理的业务服务添加一个或多个特征,这些特征对于探索业务网络拓扑具有积极意义。
  2容器内收集信息
  多见于针对云原生环境渗透测试的后渗透阶段,例如,攻击者事先通过Web服务文件上传漏洞上传了一个Webshell。除此之外,云服务商提供的CaaS也允许攻击者作为用户直接创建并访问容器。
  (1)容器内通过本地操作收集信息
  虽然起点不同,但这两个场景中攻击者的目的是类似的:突破容器到宿主机或其他容器。不过,两个场景下攻击者拥有的初始权限可能不同。随着人们安全意识的增强,许多容器化业务已经采用Rootless Container部署,业务进程本身以低权限用户(如www-data)运行,这通常也是攻击者获得的Webshell的权限;然而,作为CaaS的用户,攻击者通常可以拥有容器内root权限。与前文介绍的访问控制机制类似,容器内权限大小对于容器内信息收集也有影响。但是,本文并不单独讨论权限问题给信息收集工作带来的影响,而是倡导一种“因地制宜”的随机应变能力。
  “在容器内收集信息”或许是不少朋友看到本文标题后想到的第一个场景。没错,从容器内部能够收集到当前环境的大量信息。《容器逃逸技术概览》[2]中曾介绍过的通过判定/.dockerenv文件是否存在来推断是否处于Docker创建的容器环境等手法,就是典型的容器内信息收集。
  (2)容器内通过网络交互收集信息
  与前文介绍的远程交互方式相比,容器内的网络交互对于攻击者来说具有独特优势。因此,我们将这部分内容放在这里,强调“容器内”,而不是在前面一起介绍。这种优势主要有两个方面:
  1.访问内部网络。容器拥有云原生集群的内部IP,默认配置下还会有CAP_NET_RAW权限,攻击者可以通过网络扫描等方式摸清集群内部网络拓扑,发现有价值的服务,某些场景下甚至能够访问到节点主机的元数据服务。这种网络可达的优势是值得重视的,外部攻击者通常需要借助SSRF等手段才能达到相同的目的。
  2.获得一定权限。云原生集群的容器内可能会有某种形式的访问凭证,如Pod携带的ServiceAccount token等。利用此token可以向Kubernetes API Server发起访问,纵使权限很小,至少不再是“匿名访问”,能够访问/version获得版本信息。
  3基于镜像收集信息
  近年来,软件供应链安全事件频发,人们的重视程度也日渐提高。容器从镜像创建而来,就像进程从程序创建而来一样。因此,依托于镜像,攻击者能够收集到许多有价值的信息,方式主要有两种:
  1.利用镜像和镜像仓库收集信息。有时,攻击者在容器中的权限是有限的,无法读写关键文件及其元数据。如果能够获取到目标环境使用的镜像甚至找到其公开的镜像仓库,就能够分析其镜像组件的脆弱性,找到突破口。
  2.利用特殊镜像收集运行时环境信息。由于runC等容器运行时的设计问题,攻击者能够通过在目标环境部署特殊镜像来获取环境中的容器运行时二进制程序文件,进而获得版本信息,发现潜在脆弱性。《容器运行时信息收集技术介绍》[3]一文对该技术进行了详细介绍。
  总结
  本文是“深入浅出云原生环境信息收集技术”系列的开篇,帮助大家梳理了云原生环境下常见的信息收集方式。有了这些知识作为基础,我们就能够逐渐展开讨论如何在云原生环境下体系化地收集琐碎复杂的信息。以攻促防,知攻知防。一起来守护云原生安全。
  后续文章更加精彩,敬请期待!
  参考文献
  1. %20CIS%20-%20Attack%20in%20a%20Service%20Mesh%20-%20Public.pptx.pdf
  2.
  3.
  关于星云实验室
  星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案。 查看全部

  深入浅出云原生环境信息收集技术(一)
  前言
  信息收集在攻击和防御两端都是非常重要的一环。从宏观的角度来说,大多数信息相关的工作都可以看作信息收集和信息处理交替进行的循环。优质的信息收集成果是后续工作顺利展开的首要条件。《孙子兵法》有云:故善战人之势,如转圆石于千仞之山者,势也。在掌握了充足信息后,攻防工作将“如转圆石于千仞之山”。
  然而,信息的琐碎性和云原生本身的复杂组成为云原生环境下的信息收集工作带来了一定挑战。有些朋友也许会说,这有何难?比如,执行uname -a命令,就能收集到内核信息了。没错,信息收集确实是一步步进行、一项项完成的。但是,如果只是想当然地进行,收集到的信息难免陷于凌乱琐碎,也很可能不全面。
  对此,笔者结合在攻、防两端积累的经验,希望与大家探讨四个问题:
  1. 站在攻击者视角,云原生环境下的信息收集方式有哪些?
  2. 站在攻击者视角,云原生环境下的信息分类维度有哪些?
  3. 站在攻击者视角,收集到的云原生环境信息有什么价值?
  4. 站在攻击者视角,有没有可能阻碍或影响防守者收集信息?
  就“信息收集”这个话题而言,毫无疑问,防守者是占尽天时地利的,无论是能够收集到的信息种类、规模,还是信息收集开始的时间、收集信息所需的权限,都远远在攻击者之上。防守者更需要关注的是如何使用、分析收集到的信息。因此,我们从攻击者的角度出发进行探讨。这并不意味着防守的同学不需要关注。相反,只有对攻击者的技术了然于胸,才能更好地识别攻击行为、判定攻击意图。
  作为本系列的第一篇文章,本文将展开讨论第一个问题:站在攻击者视角,云原生环境下的收集信息方式有哪些?
  注:文中案例相关操作均在实验环境中进行,相关技术仅供研究交流,请勿应用于未授权的渗透测试。
  站在攻击者视角,云原生环境下的信息收集方式有哪些?
  思路重在“有章可循”。先有一个点,再进行发散。信息收集方式通常与攻击场景紧密相关。在云原生环境下,攻击场景通常有三种:
  1.攻击从远程发起。远程发起的攻击十分常见,例如,通过存在未授权访问漏洞的KubernetesAPI Server或DockerDaemon执行命令。
  2.攻击从容器内发起。容器内发起的攻击通常属于一次渗透测试的后渗透阶段——它的前提是获得了容器内某种权限的Shell,或者是Containeras a Service(后文简称CaaS)的场景——攻击者本身就是容器服务的“客户”。
  3.依托于镜像的软件供应链攻击。包括“镜像漏洞利用”和“镜像投毒”等,《云原生安全:攻防实践与体系构建》第三章对此进行了详细介绍。
  相应地,信息收集方式主要也有这三种,与攻击场景相伴而生。让我们来一起看一下。
  1通过远程交互收集信息
  综合来看,云原生环境中开放的远程服务主要有两类:云原生控制面服务和容器化业务服务。远程交互,顾名思义,网络可达就行,别无限制。收集到的信息数量和价值主要取决于目标的访问控制机制。
  (1)从云原生控制面服务收集信息
  如前所述,如果遇到存在未授权访问漏洞的Kubernetes API Server,不费吹灰之力即可控制整个云原生集群;如果目标设置了合理的访问控制机制,则获取到的有价值信息将大大减少,但也并非毫无所得。例如,许多Kubernetes API Server允许匿名用户访问部分API endpoints。在下面的示例中,攻击者通过访问/version,获得了目标Kubernetes的版本号:
  rambo@t-matrix:~$ curl -k https://1.1.1.1:6443/version{ "major": "1", "minor": "16", "gitVersion": "v1.16.2", "gitCommit": "c97fe5036ef3df2967d086711e6c0c405941e14b", "gitTreeState": "clean", "buildDate": "2019-10-15T19:09:08Z", "goVersion": "go1.12.10", "compiler": "gc", "platform": "linux/amd64"}
  通过版本匹配,攻击者能够判断目标Kubernetes可能存在哪些漏洞,从而进一步利用,这便是版本信息的价值。
  即使目标设置了非常严格的访问控制,攻击者通常也能够获得一些信息。例如,即使访问/version失败,根据失败信息我们能够得知目标是一个Kubernetes集群,从而利用Kubernetes的背景知识,去探测其他Kubernetes控制面服务端口(如kubelet、etcd等)。
  (2)从容器化业务服务收集信息
  大多数情况下,就信息收集而言,容器化与非容器化业务服务没有显著不同之处,收集到的信息均与业务服务(如Web服务、数据库服务等)本身强相关。关于这些信息的收集方法,安全社区已经有很多总结,本文不再展开讲述。
  然而,许多业务在云原生化的过程中,其自身架构或部署形态也会发生变化,引入微服务治理(如服务网格)、API治理(如API网关)的特征。这些特征有时是有价值的信息,提供了承载业务的云原生环境的一些线索,同样值得收集。例如,如果我们发现与服务交互的HTTP返回头中包含了x-envoy-开头的项,可以推测该服务处于一个由Istio/Envoy进行服务网格管理的云原生环境中。其中,x-envoy-peer-metadata-id更是包含了服务所在的Pod、Pod的内部IP和Kubernetes命名空间等重要信息[1]:
  rambo@t-matrix:~$ curl -k https://1.1.1.1 -vv 2>&1 | grepx-envoy-peer-metadata-id< x-envoy-peer-metadata-id:sidecar~2.2.2.2~PodName.Namespace~Namespace.svc.cluster.local
  事实上,网络空间测绘的关键步骤之一就是通过远程交互收集信息。我们通过测绘发现,Istio、Envoy和Kong等云原生治理程序都会给被治理的业务服务添加一个或多个特征,这些特征对于探索业务网络拓扑具有积极意义。
  2容器内收集信息
  多见于针对云原生环境渗透测试的后渗透阶段,例如,攻击者事先通过Web服务文件上传漏洞上传了一个Webshell。除此之外,云服务商提供的CaaS也允许攻击者作为用户直接创建并访问容器。
  (1)容器内通过本地操作收集信息
  虽然起点不同,但这两个场景中攻击者的目的是类似的:突破容器到宿主机或其他容器。不过,两个场景下攻击者拥有的初始权限可能不同。随着人们安全意识的增强,许多容器化业务已经采用Rootless Container部署,业务进程本身以低权限用户(如www-data)运行,这通常也是攻击者获得的Webshell的权限;然而,作为CaaS的用户,攻击者通常可以拥有容器内root权限。与前文介绍的访问控制机制类似,容器内权限大小对于容器内信息收集也有影响。但是,本文并不单独讨论权限问题给信息收集工作带来的影响,而是倡导一种“因地制宜”的随机应变能力。
  “在容器内收集信息”或许是不少朋友看到本文标题后想到的第一个场景。没错,从容器内部能够收集到当前环境的大量信息。《容器逃逸技术概览》[2]中曾介绍过的通过判定/.dockerenv文件是否存在来推断是否处于Docker创建的容器环境等手法,就是典型的容器内信息收集。
  (2)容器内通过网络交互收集信息
  与前文介绍的远程交互方式相比,容器内的网络交互对于攻击者来说具有独特优势。因此,我们将这部分内容放在这里,强调“容器内”,而不是在前面一起介绍。这种优势主要有两个方面:
  1.访问内部网络。容器拥有云原生集群的内部IP,默认配置下还会有CAP_NET_RAW权限,攻击者可以通过网络扫描等方式摸清集群内部网络拓扑,发现有价值的服务,某些场景下甚至能够访问到节点主机的元数据服务。这种网络可达的优势是值得重视的,外部攻击者通常需要借助SSRF等手段才能达到相同的目的。
  2.获得一定权限。云原生集群的容器内可能会有某种形式的访问凭证,如Pod携带的ServiceAccount token等。利用此token可以向Kubernetes API Server发起访问,纵使权限很小,至少不再是“匿名访问”,能够访问/version获得版本信息。
  3基于镜像收集信息
  近年来,软件供应链安全事件频发,人们的重视程度也日渐提高。容器从镜像创建而来,就像进程从程序创建而来一样。因此,依托于镜像,攻击者能够收集到许多有价值的信息,方式主要有两种:
  1.利用镜像和镜像仓库收集信息。有时,攻击者在容器中的权限是有限的,无法读写关键文件及其元数据。如果能够获取到目标环境使用的镜像甚至找到其公开的镜像仓库,就能够分析其镜像组件的脆弱性,找到突破口。
  2.利用特殊镜像收集运行时环境信息。由于runC等容器运行时的设计问题,攻击者能够通过在目标环境部署特殊镜像来获取环境中的容器运行时二进制程序文件,进而获得版本信息,发现潜在脆弱性。《容器运行时信息收集技术介绍》[3]一文对该技术进行了详细介绍。
  总结
  本文是“深入浅出云原生环境信息收集技术”系列的开篇,帮助大家梳理了云原生环境下常见的信息收集方式。有了这些知识作为基础,我们就能够逐渐展开讨论如何在云原生环境下体系化地收集琐碎复杂的信息。以攻促防,知攻知防。一起来守护云原生安全。
  后续文章更加精彩,敬请期待!
  参考文献
  1. %20CIS%20-%20Attack%20in%20a%20Service%20Mesh%20-%20Public.pptx.pdf
  2.
  3.
  关于星云实验室
  星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案。

聊聊Spring Boot服务监控,健康检查,线程信息,JVM堆信息,指标收集

采集交流优采云 发表了文章 • 0 个评论 • 87 次浏览 • 2022-06-02 11:23 • 来自相关话题

  聊聊Spring Boot服务监控,健康检查,线程信息,JVM堆信息,指标收集
  点击上方“芋道源码”,选择“”
  管她前浪,还是后浪?
  能浪的浪,才是好浪!
  每天 10:33更新文章,每天掉亿点点头发...
  源码精品专栏
  去年我们项目做了微服务1.0的架构转型,但是服务监控这块却没有跟上。这不,最近我就被分配了要将我们核心的微服务应用全部监控起来的任务。我们的微服务应用都是SpringBoot 应用,因此就自然而然的想到了借助Spring Boot 的Actuator 模块。(没吃过猪肉总听过猪叫见过猪跑吧)。
  本篇是我在完成这个工单之后,对Spring Boot Actuator模块 学习应用的总结。在本篇文章中,你可以学习到:
  之后我还会介绍:
  推荐下自己做的 Spring Boot 的实战项目:
  Spring Boot Actuator 模块提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等,帮助我们监控和管理Spring Boot 应用。这个模块是一个采集应用内部信息暴露给外部的模块,上述的功能都可以通过HTTP 和 JMX 访问。
  因为暴露内部信息的特性,Actuator 也可以和一些外部的应用监控系统整合(Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic等)。这些监控系统提供了出色的仪表板,图形,分析和警报,可帮助你通过一个统一友好的界面,监视和管理你的应用程序。
  Actuator使用Micrometer与这些外部应用程序监视系统集成。这样一来,只需很少的配置即可轻松集成外部的监控系统。
  Micrometer 为 Java 平台上的性能数据收集提供了一个通用的 API,应用程序只需要使用 Micrometer 的通用 API 来收集性能指标即可。Micrometer 会负责完成与不同监控系统的适配工作。这就使得切换监控系统变得很容易。
  对比 Slf4j 之于 Java Logger 中的定位。
  推荐下自己做的 Spring Cloud 的实战项目:
  我们先创建一个demo应用。
  spring init -d=web,actuator -n=actuator-demo actuator-demo<br /><br />
  image.png
  <br />    ...<br /> <br />  org.springframework.boot<br />  spring-boot-starter-actuator<br /> <br />    ...<br /><br /><br /><br />
  dependencies {<br /> compile("org.springframework.boot:spring-boot-starter-actuator")<br />}<br /><br />
  Spring Boot 提供了所谓的 endpoints (下文翻译为端点)给外部来与应用程序进行访问和交互。
  打比方来说,/health 端点 提供了关于应用健康情况的一些基础信息。metrics 端点提供了一些有用的应用程序指标(JVM 内存使用、系统CPU使用等)。
  这些 Actuator 模块本来就有的端点我们称之为原生端点。根据端点的作用的话,我们大概可以分为三大类:
  详细的原生端点介绍,请以官网为准,这里就不赘述徒增篇幅。
  需要注意的就是:
  我们可以通过以下配置,来配置通过JMX 和 HTTP 暴露的端点。
  PropertyDefault
  management.endpoints.jmx.exposure.exclude
  management.endpoints.jmx.exposure.include
  *
  management.endpoints.web.exposure.exclude
  management.endpoints.web.exposure.include
  info, healt
  可以打开所有的监控点
  management.endpoints.web.exposure.include=*<br /><br />
  也可以选择打开部分,"*" 代表暴露所有的端点,如果指定多个端点,用","分开
  management.endpoints.web.exposure.exclude=beans,trace<br /><br />
  Actuator 默认所有的监控点路径都在/actuator/*,当然如果有需要这个路径也支持定制。
  management.endpoints.web.base-path=/minitor<br /><br />
  设置完重启后,再次访问地址就会变成/minitor/*。
  现在我们按照如下配置:
  # "*" 代表暴露所有的端点 如果指定多个端点,用","分开<br />management.endpoints.web.exposure.include=*<br /># 赋值规则同上<br />management.endpoints.web.exposure.exclude=<br /><br />
  启动DEMO程序,访问:8080/actuator,查看暴露出来的端点:
  image.png
  上面这样显示是因为chrome 浏览器安装了 JSON-handle 插件,实际上就是返回一大段json
  下面,我会着重介绍几个比较重要的端点。
  /health端点会聚合你程序的健康指标,来检查程序的健康情况。端点公开的应用健康信息取决于:
  management.endpoint.health.show-details=always<br /><br />
  该属性可以使用以下值之一进行配置:
  NameDescription
  never
  不展示详细信息,up或者down的状态,默认配置
  when-authorized
  详细信息将会展示给通过认证的用户。授权的角色可以通过management.endpoint.health.roles配置
  always
  对所有用户暴露详细信息
  按照上述配置,配置成always之后,我们启动项目,访问:8080/actuator/health端口,可以看到这样的信息:
  image.png
  是不是感觉好像健康信息有点少?先别急,那是因为我们创建的是一个最基础的Demo项目,没有依赖很多的组件。
  /health端点有很多自动配置的健康指示器:如redis、rabbitmq、db等组件。当你的项目有依赖对应组件的时候,这些健康指示器就会被自动装配,继而采集对应的信息。如上面的 diskSpace 节点信息就是DiskSpaceHealthIndicator 在起作用。
  
  image.png
  上述截图取自官方文档
  这是我另一个项目的/health端点信息。
  image.png
  当如上的组件有一个状态异常,应用服务的整体状态即为down。我们也可以通过配置禁用某个组件的健康监测。
  management.health.mongo.enabled: false<br /><br />
  或者禁用所有自动配置的健康指示器:
  management.health.defaults.enabled: false<br /><br />
  当然你也可以自定义一个Health Indicator,只需要实现HealthIndicator 接口或者继承AbstractHealthIndicator类。
  /**<br /> * @author Richard_yyf<br /> * @version 1.0 2020/1/16<br /> */<br />@Component<br />public class CustomHealthIndicator extends AbstractHealthIndicator {<br /><br />    @Override<br />    protected void doHealthCheck(Health.Builder builder) throws Exception {<br />        // 使用 builder 来创建健康状态信息<br />        // 如果你throw 了一个 exception,那么status 就会被置为DOWN,异常信息会被记录下来<br />        builder.up()<br />                .withDetail("app", "这个项目很健康")<br />                .withDetail("error", "Nothing, I'm very good");<br />    }<br />}<br /><br />
  最终效果:
  image.png
  /metrics端点用来返回当前应用的各类重要度量指标,比如:内存信息、线程信息、垃圾回收信息、tomcat、数据库连接池等。
  {<br />    "names": [<br />        "tomcat.threads.busy",<br />        "jvm.threads.states",<br />        "jdbc.connections.active",<br />        "jvm.gc.memory.promoted",<br />        "http.server.requests",<br />        "hikaricp.connections.max",<br />        "hikaricp.connections.min",<br />        "jvm.memory.used",<br />        "jvm.gc.max.data.size",<br />        "jdbc.connections.max",<br />         ....<br />    ]<br />}<br /><br />
  不同于1.x,Actuator在这个界面看不到具体的指标信息,只是展示了一个指标列表。 为了获取到某个指标的详细信息,我们可以请求具体的指标信息,像这样:
  http://localhost:8080/actuator/metrics/{MetricName}<br /><br />
  比如我访问/actuator/metrics/jvm.memory.max,返回信息如下:
  image.png
  你也可以用query param的方式查看单独的一块区域。比如你可以访问/actuator/metrics/jvm.memory.max?tag=id:Metaspace。结果就是:
  image.png
  /loggers 端点暴露了我们程序内部配置的所有logger的信息。我们访问/actuator/loggers可以看到,
  image.png
  你也可以通过下述方式访问单独一个logger,
  http://localhost:8080/actuator/loggers/{name}<br /><br />
  比如我现在访问 root logger,:8080/actuator/loggers/root
  {<br />    "configuredLevel": "INFO",<br />    "effectiveLevel": "INFO"<br />}<br /><br />
  /loggers端点我最想提的就是这个功能,能够动态修改你的日志等级。
  比如,我们可以通过下述方式来修改 root logger的日志等级。我们只需要发起一个URL 为:8080/actuator/loggers/root的POST请求,POST报文如下:
  {<br />   "configuredLevel": "DEBUG"<br />}<br /><br />
  image.png
  仔细想想,这个功能是不是非常有用。如果在生产环境中,你想要你的应用输出一些Debug信息以便于你诊断一些异常情况,你你只需要按照上述方式就可以修改,而不需要重启应用。
  如果想重置成默认值,把value 改成 null
  /info端点可以用来展示你程序的信息。我理解过来就是一些程序的基础信息。并且你可以按照自己的需求在配置文件application.properties中个性化配置(默认情况下,该端点只会返回一个空的json内容。):
  info.app.name=actuator-test-demo<br />info.app.encoding=UTF-8<br />info.app.java.source=1.8<br />info.app.java.target=1.8<br /># 在 maven 项目中你可以直接用下列方式引用 maven properties的值<br /># info.app.encoding=@project.build.sourceEncoding@<br /># info.app.java.source=@java.version@<br /># info.app.java.target=@java.version@<br /><br />
  启动项目,访问:8080/actuator/info:
  {<br />    "app": {<br />        "encoding": "UTF-8",<br />        "java": {<br />            "source": "1.8.0_131",<br />            "target": "1.8.0_131"<br />        },<br />        "name": "actuator-test-demo"<br />    }<br />}<br /><br />
  /beans端点会返回Spring 容器中所有bean的别名、类型、是否单例、依赖等信息。
  访问:8080/actuator/beans,返回如下:
  image.png
  访问::8080/actuator/heapdump会自动生成一个 Jvm 的堆文件 heapdump。我们可以使用 JDK 自带的 Jvm 监控工具 VisualVM 打开此文件查看内存快照。
  image.png
  这个端点我个人觉得特别有用,方便我们在日常定位问题的时候查看线程的情况。主要展示了线程名、线程ID、线程的状态、是否等待锁资源、线程堆栈等信息。就是可能查看起来不太直观。访问:8080/actuator/threaddump返回如下:
  image.png
  这个端点属于操作控制类端点,可以优雅关闭 Spring Boot 应用。要使用这个功能首先需要在配置文件中开启:
  management.endpoint.shutdown.enabled=true<br /><br />
  由于 shutdown 接口默认只支持 POST 请求 ,我们启动Demo项目,向:8080/actuator/shutdown发起POST请求。返回信息:
  {<br />    "message": "Shutting down, bye..."<br />}<br /><br />
  然后应用程序被关闭。
  由于开放关闭应用的操作本身是一件非常危险 的事,所以真正在线上使用的时候,我们需要对其加入一定的保护机制,比如:定制Actuator的端点路径、整合Spring Security进行安全校验 等。(不是特别必要的话,这个端点不用开)
  由于端点的信息和产生的交互都是非常敏感的,必须防止未经授权的外部访问。如果您的应用程序中存在Spring Security 的依赖,则默认情况下使用基于表单的HTTP身份验证 来保护端点。
  如果没有,只需要增加对应的依赖即可:
  <br />   org.springframework.boot<br />   spring-boot-starter-security<br /><br /><br />
  添加之后,我们需要定义安全校验规则,来覆盖Spring Security 的默认配置。
  这里我给出了两个版本的模板配置:
  import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;<br />import org.springframework.boot.actuate.context.ShutdownEndpoint;<br />import org.springframework.boot.autoconfigure.security.servlet.PathRequest;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.security.config.annotation.web.builders.HttpSecurity;<br />import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;<br /><br />/**<br /> * @author Richard_yyf<br /> */<br />@Configuration<br />public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {<br /><br />    /*<br />     * version1:<br />     * 1. 限制 '/shutdown'端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问其他的端点<br />     * 3. 允许外部访问静态资源<br />     * 4. 允许外部访问 '/'<br />     * 5. 其他的访问需要被校验<br />     * version2:<br />     * 1. 限制所有端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问静态资源<br />     * 3. 允许外部访问 '/'<br />     * 4. 其他的访问需要被校验<br />     */<br /><br />    @Override<br />    protected void configure(HttpSecurity http) throws Exception {<br />        // version1<br />//        http<br />//                .authorizeRequests()<br />//                    .requestMatchers(EndpointRequest.to(ShutdownEndpoint.class))<br />//                        .hasRole("ACTUATOR_ADMIN")<br />//                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />//                    .permitAll()<br />//                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />//                    .permitAll()<br />//                .antMatchers("/")<br />//                    .permitAll()<br />//                .antMatchers("/**")<br />//                    .authenticated()<br />//                .and()<br />//                .httpBasic();<br /><br />        // version2<br />        http<br />                .authorizeRequests()<br />                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />                    .hasRole("ACTUATOR_ADMIN")<br />                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />                    .permitAll()<br />                .antMatchers("/")<br />                    .permitAll()<br />                .antMatchers("/**")<br />                    .authenticated()<br />                .and()<br />                .httpBasic();<br />    }<br />}<br /><br />
  application.properties的相关配置如下:
  # Spring Security Default user name and password<br />spring.security.user.name=actuator<br />spring.security.user.password=actuator<br />spring.security.user.roles=ACTUATOR_ADMIN<br />
  - END -
  欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
  
  已在知识星球更新源码解析如下:
  最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
  提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。 查看全部

  聊聊Spring Boot服务监控,健康检查,线程信息,JVM堆信息,指标收集
  点击上方“芋道源码”,选择“”
  管她前浪,还是后浪?
  能浪的浪,才是好浪!
  每天 10:33更新文章,每天掉亿点点头发...
  源码精品专栏
  去年我们项目做了微服务1.0的架构转型,但是服务监控这块却没有跟上。这不,最近我就被分配了要将我们核心的微服务应用全部监控起来的任务。我们的微服务应用都是SpringBoot 应用,因此就自然而然的想到了借助Spring Boot 的Actuator 模块。(没吃过猪肉总听过猪叫见过猪跑吧)。
  本篇是我在完成这个工单之后,对Spring Boot Actuator模块 学习应用的总结。在本篇文章中,你可以学习到:
  之后我还会介绍:
  推荐下自己做的 Spring Boot 的实战项目:
  Spring Boot Actuator 模块提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等,帮助我们监控和管理Spring Boot 应用。这个模块是一个采集应用内部信息暴露给外部的模块,上述的功能都可以通过HTTP 和 JMX 访问。
  因为暴露内部信息的特性,Actuator 也可以和一些外部的应用监控系统整合(Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic等)。这些监控系统提供了出色的仪表板,图形,分析和警报,可帮助你通过一个统一友好的界面,监视和管理你的应用程序。
  Actuator使用Micrometer与这些外部应用程序监视系统集成。这样一来,只需很少的配置即可轻松集成外部的监控系统。
  Micrometer 为 Java 平台上的性能数据收集提供了一个通用的 API,应用程序只需要使用 Micrometer 的通用 API 来收集性能指标即可。Micrometer 会负责完成与不同监控系统的适配工作。这就使得切换监控系统变得很容易。
  对比 Slf4j 之于 Java Logger 中的定位。
  推荐下自己做的 Spring Cloud 的实战项目:
  我们先创建一个demo应用。
  spring init -d=web,actuator -n=actuator-demo actuator-demo<br /><br />
  image.png
  <br />    ...<br /> <br />  org.springframework.boot<br />  spring-boot-starter-actuator<br /> <br />    ...<br /><br /><br /><br />
  dependencies {<br /> compile("org.springframework.boot:spring-boot-starter-actuator")<br />}<br /><br />
  Spring Boot 提供了所谓的 endpoints (下文翻译为端点)给外部来与应用程序进行访问和交互。
  打比方来说,/health 端点 提供了关于应用健康情况的一些基础信息。metrics 端点提供了一些有用的应用程序指标(JVM 内存使用、系统CPU使用等)。
  这些 Actuator 模块本来就有的端点我们称之为原生端点。根据端点的作用的话,我们大概可以分为三大类:
  详细的原生端点介绍,请以官网为准,这里就不赘述徒增篇幅。
  需要注意的就是:
  我们可以通过以下配置,来配置通过JMX 和 HTTP 暴露的端点。
  PropertyDefault
  management.endpoints.jmx.exposure.exclude
  management.endpoints.jmx.exposure.include
  *
  management.endpoints.web.exposure.exclude
  management.endpoints.web.exposure.include
  info, healt
  可以打开所有的监控点
  management.endpoints.web.exposure.include=*<br /><br />
  也可以选择打开部分,"*" 代表暴露所有的端点,如果指定多个端点,用","分开
  management.endpoints.web.exposure.exclude=beans,trace<br /><br />
  Actuator 默认所有的监控点路径都在/actuator/*,当然如果有需要这个路径也支持定制。
  management.endpoints.web.base-path=/minitor<br /><br />
  设置完重启后,再次访问地址就会变成/minitor/*。
  现在我们按照如下配置:
  # "*" 代表暴露所有的端点 如果指定多个端点,用","分开<br />management.endpoints.web.exposure.include=*<br /># 赋值规则同上<br />management.endpoints.web.exposure.exclude=<br /><br />
  启动DEMO程序,访问:8080/actuator,查看暴露出来的端点:
  image.png
  上面这样显示是因为chrome 浏览器安装了 JSON-handle 插件,实际上就是返回一大段json
  下面,我会着重介绍几个比较重要的端点。
  /health端点会聚合你程序的健康指标,来检查程序的健康情况。端点公开的应用健康信息取决于:
  management.endpoint.health.show-details=always<br /><br />
  该属性可以使用以下值之一进行配置:
  NameDescription
  never
  不展示详细信息,up或者down的状态,默认配置
  when-authorized
  详细信息将会展示给通过认证的用户。授权的角色可以通过management.endpoint.health.roles配置
  always
  对所有用户暴露详细信息
  按照上述配置,配置成always之后,我们启动项目,访问:8080/actuator/health端口,可以看到这样的信息:
  image.png
  是不是感觉好像健康信息有点少?先别急,那是因为我们创建的是一个最基础的Demo项目,没有依赖很多的组件。
  /health端点有很多自动配置的健康指示器:如redis、rabbitmq、db等组件。当你的项目有依赖对应组件的时候,这些健康指示器就会被自动装配,继而采集对应的信息。如上面的 diskSpace 节点信息就是DiskSpaceHealthIndicator 在起作用。
  
  image.png
  上述截图取自官方文档
  这是我另一个项目的/health端点信息。
  image.png
  当如上的组件有一个状态异常,应用服务的整体状态即为down。我们也可以通过配置禁用某个组件的健康监测。
  management.health.mongo.enabled: false<br /><br />
  或者禁用所有自动配置的健康指示器:
  management.health.defaults.enabled: false<br /><br />
  当然你也可以自定义一个Health Indicator,只需要实现HealthIndicator 接口或者继承AbstractHealthIndicator类。
  /**<br /> * @author Richard_yyf<br /> * @version 1.0 2020/1/16<br /> */<br />@Component<br />public class CustomHealthIndicator extends AbstractHealthIndicator {<br /><br />    @Override<br />    protected void doHealthCheck(Health.Builder builder) throws Exception {<br />        // 使用 builder 来创建健康状态信息<br />        // 如果你throw 了一个 exception,那么status 就会被置为DOWN,异常信息会被记录下来<br />        builder.up()<br />                .withDetail("app", "这个项目很健康")<br />                .withDetail("error", "Nothing, I'm very good");<br />    }<br />}<br /><br />
  最终效果:
  image.png
  /metrics端点用来返回当前应用的各类重要度量指标,比如:内存信息、线程信息、垃圾回收信息、tomcat、数据库连接池等。
  {<br />    "names": [<br />        "tomcat.threads.busy",<br />        "jvm.threads.states",<br />        "jdbc.connections.active",<br />        "jvm.gc.memory.promoted",<br />        "http.server.requests",<br />        "hikaricp.connections.max",<br />        "hikaricp.connections.min",<br />        "jvm.memory.used",<br />        "jvm.gc.max.data.size",<br />        "jdbc.connections.max",<br />         ....<br />    ]<br />}<br /><br />
  不同于1.x,Actuator在这个界面看不到具体的指标信息,只是展示了一个指标列表。 为了获取到某个指标的详细信息,我们可以请求具体的指标信息,像这样:
  http://localhost:8080/actuator/metrics/{MetricName}<br /><br />
  比如我访问/actuator/metrics/jvm.memory.max,返回信息如下:
  image.png
  你也可以用query param的方式查看单独的一块区域。比如你可以访问/actuator/metrics/jvm.memory.max?tag=id:Metaspace。结果就是:
  image.png
  /loggers 端点暴露了我们程序内部配置的所有logger的信息。我们访问/actuator/loggers可以看到,
  image.png
  你也可以通过下述方式访问单独一个logger,
  http://localhost:8080/actuator/loggers/{name}<br /><br />
  比如我现在访问 root logger,:8080/actuator/loggers/root
  {<br />    "configuredLevel": "INFO",<br />    "effectiveLevel": "INFO"<br />}<br /><br />
  /loggers端点我最想提的就是这个功能,能够动态修改你的日志等级。
  比如,我们可以通过下述方式来修改 root logger的日志等级。我们只需要发起一个URL 为:8080/actuator/loggers/root的POST请求,POST报文如下:
  {<br />   "configuredLevel": "DEBUG"<br />}<br /><br />
  image.png
  仔细想想,这个功能是不是非常有用。如果在生产环境中,你想要你的应用输出一些Debug信息以便于你诊断一些异常情况,你你只需要按照上述方式就可以修改,而不需要重启应用。
  如果想重置成默认值,把value 改成 null
  /info端点可以用来展示你程序的信息。我理解过来就是一些程序的基础信息。并且你可以按照自己的需求在配置文件application.properties中个性化配置(默认情况下,该端点只会返回一个空的json内容。):
  info.app.name=actuator-test-demo<br />info.app.encoding=UTF-8<br />info.app.java.source=1.8<br />info.app.java.target=1.8<br /># 在 maven 项目中你可以直接用下列方式引用 maven properties的值<br /># info.app.encoding=@project.build.sourceEncoding@<br /># info.app.java.source=@java.version@<br /># info.app.java.target=@java.version@<br /><br />
  启动项目,访问:8080/actuator/info:
  {<br />    "app": {<br />        "encoding": "UTF-8",<br />        "java": {<br />            "source": "1.8.0_131",<br />            "target": "1.8.0_131"<br />        },<br />        "name": "actuator-test-demo"<br />    }<br />}<br /><br />
  /beans端点会返回Spring 容器中所有bean的别名、类型、是否单例、依赖等信息。
  访问:8080/actuator/beans,返回如下:
  image.png
  访问::8080/actuator/heapdump会自动生成一个 Jvm 的堆文件 heapdump。我们可以使用 JDK 自带的 Jvm 监控工具 VisualVM 打开此文件查看内存快照。
  image.png
  这个端点我个人觉得特别有用,方便我们在日常定位问题的时候查看线程的情况。主要展示了线程名、线程ID、线程的状态、是否等待锁资源、线程堆栈等信息。就是可能查看起来不太直观。访问:8080/actuator/threaddump返回如下:
  image.png
  这个端点属于操作控制类端点,可以优雅关闭 Spring Boot 应用。要使用这个功能首先需要在配置文件中开启:
  management.endpoint.shutdown.enabled=true<br /><br />
  由于 shutdown 接口默认只支持 POST 请求 ,我们启动Demo项目,向:8080/actuator/shutdown发起POST请求。返回信息:
  {<br />    "message": "Shutting down, bye..."<br />}<br /><br />
  然后应用程序被关闭。
  由于开放关闭应用的操作本身是一件非常危险 的事,所以真正在线上使用的时候,我们需要对其加入一定的保护机制,比如:定制Actuator的端点路径、整合Spring Security进行安全校验 等。(不是特别必要的话,这个端点不用开)
  由于端点的信息和产生的交互都是非常敏感的,必须防止未经授权的外部访问。如果您的应用程序中存在Spring Security 的依赖,则默认情况下使用基于表单的HTTP身份验证 来保护端点。
  如果没有,只需要增加对应的依赖即可:
  <br />   org.springframework.boot<br />   spring-boot-starter-security<br /><br /><br />
  添加之后,我们需要定义安全校验规则,来覆盖Spring Security 的默认配置。
  这里我给出了两个版本的模板配置:
  import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;<br />import org.springframework.boot.actuate.context.ShutdownEndpoint;<br />import org.springframework.boot.autoconfigure.security.servlet.PathRequest;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.security.config.annotation.web.builders.HttpSecurity;<br />import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;<br /><br />/**<br /> * @author Richard_yyf<br /> */<br />@Configuration<br />public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {<br /><br />    /*<br />     * version1:<br />     * 1. 限制 '/shutdown'端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问其他的端点<br />     * 3. 允许外部访问静态资源<br />     * 4. 允许外部访问 '/'<br />     * 5. 其他的访问需要被校验<br />     * version2:<br />     * 1. 限制所有端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问静态资源<br />     * 3. 允许外部访问 '/'<br />     * 4. 其他的访问需要被校验<br />     */<br /><br />    @Override<br />    protected void configure(HttpSecurity http) throws Exception {<br />        // version1<br />//        http<br />//                .authorizeRequests()<br />//                    .requestMatchers(EndpointRequest.to(ShutdownEndpoint.class))<br />//                        .hasRole("ACTUATOR_ADMIN")<br />//                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />//                    .permitAll()<br />//                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />//                    .permitAll()<br />//                .antMatchers("/")<br />//                    .permitAll()<br />//                .antMatchers("/**")<br />//                    .authenticated()<br />//                .and()<br />//                .httpBasic();<br /><br />        // version2<br />        http<br />                .authorizeRequests()<br />                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />                    .hasRole("ACTUATOR_ADMIN")<br />                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />                    .permitAll()<br />                .antMatchers("/")<br />                    .permitAll()<br />                .antMatchers("/**")<br />                    .authenticated()<br />                .and()<br />                .httpBasic();<br />    }<br />}<br /><br />
  application.properties的相关配置如下:
  # Spring Security Default user name and password<br />spring.security.user.name=actuator<br />spring.security.user.password=actuator<br />spring.security.user.roles=ACTUATOR_ADMIN<br />
  - END -
  欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
  
  已在知识星球更新源码解析如下:
  最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
  提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 131 次浏览 • 2022-05-29 05:57 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 85 次浏览 • 2022-05-28 21:46 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

springcloud微服务实践:天气数据API微服务的实现

采集交流优采云 发表了文章 • 0 个评论 • 57 次浏览 • 2022-05-27 20:42 • 来自相关话题

  springcloud微服务实践:天气数据API微服务的实现
  天气数据API 微服务的实现
  天气数据API微服务包含了天气数据查询组件。天气数据查询组件提供了天气数据查询的接口。
  我们的数据已经通过天气数据采集微服务集成到了Redis 存储中,天气数据API微服务只需要从Redis获取数据,而后从接口中暴露出去即可。
  在micro-weather-report应用的基础上,我们将对其进行逐步的拆分,形成一个新的微服务msa-weather-data-server应用。
  
  所需环境
  为了演示本例子,需要采用如下开发环境。
  修改天气数据服务接口及实现
  在com.waylau.spring.cloud. weather.service包下,我们之前已经定义了该应用的天气数据服务接口WeatherDataService。
  public interface WeatherDataService {<br />*★<br />*根据城市ID查询天气数据<br />@param cityId<br />@return <br />WeatherResponse getDataByCityId(String cityId);<br />/**<br />★根据城市名称查询天气数据<br />* @param cityId<br />* Creturn<br />*/<br />Wea therResponse getDataByCityName (String cityName) ;<br />}
  对于该微服务而言,我们并不需要同步天气的业务需求,所以把之前定义的syncDataByCityId方法删除了。
  WeatherDataServicelmpl是对WeatherDataService 接口的实现,也要做出相应的调整,将同步天气的代码逻辑都删除,保留以下代码。
  package com. way1au . spr ing.cloud.weather .service;<br />import java. io. IOException;<br />import org.slf4j . Logger;<br />import org.slf4j. LoggerFactory;<br />import org.springf ramework. beans. factory . annotation.Autowired;<br />import org. springfr amework. data. redis. core. StringRedisTemplate;<br />import org. springf ramework. data. redis.core. ValueOperations;<br />import org. springf ramework. stereotype. Service;<br />import com. fasterxml. jackson.databind. objectMapper;<br />import com. waylau. spring. cloud . weather . vo . Wea therResponse;<br />/**<br />k天气数据服务.<br />@since 1.0.0 2017年10月29日 <br />* @author Way Lau<br />@Service<br />public class WeatherDataServiceImpl implements WeatherDataService {<br />private final static Logger logger = LoggerFactory .getLogger (Weather<br />DataServiceImpl.class) ;<br />@Autowi red<br />private StringRedisTemplate stringRedisTemplate; <br />private final String WEATHER API = "http://wthrcdn. etouch . cn/weather_<br />mini";<br />@Override<br />public WeatherResponse getDa taByCityId(String cityId) {<br />String uri = WEATHER API + "?citykey=" + cityId;<br />return this. doGetWea therData (uri) ;<br />@Override<br />public WeatherResponse getDataByCityName (String cityName) {<br />String uri = WEATHER_ API + "?city=" + cityName ;<br />return this. doGe tWeatherData (uri) ;<br />private WeatherResponse doGetWea therData (String uri)<br />valueOperations ops = this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />/先查缓存,查不到抛出异常<br />if (!this. stringRedisTemplate . hasKey (key)) {<br />logger .error("不存在key "+ key) ;<br />throw new Runt imeException ("没有相应的天气信息") ;<br />} else {<br />logger.info("存在key"+ key + ", value=" + ops.get (key));<br />strBody = ops.get (key) ;<br />}<br />0bj ectMapper mapper = new ObjectMapper () ;<br />WeatherResponse weather = null;<br />try {<br />weather = mapper . readvalue (strBody, WeatherResponse.class) ;<br />} catch (IOException e) {<br />logger . error ("JSON反序列化异常! ",e);<br />throw new RuntimeException ("天气信息解析失败") ;<br />return weather;<br />}<br />}
  其中需要注意的是:
  ●原有的RestTemplate用作REST客户端来进行天气数据的同步,这个类相关的代码都可以删除了;
  ●服务 会先从缓存中进行查询,查不到数据就抛出异常(有可能该城市的天气数据未同步,或者是数据已经过期) ;
  在执行反序列化JSON过程中也可能遭遇异常,同样将异常信息抛出。
  除上述WeatherDataServicelmpl、WeatherDataService 外,其他服务层的代码都可以删除了。
  调整控制层的代码
  除了WeatherController 外,其他控制层的代码都不需要了。
  WeatherController仍然是原有的代码保持不变。
  
  删除配置类、天气数据同步任务和工具类
  配置类RestConfiguration、QuartzConfiguration 及任务类WeatherDataSyncJob、 工具类Xml-
  Builder的代码都可以删除了。
  清理值对象
  值对象我们需要保留解析天气相关的类即可,其他值对象(如City. CityList等)都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并 删除的。
  同时,之前所编写的页面HTML、JS文件也要一并 删除。
  最后,要清理Thymeleaf在application.properties文件中的配置,以及build.gradle文件中的依赖。 查看全部

  springcloud微服务实践:天气数据API微服务的实现
  天气数据API 微服务的实现
  天气数据API微服务包含了天气数据查询组件。天气数据查询组件提供了天气数据查询的接口。
  我们的数据已经通过天气数据采集微服务集成到了Redis 存储中,天气数据API微服务只需要从Redis获取数据,而后从接口中暴露出去即可。
  在micro-weather-report应用的基础上,我们将对其进行逐步的拆分,形成一个新的微服务msa-weather-data-server应用。
  
  所需环境
  为了演示本例子,需要采用如下开发环境。
  修改天气数据服务接口及实现
  在com.waylau.spring.cloud. weather.service包下,我们之前已经定义了该应用的天气数据服务接口WeatherDataService。
  public interface WeatherDataService {<br />*★<br />*根据城市ID查询天气数据<br />@param cityId<br />@return <br />WeatherResponse getDataByCityId(String cityId);<br />/**<br />★根据城市名称查询天气数据<br />* @param cityId<br />* Creturn<br />*/<br />Wea therResponse getDataByCityName (String cityName) ;<br />}
  对于该微服务而言,我们并不需要同步天气的业务需求,所以把之前定义的syncDataByCityId方法删除了。
  WeatherDataServicelmpl是对WeatherDataService 接口的实现,也要做出相应的调整,将同步天气的代码逻辑都删除,保留以下代码。
  package com. way1au . spr ing.cloud.weather .service;<br />import java. io. IOException;<br />import org.slf4j . Logger;<br />import org.slf4j. LoggerFactory;<br />import org.springf ramework. beans. factory . annotation.Autowired;<br />import org. springfr amework. data. redis. core. StringRedisTemplate;<br />import org. springf ramework. data. redis.core. ValueOperations;<br />import org. springf ramework. stereotype. Service;<br />import com. fasterxml. jackson.databind. objectMapper;<br />import com. waylau. spring. cloud . weather . vo . Wea therResponse;<br />/**<br />k天气数据服务.<br />@since 1.0.0 2017年10月29日 <br />* @author Way Lau<br />@Service<br />public class WeatherDataServiceImpl implements WeatherDataService {<br />private final static Logger logger = LoggerFactory .getLogger (Weather<br />DataServiceImpl.class) ;<br />@Autowi red<br />private StringRedisTemplate stringRedisTemplate; <br />private final String WEATHER API = "http://wthrcdn. etouch . cn/weather_<br />mini";<br />@Override<br />public WeatherResponse getDa taByCityId(String cityId) {<br />String uri = WEATHER API + "?citykey=" + cityId;<br />return this. doGetWea therData (uri) ;<br />@Override<br />public WeatherResponse getDataByCityName (String cityName) {<br />String uri = WEATHER_ API + "?city=" + cityName ;<br />return this. doGe tWeatherData (uri) ;<br />private WeatherResponse doGetWea therData (String uri)<br />valueOperations ops = this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />/先查缓存,查不到抛出异常<br />if (!this. stringRedisTemplate . hasKey (key)) {<br />logger .error("不存在key "+ key) ;<br />throw new Runt imeException ("没有相应的天气信息") ;<br />} else {<br />logger.info("存在key"+ key + ", value=" + ops.get (key));<br />strBody = ops.get (key) ;<br />}<br />0bj ectMapper mapper = new ObjectMapper () ;<br />WeatherResponse weather = null;<br />try {<br />weather = mapper . readvalue (strBody, WeatherResponse.class) ;<br />} catch (IOException e) {<br />logger . error ("JSON反序列化异常! ",e);<br />throw new RuntimeException ("天气信息解析失败") ;<br />return weather;<br />}<br />}
  其中需要注意的是:
  ●原有的RestTemplate用作REST客户端来进行天气数据的同步,这个类相关的代码都可以删除了;
  ●服务 会先从缓存中进行查询,查不到数据就抛出异常(有可能该城市的天气数据未同步,或者是数据已经过期) ;
  在执行反序列化JSON过程中也可能遭遇异常,同样将异常信息抛出。
  除上述WeatherDataServicelmpl、WeatherDataService 外,其他服务层的代码都可以删除了。
  调整控制层的代码
  除了WeatherController 外,其他控制层的代码都不需要了。
  WeatherController仍然是原有的代码保持不变。
  
  删除配置类、天气数据同步任务和工具类
  配置类RestConfiguration、QuartzConfiguration 及任务类WeatherDataSyncJob、 工具类Xml-
  Builder的代码都可以删除了。
  清理值对象
  值对象我们需要保留解析天气相关的类即可,其他值对象(如City. CityList等)都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并 删除的。
  同时,之前所编写的页面HTML、JS文件也要一并 删除。
  最后,要清理Thymeleaf在application.properties文件中的配置,以及build.gradle文件中的依赖。

天气数据采集微服务的实现:数据采集组件、数据存储组件

采集交流优采云 发表了文章 • 0 个评论 • 117 次浏览 • 2022-05-25 13:07 • 来自相关话题

  天气数据采集微服务的实现:数据采集组件、数据存储组件
  . Spring Boot Data Redis Starter 2.0.0.M4。
  .Redis 3.2.100。
  . Spring Boot Quartz Starter 2.0.0.M4。
  . Quartz Scheduler 2.3.0。
  新增天气数据采集服务接口及实现
  在
  com.waylau.spring.cloud.weather.service包下,我们定义了该应用的天气数据采集服务接口WeatherDataCollectionService。
  public interface WeatherDataCollectionService {<br />/**<br />*根据城市工D同步天气数据<br />*<br />*@param cityId<br />*@return<br />*/<br />void syncDataByCityId(String cityId);<br />}
  WeatherDataCollectionService只有一个同步天气数据的方法。WeatherDataCollectionServicelmpl是对WeatherDataCollectionService接口的实现。
  package com.waylau.spring.cloud.weather.service;<br />import java.util.concurrent.TimeUnit;<br />import org.slf4j.Logger;<br />import org.slf4j-LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.data.redis.core.StringRedisTemplate;<br />import org.springframework.data.redis.core.ValueOperations;<br />import org.springframework.http.ResponseEntity;<br />import org.springframework.stereotype.Service;<br />import org.springframework.web.client.RestTemplate;<br />/*★<br />*天气数据采集服务.<br />*<br />*@since 1.o.0 2017年10月29日<br />* @author Way Lau<br />*/<br />@service<br />public class WeatherDataCollectionServicelmpl implements WeatherData<br />CollectionService {<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatacollectionServicelmpl.class);<br />@Autowired<br />private RestTemplate restTemplate;<br />@Autowired<br />private stringRedisTemplate stringRedisTemplate;<br />private final String WEATHER_API = "http://wthrcdn.etouch.cn/weather_mini";<br />private final Long TIME_OUT = 1800L;//缓存超时时间<br />@override<br />public void syncDataByCityId(String cityId) {<br />logger.info ("Start同步天气.cityId: "+cityId);<br />String uri = WEATHER_API +"?citykey=" +cityId;<br />this.saveweatherData (uri);<br />logger.info("End同步天气");<br />private void saveWeatherData(String uri) {<br />ValueOperations ops= this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />ResponseEntity response = restTemplate.getForEntity(uri,<br />String.class);<br />if(response.getStatusCodeValue()=-200) f<br />strBody=response.getBody(;<br />ops.set(key,strBody,TIME_OUT,TimeUnit.SECONDS);<br />}<br />}
  WeatherDataCollectionServiceImpl的实现过程,我们在之前的章节中也已经详细介绍过,大家也已经非常熟悉了。无非就是通过REST客户端去调用第三方的天气数据接口,并将返回的数据直接放入Redis存储中。
  同时,我们需要设置Redis数据的过期时间。
  修改天气数据同步任务
  对于天气数据同步任务WeatherDataSyncJob,我们要做一些调整。把之前所依赖的CityData-Service、WeatherDataService改为
  WeatherDataCollectionService。
  import java.util.ArrayList;<br />import java.util.List;<br />import org.quartz.JobExecutionContext;<br />import org.quartz.JobExecutionException;<br />import org.slf4j-Logger;<br />import org.slf4j.LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.scheduling.quartz.QuartzJobBean;<br />import com.waylau.spring.cloud.weather.service.WeatherDataCollection<br />service;<br />import com.waylau.spring.cloud.weather.vo.City;<br />*★<br />天气数据同步任务.<br />*<br />*@since 1.0.0 2017年10月29日<br />* author <a href=span style="box-sizing: border-box;border-width: 0px;border-style: initial;border-color: initial;color: rgb(0, 117, 59);""https://waylau.com"/span>Way Lau</a><br />*/<br />public class WeatherDataSyncJob extends QuartzJobBean<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatasyncJob.class);<br />@Autowired<br />private WeatherDataCollectionService weatherDataCollectionService;<br />@override<br />protected void executeInternal (JobExecutionContext context) throws<br />JobExecutionException{<br />logger.info("'Start天气数据同步任务");<br />/TODO改为由城市数据API微服务来提供数据<br />工istcityList =null;<br />trY {<br />//TODO 调用城市数据APT<br />cityList = new ArrayEist();<br />City city = new City();<br />city.setCityId("101280601");<br />cityList.add(city);<br />}catch(Exception e){<br />logger.error("获取城市信息异常!",e);<br />throw new RuntimeException("获取城市信息异常!",e);<br />}<br />for(City city : cityList){<br />String cityld = city.getCityld(;<br />logger.info("天气数据同步任务中,cityId:" +cityId);<br />//根据城市ID同步天气数据<br />weatherDataCollectionService.syncDataByCityId(cityId);<br />logger.info("End 天气数据同步任务");<br />}<br />}
  这里需要注意的是,定时器仍然对城市ID列表有依赖,只不过这个依赖最终会由其他应用(城市数据API微服务)来提供,所以这里暂时还没有办法完全写完,先用“TODO”来标识这个方法,后期还需要改进。但为了能让整个程序可以完整地走下去,我们在程序里面假设返回了一个城市ID为“101280601”的城市信息。
  配置类
  配置类仍然保留之前的RestConfiguration、QuartzConfiguration的代码不变,如下所示。
  1.RestConfiguration
  RestConfiguration用于配置REST客户端。
  import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.boot.web.client.RestTemplateBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.web.client.RestTemplate;<br />/**<br />*REST 配置类.<br />*<br />*@since 1.0.0 2017年10月18日<br />* @author Way Lau<br />*/<br />@configuration<br />public class RestConfiguration {<br />@Autowired<br />private RestTemplateBuilder builder;<br />CBean<br />public RestTemplate restTemplate(){<br />return builder.build();<br />}<br />}
  2.QuartzConfiguration
  QuartzConfiguration类用于定时任务。
  import org.quartz.JobBuilder;<br />import org.quartz.JobDetail;<br />import org.quartz.SimpleScheduleBuilder;<br />import org.quartz.Trigger;<br />import org.quartz.TriggerBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import com.waylau.spring.cloud.weather.job.WeatherDataSyncJob;<br />/*★<br />*Quartz配置类.<br />*<br />*since 1.0.0 2017年10月23日<br />* author Way Lau<br />*/<br />@configuration<br />public class QuartzConfiguration <br />private final int TIME=1800;1/更新频率<br />@Bean<br />public JobDetail weatherDataSyncJobJobDetail(){<br />return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity<br />("weatherDataSyncJob")<br />.storeDurably() .build(;<br />}<br />CBean<br />public Trigger sampleJobTrigger({<br />SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.<br />simpleschedule()<br />.withIntervalInSeconds (TIME).repeatForever();<br />return TriggerBuilder.newTrigger().forJob(weatherDataSyncJob-<br />JobDetail())<br />.withIdentity("weatherDataSyncTrigger").withSchedule<br />(scheduleBuilder).build();<br />}<br />}
  值对象
  值对象我们只需要保留City即可,其他值对象都可以删除了。需要注意的是,由于天气数据采集微服务并未涉及对XML数据的解析,所以之前在City上添加的相关的JABX注解,都是可以一并删除的。
  以下是新的City类。
  public class City {<br />private String cityId;<br />private string cityName;<br />private string cityCode;<br />private String province;<br />1/省略getter/setter方法}
  工具类
  工具类XmlBuilder的代码都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并删除的。
  同时,之前所编写的页面HTML、JS文件也要一并删除。
  最后,要清理Thymeleaf在 application.properties文件中的配置,以及build.gradle文件中的依赖。
  测试和运行
  首先,在进行测试前,需要将Redis服务器启动起来。
  而后再启动应用。启动应用之后,定时器就自动开始执行。整个同步过程可以通过以下控制台信息看到。
  2017-10-29 22:26:41.748 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.WeatherDatasyncJob<br />:Start天气数据同步任务<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.weatherDataSyncJob:天气数据同步任务中,cityId:101280601<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] s.c.w.s.Weather<br />DataCollectionServiceImpl: Start同步天气.cityId:101280601<br />2017-10-29 22:26:41.836 INFO 13956 ---[<br />main]o.s.b.w.embedded.<br />tomcat.TomcatwebServer: Tomcat started on port(s):8080 (http)<br />2017-10-29 22:26:41.840 INFO 13956 ---[<br />main]c.w.spring.<br />cloud.weather.Application:Started Application in 4.447 seconds<br />(JVM running for 4.788)<br />2017-10-29 22:26:41.919 INFO 13956---[eduler_Worker-1] S.c.w.s.eather<br />DatacollectionServiceImpl :End同步天气<br />2017-10-29 22:26:41.920 INFO 13956---[eduler Worker-1] C.W.s.c.weather.<br />job.WeatherDataSyncJob:End 天气数据同步任务
  由于我们只是在代码里面“硬编码”了一个城市ID为“101280601”的城市信息,所以,只有一条同步记录。
  当然,我们也能通过Redis Desktop Manager,来方便查看存储到Redis里面的数据,如图7-3所示。
  
  本篇内容给大家讲解的是天气数据采集微服务的实现
  下篇文章给大家讲解天气数据API微服务的实现;
  觉得文章不错的朋友可以转发此文关注小编;
  感谢大家的支持!!
  本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。 查看全部

  天气数据采集微服务的实现:数据采集组件、数据存储组件
  . Spring Boot Data Redis Starter 2.0.0.M4。
  .Redis 3.2.100。
  . Spring Boot Quartz Starter 2.0.0.M4。
  . Quartz Scheduler 2.3.0。
  新增天气数据采集服务接口及实现
  在
  com.waylau.spring.cloud.weather.service包下,我们定义了该应用的天气数据采集服务接口WeatherDataCollectionService。
  public interface WeatherDataCollectionService {<br />/**<br />*根据城市工D同步天气数据<br />*<br />*@param cityId<br />*@return<br />*/<br />void syncDataByCityId(String cityId);<br />}
  WeatherDataCollectionService只有一个同步天气数据的方法。WeatherDataCollectionServicelmpl是对WeatherDataCollectionService接口的实现。
  package com.waylau.spring.cloud.weather.service;<br />import java.util.concurrent.TimeUnit;<br />import org.slf4j.Logger;<br />import org.slf4j-LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.data.redis.core.StringRedisTemplate;<br />import org.springframework.data.redis.core.ValueOperations;<br />import org.springframework.http.ResponseEntity;<br />import org.springframework.stereotype.Service;<br />import org.springframework.web.client.RestTemplate;<br />/*★<br />*天气数据采集服务.<br />*<br />*@since 1.o.0 2017年10月29日<br />* @author Way Lau<br />*/<br />@service<br />public class WeatherDataCollectionServicelmpl implements WeatherData<br />CollectionService {<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatacollectionServicelmpl.class);<br />@Autowired<br />private RestTemplate restTemplate;<br />@Autowired<br />private stringRedisTemplate stringRedisTemplate;<br />private final String WEATHER_API = "http://wthrcdn.etouch.cn/weather_mini";<br />private final Long TIME_OUT = 1800L;//缓存超时时间<br />@override<br />public void syncDataByCityId(String cityId) {<br />logger.info ("Start同步天气.cityId: "+cityId);<br />String uri = WEATHER_API +"?citykey=" +cityId;<br />this.saveweatherData (uri);<br />logger.info("End同步天气");<br />private void saveWeatherData(String uri) {<br />ValueOperations ops= this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />ResponseEntity response = restTemplate.getForEntity(uri,<br />String.class);<br />if(response.getStatusCodeValue()=-200) f<br />strBody=response.getBody(;<br />ops.set(key,strBody,TIME_OUT,TimeUnit.SECONDS);<br />}<br />}
  WeatherDataCollectionServiceImpl的实现过程,我们在之前的章节中也已经详细介绍过,大家也已经非常熟悉了。无非就是通过REST客户端去调用第三方的天气数据接口,并将返回的数据直接放入Redis存储中。
  同时,我们需要设置Redis数据的过期时间。
  修改天气数据同步任务
  对于天气数据同步任务WeatherDataSyncJob,我们要做一些调整。把之前所依赖的CityData-Service、WeatherDataService改为
  WeatherDataCollectionService。
  import java.util.ArrayList;<br />import java.util.List;<br />import org.quartz.JobExecutionContext;<br />import org.quartz.JobExecutionException;<br />import org.slf4j-Logger;<br />import org.slf4j.LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.scheduling.quartz.QuartzJobBean;<br />import com.waylau.spring.cloud.weather.service.WeatherDataCollection<br />service;<br />import com.waylau.spring.cloud.weather.vo.City;<br />*★<br />天气数据同步任务.<br />*<br />*@since 1.0.0 2017年10月29日<br />* author <a href=span style="box-sizing: border-box;border-width: 0px;border-style: initial;border-color: initial;color: rgb(0, 117, 59);""https://waylau.com"/span>Way Lau</a><br />*/<br />public class WeatherDataSyncJob extends QuartzJobBean<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatasyncJob.class);<br />@Autowired<br />private WeatherDataCollectionService weatherDataCollectionService;<br />@override<br />protected void executeInternal (JobExecutionContext context) throws<br />JobExecutionException{<br />logger.info("'Start天气数据同步任务");<br />/TODO改为由城市数据API微服务来提供数据<br />工istcityList =null;<br />trY {<br />//TODO 调用城市数据APT<br />cityList = new ArrayEist();<br />City city = new City();<br />city.setCityId("101280601");<br />cityList.add(city);<br />}catch(Exception e){<br />logger.error("获取城市信息异常!",e);<br />throw new RuntimeException("获取城市信息异常!",e);<br />}<br />for(City city : cityList){<br />String cityld = city.getCityld(;<br />logger.info("天气数据同步任务中,cityId:" +cityId);<br />//根据城市ID同步天气数据<br />weatherDataCollectionService.syncDataByCityId(cityId);<br />logger.info("End 天气数据同步任务");<br />}<br />}
  这里需要注意的是,定时器仍然对城市ID列表有依赖,只不过这个依赖最终会由其他应用(城市数据API微服务)来提供,所以这里暂时还没有办法完全写完,先用“TODO”来标识这个方法,后期还需要改进。但为了能让整个程序可以完整地走下去,我们在程序里面假设返回了一个城市ID为“101280601”的城市信息。
  配置类
  配置类仍然保留之前的RestConfiguration、QuartzConfiguration的代码不变,如下所示。
  1.RestConfiguration
  RestConfiguration用于配置REST客户端。
  import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.boot.web.client.RestTemplateBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.web.client.RestTemplate;<br />/**<br />*REST 配置类.<br />*<br />*@since 1.0.0 2017年10月18日<br />* @author Way Lau<br />*/<br />@configuration<br />public class RestConfiguration {<br />@Autowired<br />private RestTemplateBuilder builder;<br />CBean<br />public RestTemplate restTemplate(){<br />return builder.build();<br />}<br />}
  2.QuartzConfiguration
  QuartzConfiguration类用于定时任务。
  import org.quartz.JobBuilder;<br />import org.quartz.JobDetail;<br />import org.quartz.SimpleScheduleBuilder;<br />import org.quartz.Trigger;<br />import org.quartz.TriggerBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import com.waylau.spring.cloud.weather.job.WeatherDataSyncJob;<br />/*★<br />*Quartz配置类.<br />*<br />*since 1.0.0 2017年10月23日<br />* author Way Lau<br />*/<br />@configuration<br />public class QuartzConfiguration <br />private final int TIME=1800;1/更新频率<br />@Bean<br />public JobDetail weatherDataSyncJobJobDetail(){<br />return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity<br />("weatherDataSyncJob")<br />.storeDurably() .build(;<br />}<br />CBean<br />public Trigger sampleJobTrigger({<br />SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.<br />simpleschedule()<br />.withIntervalInSeconds (TIME).repeatForever();<br />return TriggerBuilder.newTrigger().forJob(weatherDataSyncJob-<br />JobDetail())<br />.withIdentity("weatherDataSyncTrigger").withSchedule<br />(scheduleBuilder).build();<br />}<br />}
  值对象
  值对象我们只需要保留City即可,其他值对象都可以删除了。需要注意的是,由于天气数据采集微服务并未涉及对XML数据的解析,所以之前在City上添加的相关的JABX注解,都是可以一并删除的。
  以下是新的City类。
  public class City {<br />private String cityId;<br />private string cityName;<br />private string cityCode;<br />private String province;<br />1/省略getter/setter方法}
  工具类
  工具类XmlBuilder的代码都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并删除的。
  同时,之前所编写的页面HTML、JS文件也要一并删除。
  最后,要清理Thymeleaf在 application.properties文件中的配置,以及build.gradle文件中的依赖。
  测试和运行
  首先,在进行测试前,需要将Redis服务器启动起来。
  而后再启动应用。启动应用之后,定时器就自动开始执行。整个同步过程可以通过以下控制台信息看到。
  2017-10-29 22:26:41.748 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.WeatherDatasyncJob<br />:Start天气数据同步任务<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.weatherDataSyncJob:天气数据同步任务中,cityId:101280601<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] s.c.w.s.Weather<br />DataCollectionServiceImpl: Start同步天气.cityId:101280601<br />2017-10-29 22:26:41.836 INFO 13956 ---[<br />main]o.s.b.w.embedded.<br />tomcat.TomcatwebServer: Tomcat started on port(s):8080 (http)<br />2017-10-29 22:26:41.840 INFO 13956 ---[<br />main]c.w.spring.<br />cloud.weather.Application:Started Application in 4.447 seconds<br />(JVM running for 4.788)<br />2017-10-29 22:26:41.919 INFO 13956---[eduler_Worker-1] S.c.w.s.eather<br />DatacollectionServiceImpl :End同步天气<br />2017-10-29 22:26:41.920 INFO 13956---[eduler Worker-1] C.W.s.c.weather.<br />job.WeatherDataSyncJob:End 天气数据同步任务
  由于我们只是在代码里面“硬编码”了一个城市ID为“101280601”的城市信息,所以,只有一条同步记录。
  当然,我们也能通过Redis Desktop Manager,来方便查看存储到Redis里面的数据,如图7-3所示。
  
  本篇内容给大家讲解的是天气数据采集微服务的实现
  下篇文章给大家讲解天气数据API微服务的实现;
  觉得文章不错的朋友可以转发此文关注小编;
  感谢大家的支持!!
  本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 119 次浏览 • 2022-05-21 18:02 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

再见笨重的ELK!这套轻量级日志收集方案要火!

采集交流优采云 发表了文章 • 0 个评论 • 68 次浏览 • 2022-05-21 01:39 • 来自相关话题

  再见笨重的ELK!这套轻量级日志收集方案要火!
  之前一直使用的日志收集方案是ELK,动辄占用几个G的内存,有些配置不好的服务器有点顶不住!最近发现一套轻量级日志收集方案:Loki+Promtail+Grafana(简称LPG), 几百M内存就够了,而且界面也挺不错的,推荐给大家!
  简介
  LPG日志收集方案内存占用很少,经济且高效!它不像ELK日志系统那样为日志建立索引,而是为每个日志流设置一组标签。下面分别介绍下它的核心组件:
  
  日志收集流程图安装
  实现这套日志收集方案需要安装Loki、Promtail、Grafana这些服务,直接使用docker-compose来安装非常方便。
  version: "3"<br /><br />services:<br />  # 日志存储和解析<br />  loki:<br />    image: grafana/loki<br />    container_name: lpg-loki<br />    volumes:<br />      - /mydata/loki/:/etc/loki/<br />    # 修改loki默认配置文件路径<br />    command: -config.file=/etc/loki/loki.yml<br />    ports:<br />      - 3100:3100<br /><br />  # 日志收集器<br />  promtail:<br />    image: grafana/promtail<br />    container_name: lpg-promtail<br />    volumes:<br />      # 将需要收集的日志所在目录挂载到promtail容器中<br />      - /mydata/app/mall-tiny-loki/logs/:/var/log/<br />      - /mydata/promtail:/etc/promtail/<br />    # 修改promtail默认配置文件路径<br />    command: -config.file=/etc/promtail/promtail.yml<br /><br />  # 日志可视化<br />  grafana:<br />    image: grafana/grafana<br />    container_name: lpg-grafana<br />    ports:<br />      - 3000:3000<br />
  auth_enabled: false<br /><br />server:<br />  http_listen_port: 3100<br /><br />ingester:<br />  lifecycler:<br />    address: 127.0.0.1<br />    ring:<br />      kvstore:<br />        store: inmemory<br />      replication_factor: 1<br />    final_sleep: 0s<br />  chunk_idle_period: 1h       # Any chunk not receiving new logs in this time will be flushed<br />  max_chunk_age: 1h           # All chunks will be flushed when they hit this age, default is 1h<br />  chunk_target_size: 1048576  # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first<br />  chunk_retain_period: 30s    # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)<br />  max_transfer_retries: 0     # Chunk transfers disabled<br /><br />schema_config:<br />  configs:<br />    - from: 2020-10-24<br />      store: boltdb-shipper<br />      object_store: filesystem<br />      schema: v11<br />      index:<br />        prefix: index_<br />        period: 24h<br /><br />storage_config:<br />  boltdb_shipper:<br />    active_index_directory: /loki/boltdb-shipper-active<br />    cache_location: /loki/boltdb-shipper-cache<br />    cache_ttl: 24h         # Can be increased for faster performance over longer query periods, uses more disk space<br />    shared_store: filesystem<br />  filesystem:<br />    directory: /loki/chunks<br /><br />compactor:<br />  working_directory: /loki/boltdb-shipper-compactor<br />  shared_store: filesystem<br /><br />limits_config:<br />  reject_old_samples: true<br />  reject_old_samples_max_age: 168h<br /><br />chunk_store_config:<br />  max_look_back_period: 0s<br /><br />table_manager:<br />  retention_deletes_enabled: false<br />  retention_period: 0s<br /><br />ruler:<br />  storage:<br />    type: local<br />    local:<br />      directory: /loki/rules<br />  rule_path: /loki/rules-temp<br />  alertmanager_url: http://localhost:9093<br />  ring:<br />    kvstore:<br />      store: inmemory<br />  enable_api: true<br />
  server:<br />  http_listen_port: 9080<br />  grpc_listen_port: 0<br /><br />positions:<br />  filename: /tmp/positions.yaml<br /><br />clients:<br />  - url: http://loki:3100/loki/api/v1/push<br /><br />scrape_configs:<br />- job_name: system<br />  static_configs:<br />  - targets:<br />      - localhost<br />    labels:<br />      job: varlogs<br />      __path__: /var/log/*log<br />
  docker-compose up -d<br />
  [root@local-linux lpg]# docker ps |grep lpg<br />64761b407423        grafana/loki                            "/usr/bin/loki -conf…"   3 minutes ago       Up 3 minutes        0.0.0.0:3100->3100/tcp                           lpg-loki<br />67f0f0912971        grafana/grafana                         "/run.sh"                3 minutes ago       Up 3 minutes        0.0.0.0:3000->3000/tcp                           lpg-grafana<br />f2d78eb188d1        grafana/promtail                        "/usr/bin/promtail -…"   3 minutes ago       Up 3 minutes                                                         lpg-promtail<br />
  使用
  接下来我们将使用LPG日志收集系统来收集SpringBoot应用的日志,SpringBoot应用基本不用做特殊配置。
  spring:<br />  application:<br />    name: mall-tiny-loki<br /><br />logging:<br />  path: /var/logs<br />  level:<br />    com.macro.mall.tiny: debug<br />
  docker run -p 8088:8088 --name mall-tiny-loki \<br />-v /etc/localtime:/etc/localtime \<br />-v /mydata/app/mall-tiny-loki/logs:/var/logs \<br />-e TZ="Asia/Shanghai" \<br />-d mall-tiny/mall-tiny-loki:1.0-SNAPSHOT<br />
  
  
  
  
  总结
  本文主要介绍了LPG日志系统的搭建及使用它收集SpringBoot应用的日志,LPG日志收集方案确实非常轻量级,性能也不错!不过如果你有对日志进行全文搜索的需求的话,还是得使用ELK系统。如果你对Grafana还不熟悉的话,可以参考下这篇文章。
  参考资料项目源码地址
  微信8.0将好友放开到了一万,小伙伴可以加我大号了,先到先得,再满就真没了
  扫描下方二维码即可加我微信啦,2021,抱团取暖,一起牛逼。
   查看全部

  再见笨重的ELK!这套轻量级日志收集方案要火!
  之前一直使用的日志收集方案是ELK,动辄占用几个G的内存,有些配置不好的服务器有点顶不住!最近发现一套轻量级日志收集方案:Loki+Promtail+Grafana(简称LPG), 几百M内存就够了,而且界面也挺不错的,推荐给大家!
  简介
  LPG日志收集方案内存占用很少,经济且高效!它不像ELK日志系统那样为日志建立索引,而是为每个日志流设置一组标签。下面分别介绍下它的核心组件:
  
  日志收集流程图安装
  实现这套日志收集方案需要安装Loki、Promtail、Grafana这些服务,直接使用docker-compose来安装非常方便。
  version: "3"<br /><br />services:<br />  # 日志存储和解析<br />  loki:<br />    image: grafana/loki<br />    container_name: lpg-loki<br />    volumes:<br />      - /mydata/loki/:/etc/loki/<br />    # 修改loki默认配置文件路径<br />    command: -config.file=/etc/loki/loki.yml<br />    ports:<br />      - 3100:3100<br /><br />  # 日志收集器<br />  promtail:<br />    image: grafana/promtail<br />    container_name: lpg-promtail<br />    volumes:<br />      # 将需要收集的日志所在目录挂载到promtail容器中<br />      - /mydata/app/mall-tiny-loki/logs/:/var/log/<br />      - /mydata/promtail:/etc/promtail/<br />    # 修改promtail默认配置文件路径<br />    command: -config.file=/etc/promtail/promtail.yml<br /><br />  # 日志可视化<br />  grafana:<br />    image: grafana/grafana<br />    container_name: lpg-grafana<br />    ports:<br />      - 3000:3000<br />
  auth_enabled: false<br /><br />server:<br />  http_listen_port: 3100<br /><br />ingester:<br />  lifecycler:<br />    address: 127.0.0.1<br />    ring:<br />      kvstore:<br />        store: inmemory<br />      replication_factor: 1<br />    final_sleep: 0s<br />  chunk_idle_period: 1h       # Any chunk not receiving new logs in this time will be flushed<br />  max_chunk_age: 1h           # All chunks will be flushed when they hit this age, default is 1h<br />  chunk_target_size: 1048576  # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first<br />  chunk_retain_period: 30s    # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)<br />  max_transfer_retries: 0     # Chunk transfers disabled<br /><br />schema_config:<br />  configs:<br />    - from: 2020-10-24<br />      store: boltdb-shipper<br />      object_store: filesystem<br />      schema: v11<br />      index:<br />        prefix: index_<br />        period: 24h<br /><br />storage_config:<br />  boltdb_shipper:<br />    active_index_directory: /loki/boltdb-shipper-active<br />    cache_location: /loki/boltdb-shipper-cache<br />    cache_ttl: 24h         # Can be increased for faster performance over longer query periods, uses more disk space<br />    shared_store: filesystem<br />  filesystem:<br />    directory: /loki/chunks<br /><br />compactor:<br />  working_directory: /loki/boltdb-shipper-compactor<br />  shared_store: filesystem<br /><br />limits_config:<br />  reject_old_samples: true<br />  reject_old_samples_max_age: 168h<br /><br />chunk_store_config:<br />  max_look_back_period: 0s<br /><br />table_manager:<br />  retention_deletes_enabled: false<br />  retention_period: 0s<br /><br />ruler:<br />  storage:<br />    type: local<br />    local:<br />      directory: /loki/rules<br />  rule_path: /loki/rules-temp<br />  alertmanager_url: http://localhost:9093<br />  ring:<br />    kvstore:<br />      store: inmemory<br />  enable_api: true<br />
  server:<br />  http_listen_port: 9080<br />  grpc_listen_port: 0<br /><br />positions:<br />  filename: /tmp/positions.yaml<br /><br />clients:<br />  - url: http://loki:3100/loki/api/v1/push<br /><br />scrape_configs:<br />- job_name: system<br />  static_configs:<br />  - targets:<br />      - localhost<br />    labels:<br />      job: varlogs<br />      __path__: /var/log/*log<br />
  docker-compose up -d<br />
  [root@local-linux lpg]# docker ps |grep lpg<br />64761b407423        grafana/loki                            "/usr/bin/loki -conf…"   3 minutes ago       Up 3 minutes        0.0.0.0:3100->3100/tcp                           lpg-loki<br />67f0f0912971        grafana/grafana                         "/run.sh"                3 minutes ago       Up 3 minutes        0.0.0.0:3000->3000/tcp                           lpg-grafana<br />f2d78eb188d1        grafana/promtail                        "/usr/bin/promtail -…"   3 minutes ago       Up 3 minutes                                                         lpg-promtail<br />
  使用
  接下来我们将使用LPG日志收集系统来收集SpringBoot应用的日志,SpringBoot应用基本不用做特殊配置。
  spring:<br />  application:<br />    name: mall-tiny-loki<br /><br />logging:<br />  path: /var/logs<br />  level:<br />    com.macro.mall.tiny: debug<br />
  docker run -p 8088:8088 --name mall-tiny-loki \<br />-v /etc/localtime:/etc/localtime \<br />-v /mydata/app/mall-tiny-loki/logs:/var/logs \<br />-e TZ="Asia/Shanghai" \<br />-d mall-tiny/mall-tiny-loki:1.0-SNAPSHOT<br />
  
  
  
  
  总结
  本文主要介绍了LPG日志系统的搭建及使用它收集SpringBoot应用的日志,LPG日志收集方案确实非常轻量级,性能也不错!不过如果你有对日志进行全文搜索的需求的话,还是得使用ELK系统。如果你对Grafana还不熟悉的话,可以参考下这篇文章。
  参考资料项目源码地址
  微信8.0将好友放开到了一万,小伙伴可以加我大号了,先到先得,再满就真没了
  扫描下方二维码即可加我微信啦,2021,抱团取暖,一起牛逼。
  

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 113 次浏览 • 2022-05-21 01:37 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

数据治理之数字画像

采集交流优采云 发表了文章 • 0 个评论 • 160 次浏览 • 2022-07-10 02:00 • 来自相关话题

  数据治理之数字画像
  00引言
  随着全网步入大数据时代,企业的目光日益聚焦在利用大数据服务精细化营销、精细化运营上,各类客户画像、员工画像理论如雨后春笋般兴起,而数据应用的底层——数据治理,却鲜有整体的理论体系。如何避免治理工作自身“无的放矢”,如何量化数据基础建设的贡献,我们需要为数据治理工作描绘一张“数字画像”。这个命题的内涵外延非常丰富,在此我们选取用户体验、架构质量两个角度进行讨论。
  01用户体验的数字画像
  基于不同的感知角度,将用户分为外部客户、内部用户、管理层、技术人员四类,针对特定的业务场景刻画四类使用者所体会到的“科技赋能”。
  1、外部客户
  功能体验指标:功能体验指标用于衡量操作平台的易用性及直观程度。可以通过各类埋点,对用户的点击行为、页面停留时间、页面浏览深度进行跟踪。从而挖掘用户常使用的功能,探究的实际需要,对于常用功能可以开展功能的改版优化,进行同业产品比较、用户反馈调查等,重点关注主要功能的流畅度、实用性。
  平台服务指标:
  (1)服务平台一般利用API接口向外提供数据,因此,通过计算API调用率可以计算出其向外输出服务的活跃程度。
  (2)由数据服务带来的产品升值也是需要衡量的一大重要指标。营销、运营等商业活动价值提升以一定的比例分配给其相应的数据治理工作,从业务部门有感的角度评估数据治理工作对营销等活动的赋能。
  2、内部用户
  
  便利性:过去业务部门向数据管理部门申请使用数据,通常使用邮件或行政流程的方式,这种方法无法实时跟踪申请进度,也无法在统一的页面集中管理,导致相关工作人员在查询与沟通上花费了大量精力。建立自动化、规范化流程以及线上运营工具,将极大地便利流程,因此,手工提取工单压降比率可以作为度量内部用户程度的指标。
  时效性:线上化数据治理意味着资产地图、标准架构即存放在用户指尖,关键作业的全链路交付时间是触达内部用户的另一直观感受。该指标可以通过统计各节点的流转时间,计算相应平均值获得。
  贡献度:不仅是底层的数据管理,数据的应用输出同样能够为用户带来实际业务价值。BI工具的使用、模型提供数量等指标标志着用户对于应用类数据成果的满意度。
  3、管理层
  质量提升:对于管理层而言,保障数据仓库、数据湖的“清澈”是他们关心的问题。由于监管报送结果是银保监对银行的重点考核指标,报送规定的达标率成为对于管理层数据治理成效最直观的反映。基于DQC的一系列指标同样可作为面向管理层的数据清洁度体现。
  效率提升:除监管要求之外,数据运营成本对于全行管理也是至关重要的。只有建立规范和高效的数据架构,压降数据报表,降低储存、运维成本,才能实现精细化营运,维持高效率盈利。
  4、技术人员
  数据字典评分:当企业实施开发过程强管控时,数据字典的角色可看作是法律之于社会,其整体逻辑必须经得起反复推敲。在数据字典的查询页面设立评分反馈是一种简单但行之有效的方法。页面上有计划的引导,反映设计者关注开发人员的使用体验,从而让“吐槽”变成建议,优化和解决使用数据字典时遇到的问题。
  02架构质量的数字画像
  全行统一的数据架构应在追求高效率的同时降低成本,根据《华为数据之道》中信息架构的经典四范式,我们将从模型、分布、标准、资产四个角度对架构赋能能力进行度量。
  1、模型
  公共层加工频率:公共层中存放有事实数据、维表数据等等,它们支撑着指标体系中的一级指标层。在建立指标时,将规范化、集约化,提高公共指标的复用性,减少重复加工的操作,故公共层数据模型的复用率可作为公共层架构评估的指数之一。
  
  应用层引用频率:类似于人际关系网络拓扑结构中的核心人物算法,该指数直接衡量应用层中数据的系统性重要程度,引导资产盘点的目标。数据血缘关系是一种有向的、无权值、无自环的网络图。被引用频率高的资产一般来源于关键业务实体中最准确和最及时的业务记录。这一些资产被跨部门、跨业务领域调用的概率最大,需要实现所有部门可访问并且访问到相同的数据。该指数还能够有效地筛选出“孤儿表”、临时表,减少资源投入和储存成本。
  2、分布
  数据覆盖:对于大型银行而言,数以百计的系统,数以万计的库表在全国范围内分布式储存。采集是资产盘点的第一步,测量采集数在全量系统的覆盖率帮助我们明确当前采集的进度,定位未采集的数据来源。
  数据冗余:数据冗余指同层数据的冗余,具体可分为两个来源。第一,多个物理位置中存储了相同意义的数据;第二,架构模型本身在设计上有较多的重复交叉项。
  数据容量:数据容量是对数据中台的整体描述,它包括当前中台所囊括的整体数据体量的绝对值,也包含该体量随时间的增长比例。数据容量并非越高或者越低更理想,它需要结合银行的现状辩证性地看待。
  3、标准
  标准稳定性:数据标准规范化了数据含义、结构等等,应当满足内容统一、不交叉定义等条件,避免数据标准内部发生“数据打架”。
  标准落标率:在标准的技术规范完备,主题齐全,标准已权威发布的前提下,标准落标率反映了数据标准“最后一公里”的执行情况。借助自动化工具,能够计算出各类分层、切片后的数据落标率,智能化地发现落标潜在问题。
  4、资产
  技术元数据统计:技术元数据打通了源数据和,记录了数据从产生到消亡的过程。我们从中挑选出系统覆盖率、系统内表级覆盖率、表名以及字段名的有效率、枚举值的有效率等统计指标表示数据架构中技术类资产的产出效益。
  企业活动命中率:数据资产是从业务流程、业务模型中抽取出来的数字化描述。标签资产对业务行为的命中率、指标资产对报表统计的命中率、报表资产的用户访问量等数值越高,代表着资产内容映射企业活动的准确度越高。
  03结语
  伴随着企业数字化转型不断深入,“数据治理的数字画像”从方法论到实践都将趋于完善,内容价值、安全性能、用户体验也会随之提高。如何动态地衡量数据治理工作成效,建立适合自身企业的“北极星指标”,是每一家处于智慧转型阶段的公司所必须研究的,它的成功将创造出不可估量的商业价值。 查看全部

  数据治理之数字画像
  00引言
  随着全网步入大数据时代,企业的目光日益聚焦在利用大数据服务精细化营销、精细化运营上,各类客户画像、员工画像理论如雨后春笋般兴起,而数据应用的底层——数据治理,却鲜有整体的理论体系。如何避免治理工作自身“无的放矢”,如何量化数据基础建设的贡献,我们需要为数据治理工作描绘一张“数字画像”。这个命题的内涵外延非常丰富,在此我们选取用户体验、架构质量两个角度进行讨论。
  01用户体验的数字画像
  基于不同的感知角度,将用户分为外部客户、内部用户、管理层、技术人员四类,针对特定的业务场景刻画四类使用者所体会到的“科技赋能”。
  1、外部客户
  功能体验指标:功能体验指标用于衡量操作平台的易用性及直观程度。可以通过各类埋点,对用户的点击行为、页面停留时间、页面浏览深度进行跟踪。从而挖掘用户常使用的功能,探究的实际需要,对于常用功能可以开展功能的改版优化,进行同业产品比较、用户反馈调查等,重点关注主要功能的流畅度、实用性。
  平台服务指标:
  (1)服务平台一般利用API接口向外提供数据,因此,通过计算API调用率可以计算出其向外输出服务的活跃程度。
  (2)由数据服务带来的产品升值也是需要衡量的一大重要指标。营销、运营等商业活动价值提升以一定的比例分配给其相应的数据治理工作,从业务部门有感的角度评估数据治理工作对营销等活动的赋能。
  2、内部用户
  
  便利性:过去业务部门向数据管理部门申请使用数据,通常使用邮件或行政流程的方式,这种方法无法实时跟踪申请进度,也无法在统一的页面集中管理,导致相关工作人员在查询与沟通上花费了大量精力。建立自动化、规范化流程以及线上运营工具,将极大地便利流程,因此,手工提取工单压降比率可以作为度量内部用户程度的指标。
  时效性:线上化数据治理意味着资产地图、标准架构即存放在用户指尖,关键作业的全链路交付时间是触达内部用户的另一直观感受。该指标可以通过统计各节点的流转时间,计算相应平均值获得。
  贡献度:不仅是底层的数据管理,数据的应用输出同样能够为用户带来实际业务价值。BI工具的使用、模型提供数量等指标标志着用户对于应用类数据成果的满意度。
  3、管理层
  质量提升:对于管理层而言,保障数据仓库、数据湖的“清澈”是他们关心的问题。由于监管报送结果是银保监对银行的重点考核指标,报送规定的达标率成为对于管理层数据治理成效最直观的反映。基于DQC的一系列指标同样可作为面向管理层的数据清洁度体现。
  效率提升:除监管要求之外,数据运营成本对于全行管理也是至关重要的。只有建立规范和高效的数据架构,压降数据报表,降低储存、运维成本,才能实现精细化营运,维持高效率盈利。
  4、技术人员
  数据字典评分:当企业实施开发过程强管控时,数据字典的角色可看作是法律之于社会,其整体逻辑必须经得起反复推敲。在数据字典的查询页面设立评分反馈是一种简单但行之有效的方法。页面上有计划的引导,反映设计者关注开发人员的使用体验,从而让“吐槽”变成建议,优化和解决使用数据字典时遇到的问题。
  02架构质量的数字画像
  全行统一的数据架构应在追求高效率的同时降低成本,根据《华为数据之道》中信息架构的经典四范式,我们将从模型、分布、标准、资产四个角度对架构赋能能力进行度量。
  1、模型
  公共层加工频率:公共层中存放有事实数据、维表数据等等,它们支撑着指标体系中的一级指标层。在建立指标时,将规范化、集约化,提高公共指标的复用性,减少重复加工的操作,故公共层数据模型的复用率可作为公共层架构评估的指数之一。
  
  应用层引用频率:类似于人际关系网络拓扑结构中的核心人物算法,该指数直接衡量应用层中数据的系统性重要程度,引导资产盘点的目标。数据血缘关系是一种有向的、无权值、无自环的网络图。被引用频率高的资产一般来源于关键业务实体中最准确和最及时的业务记录。这一些资产被跨部门、跨业务领域调用的概率最大,需要实现所有部门可访问并且访问到相同的数据。该指数还能够有效地筛选出“孤儿表”、临时表,减少资源投入和储存成本。
  2、分布
  数据覆盖:对于大型银行而言,数以百计的系统,数以万计的库表在全国范围内分布式储存。采集是资产盘点的第一步,测量采集数在全量系统的覆盖率帮助我们明确当前采集的进度,定位未采集的数据来源。
  数据冗余:数据冗余指同层数据的冗余,具体可分为两个来源。第一,多个物理位置中存储了相同意义的数据;第二,架构模型本身在设计上有较多的重复交叉项。
  数据容量:数据容量是对数据中台的整体描述,它包括当前中台所囊括的整体数据体量的绝对值,也包含该体量随时间的增长比例。数据容量并非越高或者越低更理想,它需要结合银行的现状辩证性地看待。
  3、标准
  标准稳定性:数据标准规范化了数据含义、结构等等,应当满足内容统一、不交叉定义等条件,避免数据标准内部发生“数据打架”。
  标准落标率:在标准的技术规范完备,主题齐全,标准已权威发布的前提下,标准落标率反映了数据标准“最后一公里”的执行情况。借助自动化工具,能够计算出各类分层、切片后的数据落标率,智能化地发现落标潜在问题。
  4、资产
  技术元数据统计:技术元数据打通了源数据和,记录了数据从产生到消亡的过程。我们从中挑选出系统覆盖率、系统内表级覆盖率、表名以及字段名的有效率、枚举值的有效率等统计指标表示数据架构中技术类资产的产出效益。
  企业活动命中率:数据资产是从业务流程、业务模型中抽取出来的数字化描述。标签资产对业务行为的命中率、指标资产对报表统计的命中率、报表资产的用户访问量等数值越高,代表着资产内容映射企业活动的准确度越高。
  03结语
  伴随着企业数字化转型不断深入,“数据治理的数字画像”从方法论到实践都将趋于完善,内容价值、安全性能、用户体验也会随之提高。如何动态地衡量数据治理工作成效,建立适合自身企业的“北极星指标”,是每一家处于智慧转型阶段的公司所必须研究的,它的成功将创造出不可估量的商业价值。

文章采集api Zabbix 任性,文末送书 X 5

采集交流优采云 发表了文章 • 0 个评论 • 117 次浏览 • 2022-07-02 05:59 • 来自相关话题

  文章采集api Zabbix 任性,文末送书 X 5
  Zabbix版本不断升级,以满足日益增长的用户需求,支持高可用HA,k8s、指标topN、机器学习、定制前端品牌logo等!
  旧版本需要脚本才实现的功能,升级至最新版本可轻松解决!Zabbix6.0为业务服务提供商、DevOps和ITOps团队提供了附加值,优化了整体监控工作流程,并在许多不同层面提供了新见解。
  目录
  业务服务监控达到全新高度
  高阶业务服务SLA计算逻辑
  通过根因分析增强业务服务监控能力
  开箱即用的Zabbix server高可用群集
  机器学习
  Kubernetes监控
  详细高效的Zabbix审计日志模式
  可视化数据的新方法
  Zabbix性能优化
  提升Zabbix Agent2模块化,新的Zabbix Agent 监控项和功能
  原生TLS/SSL网站证书监控
  通用性改进
  通过自定义密码复杂程度要求来保护您的Zabbix登录
  支持定制前端展示品牌logo
  新增模板和集成
  其它新功能和优化
  01
  BMS业务服务监控达到全新高度
  优化Services部分,显示业务服务的状态和当前SLA级别
  通过对现有Services页面和功能的重大改进和优化,业务服务监控提升到了一个新高度。业务服务监控功能(BSM)非常适合多组件服务场景,例如服务器群集、负载平衡器和其它具有冗余组件的服务。
  Zabbix 6.0提供多种功能自定义业务服务树实现BMS业务服务监控:
  • 重新设计 Zabbix 6.0 Services页面和功能
  • 支持单个Zabbix实例监控超过10万个业务服务
  • 支持新的灵活服务状态计算逻辑
  • 能够自定义业务服务的访问权限
  • 能够为特定业务服务自定义只读和读写权限
  • 业务服务权限既可以基于显式服务列表,也可以基于服务标签的访问限制
  • 导出和导入业务服务树
  • 新的Service动作类型能让用户接收告警并对业务服务状态更改作出反应
  02
  高阶业务服务SLA计算逻辑
  提供大量可供选择的服务状态计算规则,能支持灵活的服务定义
  业务服务状态计算逻辑在Zabbix 6.0中得到了极大扩展,增加了许多新功能,例如:
  • 能够为每项业务服务分配权重
  • 仅当N个子服务都处于X严重级别的问题状态时才更改状态
  • 对处于问题状态下的子服务的权重进行分析并作出反应
  • 仅当特定百分比的子服务处于问题状态时才作出反应
  • 其它计算规则
  用户还可以自定义和访问指定服务的SLA报告。
  03
  通过根因分析增强业务服务监控能力
  根因问题会立即显示在service下
  对业务服务执行根因分析。利用根因分析功能找出可能导致业务服务SLA下降的潜在问题列表:
  • 在Zabbix前端Services页面查看根因问题列表
  • 接收告警中的根因问题列表
  • 通过Zabbix API收集根因问题信息
  04
  开箱即用的Zabbix server高可用群集
  在系统信息组件中跟踪集群集节点状态
  Zabbix server高可用防止硬件故障或计划维护期的停机:
  • 原生选择加入HA群集配置
  • 定义一个或多个备用节点
  
  •实时监控Zabbix server群集节点的状态
  • 不需要外部工具即可将Zabbix server配置为HA群集模式
  05
  机器学习
  使用新函数对意外异常率或与指标基准的偏差做出反应
  新的基线监控和异常检测趋势功能以动态方式检测问题,而不是静态阈值方式:
  •新的趋势函数-baselinewma and baselinedev ,能计算指标基线和偏离值
  •新的趋势函数-trendstl,能检测异常指标行为
  •能够指定异常检测偏差算法及季节性
  06
  Kubernetes监控
  Zabbix 6.0 LTS添加了多个新模板,用于监控不同的Kubernetes组件
  Zabbix 6.0 LTS新增Kubernetes监控功能,可以在Kubernetes系统从多个维度采集指标:
  •Kubernetes节点和pods的自动发现和监控
  •无代理方式采集Kubernetes pods和节点的信息
  •获取Kubernetes节点主机高水平信息
  Kubernetes监控还能够监控Kubernetes组件,例如
  •kube-controller-manager
  •kube-proxy
  •kube-apiserver
  •kube-scheduler
  •kubelet
  07
  详细高效的Zabbix审计日志模式
  重新设计的审计日志能提供全新的详细信息,并优化筛选功能。
  新的审计日志模式允许用户对Zabbix前端、Zabbix API和Zabbix server记录执行详细审计。通过修改审计日志,对Zabbix实例执行的所有更改都将记录在审计日志中:
  •创建、修改或删除新对象
  •通过LLD发现新实体
  •API命令
  •定期登录/退出
  •Zabbix实例中发生的所有其它事情
  新的审计日志模式在设计时考虑了最佳性能,因此扩展的功能不会影响Zabbix实例的性能。审计日志模式的工作是一项持续的工作,会在后续Zabbix发布周期中持续进行。
  08
  可视化数据的新方法
  主机排序组件可显示按监控项值排序的前N个或后N个主机的列表
  Zabbix 6.0新增的构件提供了展示信息的许多新方法。
  •地理地图构件能在地图上显示主机和问题
  •数据表构件能创建有关主机指标状态的摘要视图
  •数据表构件的前N和后N函数能展示最高或最低的监控项值
  •单一监控项构件能展示单个指标的值
  •对现有矢量图的许多改进,例如新的矢量图类型、引用单一监控项等
  •SLA构件能显示特定业务服务的当前SLA
  09
  Zabbix性能优化
  针对不同的Zabbix组件进行多项性能优化:
  •提升链接模板时的性能
  •提升Zabbix proxy性能和内存使用率
  历史数据表使用主键,这有多种好处,例如:
  •提高Zabbix server和Zabbix前端的性能
  •减少历史数据表的大小
  10
  提升Zabbix Agent2模块化,
  新的Zabbix Agent 监控项和功能
  优化的Zabbix agent现在能够开箱即用监控一组指标
  Zabbix 6.0为Zabbix Agent和Agent2提供了一套新的监控项。支持以下功能:
  •获取额外文件信息,如文件所有者和文件权限
  •采集agent主机元数据作为指标
  
  •计数匹配的TCP/UDP sockets
  某些已有的监控项支持新的功能:
  •vfs.fs.discovery-在Windows上添加了对{#FSLABEL}宏的支持
  •vfs.fs.get-在Windows上添加了对{#FSLABEL}宏的支持
  • vfs.file.size-添加了一个新的模式参数。设置以字节数或行数为单位
  Zabbix Agent2现在支持加载独立插件,而无需重新编译Agent2。
  11
  原生TLS/SSL网站证书监控
  使用新的Zabbix agent2 监控项监控SSL/TLS证书
  支持使用新的Zabbix agent 2监控项来监控SSL/TLS证书。监控项可用于验证TLS/SSL证书,并提供其它证书详细信息。
  12
  通用性改进
  通过优化的创建主机UI,使创建新主机从未如此简单
  Zabbix 6.0使Zabbix配置工作流程更精简!Zabbix用户现在可直接在Monitoring页面创建主机和监控项:
  •直接从Monitoring -Hosts页面创建主机
  •直接从Monitoring -Latest data页面创建监控项
  •删除了Monitoring -Overview页面。为了改善用户体验,现在只能通过仪表盘构件访问触发器和数据概览功能。
  现在将根据监控项的键值自动选择监控项的默认信息类型。
  拓扑图标签和图形名称中的简单宏已替换为表达式宏,以确保与新的触发器表达式语法一致。
  13
  通过自定义密码复杂程度要求
  来保护您的Zabbix登录
  设置密码复杂程度确保前端登录安全
  Zabbix超级管理员现在能够定义密码复杂程度要求。现在可以:
  •设置最小密码长度
  •定义密码字符要求
  •通过禁止使用最常见的密码字符串来降低字典攻击的风险。
  14
  支持定制前端展示品牌logo
  定制Zabbix实例代表您的公司。将现有的Zabbix品牌和帮助页面URL替换为您自己的公司品牌和自定义网站URL。
  改名功能不会违反Zabbix许可协议-可以自由更换Zabbix品牌!
  15
  新增模板和集成
  Zabbix 6.0为最受欢迎的供应商提供了许多新模板:
  •f5 BIG-IP
  •Cisco ASAv
  •HPE ProLiant servers
  •Cloudflare
  •InfluxDB
  •Travis CI
  •Dell PowerEdge
  Zabbix 6.0还带来了一个新的Github webhook集成,能基于Zabbix问题或恢复事件生成Github问题!
  所有官方的Zabbix模板现在都是独立的,不需要依赖导入其他模板。
  请查看当前可用集成的完整列表。
  16
  其它新功能和优化
  更多改进功能(部分):
  •使用新聚合函数计数返回值或匹配监控项的数量-count和item_count函数
  •在未配置交换空间的情况下提升system.swap监控项行为
  •使用新的单调历史函数检测连续增加或减少的值
  •支持两个新的Prometheus预处理标签匹配运算符!= 及 !~
  •当从构件链接导航到列表样式页面时,构件显示能更可靠地转换为不同的筛选器选项
  •使用新配置参数ListenBacklog为Zabbix server、Zabbix proxy、Zabbix agent配置TCP队列中挂起连接的最大数量
  •文档页面字体和可读性的改进
  •调整许多现有模板和修复小bug
  •新增utf8mb4作为受支持的MySQL字符集和校对集
  •新增对Webhook的额外HTTP方法的支持
  •对Zabbix命令行工具的超时设置
  Zabbix官方首本工具书《Zabbix监控系统之深度解析和实践》现已出版,欢迎阅读。 查看全部

  文章采集api Zabbix 任性,文末送书 X 5
  Zabbix版本不断升级,以满足日益增长的用户需求,支持高可用HA,k8s、指标topN、机器学习、定制前端品牌logo等!
  旧版本需要脚本才实现的功能,升级至最新版本可轻松解决!Zabbix6.0为业务服务提供商、DevOps和ITOps团队提供了附加值,优化了整体监控工作流程,并在许多不同层面提供了新见解。
  目录
  业务服务监控达到全新高度
  高阶业务服务SLA计算逻辑
  通过根因分析增强业务服务监控能力
  开箱即用的Zabbix server高可用群集
  机器学习
  Kubernetes监控
  详细高效的Zabbix审计日志模式
  可视化数据的新方法
  Zabbix性能优化
  提升Zabbix Agent2模块化,新的Zabbix Agent 监控项和功能
  原生TLS/SSL网站证书监控
  通用性改进
  通过自定义密码复杂程度要求来保护您的Zabbix登录
  支持定制前端展示品牌logo
  新增模板和集成
  其它新功能和优化
  01
  BMS业务服务监控达到全新高度
  优化Services部分,显示业务服务的状态和当前SLA级别
  通过对现有Services页面和功能的重大改进和优化,业务服务监控提升到了一个新高度。业务服务监控功能(BSM)非常适合多组件服务场景,例如服务器群集、负载平衡器和其它具有冗余组件的服务。
  Zabbix 6.0提供多种功能自定义业务服务树实现BMS业务服务监控:
  • 重新设计 Zabbix 6.0 Services页面和功能
  • 支持单个Zabbix实例监控超过10万个业务服务
  • 支持新的灵活服务状态计算逻辑
  • 能够自定义业务服务的访问权限
  • 能够为特定业务服务自定义只读和读写权限
  • 业务服务权限既可以基于显式服务列表,也可以基于服务标签的访问限制
  • 导出和导入业务服务树
  • 新的Service动作类型能让用户接收告警并对业务服务状态更改作出反应
  02
  高阶业务服务SLA计算逻辑
  提供大量可供选择的服务状态计算规则,能支持灵活的服务定义
  业务服务状态计算逻辑在Zabbix 6.0中得到了极大扩展,增加了许多新功能,例如:
  • 能够为每项业务服务分配权重
  • 仅当N个子服务都处于X严重级别的问题状态时才更改状态
  • 对处于问题状态下的子服务的权重进行分析并作出反应
  • 仅当特定百分比的子服务处于问题状态时才作出反应
  • 其它计算规则
  用户还可以自定义和访问指定服务的SLA报告。
  03
  通过根因分析增强业务服务监控能力
  根因问题会立即显示在service下
  对业务服务执行根因分析。利用根因分析功能找出可能导致业务服务SLA下降的潜在问题列表:
  • 在Zabbix前端Services页面查看根因问题列表
  • 接收告警中的根因问题列表
  • 通过Zabbix API收集根因问题信息
  04
  开箱即用的Zabbix server高可用群集
  在系统信息组件中跟踪集群集节点状态
  Zabbix server高可用防止硬件故障或计划维护期的停机:
  • 原生选择加入HA群集配置
  • 定义一个或多个备用节点
  
  •实时监控Zabbix server群集节点的状态
  • 不需要外部工具即可将Zabbix server配置为HA群集模式
  05
  机器学习
  使用新函数对意外异常率或与指标基准的偏差做出反应
  新的基线监控和异常检测趋势功能以动态方式检测问题,而不是静态阈值方式:
  •新的趋势函数-baselinewma and baselinedev ,能计算指标基线和偏离值
  •新的趋势函数-trendstl,能检测异常指标行为
  •能够指定异常检测偏差算法及季节性
  06
  Kubernetes监控
  Zabbix 6.0 LTS添加了多个新模板,用于监控不同的Kubernetes组件
  Zabbix 6.0 LTS新增Kubernetes监控功能,可以在Kubernetes系统从多个维度采集指标:
  •Kubernetes节点和pods的自动发现和监控
  •无代理方式采集Kubernetes pods和节点的信息
  •获取Kubernetes节点主机高水平信息
  Kubernetes监控还能够监控Kubernetes组件,例如
  •kube-controller-manager
  •kube-proxy
  •kube-apiserver
  •kube-scheduler
  •kubelet
  07
  详细高效的Zabbix审计日志模式
  重新设计的审计日志能提供全新的详细信息,并优化筛选功能。
  新的审计日志模式允许用户对Zabbix前端、Zabbix API和Zabbix server记录执行详细审计。通过修改审计日志,对Zabbix实例执行的所有更改都将记录在审计日志中:
  •创建、修改或删除新对象
  •通过LLD发现新实体
  •API命令
  •定期登录/退出
  •Zabbix实例中发生的所有其它事情
  新的审计日志模式在设计时考虑了最佳性能,因此扩展的功能不会影响Zabbix实例的性能。审计日志模式的工作是一项持续的工作,会在后续Zabbix发布周期中持续进行。
  08
  可视化数据的新方法
  主机排序组件可显示按监控项值排序的前N个或后N个主机的列表
  Zabbix 6.0新增的构件提供了展示信息的许多新方法。
  •地理地图构件能在地图上显示主机和问题
  •数据表构件能创建有关主机指标状态的摘要视图
  •数据表构件的前N和后N函数能展示最高或最低的监控项值
  •单一监控项构件能展示单个指标的值
  •对现有矢量图的许多改进,例如新的矢量图类型、引用单一监控项等
  •SLA构件能显示特定业务服务的当前SLA
  09
  Zabbix性能优化
  针对不同的Zabbix组件进行多项性能优化:
  •提升链接模板时的性能
  •提升Zabbix proxy性能和内存使用率
  历史数据表使用主键,这有多种好处,例如:
  •提高Zabbix server和Zabbix前端的性能
  •减少历史数据表的大小
  10
  提升Zabbix Agent2模块化,
  新的Zabbix Agent 监控项和功能
  优化的Zabbix agent现在能够开箱即用监控一组指标
  Zabbix 6.0为Zabbix Agent和Agent2提供了一套新的监控项。支持以下功能:
  •获取额外文件信息,如文件所有者和文件权限
  •采集agent主机元数据作为指标
  
  •计数匹配的TCP/UDP sockets
  某些已有的监控项支持新的功能:
  •vfs.fs.discovery-在Windows上添加了对{#FSLABEL}宏的支持
  •vfs.fs.get-在Windows上添加了对{#FSLABEL}宏的支持
  • vfs.file.size-添加了一个新的模式参数。设置以字节数或行数为单位
  Zabbix Agent2现在支持加载独立插件,而无需重新编译Agent2。
  11
  原生TLS/SSL网站证书监控
  使用新的Zabbix agent2 监控项监控SSL/TLS证书
  支持使用新的Zabbix agent 2监控项来监控SSL/TLS证书。监控项可用于验证TLS/SSL证书,并提供其它证书详细信息。
  12
  通用性改进
  通过优化的创建主机UI,使创建新主机从未如此简单
  Zabbix 6.0使Zabbix配置工作流程更精简!Zabbix用户现在可直接在Monitoring页面创建主机和监控项:
  •直接从Monitoring -Hosts页面创建主机
  •直接从Monitoring -Latest data页面创建监控项
  •删除了Monitoring -Overview页面。为了改善用户体验,现在只能通过仪表盘构件访问触发器和数据概览功能。
  现在将根据监控项的键值自动选择监控项的默认信息类型。
  拓扑图标签和图形名称中的简单宏已替换为表达式宏,以确保与新的触发器表达式语法一致。
  13
  通过自定义密码复杂程度要求
  来保护您的Zabbix登录
  设置密码复杂程度确保前端登录安全
  Zabbix超级管理员现在能够定义密码复杂程度要求。现在可以:
  •设置最小密码长度
  •定义密码字符要求
  •通过禁止使用最常见的密码字符串来降低字典攻击的风险。
  14
  支持定制前端展示品牌logo
  定制Zabbix实例代表您的公司。将现有的Zabbix品牌和帮助页面URL替换为您自己的公司品牌和自定义网站URL。
  改名功能不会违反Zabbix许可协议-可以自由更换Zabbix品牌!
  15
  新增模板和集成
  Zabbix 6.0为最受欢迎的供应商提供了许多新模板:
  •f5 BIG-IP
  •Cisco ASAv
  •HPE ProLiant servers
  •Cloudflare
  •InfluxDB
  •Travis CI
  •Dell PowerEdge
  Zabbix 6.0还带来了一个新的Github webhook集成,能基于Zabbix问题或恢复事件生成Github问题!
  所有官方的Zabbix模板现在都是独立的,不需要依赖导入其他模板。
  请查看当前可用集成的完整列表。
  16
  其它新功能和优化
  更多改进功能(部分):
  •使用新聚合函数计数返回值或匹配监控项的数量-count和item_count函数
  •在未配置交换空间的情况下提升system.swap监控项行为
  •使用新的单调历史函数检测连续增加或减少的值
  •支持两个新的Prometheus预处理标签匹配运算符!= 及 !~
  •当从构件链接导航到列表样式页面时,构件显示能更可靠地转换为不同的筛选器选项
  •使用新配置参数ListenBacklog为Zabbix server、Zabbix proxy、Zabbix agent配置TCP队列中挂起连接的最大数量
  •文档页面字体和可读性的改进
  •调整许多现有模板和修复小bug
  •新增utf8mb4作为受支持的MySQL字符集和校对集
  •新增对Webhook的额外HTTP方法的支持
  •对Zabbix命令行工具的超时设置
  Zabbix官方首本工具书《Zabbix监控系统之深度解析和实践》现已出版,欢迎阅读。

腾讯3面:说说前端监控平台/监控SDK的架构设计和难点亮点?

采集交流优采云 发表了文章 • 0 个评论 • 53 次浏览 • 2022-06-28 05:55 • 来自相关话题

  腾讯3面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在 腾讯三轮面试 的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计 ;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的 整体架构 和 可以做的亮点优化 ;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读; 传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过 削峰限流 和 数据加工 后,将原始日志存储于 ES 中,再经过 数据清洗 、数据聚合 后,将 issue(聚合的数据) 持久化存储 于 MySQL ,最后提供 RESTful API 提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是 内核+插件 的插件式设计;每个 SDK 首先继承于平台无关的 Core 层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个 web环境 下的数据,包括 Nodejs、微信小程序、Electron 等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个 SDK 可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护 ;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改 build 的脚本,对 packages 文件夹下的每个平台都单独打一个包,并且分开上传到 npm 平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK 也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件 的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加 Monitor 插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br />class WebSdk extends Core {<br />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br />  public performanceInstance: WebVitals;<br /><br />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br />  public userInstance: UserVitals;<br /><br />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br />  public errorInstance: ErrorVitals;<br /><br />  // 上报实例,这里面封装上报方法<br />  public transportInstance: TransportInstance;<br /><br />  // 数据格式化实例<br />  public builderInstance: BuilderInstance;<br /><br />  // 维度实例,用以初始化 uid、sid等信息<br />  public dimensionInstance: DimensionInstance;<br /><br />  // 参数初始化实例<br />  public configInstance: ConfigInstance;<br /><br />  private options: initOptions;<br /><br />  constructor(options: initOptions) {<br />    super();<br />    this.configInstance = new ConfigInstance(this, options);<br />    // 各种初始化......<br />  }<br />}<br /><br />export default WebSdk;<br />
  看上面的代码,我在初始化每个插件的时候,都将 this 传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的 内核+插件 设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有 单元测试 的来保证业务的稳定性;
  我们可以引入单元测试,并对 每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个 SDK 都用 try catch 包裹起来,那么这样子即使出现了错误,也会被拦截在我们的 catch 里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过 JS 调用 new Date() 获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说 API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头 上有一个字段 Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的 Date 值后计算 Diff差值 存在本地;
  这样子就可以提供一个 公共API,来提供一个时间校对的服务,让本地的时间 比较趋近于 服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br />export const diffTime = (date: string) => {<br />  const serverDate = new Date(date);<br />  const inDiff = Date.now() - serverDate.getTime();<br />  if (diff === 0 || diff > inDiff) {<br />    diff = inDiff;<br />  }<br />};<br /><br />export const getTime = () => {<br />  return new Date(Date.now() - diff);<br />};<br />
  当然,这里还可以做的更精确一点,我们可以让后端服务在返回的时候,带上 API 请求在后端服务执行完毕所消耗的时间 server-timing,放在响应头里;我们取到数据后,将 ttfb 耗时 减去返回的 server-timing 再除以 2;就是单程传输的耗时;那这样我们上文的计算中差的 单程传输耗时的误差 就可以补上了;
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成 错误mid 的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br />export const getErrorUid = (input: string) => {<br />  return window.btoa(unescape(encodeURIComponent(input)));<br />};<br />
  
  所以说我们传入的参数,是 错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的 错误mid 都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br />errorSendHandler = (data: ExceptionMetrics) => {<br />  // 统一加上 用户行为追踪 和 页面基本信息<br />  const submitParams = {<br />    ...data,<br />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br />  } as ExceptionMetrics;<br />  // 判断同一个错误在本次页面访问中是否已经发生过;<br />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br />  // 检查一下错误在本次页面访问中,是否已经产生过<br />  if (hasSubmitStatus) return;<br />  this.submitErrorUids.push(submitParams.errorUid);<br />  // 记录后清除 breadcrumbs<br />  this.engineInstance.userInstance.breadcrumbs.clear();<br />  // 一般来说,有报错就立刻上报;<br />  this.engineInstance.transportInstance.kernelTransportHandler(<br />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br />  );<br />};<br />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先 Beacon API[6] 是一个较新的 API
  然后 Ajax 请求方式就不用我多说了,大家应该平常用的最多的异步请求就是 Ajax;
  最后来说一下 Image 上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用 sendBeacon + xmlHttpRequest 降级上报的方式,当浏览器不支持 sendBeacon 或者 传输的数据量超过了 sendBeacon 的限制,我们就降级采用 xmlHttpRequest 进行上报数据;
  优先选用 Beacon API 的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载 的传输方式。
  而降级使用 XMLHttpRequest 的原因是, Beacon API 现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且 sendbeacon 不能传输大数据量的信息,这个时候还是得回到 Ajax 来;
  看到了这里,有的同学可能会问:为什么不用 Image 呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br />  // PV访问数据<br />  PV = 'pv',<br />  // 性能数据<br />  PERF = 'perf',<br />  // api 请求数据<br />  API = 'api',<br />  // 报错数据<br />  ERROR = 'error',<br />  // 自定义行为<br />  CUS = 'custom',<br />}<br /><br />export interface DimensionStructure {<br />  // 用户id,存储于cookie<br />  uid: string;<br />  // 会话id,存储于cookiestorage<br />  sid: string;<br />  // 应用id,使用方传入<br />  pid: string;<br />  // 应用版本号<br />  release: string;<br />  // 应用环境<br />  environment: string;<br />}<br /><br />export interface TransportStructure {<br />  // 上报类别<br />  category: transportCategory;<br />  // 上报的维度信息<br />  dimension: DimensionStructure;<br />  // 上报对象(正文)<br />  context?: Object;<br />  // 上报对象数组<br />  contexts?: Array;<br />  // 捕获的sdk版本信息,版本号等...<br />  sdk: Object;<br />}<br /><br />export default class TransportInstance {<br />  private engineInstance: EngineInstance;<br /><br />  public kernelTransportHandler: Function;<br /><br />  private options: TransportParams;<br /><br />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br />    this.engineInstance = engineInstance;<br />    this.options = options;<br />    this.kernelTransportHandler = this.initTransportHandler();<br />  }<br /><br />  // 格式化数据,传入部分为 category 和 context \ contexts<br />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br />    const transportStructure = {<br />      category,<br />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br />      sdk: getSdkVersion(),<br />    } as TransportStructure;<br />    if (data instanceof Array) {<br />      transportStructure.contexts = data;<br />    } else {<br />      transportStructure.context = data;<br />    }<br />    return transportStructure;<br />  };<br /><br />  // 初始化上报方法<br />  initTransportHandler = () => {<br />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br />  };<br /><br />  // beacon 形式上报<br />  beaconTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br />      if (!status) this.xmlTransport().apply(this, data);<br />    };<br />    return handler;<br />  };<br /><br />  // XMLHttpRequest 形式上报<br />  xmlTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const xhr = new (window as any).oXMLHttpRequest();<br />      xhr.open('POST', this.options.transportUrl, true);<br />      xhr.send(JSON.stringify(data));<br />    };<br />    return handler;<br />  };<br />}<br />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是 Math.random() ,我们让用户传入一个采样率,
<p>if(Math.random() 查看全部

  腾讯3面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在 腾讯三轮面试 的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计 ;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的 整体架构 和 可以做的亮点优化 ;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读; 传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过 削峰限流 和 数据加工 后,将原始日志存储于 ES 中,再经过 数据清洗 、数据聚合 后,将 issue(聚合的数据) 持久化存储 于 MySQL ,最后提供 RESTful API 提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是 内核+插件 的插件式设计;每个 SDK 首先继承于平台无关的 Core 层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个 web环境 下的数据,包括 Nodejs、微信小程序、Electron 等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个 SDK 可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护 ;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改 build 的脚本,对 packages 文件夹下的每个平台都单独打一个包,并且分开上传到 npm 平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK 也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件 的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加 Monitor 插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br />class WebSdk extends Core {<br />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br />  public performanceInstance: WebVitals;<br /><br />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br />  public userInstance: UserVitals;<br /><br />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br />  public errorInstance: ErrorVitals;<br /><br />  // 上报实例,这里面封装上报方法<br />  public transportInstance: TransportInstance;<br /><br />  // 数据格式化实例<br />  public builderInstance: BuilderInstance;<br /><br />  // 维度实例,用以初始化 uid、sid等信息<br />  public dimensionInstance: DimensionInstance;<br /><br />  // 参数初始化实例<br />  public configInstance: ConfigInstance;<br /><br />  private options: initOptions;<br /><br />  constructor(options: initOptions) {<br />    super();<br />    this.configInstance = new ConfigInstance(this, options);<br />    // 各种初始化......<br />  }<br />}<br /><br />export default WebSdk;<br />
  看上面的代码,我在初始化每个插件的时候,都将 this 传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的 内核+插件 设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有 单元测试 的来保证业务的稳定性;
  我们可以引入单元测试,并对 每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个 SDK 都用 try catch 包裹起来,那么这样子即使出现了错误,也会被拦截在我们的 catch 里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过 JS 调用 new Date() 获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说 API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头 上有一个字段 Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的 Date 值后计算 Diff差值 存在本地;
  这样子就可以提供一个 公共API,来提供一个时间校对的服务,让本地的时间 比较趋近于 服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br />export const diffTime = (date: string) => {<br />  const serverDate = new Date(date);<br />  const inDiff = Date.now() - serverDate.getTime();<br />  if (diff === 0 || diff > inDiff) {<br />    diff = inDiff;<br />  }<br />};<br /><br />export const getTime = () => {<br />  return new Date(Date.now() - diff);<br />};<br />
  当然,这里还可以做的更精确一点,我们可以让后端服务在返回的时候,带上 API 请求在后端服务执行完毕所消耗的时间 server-timing,放在响应头里;我们取到数据后,将 ttfb 耗时 减去返回的 server-timing 再除以 2;就是单程传输的耗时;那这样我们上文的计算中差的 单程传输耗时的误差 就可以补上了;
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成 错误mid 的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br />export const getErrorUid = (input: string) => {<br />  return window.btoa(unescape(encodeURIComponent(input)));<br />};<br />
  
  所以说我们传入的参数,是 错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的 错误mid 都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br />errorSendHandler = (data: ExceptionMetrics) => {<br />  // 统一加上 用户行为追踪 和 页面基本信息<br />  const submitParams = {<br />    ...data,<br />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br />  } as ExceptionMetrics;<br />  // 判断同一个错误在本次页面访问中是否已经发生过;<br />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br />  // 检查一下错误在本次页面访问中,是否已经产生过<br />  if (hasSubmitStatus) return;<br />  this.submitErrorUids.push(submitParams.errorUid);<br />  // 记录后清除 breadcrumbs<br />  this.engineInstance.userInstance.breadcrumbs.clear();<br />  // 一般来说,有报错就立刻上报;<br />  this.engineInstance.transportInstance.kernelTransportHandler(<br />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br />  );<br />};<br />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先 Beacon API[6] 是一个较新的 API
  然后 Ajax 请求方式就不用我多说了,大家应该平常用的最多的异步请求就是 Ajax;
  最后来说一下 Image 上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用 sendBeacon + xmlHttpRequest 降级上报的方式,当浏览器不支持 sendBeacon 或者 传输的数据量超过了 sendBeacon 的限制,我们就降级采用 xmlHttpRequest 进行上报数据;
  优先选用 Beacon API 的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载 的传输方式。
  而降级使用 XMLHttpRequest 的原因是, Beacon API 现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且 sendbeacon 不能传输大数据量的信息,这个时候还是得回到 Ajax 来;
  看到了这里,有的同学可能会问:为什么不用 Image 呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br />  // PV访问数据<br />  PV = 'pv',<br />  // 性能数据<br />  PERF = 'perf',<br />  // api 请求数据<br />  API = 'api',<br />  // 报错数据<br />  ERROR = 'error',<br />  // 自定义行为<br />  CUS = 'custom',<br />}<br /><br />export interface DimensionStructure {<br />  // 用户id,存储于cookie<br />  uid: string;<br />  // 会话id,存储于cookiestorage<br />  sid: string;<br />  // 应用id,使用方传入<br />  pid: string;<br />  // 应用版本号<br />  release: string;<br />  // 应用环境<br />  environment: string;<br />}<br /><br />export interface TransportStructure {<br />  // 上报类别<br />  category: transportCategory;<br />  // 上报的维度信息<br />  dimension: DimensionStructure;<br />  // 上报对象(正文)<br />  context?: Object;<br />  // 上报对象数组<br />  contexts?: Array;<br />  // 捕获的sdk版本信息,版本号等...<br />  sdk: Object;<br />}<br /><br />export default class TransportInstance {<br />  private engineInstance: EngineInstance;<br /><br />  public kernelTransportHandler: Function;<br /><br />  private options: TransportParams;<br /><br />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br />    this.engineInstance = engineInstance;<br />    this.options = options;<br />    this.kernelTransportHandler = this.initTransportHandler();<br />  }<br /><br />  // 格式化数据,传入部分为 category 和 context \ contexts<br />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br />    const transportStructure = {<br />      category,<br />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br />      sdk: getSdkVersion(),<br />    } as TransportStructure;<br />    if (data instanceof Array) {<br />      transportStructure.contexts = data;<br />    } else {<br />      transportStructure.context = data;<br />    }<br />    return transportStructure;<br />  };<br /><br />  // 初始化上报方法<br />  initTransportHandler = () => {<br />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br />  };<br /><br />  // beacon 形式上报<br />  beaconTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br />      if (!status) this.xmlTransport().apply(this, data);<br />    };<br />    return handler;<br />  };<br /><br />  // XMLHttpRequest 形式上报<br />  xmlTransport = (): Function => {<br />    const handler = (data: TransportStructure) => {<br />      const xhr = new (window as any).oXMLHttpRequest();<br />      xhr.open('POST', this.options.transportUrl, true);<br />      xhr.send(JSON.stringify(data));<br />    };<br />    return handler;<br />  };<br />}<br />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是 Math.random() ,我们让用户传入一个采样率,
<p>if(Math.random()

腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?

采集交流优采云 发表了文章 • 0 个评论 • 93 次浏览 • 2022-06-27 21:05 • 来自相关话题

  腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在腾讯三轮面试的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的整体架构和可以做的亮点优化;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读;传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过削峰限流和数据加工后,将原始日志存储于ES中,再经过数据清洗、数据聚合后,将issue(聚合的数据)持久化存储于MySQL,最后提供RESTful API提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是内核+插件的插件式设计;每个SDK首先继承于平台无关的Core层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个web环境下的数据,包括Nodejs、微信小程序、Electron等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个SDK可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改build的脚本,对packages文件夹下的每个平台都单独打一个包,并且分开上传到npm平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加Monitor插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />class WebSdk extends Core {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public performanceInstance: WebVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public userInstance: UserVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public errorInstance: ErrorVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报实例,这里面封装上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public transportInstance: TransportInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 数据格式化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public builderInstance: BuilderInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 维度实例,用以初始化 uid、sid等信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public dimensionInstance: DimensionInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 参数初始化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public configInstance: ConfigInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: initOptions;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(options: initOptions) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    super();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.configInstance = new ConfigInstance(this, options);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    // 各种初始化......<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default WebSdk;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  看上面的代码,我在初始化每个插件的时候,都将this传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的内核+插件设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有单元测试的来保证业务的稳定性;
  我们可以引入单元测试,并对每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个SDK都用try catch包裹起来,那么这样子即使出现了错误,也会被拦截在我们的catch里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过JS调用new Date()获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头上有一个字段Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的Date值后计算Diff差值存在本地;
  这样子就可以提供一个公共API,来提供一个时间校对的服务,让本地的时间比较趋近于服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const diffTime = (date: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const serverDate = new Date(date);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const inDiff = Date.now() - serverDate.getTime();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (diff === 0 || diff > inDiff) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    diff = inDiff;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getTime = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return new Date(Date.now() - diff);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成错误mid的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getErrorUid = (input: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return window.btoa(unescape(encodeURIComponent(input)));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  所以说我们传入的参数,是错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的错误mid都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />errorSendHandler = (data: ExceptionMetrics) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 统一加上 用户行为追踪 和 页面基本信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const submitParams = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    ...data,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  } as ExceptionMetrics;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 判断同一个错误在本次页面访问中是否已经发生过;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 检查一下错误在本次页面访问中,是否已经产生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (hasSubmitStatus) return;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.submitErrorUids.push(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 记录后清除 breadcrumbs<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.userInstance.breadcrumbs.clear();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 一般来说,有报错就立刻上报;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.transportInstance.kernelTransportHandler(<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  );<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先Beacon API[6]是一个较新的 API
  然后Ajax请求方式就不用我多说了,大家应该平常用的最多的异步请求就是Ajax;
  最后来说一下Image上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用sendBeacon+xmlHttpRequest降级上报的方式,当浏览器不支持sendBeacon或者传输的数据量超过了sendBeacon的限制,我们就降级采用xmlHttpRequest进行上报数据;
  优先选用Beacon API的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载的传输方式。
  而降级使用XMLHttpRequest的原因是,Beacon API现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且sendbeacon不能传输大数据量的信息,这个时候还是得回到Ajax来;
  看到了这里,有的同学可能会问:为什么不用Image呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // PV访问数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PV = 'pv',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PERF = 'perf',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // api 请求数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  API = 'api',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 报错数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  ERROR = 'error',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 自定义行为<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  CUS = 'custom',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface DimensionStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 用户id,存储于cookie<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  uid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 会话id,存储于cookiestorage<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用id,使用方传入<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  pid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用版本号<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  release: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用环境<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  environment: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface TransportStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报类别<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  category: transportCategory;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报的维度信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  dimension: DimensionStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象(正文)<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  context?: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象数组<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  contexts?: Array;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 捕获的sdk版本信息,版本号等...<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sdk: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default class TransportInstance {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private engineInstance: EngineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public kernelTransportHandler: Function;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: TransportParams;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance = engineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.options = options;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.kernelTransportHandler = this.initTransportHandler();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 格式化数据,传入部分为 category 和 context \ contexts<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const transportStructure = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      category,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      sdk: getSdkVersion(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } as TransportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    if (data instanceof Array) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.contexts = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } else {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.context = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return transportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 初始化上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  initTransportHandler = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // beacon 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  beaconTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      if (!status) this.xmlTransport().apply(this, data);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // XMLHttpRequest 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  xmlTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const xhr = new (window as any).oXMLHttpRequest();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.open('POST', this.options.transportUrl, true);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.send(JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是Math.random(),我们让用户传入一个采样率,
<p>if(Math.random() 查看全部

  腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?
  前言
  事情是这样的,上周,我的一位两年前端经验的发小,在腾讯三轮面试的时候被问了一个问题:说说你们公司前端监控项目的架构设计和亮点设计;
  而说回我这位发小,因为做过他们公司监控项目的可视化报表界面,所以简历上有写着前端监控项目的项目经验;但是不幸的是,他虽然前端基础相当不错,但并没有实际参与监控SDK的设计开发(只负责写监控的可视化分析界面),所以被问到这个问题,直接就一个懵了;结果也很正常,面试没过;
  那么这篇文章,我就来介绍一下对于前端监控项目的整体架构和可以做的亮点优化;前文几篇文章有介绍具体的前端监控实现,感兴趣的小伙伴可以点击链接跳转过去阅读;传送门就在下面。
  传送门
  这篇文章的标题原拟定是:一文摸清前端监控实践要点(四)架构设计;但是我的发小面试刚好碰上了这么一个问题,于是我便将标题改为了这个。
  一文摸清前端监控实践要点(一)性能监控[1]
  一文摸清前端监控实践要点(二)行为监控[2]
  一文摸清前端监控实践要点(三)错误监控[3]
  腾讯三面:说说前端监控告警分析平台的架构设计和难点亮点?[4]
  整体 架构设计
  image.png
  直接上图,我们在应用层SDK上报的数据,在接入层经过削峰限流和数据加工后,将原始日志存储于ES中,再经过数据清洗、数据聚合后,将issue(聚合的数据)持久化存储于MySQL,最后提供RESTful API提供给监控平台调用;
  SDK 架构设计
  为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是内核+插件的插件式设计;每个SDK首先继承于平台无关的Core层代码。然后在自身SDK中,初始化内核实例和插件;
  image.png
  image.png值得一谈的点
  下面将主要谈谈这些内容:前端监控项目除了正常的数据采集、数据报表分析以外;会碰上哪些难点可以去突破,或者说可以做出哪些亮点的内容?
  SDK 如何设计成多平台支持?
  首先我们先来了解一下,在前端监控的领域里,我们可能不仅仅只是监控一个web环境下的数据,包括Nodejs、微信小程序、Electron等各种其余的环境都是有监控的业务需求在的;
  那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个SDK可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?
  最简单的办法:将每个平台单独放一个仓库,单独维护;但是这种办法的问题也很严重:人力资源浪费严重;会导致一些重复的代码很多;维护非常困难;
  而较好一点的解决方案:我们可以通过插件化对代码进行组织:见下图
  image.png
  这样子进行 SDK 的设计有很多好处:
  最后打包上线时,我们通过修改build的脚本,对packages文件夹下的每个平台都单独打一个包,并且分开上传到npm平台;
  SDK 如何方便的进行业务拓展和定制?
  业务功能总是会不断迭代的,SDK也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性;我们来看下图:
  image.png
  上图是 SDK 内部的一个架构设计 :内核+插件的设计;
  而看了上图已经上文的解释,可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加Monitor插件的数量就可以了;
  至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....
  我这边举个代码例子,大家可以参考着看看就行:
  // 服务于 Web 的SDK,继承了 Core 上的与平台无关方法;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />class WebSdk extends Core {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能监控实例,实例里每个插件实现一个性能监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public performanceInstance: WebVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 行为监控实例,实例里每个插件实现一个行为监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public userInstance: UserVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 错误监控实例,实例里每个插件实现一个错误监控功能;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public errorInstance: ErrorVitals;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报实例,这里面封装上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public transportInstance: TransportInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 数据格式化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public builderInstance: BuilderInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 维度实例,用以初始化 uid、sid等信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public dimensionInstance: DimensionInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 参数初始化实例<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public configInstance: ConfigInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: initOptions;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(options: initOptions) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    super();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.configInstance = new ConfigInstance(this, options);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    // 各种初始化......<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default WebSdk;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  看上面的代码,我在初始化每个插件的时候,都将this传入进去,那么每个插件里面都可以访问内核里的方法;
  SDK 在拓展新业务的时候,如何保证原有业务的正确性?
  在上述的内核+插件设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有单元测试的来保证业务的稳定性;
  我们可以引入单元测试,并对每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;
  SDK 如何实现异常隔离以及上报?
  首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接收的;
  实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个SDK都用try catch包裹起来,那么这样子即使出现了错误,也会被拦截在我们的catch里面;
  但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:
  那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报;
  这样子,就完成了一个异常隔离机制:
  SDK 如何实现服务端时间的校对?
  看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过JS调用new Date()获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;
  那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;
  image.png
  如上图,我们先要了解的是,http响应头上有一个字段Date;它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的Date值后计算Diff差值存在本地;
  这样子就可以提供一个公共API,来提供一个时间校对的服务,让本地的时间比较趋近于服务端的真实时间;(只是比较趋近的原因是:还会有一个单程传输耗时的误差)
  let diff = 0;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const diffTime = (date: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const serverDate = new Date(date);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const inDiff = Date.now() - serverDate.getTime();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (diff === 0 || diff > inDiff) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    diff = inDiff;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getTime = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return new Date(Date.now() - diff);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 如何实现会话级别的错误上报去重?
  首先,我们需要理清一个概念,我们可以认为:
  为什么有上面的结论呢?理由很简单:
  所以说我们在第三篇文章《一文摸清前端监控实践要点(三)错误监控》[5]中有一个生成错误mid的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一;
  // 对每一个错误详情,生成一串编码<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export const getErrorUid = (input: string) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  return window.btoa(unescape(encodeURIComponent(input)));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  所以说我们传入的参数,是错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的错误mid都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重;
  // 封装错误的上报入口,上报前,判断错误是否已经发生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />errorSendHandler = (data: ExceptionMetrics) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 统一加上 用户行为追踪 和 页面基本信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const submitParams = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    ...data,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    breadcrumbs: this.engineInstance.userInstance.breadcrumbs.get(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    pageInformation: this.engineInstance.userInstance.metrics.get('page-information'),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  } as ExceptionMetrics;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 判断同一个错误在本次页面访问中是否已经发生过;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  const hasSubmitStatus = this.submitErrorUids.includes(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 检查一下错误在本次页面访问中,是否已经产生过<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  if (hasSubmitStatus) return;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.submitErrorUids.push(submitParams.errorUid);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 记录后清除 breadcrumbs<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.userInstance.breadcrumbs.clear();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 一般来说,有报错就立刻上报;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  this.engineInstance.transportInstance.kernelTransportHandler(<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance.transportInstance.formatTransportData(transportCategory.ERROR, submitParams),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  );<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />};<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  SDK 采用什么样的上报策略?
  对于上报方面来说,SDK的数据上报可不是随随便便就上报上去了,里面有涉及到数据上报的方式取舍以及上报时机的选择等等,还有一些可以让数据上报更加优雅的优化点;
  首先,日志上报并不是应用的主要功能逻辑,日志上报行为不应该影响业务逻辑,不应该占用业务计算资源;那么在往下阅读之前,我们先来了解一下目前通用的几个上报方式:
  我们来简单讲一下上述的几个上报方式
  首先Beacon API[6]是一个较新的 API
  然后Ajax请求方式就不用我多说了,大家应该平常用的最多的异步请求就是Ajax;
  最后来说一下Image上报方式:我们可以以向服务端请求图片资源的形式,像服务端传输少量数据,这种方式不会造成跨域;
  上报方式
  看了上面的三种上报方式,我们最终采用sendBeacon+xmlHttpRequest降级上报的方式,当浏览器不支持sendBeacon或者传输的数据量超过了sendBeacon的限制,我们就降级采用xmlHttpRequest进行上报数据;
  优先选用Beacon API的理由上文已经有提到:它可以保证页面卸载之前启动信标请求,是一种数据可靠,传输异步并且不会影响下一页面的加载的传输方式。
  而降级使用XMLHttpRequest的原因是,Beacon API现在并不是所有的浏览器都完全支持,我们需要一个保险方案兜底,并且sendbeacon不能传输大数据量的信息,这个时候还是得回到Ajax来;
  看到了这里,有的同学可能会问:为什么不用Image呀?那跨域怎么办呀?原因也很简单:
  我们将其简单封装一下:
  export enum transportCategory {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // PV访问数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PV = 'pv',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 性能数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  PERF = 'perf',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // api 请求数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  API = 'api',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 报错数据<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  ERROR = 'error',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 自定义行为<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  CUS = 'custom',<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface DimensionStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 用户id,存储于cookie<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  uid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 会话id,存储于cookiestorage<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用id,使用方传入<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  pid: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用版本号<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  release: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 应用环境<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  environment: string;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export interface TransportStructure {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报类别<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  category: transportCategory;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报的维度信息<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  dimension: DimensionStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象(正文)<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  context?: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 上报对象数组<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  contexts?: Array;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 捕获的sdk版本信息,版本号等...<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  sdk: Object;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />export default class TransportInstance {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private engineInstance: EngineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  public kernelTransportHandler: Function;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  private options: TransportParams;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  constructor(engineInstance: EngineInstance, options: TransportParams) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.engineInstance = engineInstance;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.options = options;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    this.kernelTransportHandler = this.initTransportHandler();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 格式化数据,传入部分为 category 和 context \ contexts<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  formatTransportData = (category: transportCategory, data: Object | Array): TransportStructure => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const transportStructure = {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      category,<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      dimension: this.engineInstance.dimensionInstance.getDimension(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      sdk: getSdkVersion(),<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } as TransportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    if (data instanceof Array) {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.contexts = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    } else {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      transportStructure.context = data;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    }<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return transportStructure;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // 初始化上报方法<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  initTransportHandler = () => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return typeof navigator.sendBeacon === 'function' ? this.beaconTransport() : this.xmlTransport();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // beacon 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  beaconTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const status = window.navigator.sendBeacon(this.options.transportUrl, JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      // 如果数据量过大,则本次大数据量用 XMLHttpRequest 上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      if (!status) this.xmlTransport().apply(this, data);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" /><br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  // XMLHttpRequest 形式上报<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  xmlTransport = (): Function => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    const handler = (data: TransportStructure) => {<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      const xhr = new (window as any).oXMLHttpRequest();<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.open('POST', this.options.transportUrl, true);<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />      xhr.send(JSON.stringify(data));<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />    return handler;<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />  };<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />}<br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  上报时机
  上报时机这里,一般来说:
  上报优化
  或许,我们想把我们的数据上报做的再优雅一点,那么我们还有什么可以优化的点呢?还是有的:
  平台数据如何进行 削峰限流?
  假设说,有某一个时间点,突然间流量爆炸,无数的数据向服务器访问过来,这时如果没有一个削峰限流的策略,很可能会导致机器Down掉,
  所以说我们有必要去做一个削峰限流,从概率学的角度上讲,在大数据量的基础上我们对于整体数据做一个百分比的截断,并不会影响整体的一个数据比例。
  简单方案-随机丢弃策略进行限流
  前端做削峰限流最简单的方法是什么?没错,就是Math.random(),我们让用户传入一个采样率,
<p>if(Math.random()

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 127 次浏览 • 2022-06-26 01:10 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。 查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。

信息收集组合拳之从废弃接口中寻找漏洞

采集交流优采云 发表了文章 • 0 个评论 • 59 次浏览 • 2022-06-21 06:53 • 来自相关话题

  信息收集组合拳之从废弃接口中寻找漏洞
  使用OneForAll工具通过域名收集网址和ip
  工具地址:GitHub - shmilylty/OneForAll: OneForAll是一款功能强大的子域收集工具
  常用命令:python oneforall.py --targets targets.txt run
  targets.txt中放入需要扫描的域名,运行后会在results文件夹下生成扫描结果
  在运行结果中我们可以看到有url、子域名、ip
  其中运行结果中ip是一行多个、还有重复的,我们需要提取ip转换成每行一个ip且没有重复的txt文本中,方便其他工具扫描
  脚本:删除重复ip
  #!/usr/bin/env python# conding:utf-8<br />##把同一行的ip换行,然后写进result.txt的文件里with open('ip.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): with open('result.txt','a',encoding='utf-8') as writelist: b = dirs.replace(",", '\n') writelist.write(b)<br />#去除重复ip,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://blog.csdn.net/qq_22764 ... s%3D1
  提取成这样单行一个ip且不重复的文本,我们就可以放到goby、fscan、小米范等工具中扫描
  
  fscan工具扫描ip
  工具地址:GitHub - shadow1ng/fscan: 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
  这款工具主要是用于内网扫描,发现资产以及进行漏洞扫描与弱口令爆破,运行速度很快,用于外网探测发现一些web资产也是不错的选择
  常用命令:全端口扫描 fscan64.exe -hf ip.txt -p 1-65535 -o result.txt
  ip.txt中放入需要扫描的ip地址,result.txt为运行结果
  
  小米范
  工具地址:我的小工具 - 标签 - 范世强 - 博客园
  (找不到这个版本的地址了,就贴个作者的博客地址吧)
  JSFinder扫描js及url
  工具地址:GitHub - Threezh1/JSFinder: JSFinder is a tool for quickly extracting URLs and subdomains from JS files on a website.
  常用命令:python JSFinder.py -f targets.txt -d -ou JSurl.txt -os JSdomain.txt
  targets.txt中放入需要扫描的url,运行结束后生会成两个txt文本, JSurl.txt为URL,JSdomain.txt为子域名
  上面这些工具的扫描结果中含有很多的url,我们需要效率高一些的话我们可以优先从参数下手,于是需要筛选含有参数的url
  脚本:提取含有参数的url
  #!/usr/bin/env python# conding:utf-8<br />#字符串中有“?”且不在字符串的结尾的就写入result.txt中with open('JSurl.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): # re_result=re.search(r"'?'",dirs) # re_result=str(re_result) if "?" in dirs :#判断字符中是否有“?”,如果有则返回该字符串的位置,是从坐标0开始算的 re = dirs.find("?") # a=len(dirs)-2是为了判断“?”是不是在最后一个字符,len()与find()不同是从一开始算字符串的长度的,在加上每行字符中\n换行符也占了一个字符,所以要减2 a=len(dirs)-2#判断字符串中“?”是不是在字符的最后 if re < a : with open('result.txt','a',encoding='utf-8') as writelist: writelist.write(dirs)<br />#去除result.txt中的重复字符串,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://www.cnblogs.com/luguankun/p/11846401.html(判断一个字符是否在一个字符串中)
  <br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  运行完脚本生成的含有参数的url如下所示
  从废弃接口中寻找漏洞
  有些网站经过了多轮渗透,正常业务都测烂的,连逻辑漏洞都找不到,经历了上面一番信息收集后,一般能收集到网站的历史业务中的url。
  然后我们将url使用脚本处理筛选之后的结果可以批量的放到sqlmap中跑,还有一些敏感接口可以尝试寻找越权、信息泄露等。
  sqlmap批量扫描
  常用命令:python sqlmap.py -m urls.txt --batch
  在urls.txt文件内放入我们使用“提取含有参数的url”这个脚本筛选后的url
  除了参数以外也可以用同样的思路把脚本修改一下去找敏感接口与url跳转参数等
  常见敏感接口
  常见跳转参数
  toUrl=
  login_url=
  register_url
  redirect_url=
  load_url=
  proxy_url=
  file_url=
  jump_url=
  某次项目中客户都疑惑我怎么找到的接口
  
  转载于:
  推荐↓↓↓ 查看全部

  信息收集组合拳之从废弃接口中寻找漏洞
  使用OneForAll工具通过域名收集网址和ip
  工具地址:GitHub - shmilylty/OneForAll: OneForAll是一款功能强大的子域收集工具
  常用命令:python oneforall.py --targets targets.txt run
  targets.txt中放入需要扫描的域名,运行后会在results文件夹下生成扫描结果
  在运行结果中我们可以看到有url、子域名、ip
  其中运行结果中ip是一行多个、还有重复的,我们需要提取ip转换成每行一个ip且没有重复的txt文本中,方便其他工具扫描
  脚本:删除重复ip
  #!/usr/bin/env python# conding:utf-8<br />##把同一行的ip换行,然后写进result.txt的文件里with open('ip.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): with open('result.txt','a',encoding='utf-8') as writelist: b = dirs.replace(",", '\n') writelist.write(b)<br />#去除重复ip,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://blog.csdn.net/qq_22764 ... s%3D1
  提取成这样单行一个ip且不重复的文本,我们就可以放到goby、fscan、小米范等工具中扫描
  
  fscan工具扫描ip
  工具地址:GitHub - shadow1ng/fscan: 一款内网综合扫描工具,方便一键自动化、全方位漏扫扫描。
  这款工具主要是用于内网扫描,发现资产以及进行漏洞扫描与弱口令爆破,运行速度很快,用于外网探测发现一些web资产也是不错的选择
  常用命令:全端口扫描 fscan64.exe -hf ip.txt -p 1-65535 -o result.txt
  ip.txt中放入需要扫描的ip地址,result.txt为运行结果
  
  小米范
  工具地址:我的小工具 - 标签 - 范世强 - 博客园
  (找不到这个版本的地址了,就贴个作者的博客地址吧)
  JSFinder扫描js及url
  工具地址:GitHub - Threezh1/JSFinder: JSFinder is a tool for quickly extracting URLs and subdomains from JS files on a website.
  常用命令:python JSFinder.py -f targets.txt -d -ou JSurl.txt -os JSdomain.txt
  targets.txt中放入需要扫描的url,运行结束后生会成两个txt文本, JSurl.txt为URL,JSdomain.txt为子域名
  上面这些工具的扫描结果中含有很多的url,我们需要效率高一些的话我们可以优先从参数下手,于是需要筛选含有参数的url
  脚本:提取含有参数的url
  #!/usr/bin/env python# conding:utf-8<br />#字符串中有“?”且不在字符串的结尾的就写入result.txt中with open('JSurl.txt','r',encoding='utf-8') as readlist: for dirs in readlist.readlines(): # re_result=re.search(r"'?'",dirs) # re_result=str(re_result) if "?" in dirs :#判断字符中是否有“?”,如果有则返回该字符串的位置,是从坐标0开始算的 re = dirs.find("?") # a=len(dirs)-2是为了判断“?”是不是在最后一个字符,len()与find()不同是从一开始算字符串的长度的,在加上每行字符中\n换行符也占了一个字符,所以要减2 a=len(dirs)-2#判断字符串中“?”是不是在字符的最后 if re < a : with open('result.txt','a',encoding='utf-8') as writelist: writelist.write(dirs)<br />#去除result.txt中的重复字符串,然后把结果写进only.txt文件里with open('result.txt','r',encoding='utf-8') as readlist: lines_seen = set() for line in readlist.readlines(): if line not in lines_seen: lines_seen.add(line) with open('only.txt','a',encoding='utf-8') as writelist: writelist.write(line)<br />#参考文章:https://www.cnblogs.com/luguankun/p/11846401.html(判断一个字符是否在一个字符串中)
  <br style="margin: 0px;padding: 0px;outline: 0px;max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;" />
  
  运行完脚本生成的含有参数的url如下所示
  从废弃接口中寻找漏洞
  有些网站经过了多轮渗透,正常业务都测烂的,连逻辑漏洞都找不到,经历了上面一番信息收集后,一般能收集到网站的历史业务中的url。
  然后我们将url使用脚本处理筛选之后的结果可以批量的放到sqlmap中跑,还有一些敏感接口可以尝试寻找越权、信息泄露等。
  sqlmap批量扫描
  常用命令:python sqlmap.py -m urls.txt --batch
  在urls.txt文件内放入我们使用“提取含有参数的url”这个脚本筛选后的url
  除了参数以外也可以用同样的思路把脚本修改一下去找敏感接口与url跳转参数等
  常见敏感接口
  常见跳转参数
  toUrl=
  login_url=
  register_url
  redirect_url=
  load_url=
  proxy_url=
  file_url=
  jump_url=
  某次项目中客户都疑惑我怎么找到的接口
  
  转载于:
  推荐↓↓↓

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 103 次浏览 • 2022-06-19 21:31 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。 查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。

图数据库无缝集成Tushare接口

采集交流优采云 发表了文章 • 0 个评论 • 66 次浏览 • 2022-06-11 03:29 • 来自相关话题

  图数据库无缝集成Tushare接口
  
  @TOC[1]Here's the table of contents:
  图数据库无缝集成Tushare接口
  使用第三方API,有助于我们快速集成数据,构建业务分析需要的知识图谱数据。这篇文章主要介绍如何将Tushare HTTP接口集成到图数据库,并使用Cypher构建知识图谱。
  在开始集成前,请确保你的图数据库安装了APOC组件,并保证apoc.load.jsonParams过程可以正常使用。APOC同时还支持JSON Path,即以特定模式搜索JSON文档中的数据项并返回其内容,其概念类似应用于XML的XPath和应用于HTML的jQuery。更多使用方式可以查看Neo4j社区技术专家俞博士的文章Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据[2]。
  一、Tushare介绍
  Tushare[3]是一个免费、开源的python财经数据接口包。主要实现对股票等金融数据从数据采集、清洗加工到数据存储的过程,能够为金融分析人员提供快速、整洁、和多样的便于分析的数据,为他们在数据获取方面极大地减轻工作量,使他们更加专注于策略和模型的研究与实现上。
  
  Tushare官网二、集成Tushare接口
  在开始集成之前请确保你有一个Tushare的访问账号。
  在图数据库的安装目录下找到conf文件夹,并在neo4j.conf文件中为Tushare HTTP API的URL定义别名,新增一行配置即可。修改配置后,重启数据库服务即可。
  apoc.json.tushare.url=http://api.tushare.pro
  三、使用接口数据
  现在我们可以编写Cypher代码很方便地从Tushare获取数据了。下面我将演示一个申万成分股图谱构建的案例。请注意在使用Cypher脚本时请设置私有token。
  下面的代码通过分批循环调用首先从stock_basic接口获取到股票代码,然后再使用股票代码获取申万成分股时间序列数据。每个股票代码调用index_member接口之前,设置了执行四百万次加法运算表示进行延时1~2秒,这个操作的目的是为了保证HTTP接口调用时不要超过接口频率限制。
  //循环获取所有股票代码<br />WITH RANGE(1,10) AS list,1000 AS limit<br />UNWIND list AS num<br />WITH num*limit AS offset,limit<br />//分批获取股票代码<br />WITH <br />    '{"api_name":"stock_basic","token":"xxxxxxxxxxx","params":{"limit":"'+limit+'","offset":"'+offset+'"},"fields":"ts_code"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.has_more AS has_more,value.data.items AS stocks<br />WHERE has_more<br />//获取申万成分数据<br />WITH has_more,stocks<br />UNWIND stocks AS stock<br />WITH stock<br />//延时执行【HTTP API对调用频率有限制,让函数执行四百万次加法,耗时大概1~2秒】<br />WITH RANGE(1,2000) AS l,stock UNWIND l AS a UNWIND l AS b WITH SUM(a+b) AS delay,stock<br />WITH <br />    '{"api_name":"index_member","token":"xxxxxxxxxxx","params":{"ts_code":"'+stock[0]+'"},"fields":"index_code,index_name,con_code,con_name,in_date,out_date,is_new"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.items AS items<br />//构建申万行业成分股图谱<br />UNWIND items AS item<br />WITH item<br />MERGE (stk:股票 {code:item[2]}) SET stk+={name:item[3]+'('+item[2]+')'}<br />MERGE (hy:申万行业 {code:item[0]}) SET hy+={name:item[1]+'('+item[0]+')'}<br />CREATE (stk)-[r:属于]->(hy) SET r+={in_date:item[4],out_date:item[5],is_new:item[6]}
  
  申万行业成分股时序图谱引用链接
  [1]TOC:图数据库无缝集成Tushare接口
  [2]Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据:
  [3]Tushare:
   查看全部

  图数据库无缝集成Tushare接口
  
  @TOC[1]Here's the table of contents:
  图数据库无缝集成Tushare接口
  使用第三方API,有助于我们快速集成数据,构建业务分析需要的知识图谱数据。这篇文章主要介绍如何将Tushare HTTP接口集成到图数据库,并使用Cypher构建知识图谱。
  在开始集成前,请确保你的图数据库安装了APOC组件,并保证apoc.load.jsonParams过程可以正常使用。APOC同时还支持JSON Path,即以特定模式搜索JSON文档中的数据项并返回其内容,其概念类似应用于XML的XPath和应用于HTML的jQuery。更多使用方式可以查看Neo4j社区技术专家俞博士的文章Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据[2]。
  一、Tushare介绍
  Tushare[3]是一个免费、开源的python财经数据接口包。主要实现对股票等金融数据从数据采集、清洗加工到数据存储的过程,能够为金融分析人员提供快速、整洁、和多样的便于分析的数据,为他们在数据获取方面极大地减轻工作量,使他们更加专注于策略和模型的研究与实现上。
  
  Tushare官网二、集成Tushare接口
  在开始集成之前请确保你有一个Tushare的访问账号。
  在图数据库的安装目录下找到conf文件夹,并在neo4j.conf文件中为Tushare HTTP API的URL定义别名,新增一行配置即可。修改配置后,重启数据库服务即可。
  apoc.json.tushare.url=http://api.tushare.pro
  三、使用接口数据
  现在我们可以编写Cypher代码很方便地从Tushare获取数据了。下面我将演示一个申万成分股图谱构建的案例。请注意在使用Cypher脚本时请设置私有token。
  下面的代码通过分批循环调用首先从stock_basic接口获取到股票代码,然后再使用股票代码获取申万成分股时间序列数据。每个股票代码调用index_member接口之前,设置了执行四百万次加法运算表示进行延时1~2秒,这个操作的目的是为了保证HTTP接口调用时不要超过接口频率限制。
  //循环获取所有股票代码<br />WITH RANGE(1,10) AS list,1000 AS limit<br />UNWIND list AS num<br />WITH num*limit AS offset,limit<br />//分批获取股票代码<br />WITH <br />    '{"api_name":"stock_basic","token":"xxxxxxxxxxx","params":{"limit":"'+limit+'","offset":"'+offset+'"},"fields":"ts_code"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.has_more AS has_more,value.data.items AS stocks<br />WHERE has_more<br />//获取申万成分数据<br />WITH has_more,stocks<br />UNWIND stocks AS stock<br />WITH stock<br />//延时执行【HTTP API对调用频率有限制,让函数执行四百万次加法,耗时大概1~2秒】<br />WITH RANGE(1,2000) AS l,stock UNWIND l AS a UNWIND l AS b WITH SUM(a+b) AS delay,stock<br />WITH <br />    '{"api_name":"index_member","token":"xxxxxxxxxxx","params":{"ts_code":"'+stock[0]+'"},"fields":"index_code,index_name,con_code,con_name,in_date,out_date,is_new"}' AS payload<br />CALL apoc.load.jsonParams(<br />    'tushare',<br />    NULL,<br />    payload,<br />    NULL,<br />    {}) yield value<br />WITH value.data.items AS items<br />//构建申万行业成分股图谱<br />UNWIND items AS item<br />WITH item<br />MERGE (stk:股票 {code:item[2]}) SET stk+={name:item[3]+'('+item[2]+')'}<br />MERGE (hy:申万行业 {code:item[0]}) SET hy+={name:item[1]+'('+item[0]+')'}<br />CREATE (stk)-[r:属于]->(hy) SET r+={in_date:item[4],out_date:item[5],is_new:item[6]}
  
  申万行业成分股时序图谱引用链接
  [1]TOC:图数据库无缝集成Tushare接口
  [2]Neo4j图数据库高级应用系列 / 服务器扩展指南 APOC(5.5) - 导入JSON数据:
  [3]Tushare:
  

【干货】一篇文章讲透数据挖掘

采集交流优采云 发表了文章 • 0 个评论 • 108 次浏览 • 2022-06-10 03:56 • 来自相关话题

  【干货】一篇文章讲透数据挖掘
  (1)数据挖掘流程
  数据挖掘一般是指从大量的数据中通过算法搜索隐藏于其中信息的过程。它通常与计算机科学有关,并通过统计、在线分析处理、情报检索、机器学习、专家系统(依靠过去的经验法则)和模式识别等诸多方法来实现上述目标。它的分析方法包括:分类、估计、预测、相关性分组或关联规则、聚类和复杂数据类型挖掘。
  
  1)数据的采集
  首先得有数据,数据的收集有两个方式,第一个方式是拿,专业点的说法叫抓取或者爬取,例如搜索引擎就是这么做的,它把网上的所有的信息都下载到它的数据中心,然后你一搜才能搜出来。
  2)数据的传输
  一般会通过队列方式进行,因为数据量实在是太大了,数据必须经过处理才会有用,可是系统处理不过来,只好排好队,慢慢的处理。
  3)数据的存储
  现在数据就是金钱,掌握了数据就相当于掌握了钱。要不然网站怎么知道你想买什么呢?就是因为它有你历史的交易的数据,这个信息可不能给别人,十分宝贵,所以需要存储下来。
  4)数据的清洗和分析
  上面存储的数据是原始数据,原始数据多是杂乱无章的,有很多垃圾数据在里面,因而需要清洗和过滤,得到一些高质量的数据。对于高质量的数据,就可以进行分析,从而对数据进行分类,或者发现数据之间的相互关系,得到知识。
  注:第三与第四个步骤,现存后清洗和先清洗再存,在真是的业务场景中可以适当互换。
  5)数据的检索和挖掘
  检索就是搜索,所谓外事问google,内事问百度。挖掘,仅仅搜索出来已经不能满足人们的要求了,还需要从信息中挖掘出相互的关系。
  6)数据的加载与应用
  怎么友好的展示与传递给用户为数据挖掘工作做好闭环。
  (2)数据挖掘工具类1)数据采集工具
  1、针对日志文件类
  工具
  定义
  Logstash
  Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash可以动态地将来自不同数据源的数据统一起来,并将数据标准化到所选择的目的地。
  Filebeat
  Filebeat 作为一个轻量级的日志传输工具可以将日志推送到中心 Logstash。
  Fluentd
  Fluentd 创建的初衷主要是尽可能的使用 JSON 作为日志输出,所以传输工具及其下游的传输线不需要猜测子字符串里面各个字段的类型。这样,它为几乎所有的语言都提供库,即可以将它插入到自定义的程序中。
  Logagent
  Logagent 是 Sematext 提供的传输工具,它用来将日志传输到 Logsene(一个基于 SaaS 平台的 Elasticsearch API)。
  Rsylog
  绝大多数 Linux 发布版本默认的守护进程,rsyslog 读取并写入 /var/log/messages 。它可以提取文件、解析、缓冲(磁盘和内存)以及将它们传输到多个目的地,包括 Elasticsearch 。可以从此处找到如何处理 Apache 以及系统日志。
  Logtail
  阿里云日志服务的生产者,目前在阿里集团内部机器上运行,经过3年多时间的考验,目前为阿里公有云用户提供日志收集服务。
  2、针对爬虫类
  页面下载 --> 页面解析 --> 数据存储
  (1)页面下载器
  对于下载器而言,python的库requests能满足大部分测试+抓取需求,进阶工程化scrapy,动态网页优先找API接口,如果有简单加密就破解,实在困难就使用splash渲染。
  (2)页面解析器
  ①BeautifulSoup(入门级):Python爬虫入门BeautifulSoup模块
  ②pyquery(类似jQuery):Python爬虫:pyquery模块解析网页
  ③lxml:Python爬虫:使用lxml解析网页内容
  ④parsel:Extract text using CSS or XPath selectors
  ⑤scrapy的Selector (强烈推荐, 比较高级的封装,基于parsel)
  ⑥选择器(Selectors):python爬虫:scrapy框架xpath和css选择器语法
  ---------------------
  总结:
  解析器直接使用scrapy的Selector 就行,简单、直接、高效。
  (3)数据存储
  ①txt文本:Python全栈之路:文件file常用操作
  ②csv文件:python读取写入csv文件
  ③sqlite3 (python自带):Python编程:使用数据库sqlite3
  ④MySQL:SQL:pymysql模块读写mysql数据
  ⑤MongoDB:Python编程:mongodb的基本增删改查操作
  ---------------------
  总结:
  数据存储没有什么可深究的,按照业务需求来就行,一般快速测试使用MongoDB,业务使用MySQL
  (4)其他工具
  ①execjs :执行js
  Python爬虫:execjs在python中运行javascript代码
  ②pyv8: 执行js
  mac安装pyv8模块-JavaScript翻译成python
  ③html5lib
  Python爬虫:scrapy利用html5lib解析不规范的html文本
  2)数据清洗工具
  1、DataWrangler
  基于网络的服务是斯坦福大学的可视化组设计来清洗和重排数据的.文本编辑非常简单。例如,当我选择大标题为“Reported crime in Alabama”的样本数据的某行的“Alabama”,然后选择另一组数据的“Alaska”,它会建议提取每州的名字。把鼠标停留在建议上,就可以看到用红色突出显示的行。
  2、Google Refine
  它可以导入导出多种格式的数据,如标签或逗号分隔的文本文件、Excel、XML和JSON文件。Refine设有内置算法,可以发现一些拼写不一样但实际上应分为一组的文本。导入你的数据后,选择编辑单元格->聚类,编辑,然后选择要用的算法。数据选项,提供快速简单的数据分布概貌。这个功能可以揭示那些可能由于输入错误导致的异常——例如,工资记录不是80,000美元而竟然是800,000美元;或指出不一致的地方——例如薪酬数据记录之间的差异,有的是计时工资,有的是每周支付,有的是年薪。除了数据管家功能,Google Refine还提供了一些有用的分析工具,例如排序和筛选。
  3、Logstash
  Logstash 是一款强大的数据处理工具,它可以实现数据传输,格式处理,格式化输出,还有强大的插件功能,常用于日志处理。
  3)数据存储工具
  数据存储主要分为结构化数据的存储和非结构化数据的存储。
  1、结构化数据
  (1)定义
  一般指存储在数据库中,具有一定逻辑结构和物理结构的数据,最为常见的是存储在关系数据库中的数据;非结构化数据:一般指结构化数据以外的数据,这些数据不存储在数据库中,而是以各种类型的文本形式存放,其中Web上的一些数据(内嵌于HTML或XML标记中)又具有一定的逻辑结构和物理结构,被称为半结构数据。
  (2)存储系统
  目前比较成熟的结构化存储系统有Oracle、MySQL、Hadoop等。
  2、非结构化数据
  (1)定义
  非结构化数据是数据结构不规则或不完整,没有预定义的数据模型,不方便用数据库二维逻辑表来表现的数据。包括所有格式的办公文档、文本、图片、XML, HTML、各类报表、图像和音频/视频信息等等。
  (2)存储方式
  1)使用文件系统存储文件,而在数据库中存储访问路径。这种方式的优点是实现简单,不需要DBMS的高级功能,但是这种方式无法实现文件的事务性访问,不便于数据备份和恢复,不便于数据迁移等;
  2)使用阿里云OSS的文件存储功能。
  4)数据计算工具
  数据计算分为实时计算、在线计算、离线计算。
  1、数据实时计算
  ApacheStorm
  2、数据在线计算
  Elasticsearch
  MySQL
  3、数据离线计算
  HaDoop Hive
  5)数据分析工具
  1、对数据矩阵科学计算:Python的numpy库
  2、对数据切片等常规处理:强大的pandas库
  3、对数据建模处理:sklearn库
  6)数据加载工具
  1、数据的可视化处理:Python中的matplotlib和seaborn库
  2、常用的BI可视化工具:Tableu和帆软
  3、ECharts
  ——————————————
  阅读推荐 查看全部

  【干货】一篇文章讲透数据挖掘
  (1)数据挖掘流程
  数据挖掘一般是指从大量的数据中通过算法搜索隐藏于其中信息的过程。它通常与计算机科学有关,并通过统计、在线分析处理、情报检索、机器学习、专家系统(依靠过去的经验法则)和模式识别等诸多方法来实现上述目标。它的分析方法包括:分类、估计、预测、相关性分组或关联规则、聚类和复杂数据类型挖掘。
  
  1)数据的采集
  首先得有数据,数据的收集有两个方式,第一个方式是拿,专业点的说法叫抓取或者爬取,例如搜索引擎就是这么做的,它把网上的所有的信息都下载到它的数据中心,然后你一搜才能搜出来。
  2)数据的传输
  一般会通过队列方式进行,因为数据量实在是太大了,数据必须经过处理才会有用,可是系统处理不过来,只好排好队,慢慢的处理。
  3)数据的存储
  现在数据就是金钱,掌握了数据就相当于掌握了钱。要不然网站怎么知道你想买什么呢?就是因为它有你历史的交易的数据,这个信息可不能给别人,十分宝贵,所以需要存储下来。
  4)数据的清洗和分析
  上面存储的数据是原始数据,原始数据多是杂乱无章的,有很多垃圾数据在里面,因而需要清洗和过滤,得到一些高质量的数据。对于高质量的数据,就可以进行分析,从而对数据进行分类,或者发现数据之间的相互关系,得到知识。
  注:第三与第四个步骤,现存后清洗和先清洗再存,在真是的业务场景中可以适当互换。
  5)数据的检索和挖掘
  检索就是搜索,所谓外事问google,内事问百度。挖掘,仅仅搜索出来已经不能满足人们的要求了,还需要从信息中挖掘出相互的关系。
  6)数据的加载与应用
  怎么友好的展示与传递给用户为数据挖掘工作做好闭环。
  (2)数据挖掘工具类1)数据采集工具
  1、针对日志文件类
  工具
  定义
  Logstash
  Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash可以动态地将来自不同数据源的数据统一起来,并将数据标准化到所选择的目的地。
  Filebeat
  Filebeat 作为一个轻量级的日志传输工具可以将日志推送到中心 Logstash。
  Fluentd
  Fluentd 创建的初衷主要是尽可能的使用 JSON 作为日志输出,所以传输工具及其下游的传输线不需要猜测子字符串里面各个字段的类型。这样,它为几乎所有的语言都提供库,即可以将它插入到自定义的程序中。
  Logagent
  Logagent 是 Sematext 提供的传输工具,它用来将日志传输到 Logsene(一个基于 SaaS 平台的 Elasticsearch API)。
  Rsylog
  绝大多数 Linux 发布版本默认的守护进程,rsyslog 读取并写入 /var/log/messages 。它可以提取文件、解析、缓冲(磁盘和内存)以及将它们传输到多个目的地,包括 Elasticsearch 。可以从此处找到如何处理 Apache 以及系统日志。
  Logtail
  阿里云日志服务的生产者,目前在阿里集团内部机器上运行,经过3年多时间的考验,目前为阿里公有云用户提供日志收集服务。
  2、针对爬虫类
  页面下载 --> 页面解析 --> 数据存储
  (1)页面下载器
  对于下载器而言,python的库requests能满足大部分测试+抓取需求,进阶工程化scrapy,动态网页优先找API接口,如果有简单加密就破解,实在困难就使用splash渲染。
  (2)页面解析器
  ①BeautifulSoup(入门级):Python爬虫入门BeautifulSoup模块
  ②pyquery(类似jQuery):Python爬虫:pyquery模块解析网页
  ③lxml:Python爬虫:使用lxml解析网页内容
  ④parsel:Extract text using CSS or XPath selectors
  ⑤scrapy的Selector (强烈推荐, 比较高级的封装,基于parsel)
  ⑥选择器(Selectors):python爬虫:scrapy框架xpath和css选择器语法
  ---------------------
  总结:
  解析器直接使用scrapy的Selector 就行,简单、直接、高效。
  (3)数据存储
  ①txt文本:Python全栈之路:文件file常用操作
  ②csv文件:python读取写入csv文件
  ③sqlite3 (python自带):Python编程:使用数据库sqlite3
  ④MySQL:SQL:pymysql模块读写mysql数据
  ⑤MongoDB:Python编程:mongodb的基本增删改查操作
  ---------------------
  总结:
  数据存储没有什么可深究的,按照业务需求来就行,一般快速测试使用MongoDB,业务使用MySQL
  (4)其他工具
  ①execjs :执行js
  Python爬虫:execjs在python中运行javascript代码
  ②pyv8: 执行js
  mac安装pyv8模块-JavaScript翻译成python
  ③html5lib
  Python爬虫:scrapy利用html5lib解析不规范的html文本
  2)数据清洗工具
  1、DataWrangler
  基于网络的服务是斯坦福大学的可视化组设计来清洗和重排数据的.文本编辑非常简单。例如,当我选择大标题为“Reported crime in Alabama”的样本数据的某行的“Alabama”,然后选择另一组数据的“Alaska”,它会建议提取每州的名字。把鼠标停留在建议上,就可以看到用红色突出显示的行。
  2、Google Refine
  它可以导入导出多种格式的数据,如标签或逗号分隔的文本文件、Excel、XML和JSON文件。Refine设有内置算法,可以发现一些拼写不一样但实际上应分为一组的文本。导入你的数据后,选择编辑单元格->聚类,编辑,然后选择要用的算法。数据选项,提供快速简单的数据分布概貌。这个功能可以揭示那些可能由于输入错误导致的异常——例如,工资记录不是80,000美元而竟然是800,000美元;或指出不一致的地方——例如薪酬数据记录之间的差异,有的是计时工资,有的是每周支付,有的是年薪。除了数据管家功能,Google Refine还提供了一些有用的分析工具,例如排序和筛选。
  3、Logstash
  Logstash 是一款强大的数据处理工具,它可以实现数据传输,格式处理,格式化输出,还有强大的插件功能,常用于日志处理。
  3)数据存储工具
  数据存储主要分为结构化数据的存储和非结构化数据的存储。
  1、结构化数据
  (1)定义
  一般指存储在数据库中,具有一定逻辑结构和物理结构的数据,最为常见的是存储在关系数据库中的数据;非结构化数据:一般指结构化数据以外的数据,这些数据不存储在数据库中,而是以各种类型的文本形式存放,其中Web上的一些数据(内嵌于HTML或XML标记中)又具有一定的逻辑结构和物理结构,被称为半结构数据。
  (2)存储系统
  目前比较成熟的结构化存储系统有Oracle、MySQL、Hadoop等。
  2、非结构化数据
  (1)定义
  非结构化数据是数据结构不规则或不完整,没有预定义的数据模型,不方便用数据库二维逻辑表来表现的数据。包括所有格式的办公文档、文本、图片、XML, HTML、各类报表、图像和音频/视频信息等等。
  (2)存储方式
  1)使用文件系统存储文件,而在数据库中存储访问路径。这种方式的优点是实现简单,不需要DBMS的高级功能,但是这种方式无法实现文件的事务性访问,不便于数据备份和恢复,不便于数据迁移等;
  2)使用阿里云OSS的文件存储功能。
  4)数据计算工具
  数据计算分为实时计算、在线计算、离线计算。
  1、数据实时计算
  ApacheStorm
  2、数据在线计算
  Elasticsearch
  MySQL
  3、数据离线计算
  HaDoop Hive
  5)数据分析工具
  1、对数据矩阵科学计算:Python的numpy库
  2、对数据切片等常规处理:强大的pandas库
  3、对数据建模处理:sklearn库
  6)数据加载工具
  1、数据的可视化处理:Python中的matplotlib和seaborn库
  2、常用的BI可视化工具:Tableu和帆软
  3、ECharts
  ——————————————
  阅读推荐

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 208 次浏览 • 2022-06-07 07:15 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 132 次浏览 • 2022-06-06 08:26 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

深入浅出云原生环境信息收集技术(一)

采集交流优采云 发表了文章 • 0 个评论 • 41 次浏览 • 2022-06-05 10:27 • 来自相关话题

  深入浅出云原生环境信息收集技术(一)
  前言
  信息收集在攻击和防御两端都是非常重要的一环。从宏观的角度来说,大多数信息相关的工作都可以看作信息收集和信息处理交替进行的循环。优质的信息收集成果是后续工作顺利展开的首要条件。《孙子兵法》有云:故善战人之势,如转圆石于千仞之山者,势也。在掌握了充足信息后,攻防工作将“如转圆石于千仞之山”。
  然而,信息的琐碎性和云原生本身的复杂组成为云原生环境下的信息收集工作带来了一定挑战。有些朋友也许会说,这有何难?比如,执行uname -a命令,就能收集到内核信息了。没错,信息收集确实是一步步进行、一项项完成的。但是,如果只是想当然地进行,收集到的信息难免陷于凌乱琐碎,也很可能不全面。
  对此,笔者结合在攻、防两端积累的经验,希望与大家探讨四个问题:
  1. 站在攻击者视角,云原生环境下的信息收集方式有哪些?
  2. 站在攻击者视角,云原生环境下的信息分类维度有哪些?
  3. 站在攻击者视角,收集到的云原生环境信息有什么价值?
  4. 站在攻击者视角,有没有可能阻碍或影响防守者收集信息?
  就“信息收集”这个话题而言,毫无疑问,防守者是占尽天时地利的,无论是能够收集到的信息种类、规模,还是信息收集开始的时间、收集信息所需的权限,都远远在攻击者之上。防守者更需要关注的是如何使用、分析收集到的信息。因此,我们从攻击者的角度出发进行探讨。这并不意味着防守的同学不需要关注。相反,只有对攻击者的技术了然于胸,才能更好地识别攻击行为、判定攻击意图。
  作为本系列的第一篇文章,本文将展开讨论第一个问题:站在攻击者视角,云原生环境下的收集信息方式有哪些?
  注:文中案例相关操作均在实验环境中进行,相关技术仅供研究交流,请勿应用于未授权的渗透测试。
  站在攻击者视角,云原生环境下的信息收集方式有哪些?
  思路重在“有章可循”。先有一个点,再进行发散。信息收集方式通常与攻击场景紧密相关。在云原生环境下,攻击场景通常有三种:
  1.攻击从远程发起。远程发起的攻击十分常见,例如,通过存在未授权访问漏洞的KubernetesAPI Server或DockerDaemon执行命令。
  2.攻击从容器内发起。容器内发起的攻击通常属于一次渗透测试的后渗透阶段——它的前提是获得了容器内某种权限的Shell,或者是Containeras a Service(后文简称CaaS)的场景——攻击者本身就是容器服务的“客户”。
  3.依托于镜像的软件供应链攻击。包括“镜像漏洞利用”和“镜像投毒”等,《云原生安全:攻防实践与体系构建》第三章对此进行了详细介绍。
  相应地,信息收集方式主要也有这三种,与攻击场景相伴而生。让我们来一起看一下。
  1通过远程交互收集信息
  综合来看,云原生环境中开放的远程服务主要有两类:云原生控制面服务和容器化业务服务。远程交互,顾名思义,网络可达就行,别无限制。收集到的信息数量和价值主要取决于目标的访问控制机制。
  (1)从云原生控制面服务收集信息
  如前所述,如果遇到存在未授权访问漏洞的Kubernetes API Server,不费吹灰之力即可控制整个云原生集群;如果目标设置了合理的访问控制机制,则获取到的有价值信息将大大减少,但也并非毫无所得。例如,许多Kubernetes API Server允许匿名用户访问部分API endpoints。在下面的示例中,攻击者通过访问/version,获得了目标Kubernetes的版本号:
  rambo@t-matrix:~$ curl -k https://1.1.1.1:6443/version{ "major": "1", "minor": "16", "gitVersion": "v1.16.2", "gitCommit": "c97fe5036ef3df2967d086711e6c0c405941e14b", "gitTreeState": "clean", "buildDate": "2019-10-15T19:09:08Z", "goVersion": "go1.12.10", "compiler": "gc", "platform": "linux/amd64"}
  通过版本匹配,攻击者能够判断目标Kubernetes可能存在哪些漏洞,从而进一步利用,这便是版本信息的价值。
  即使目标设置了非常严格的访问控制,攻击者通常也能够获得一些信息。例如,即使访问/version失败,根据失败信息我们能够得知目标是一个Kubernetes集群,从而利用Kubernetes的背景知识,去探测其他Kubernetes控制面服务端口(如kubelet、etcd等)。
  (2)从容器化业务服务收集信息
  大多数情况下,就信息收集而言,容器化与非容器化业务服务没有显著不同之处,收集到的信息均与业务服务(如Web服务、数据库服务等)本身强相关。关于这些信息的收集方法,安全社区已经有很多总结,本文不再展开讲述。
  然而,许多业务在云原生化的过程中,其自身架构或部署形态也会发生变化,引入微服务治理(如服务网格)、API治理(如API网关)的特征。这些特征有时是有价值的信息,提供了承载业务的云原生环境的一些线索,同样值得收集。例如,如果我们发现与服务交互的HTTP返回头中包含了x-envoy-开头的项,可以推测该服务处于一个由Istio/Envoy进行服务网格管理的云原生环境中。其中,x-envoy-peer-metadata-id更是包含了服务所在的Pod、Pod的内部IP和Kubernetes命名空间等重要信息[1]:
  rambo@t-matrix:~$ curl -k https://1.1.1.1 -vv 2>&1 | grepx-envoy-peer-metadata-id< x-envoy-peer-metadata-id:sidecar~2.2.2.2~PodName.Namespace~Namespace.svc.cluster.local
  事实上,网络空间测绘的关键步骤之一就是通过远程交互收集信息。我们通过测绘发现,Istio、Envoy和Kong等云原生治理程序都会给被治理的业务服务添加一个或多个特征,这些特征对于探索业务网络拓扑具有积极意义。
  2容器内收集信息
  多见于针对云原生环境渗透测试的后渗透阶段,例如,攻击者事先通过Web服务文件上传漏洞上传了一个Webshell。除此之外,云服务商提供的CaaS也允许攻击者作为用户直接创建并访问容器。
  (1)容器内通过本地操作收集信息
  虽然起点不同,但这两个场景中攻击者的目的是类似的:突破容器到宿主机或其他容器。不过,两个场景下攻击者拥有的初始权限可能不同。随着人们安全意识的增强,许多容器化业务已经采用Rootless Container部署,业务进程本身以低权限用户(如www-data)运行,这通常也是攻击者获得的Webshell的权限;然而,作为CaaS的用户,攻击者通常可以拥有容器内root权限。与前文介绍的访问控制机制类似,容器内权限大小对于容器内信息收集也有影响。但是,本文并不单独讨论权限问题给信息收集工作带来的影响,而是倡导一种“因地制宜”的随机应变能力。
  “在容器内收集信息”或许是不少朋友看到本文标题后想到的第一个场景。没错,从容器内部能够收集到当前环境的大量信息。《容器逃逸技术概览》[2]中曾介绍过的通过判定/.dockerenv文件是否存在来推断是否处于Docker创建的容器环境等手法,就是典型的容器内信息收集。
  (2)容器内通过网络交互收集信息
  与前文介绍的远程交互方式相比,容器内的网络交互对于攻击者来说具有独特优势。因此,我们将这部分内容放在这里,强调“容器内”,而不是在前面一起介绍。这种优势主要有两个方面:
  1.访问内部网络。容器拥有云原生集群的内部IP,默认配置下还会有CAP_NET_RAW权限,攻击者可以通过网络扫描等方式摸清集群内部网络拓扑,发现有价值的服务,某些场景下甚至能够访问到节点主机的元数据服务。这种网络可达的优势是值得重视的,外部攻击者通常需要借助SSRF等手段才能达到相同的目的。
  2.获得一定权限。云原生集群的容器内可能会有某种形式的访问凭证,如Pod携带的ServiceAccount token等。利用此token可以向Kubernetes API Server发起访问,纵使权限很小,至少不再是“匿名访问”,能够访问/version获得版本信息。
  3基于镜像收集信息
  近年来,软件供应链安全事件频发,人们的重视程度也日渐提高。容器从镜像创建而来,就像进程从程序创建而来一样。因此,依托于镜像,攻击者能够收集到许多有价值的信息,方式主要有两种:
  1.利用镜像和镜像仓库收集信息。有时,攻击者在容器中的权限是有限的,无法读写关键文件及其元数据。如果能够获取到目标环境使用的镜像甚至找到其公开的镜像仓库,就能够分析其镜像组件的脆弱性,找到突破口。
  2.利用特殊镜像收集运行时环境信息。由于runC等容器运行时的设计问题,攻击者能够通过在目标环境部署特殊镜像来获取环境中的容器运行时二进制程序文件,进而获得版本信息,发现潜在脆弱性。《容器运行时信息收集技术介绍》[3]一文对该技术进行了详细介绍。
  总结
  本文是“深入浅出云原生环境信息收集技术”系列的开篇,帮助大家梳理了云原生环境下常见的信息收集方式。有了这些知识作为基础,我们就能够逐渐展开讨论如何在云原生环境下体系化地收集琐碎复杂的信息。以攻促防,知攻知防。一起来守护云原生安全。
  后续文章更加精彩,敬请期待!
  参考文献
  1. %20CIS%20-%20Attack%20in%20a%20Service%20Mesh%20-%20Public.pptx.pdf
  2.
  3.
  关于星云实验室
  星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案。 查看全部

  深入浅出云原生环境信息收集技术(一)
  前言
  信息收集在攻击和防御两端都是非常重要的一环。从宏观的角度来说,大多数信息相关的工作都可以看作信息收集和信息处理交替进行的循环。优质的信息收集成果是后续工作顺利展开的首要条件。《孙子兵法》有云:故善战人之势,如转圆石于千仞之山者,势也。在掌握了充足信息后,攻防工作将“如转圆石于千仞之山”。
  然而,信息的琐碎性和云原生本身的复杂组成为云原生环境下的信息收集工作带来了一定挑战。有些朋友也许会说,这有何难?比如,执行uname -a命令,就能收集到内核信息了。没错,信息收集确实是一步步进行、一项项完成的。但是,如果只是想当然地进行,收集到的信息难免陷于凌乱琐碎,也很可能不全面。
  对此,笔者结合在攻、防两端积累的经验,希望与大家探讨四个问题:
  1. 站在攻击者视角,云原生环境下的信息收集方式有哪些?
  2. 站在攻击者视角,云原生环境下的信息分类维度有哪些?
  3. 站在攻击者视角,收集到的云原生环境信息有什么价值?
  4. 站在攻击者视角,有没有可能阻碍或影响防守者收集信息?
  就“信息收集”这个话题而言,毫无疑问,防守者是占尽天时地利的,无论是能够收集到的信息种类、规模,还是信息收集开始的时间、收集信息所需的权限,都远远在攻击者之上。防守者更需要关注的是如何使用、分析收集到的信息。因此,我们从攻击者的角度出发进行探讨。这并不意味着防守的同学不需要关注。相反,只有对攻击者的技术了然于胸,才能更好地识别攻击行为、判定攻击意图。
  作为本系列的第一篇文章,本文将展开讨论第一个问题:站在攻击者视角,云原生环境下的收集信息方式有哪些?
  注:文中案例相关操作均在实验环境中进行,相关技术仅供研究交流,请勿应用于未授权的渗透测试。
  站在攻击者视角,云原生环境下的信息收集方式有哪些?
  思路重在“有章可循”。先有一个点,再进行发散。信息收集方式通常与攻击场景紧密相关。在云原生环境下,攻击场景通常有三种:
  1.攻击从远程发起。远程发起的攻击十分常见,例如,通过存在未授权访问漏洞的KubernetesAPI Server或DockerDaemon执行命令。
  2.攻击从容器内发起。容器内发起的攻击通常属于一次渗透测试的后渗透阶段——它的前提是获得了容器内某种权限的Shell,或者是Containeras a Service(后文简称CaaS)的场景——攻击者本身就是容器服务的“客户”。
  3.依托于镜像的软件供应链攻击。包括“镜像漏洞利用”和“镜像投毒”等,《云原生安全:攻防实践与体系构建》第三章对此进行了详细介绍。
  相应地,信息收集方式主要也有这三种,与攻击场景相伴而生。让我们来一起看一下。
  1通过远程交互收集信息
  综合来看,云原生环境中开放的远程服务主要有两类:云原生控制面服务和容器化业务服务。远程交互,顾名思义,网络可达就行,别无限制。收集到的信息数量和价值主要取决于目标的访问控制机制。
  (1)从云原生控制面服务收集信息
  如前所述,如果遇到存在未授权访问漏洞的Kubernetes API Server,不费吹灰之力即可控制整个云原生集群;如果目标设置了合理的访问控制机制,则获取到的有价值信息将大大减少,但也并非毫无所得。例如,许多Kubernetes API Server允许匿名用户访问部分API endpoints。在下面的示例中,攻击者通过访问/version,获得了目标Kubernetes的版本号:
  rambo@t-matrix:~$ curl -k https://1.1.1.1:6443/version{ "major": "1", "minor": "16", "gitVersion": "v1.16.2", "gitCommit": "c97fe5036ef3df2967d086711e6c0c405941e14b", "gitTreeState": "clean", "buildDate": "2019-10-15T19:09:08Z", "goVersion": "go1.12.10", "compiler": "gc", "platform": "linux/amd64"}
  通过版本匹配,攻击者能够判断目标Kubernetes可能存在哪些漏洞,从而进一步利用,这便是版本信息的价值。
  即使目标设置了非常严格的访问控制,攻击者通常也能够获得一些信息。例如,即使访问/version失败,根据失败信息我们能够得知目标是一个Kubernetes集群,从而利用Kubernetes的背景知识,去探测其他Kubernetes控制面服务端口(如kubelet、etcd等)。
  (2)从容器化业务服务收集信息
  大多数情况下,就信息收集而言,容器化与非容器化业务服务没有显著不同之处,收集到的信息均与业务服务(如Web服务、数据库服务等)本身强相关。关于这些信息的收集方法,安全社区已经有很多总结,本文不再展开讲述。
  然而,许多业务在云原生化的过程中,其自身架构或部署形态也会发生变化,引入微服务治理(如服务网格)、API治理(如API网关)的特征。这些特征有时是有价值的信息,提供了承载业务的云原生环境的一些线索,同样值得收集。例如,如果我们发现与服务交互的HTTP返回头中包含了x-envoy-开头的项,可以推测该服务处于一个由Istio/Envoy进行服务网格管理的云原生环境中。其中,x-envoy-peer-metadata-id更是包含了服务所在的Pod、Pod的内部IP和Kubernetes命名空间等重要信息[1]:
  rambo@t-matrix:~$ curl -k https://1.1.1.1 -vv 2>&1 | grepx-envoy-peer-metadata-id< x-envoy-peer-metadata-id:sidecar~2.2.2.2~PodName.Namespace~Namespace.svc.cluster.local
  事实上,网络空间测绘的关键步骤之一就是通过远程交互收集信息。我们通过测绘发现,Istio、Envoy和Kong等云原生治理程序都会给被治理的业务服务添加一个或多个特征,这些特征对于探索业务网络拓扑具有积极意义。
  2容器内收集信息
  多见于针对云原生环境渗透测试的后渗透阶段,例如,攻击者事先通过Web服务文件上传漏洞上传了一个Webshell。除此之外,云服务商提供的CaaS也允许攻击者作为用户直接创建并访问容器。
  (1)容器内通过本地操作收集信息
  虽然起点不同,但这两个场景中攻击者的目的是类似的:突破容器到宿主机或其他容器。不过,两个场景下攻击者拥有的初始权限可能不同。随着人们安全意识的增强,许多容器化业务已经采用Rootless Container部署,业务进程本身以低权限用户(如www-data)运行,这通常也是攻击者获得的Webshell的权限;然而,作为CaaS的用户,攻击者通常可以拥有容器内root权限。与前文介绍的访问控制机制类似,容器内权限大小对于容器内信息收集也有影响。但是,本文并不单独讨论权限问题给信息收集工作带来的影响,而是倡导一种“因地制宜”的随机应变能力。
  “在容器内收集信息”或许是不少朋友看到本文标题后想到的第一个场景。没错,从容器内部能够收集到当前环境的大量信息。《容器逃逸技术概览》[2]中曾介绍过的通过判定/.dockerenv文件是否存在来推断是否处于Docker创建的容器环境等手法,就是典型的容器内信息收集。
  (2)容器内通过网络交互收集信息
  与前文介绍的远程交互方式相比,容器内的网络交互对于攻击者来说具有独特优势。因此,我们将这部分内容放在这里,强调“容器内”,而不是在前面一起介绍。这种优势主要有两个方面:
  1.访问内部网络。容器拥有云原生集群的内部IP,默认配置下还会有CAP_NET_RAW权限,攻击者可以通过网络扫描等方式摸清集群内部网络拓扑,发现有价值的服务,某些场景下甚至能够访问到节点主机的元数据服务。这种网络可达的优势是值得重视的,外部攻击者通常需要借助SSRF等手段才能达到相同的目的。
  2.获得一定权限。云原生集群的容器内可能会有某种形式的访问凭证,如Pod携带的ServiceAccount token等。利用此token可以向Kubernetes API Server发起访问,纵使权限很小,至少不再是“匿名访问”,能够访问/version获得版本信息。
  3基于镜像收集信息
  近年来,软件供应链安全事件频发,人们的重视程度也日渐提高。容器从镜像创建而来,就像进程从程序创建而来一样。因此,依托于镜像,攻击者能够收集到许多有价值的信息,方式主要有两种:
  1.利用镜像和镜像仓库收集信息。有时,攻击者在容器中的权限是有限的,无法读写关键文件及其元数据。如果能够获取到目标环境使用的镜像甚至找到其公开的镜像仓库,就能够分析其镜像组件的脆弱性,找到突破口。
  2.利用特殊镜像收集运行时环境信息。由于runC等容器运行时的设计问题,攻击者能够通过在目标环境部署特殊镜像来获取环境中的容器运行时二进制程序文件,进而获得版本信息,发现潜在脆弱性。《容器运行时信息收集技术介绍》[3]一文对该技术进行了详细介绍。
  总结
  本文是“深入浅出云原生环境信息收集技术”系列的开篇,帮助大家梳理了云原生环境下常见的信息收集方式。有了这些知识作为基础,我们就能够逐渐展开讨论如何在云原生环境下体系化地收集琐碎复杂的信息。以攻促防,知攻知防。一起来守护云原生安全。
  后续文章更加精彩,敬请期待!
  参考文献
  1. %20CIS%20-%20Attack%20in%20a%20Service%20Mesh%20-%20Public.pptx.pdf
  2.
  3.
  关于星云实验室
  星云实验室专注于云计算安全、解决方案研究与虚拟化网络安全问题研究。基于IaaS环境的安全防护,利用SDN/NFV等新技术和新理念,提出了软件定义安全的云安全防护体系。承担并完成多个国家、省、市以及行业重点单位创新研究课题,已成功孵化落地绿盟科技云安全解决方案。

聊聊Spring Boot服务监控,健康检查,线程信息,JVM堆信息,指标收集

采集交流优采云 发表了文章 • 0 个评论 • 87 次浏览 • 2022-06-02 11:23 • 来自相关话题

  聊聊Spring Boot服务监控,健康检查,线程信息,JVM堆信息,指标收集
  点击上方“芋道源码”,选择“”
  管她前浪,还是后浪?
  能浪的浪,才是好浪!
  每天 10:33更新文章,每天掉亿点点头发...
  源码精品专栏
  去年我们项目做了微服务1.0的架构转型,但是服务监控这块却没有跟上。这不,最近我就被分配了要将我们核心的微服务应用全部监控起来的任务。我们的微服务应用都是SpringBoot 应用,因此就自然而然的想到了借助Spring Boot 的Actuator 模块。(没吃过猪肉总听过猪叫见过猪跑吧)。
  本篇是我在完成这个工单之后,对Spring Boot Actuator模块 学习应用的总结。在本篇文章中,你可以学习到:
  之后我还会介绍:
  推荐下自己做的 Spring Boot 的实战项目:
  Spring Boot Actuator 模块提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等,帮助我们监控和管理Spring Boot 应用。这个模块是一个采集应用内部信息暴露给外部的模块,上述的功能都可以通过HTTP 和 JMX 访问。
  因为暴露内部信息的特性,Actuator 也可以和一些外部的应用监控系统整合(Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic等)。这些监控系统提供了出色的仪表板,图形,分析和警报,可帮助你通过一个统一友好的界面,监视和管理你的应用程序。
  Actuator使用Micrometer与这些外部应用程序监视系统集成。这样一来,只需很少的配置即可轻松集成外部的监控系统。
  Micrometer 为 Java 平台上的性能数据收集提供了一个通用的 API,应用程序只需要使用 Micrometer 的通用 API 来收集性能指标即可。Micrometer 会负责完成与不同监控系统的适配工作。这就使得切换监控系统变得很容易。
  对比 Slf4j 之于 Java Logger 中的定位。
  推荐下自己做的 Spring Cloud 的实战项目:
  我们先创建一个demo应用。
  spring init -d=web,actuator -n=actuator-demo actuator-demo<br /><br />
  image.png
  <br />    ...<br /> <br />  org.springframework.boot<br />  spring-boot-starter-actuator<br /> <br />    ...<br /><br /><br /><br />
  dependencies {<br /> compile("org.springframework.boot:spring-boot-starter-actuator")<br />}<br /><br />
  Spring Boot 提供了所谓的 endpoints (下文翻译为端点)给外部来与应用程序进行访问和交互。
  打比方来说,/health 端点 提供了关于应用健康情况的一些基础信息。metrics 端点提供了一些有用的应用程序指标(JVM 内存使用、系统CPU使用等)。
  这些 Actuator 模块本来就有的端点我们称之为原生端点。根据端点的作用的话,我们大概可以分为三大类:
  详细的原生端点介绍,请以官网为准,这里就不赘述徒增篇幅。
  需要注意的就是:
  我们可以通过以下配置,来配置通过JMX 和 HTTP 暴露的端点。
  PropertyDefault
  management.endpoints.jmx.exposure.exclude
  management.endpoints.jmx.exposure.include
  *
  management.endpoints.web.exposure.exclude
  management.endpoints.web.exposure.include
  info, healt
  可以打开所有的监控点
  management.endpoints.web.exposure.include=*<br /><br />
  也可以选择打开部分,"*" 代表暴露所有的端点,如果指定多个端点,用","分开
  management.endpoints.web.exposure.exclude=beans,trace<br /><br />
  Actuator 默认所有的监控点路径都在/actuator/*,当然如果有需要这个路径也支持定制。
  management.endpoints.web.base-path=/minitor<br /><br />
  设置完重启后,再次访问地址就会变成/minitor/*。
  现在我们按照如下配置:
  # "*" 代表暴露所有的端点 如果指定多个端点,用","分开<br />management.endpoints.web.exposure.include=*<br /># 赋值规则同上<br />management.endpoints.web.exposure.exclude=<br /><br />
  启动DEMO程序,访问:8080/actuator,查看暴露出来的端点:
  image.png
  上面这样显示是因为chrome 浏览器安装了 JSON-handle 插件,实际上就是返回一大段json
  下面,我会着重介绍几个比较重要的端点。
  /health端点会聚合你程序的健康指标,来检查程序的健康情况。端点公开的应用健康信息取决于:
  management.endpoint.health.show-details=always<br /><br />
  该属性可以使用以下值之一进行配置:
  NameDescription
  never
  不展示详细信息,up或者down的状态,默认配置
  when-authorized
  详细信息将会展示给通过认证的用户。授权的角色可以通过management.endpoint.health.roles配置
  always
  对所有用户暴露详细信息
  按照上述配置,配置成always之后,我们启动项目,访问:8080/actuator/health端口,可以看到这样的信息:
  image.png
  是不是感觉好像健康信息有点少?先别急,那是因为我们创建的是一个最基础的Demo项目,没有依赖很多的组件。
  /health端点有很多自动配置的健康指示器:如redis、rabbitmq、db等组件。当你的项目有依赖对应组件的时候,这些健康指示器就会被自动装配,继而采集对应的信息。如上面的 diskSpace 节点信息就是DiskSpaceHealthIndicator 在起作用。
  
  image.png
  上述截图取自官方文档
  这是我另一个项目的/health端点信息。
  image.png
  当如上的组件有一个状态异常,应用服务的整体状态即为down。我们也可以通过配置禁用某个组件的健康监测。
  management.health.mongo.enabled: false<br /><br />
  或者禁用所有自动配置的健康指示器:
  management.health.defaults.enabled: false<br /><br />
  当然你也可以自定义一个Health Indicator,只需要实现HealthIndicator 接口或者继承AbstractHealthIndicator类。
  /**<br /> * @author Richard_yyf<br /> * @version 1.0 2020/1/16<br /> */<br />@Component<br />public class CustomHealthIndicator extends AbstractHealthIndicator {<br /><br />    @Override<br />    protected void doHealthCheck(Health.Builder builder) throws Exception {<br />        // 使用 builder 来创建健康状态信息<br />        // 如果你throw 了一个 exception,那么status 就会被置为DOWN,异常信息会被记录下来<br />        builder.up()<br />                .withDetail("app", "这个项目很健康")<br />                .withDetail("error", "Nothing, I'm very good");<br />    }<br />}<br /><br />
  最终效果:
  image.png
  /metrics端点用来返回当前应用的各类重要度量指标,比如:内存信息、线程信息、垃圾回收信息、tomcat、数据库连接池等。
  {<br />    "names": [<br />        "tomcat.threads.busy",<br />        "jvm.threads.states",<br />        "jdbc.connections.active",<br />        "jvm.gc.memory.promoted",<br />        "http.server.requests",<br />        "hikaricp.connections.max",<br />        "hikaricp.connections.min",<br />        "jvm.memory.used",<br />        "jvm.gc.max.data.size",<br />        "jdbc.connections.max",<br />         ....<br />    ]<br />}<br /><br />
  不同于1.x,Actuator在这个界面看不到具体的指标信息,只是展示了一个指标列表。 为了获取到某个指标的详细信息,我们可以请求具体的指标信息,像这样:
  http://localhost:8080/actuator/metrics/{MetricName}<br /><br />
  比如我访问/actuator/metrics/jvm.memory.max,返回信息如下:
  image.png
  你也可以用query param的方式查看单独的一块区域。比如你可以访问/actuator/metrics/jvm.memory.max?tag=id:Metaspace。结果就是:
  image.png
  /loggers 端点暴露了我们程序内部配置的所有logger的信息。我们访问/actuator/loggers可以看到,
  image.png
  你也可以通过下述方式访问单独一个logger,
  http://localhost:8080/actuator/loggers/{name}<br /><br />
  比如我现在访问 root logger,:8080/actuator/loggers/root
  {<br />    "configuredLevel": "INFO",<br />    "effectiveLevel": "INFO"<br />}<br /><br />
  /loggers端点我最想提的就是这个功能,能够动态修改你的日志等级。
  比如,我们可以通过下述方式来修改 root logger的日志等级。我们只需要发起一个URL 为:8080/actuator/loggers/root的POST请求,POST报文如下:
  {<br />   "configuredLevel": "DEBUG"<br />}<br /><br />
  image.png
  仔细想想,这个功能是不是非常有用。如果在生产环境中,你想要你的应用输出一些Debug信息以便于你诊断一些异常情况,你你只需要按照上述方式就可以修改,而不需要重启应用。
  如果想重置成默认值,把value 改成 null
  /info端点可以用来展示你程序的信息。我理解过来就是一些程序的基础信息。并且你可以按照自己的需求在配置文件application.properties中个性化配置(默认情况下,该端点只会返回一个空的json内容。):
  info.app.name=actuator-test-demo<br />info.app.encoding=UTF-8<br />info.app.java.source=1.8<br />info.app.java.target=1.8<br /># 在 maven 项目中你可以直接用下列方式引用 maven properties的值<br /># info.app.encoding=@project.build.sourceEncoding@<br /># info.app.java.source=@java.version@<br /># info.app.java.target=@java.version@<br /><br />
  启动项目,访问:8080/actuator/info:
  {<br />    "app": {<br />        "encoding": "UTF-8",<br />        "java": {<br />            "source": "1.8.0_131",<br />            "target": "1.8.0_131"<br />        },<br />        "name": "actuator-test-demo"<br />    }<br />}<br /><br />
  /beans端点会返回Spring 容器中所有bean的别名、类型、是否单例、依赖等信息。
  访问:8080/actuator/beans,返回如下:
  image.png
  访问::8080/actuator/heapdump会自动生成一个 Jvm 的堆文件 heapdump。我们可以使用 JDK 自带的 Jvm 监控工具 VisualVM 打开此文件查看内存快照。
  image.png
  这个端点我个人觉得特别有用,方便我们在日常定位问题的时候查看线程的情况。主要展示了线程名、线程ID、线程的状态、是否等待锁资源、线程堆栈等信息。就是可能查看起来不太直观。访问:8080/actuator/threaddump返回如下:
  image.png
  这个端点属于操作控制类端点,可以优雅关闭 Spring Boot 应用。要使用这个功能首先需要在配置文件中开启:
  management.endpoint.shutdown.enabled=true<br /><br />
  由于 shutdown 接口默认只支持 POST 请求 ,我们启动Demo项目,向:8080/actuator/shutdown发起POST请求。返回信息:
  {<br />    "message": "Shutting down, bye..."<br />}<br /><br />
  然后应用程序被关闭。
  由于开放关闭应用的操作本身是一件非常危险 的事,所以真正在线上使用的时候,我们需要对其加入一定的保护机制,比如:定制Actuator的端点路径、整合Spring Security进行安全校验 等。(不是特别必要的话,这个端点不用开)
  由于端点的信息和产生的交互都是非常敏感的,必须防止未经授权的外部访问。如果您的应用程序中存在Spring Security 的依赖,则默认情况下使用基于表单的HTTP身份验证 来保护端点。
  如果没有,只需要增加对应的依赖即可:
  <br />   org.springframework.boot<br />   spring-boot-starter-security<br /><br /><br />
  添加之后,我们需要定义安全校验规则,来覆盖Spring Security 的默认配置。
  这里我给出了两个版本的模板配置:
  import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;<br />import org.springframework.boot.actuate.context.ShutdownEndpoint;<br />import org.springframework.boot.autoconfigure.security.servlet.PathRequest;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.security.config.annotation.web.builders.HttpSecurity;<br />import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;<br /><br />/**<br /> * @author Richard_yyf<br /> */<br />@Configuration<br />public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {<br /><br />    /*<br />     * version1:<br />     * 1. 限制 '/shutdown'端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问其他的端点<br />     * 3. 允许外部访问静态资源<br />     * 4. 允许外部访问 '/'<br />     * 5. 其他的访问需要被校验<br />     * version2:<br />     * 1. 限制所有端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问静态资源<br />     * 3. 允许外部访问 '/'<br />     * 4. 其他的访问需要被校验<br />     */<br /><br />    @Override<br />    protected void configure(HttpSecurity http) throws Exception {<br />        // version1<br />//        http<br />//                .authorizeRequests()<br />//                    .requestMatchers(EndpointRequest.to(ShutdownEndpoint.class))<br />//                        .hasRole("ACTUATOR_ADMIN")<br />//                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />//                    .permitAll()<br />//                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />//                    .permitAll()<br />//                .antMatchers("/")<br />//                    .permitAll()<br />//                .antMatchers("/**")<br />//                    .authenticated()<br />//                .and()<br />//                .httpBasic();<br /><br />        // version2<br />        http<br />                .authorizeRequests()<br />                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />                    .hasRole("ACTUATOR_ADMIN")<br />                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />                    .permitAll()<br />                .antMatchers("/")<br />                    .permitAll()<br />                .antMatchers("/**")<br />                    .authenticated()<br />                .and()<br />                .httpBasic();<br />    }<br />}<br /><br />
  application.properties的相关配置如下:
  # Spring Security Default user name and password<br />spring.security.user.name=actuator<br />spring.security.user.password=actuator<br />spring.security.user.roles=ACTUATOR_ADMIN<br />
  - END -
  欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
  
  已在知识星球更新源码解析如下:
  最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
  提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。 查看全部

  聊聊Spring Boot服务监控,健康检查,线程信息,JVM堆信息,指标收集
  点击上方“芋道源码”,选择“”
  管她前浪,还是后浪?
  能浪的浪,才是好浪!
  每天 10:33更新文章,每天掉亿点点头发...
  源码精品专栏
  去年我们项目做了微服务1.0的架构转型,但是服务监控这块却没有跟上。这不,最近我就被分配了要将我们核心的微服务应用全部监控起来的任务。我们的微服务应用都是SpringBoot 应用,因此就自然而然的想到了借助Spring Boot 的Actuator 模块。(没吃过猪肉总听过猪叫见过猪跑吧)。
  本篇是我在完成这个工单之后,对Spring Boot Actuator模块 学习应用的总结。在本篇文章中,你可以学习到:
  之后我还会介绍:
  推荐下自己做的 Spring Boot 的实战项目:
  Spring Boot Actuator 模块提供了生产级别的功能,比如健康检查,审计,指标收集,HTTP 跟踪等,帮助我们监控和管理Spring Boot 应用。这个模块是一个采集应用内部信息暴露给外部的模块,上述的功能都可以通过HTTP 和 JMX 访问。
  因为暴露内部信息的特性,Actuator 也可以和一些外部的应用监控系统整合(Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic等)。这些监控系统提供了出色的仪表板,图形,分析和警报,可帮助你通过一个统一友好的界面,监视和管理你的应用程序。
  Actuator使用Micrometer与这些外部应用程序监视系统集成。这样一来,只需很少的配置即可轻松集成外部的监控系统。
  Micrometer 为 Java 平台上的性能数据收集提供了一个通用的 API,应用程序只需要使用 Micrometer 的通用 API 来收集性能指标即可。Micrometer 会负责完成与不同监控系统的适配工作。这就使得切换监控系统变得很容易。
  对比 Slf4j 之于 Java Logger 中的定位。
  推荐下自己做的 Spring Cloud 的实战项目:
  我们先创建一个demo应用。
  spring init -d=web,actuator -n=actuator-demo actuator-demo<br /><br />
  image.png
  <br />    ...<br /> <br />  org.springframework.boot<br />  spring-boot-starter-actuator<br /> <br />    ...<br /><br /><br /><br />
  dependencies {<br /> compile("org.springframework.boot:spring-boot-starter-actuator")<br />}<br /><br />
  Spring Boot 提供了所谓的 endpoints (下文翻译为端点)给外部来与应用程序进行访问和交互。
  打比方来说,/health 端点 提供了关于应用健康情况的一些基础信息。metrics 端点提供了一些有用的应用程序指标(JVM 内存使用、系统CPU使用等)。
  这些 Actuator 模块本来就有的端点我们称之为原生端点。根据端点的作用的话,我们大概可以分为三大类:
  详细的原生端点介绍,请以官网为准,这里就不赘述徒增篇幅。
  需要注意的就是:
  我们可以通过以下配置,来配置通过JMX 和 HTTP 暴露的端点。
  PropertyDefault
  management.endpoints.jmx.exposure.exclude
  management.endpoints.jmx.exposure.include
  *
  management.endpoints.web.exposure.exclude
  management.endpoints.web.exposure.include
  info, healt
  可以打开所有的监控点
  management.endpoints.web.exposure.include=*<br /><br />
  也可以选择打开部分,"*" 代表暴露所有的端点,如果指定多个端点,用","分开
  management.endpoints.web.exposure.exclude=beans,trace<br /><br />
  Actuator 默认所有的监控点路径都在/actuator/*,当然如果有需要这个路径也支持定制。
  management.endpoints.web.base-path=/minitor<br /><br />
  设置完重启后,再次访问地址就会变成/minitor/*。
  现在我们按照如下配置:
  # "*" 代表暴露所有的端点 如果指定多个端点,用","分开<br />management.endpoints.web.exposure.include=*<br /># 赋值规则同上<br />management.endpoints.web.exposure.exclude=<br /><br />
  启动DEMO程序,访问:8080/actuator,查看暴露出来的端点:
  image.png
  上面这样显示是因为chrome 浏览器安装了 JSON-handle 插件,实际上就是返回一大段json
  下面,我会着重介绍几个比较重要的端点。
  /health端点会聚合你程序的健康指标,来检查程序的健康情况。端点公开的应用健康信息取决于:
  management.endpoint.health.show-details=always<br /><br />
  该属性可以使用以下值之一进行配置:
  NameDescription
  never
  不展示详细信息,up或者down的状态,默认配置
  when-authorized
  详细信息将会展示给通过认证的用户。授权的角色可以通过management.endpoint.health.roles配置
  always
  对所有用户暴露详细信息
  按照上述配置,配置成always之后,我们启动项目,访问:8080/actuator/health端口,可以看到这样的信息:
  image.png
  是不是感觉好像健康信息有点少?先别急,那是因为我们创建的是一个最基础的Demo项目,没有依赖很多的组件。
  /health端点有很多自动配置的健康指示器:如redis、rabbitmq、db等组件。当你的项目有依赖对应组件的时候,这些健康指示器就会被自动装配,继而采集对应的信息。如上面的 diskSpace 节点信息就是DiskSpaceHealthIndicator 在起作用。
  
  image.png
  上述截图取自官方文档
  这是我另一个项目的/health端点信息。
  image.png
  当如上的组件有一个状态异常,应用服务的整体状态即为down。我们也可以通过配置禁用某个组件的健康监测。
  management.health.mongo.enabled: false<br /><br />
  或者禁用所有自动配置的健康指示器:
  management.health.defaults.enabled: false<br /><br />
  当然你也可以自定义一个Health Indicator,只需要实现HealthIndicator 接口或者继承AbstractHealthIndicator类。
  /**<br /> * @author Richard_yyf<br /> * @version 1.0 2020/1/16<br /> */<br />@Component<br />public class CustomHealthIndicator extends AbstractHealthIndicator {<br /><br />    @Override<br />    protected void doHealthCheck(Health.Builder builder) throws Exception {<br />        // 使用 builder 来创建健康状态信息<br />        // 如果你throw 了一个 exception,那么status 就会被置为DOWN,异常信息会被记录下来<br />        builder.up()<br />                .withDetail("app", "这个项目很健康")<br />                .withDetail("error", "Nothing, I'm very good");<br />    }<br />}<br /><br />
  最终效果:
  image.png
  /metrics端点用来返回当前应用的各类重要度量指标,比如:内存信息、线程信息、垃圾回收信息、tomcat、数据库连接池等。
  {<br />    "names": [<br />        "tomcat.threads.busy",<br />        "jvm.threads.states",<br />        "jdbc.connections.active",<br />        "jvm.gc.memory.promoted",<br />        "http.server.requests",<br />        "hikaricp.connections.max",<br />        "hikaricp.connections.min",<br />        "jvm.memory.used",<br />        "jvm.gc.max.data.size",<br />        "jdbc.connections.max",<br />         ....<br />    ]<br />}<br /><br />
  不同于1.x,Actuator在这个界面看不到具体的指标信息,只是展示了一个指标列表。 为了获取到某个指标的详细信息,我们可以请求具体的指标信息,像这样:
  http://localhost:8080/actuator/metrics/{MetricName}<br /><br />
  比如我访问/actuator/metrics/jvm.memory.max,返回信息如下:
  image.png
  你也可以用query param的方式查看单独的一块区域。比如你可以访问/actuator/metrics/jvm.memory.max?tag=id:Metaspace。结果就是:
  image.png
  /loggers 端点暴露了我们程序内部配置的所有logger的信息。我们访问/actuator/loggers可以看到,
  image.png
  你也可以通过下述方式访问单独一个logger,
  http://localhost:8080/actuator/loggers/{name}<br /><br />
  比如我现在访问 root logger,:8080/actuator/loggers/root
  {<br />    "configuredLevel": "INFO",<br />    "effectiveLevel": "INFO"<br />}<br /><br />
  /loggers端点我最想提的就是这个功能,能够动态修改你的日志等级。
  比如,我们可以通过下述方式来修改 root logger的日志等级。我们只需要发起一个URL 为:8080/actuator/loggers/root的POST请求,POST报文如下:
  {<br />   "configuredLevel": "DEBUG"<br />}<br /><br />
  image.png
  仔细想想,这个功能是不是非常有用。如果在生产环境中,你想要你的应用输出一些Debug信息以便于你诊断一些异常情况,你你只需要按照上述方式就可以修改,而不需要重启应用。
  如果想重置成默认值,把value 改成 null
  /info端点可以用来展示你程序的信息。我理解过来就是一些程序的基础信息。并且你可以按照自己的需求在配置文件application.properties中个性化配置(默认情况下,该端点只会返回一个空的json内容。):
  info.app.name=actuator-test-demo<br />info.app.encoding=UTF-8<br />info.app.java.source=1.8<br />info.app.java.target=1.8<br /># 在 maven 项目中你可以直接用下列方式引用 maven properties的值<br /># info.app.encoding=@project.build.sourceEncoding@<br /># info.app.java.source=@java.version@<br /># info.app.java.target=@java.version@<br /><br />
  启动项目,访问:8080/actuator/info:
  {<br />    "app": {<br />        "encoding": "UTF-8",<br />        "java": {<br />            "source": "1.8.0_131",<br />            "target": "1.8.0_131"<br />        },<br />        "name": "actuator-test-demo"<br />    }<br />}<br /><br />
  /beans端点会返回Spring 容器中所有bean的别名、类型、是否单例、依赖等信息。
  访问:8080/actuator/beans,返回如下:
  image.png
  访问::8080/actuator/heapdump会自动生成一个 Jvm 的堆文件 heapdump。我们可以使用 JDK 自带的 Jvm 监控工具 VisualVM 打开此文件查看内存快照。
  image.png
  这个端点我个人觉得特别有用,方便我们在日常定位问题的时候查看线程的情况。主要展示了线程名、线程ID、线程的状态、是否等待锁资源、线程堆栈等信息。就是可能查看起来不太直观。访问:8080/actuator/threaddump返回如下:
  image.png
  这个端点属于操作控制类端点,可以优雅关闭 Spring Boot 应用。要使用这个功能首先需要在配置文件中开启:
  management.endpoint.shutdown.enabled=true<br /><br />
  由于 shutdown 接口默认只支持 POST 请求 ,我们启动Demo项目,向:8080/actuator/shutdown发起POST请求。返回信息:
  {<br />    "message": "Shutting down, bye..."<br />}<br /><br />
  然后应用程序被关闭。
  由于开放关闭应用的操作本身是一件非常危险 的事,所以真正在线上使用的时候,我们需要对其加入一定的保护机制,比如:定制Actuator的端点路径、整合Spring Security进行安全校验 等。(不是特别必要的话,这个端点不用开)
  由于端点的信息和产生的交互都是非常敏感的,必须防止未经授权的外部访问。如果您的应用程序中存在Spring Security 的依赖,则默认情况下使用基于表单的HTTP身份验证 来保护端点。
  如果没有,只需要增加对应的依赖即可:
  <br />   org.springframework.boot<br />   spring-boot-starter-security<br /><br /><br />
  添加之后,我们需要定义安全校验规则,来覆盖Spring Security 的默认配置。
  这里我给出了两个版本的模板配置:
  import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;<br />import org.springframework.boot.actuate.context.ShutdownEndpoint;<br />import org.springframework.boot.autoconfigure.security.servlet.PathRequest;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.security.config.annotation.web.builders.HttpSecurity;<br />import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;<br /><br />/**<br /> * @author Richard_yyf<br /> */<br />@Configuration<br />public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {<br /><br />    /*<br />     * version1:<br />     * 1. 限制 '/shutdown'端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问其他的端点<br />     * 3. 允许外部访问静态资源<br />     * 4. 允许外部访问 '/'<br />     * 5. 其他的访问需要被校验<br />     * version2:<br />     * 1. 限制所有端点的访问,只允许ACTUATOR_ADMIN访问<br />     * 2. 允许外部访问静态资源<br />     * 3. 允许外部访问 '/'<br />     * 4. 其他的访问需要被校验<br />     */<br /><br />    @Override<br />    protected void configure(HttpSecurity http) throws Exception {<br />        // version1<br />//        http<br />//                .authorizeRequests()<br />//                    .requestMatchers(EndpointRequest.to(ShutdownEndpoint.class))<br />//                        .hasRole("ACTUATOR_ADMIN")<br />//                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />//                    .permitAll()<br />//                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />//                    .permitAll()<br />//                .antMatchers("/")<br />//                    .permitAll()<br />//                .antMatchers("/**")<br />//                    .authenticated()<br />//                .and()<br />//                .httpBasic();<br /><br />        // version2<br />        http<br />                .authorizeRequests()<br />                .requestMatchers(EndpointRequest.toAnyEndpoint())<br />                    .hasRole("ACTUATOR_ADMIN")<br />                .requestMatchers(PathRequest.toStaticResources().atCommonLocations())<br />                    .permitAll()<br />                .antMatchers("/")<br />                    .permitAll()<br />                .antMatchers("/**")<br />                    .authenticated()<br />                .and()<br />                .httpBasic();<br />    }<br />}<br /><br />
  application.properties的相关配置如下:
  # Spring Security Default user name and password<br />spring.security.user.name=actuator<br />spring.security.user.password=actuator<br />spring.security.user.roles=ACTUATOR_ADMIN<br />
  - END -
  欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
  
  已在知识星球更新源码解析如下:
  最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
  提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 131 次浏览 • 2022-05-29 05:57 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 85 次浏览 • 2022-05-28 21:46 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

springcloud微服务实践:天气数据API微服务的实现

采集交流优采云 发表了文章 • 0 个评论 • 57 次浏览 • 2022-05-27 20:42 • 来自相关话题

  springcloud微服务实践:天气数据API微服务的实现
  天气数据API 微服务的实现
  天气数据API微服务包含了天气数据查询组件。天气数据查询组件提供了天气数据查询的接口。
  我们的数据已经通过天气数据采集微服务集成到了Redis 存储中,天气数据API微服务只需要从Redis获取数据,而后从接口中暴露出去即可。
  在micro-weather-report应用的基础上,我们将对其进行逐步的拆分,形成一个新的微服务msa-weather-data-server应用。
  
  所需环境
  为了演示本例子,需要采用如下开发环境。
  修改天气数据服务接口及实现
  在com.waylau.spring.cloud. weather.service包下,我们之前已经定义了该应用的天气数据服务接口WeatherDataService。
  public interface WeatherDataService {<br />*★<br />*根据城市ID查询天气数据<br />@param cityId<br />@return <br />WeatherResponse getDataByCityId(String cityId);<br />/**<br />★根据城市名称查询天气数据<br />* @param cityId<br />* Creturn<br />*/<br />Wea therResponse getDataByCityName (String cityName) ;<br />}
  对于该微服务而言,我们并不需要同步天气的业务需求,所以把之前定义的syncDataByCityId方法删除了。
  WeatherDataServicelmpl是对WeatherDataService 接口的实现,也要做出相应的调整,将同步天气的代码逻辑都删除,保留以下代码。
  package com. way1au . spr ing.cloud.weather .service;<br />import java. io. IOException;<br />import org.slf4j . Logger;<br />import org.slf4j. LoggerFactory;<br />import org.springf ramework. beans. factory . annotation.Autowired;<br />import org. springfr amework. data. redis. core. StringRedisTemplate;<br />import org. springf ramework. data. redis.core. ValueOperations;<br />import org. springf ramework. stereotype. Service;<br />import com. fasterxml. jackson.databind. objectMapper;<br />import com. waylau. spring. cloud . weather . vo . Wea therResponse;<br />/**<br />k天气数据服务.<br />@since 1.0.0 2017年10月29日 <br />* @author Way Lau<br />@Service<br />public class WeatherDataServiceImpl implements WeatherDataService {<br />private final static Logger logger = LoggerFactory .getLogger (Weather<br />DataServiceImpl.class) ;<br />@Autowi red<br />private StringRedisTemplate stringRedisTemplate; <br />private final String WEATHER API = "http://wthrcdn. etouch . cn/weather_<br />mini";<br />@Override<br />public WeatherResponse getDa taByCityId(String cityId) {<br />String uri = WEATHER API + "?citykey=" + cityId;<br />return this. doGetWea therData (uri) ;<br />@Override<br />public WeatherResponse getDataByCityName (String cityName) {<br />String uri = WEATHER_ API + "?city=" + cityName ;<br />return this. doGe tWeatherData (uri) ;<br />private WeatherResponse doGetWea therData (String uri)<br />valueOperations ops = this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />/先查缓存,查不到抛出异常<br />if (!this. stringRedisTemplate . hasKey (key)) {<br />logger .error("不存在key "+ key) ;<br />throw new Runt imeException ("没有相应的天气信息") ;<br />} else {<br />logger.info("存在key"+ key + ", value=" + ops.get (key));<br />strBody = ops.get (key) ;<br />}<br />0bj ectMapper mapper = new ObjectMapper () ;<br />WeatherResponse weather = null;<br />try {<br />weather = mapper . readvalue (strBody, WeatherResponse.class) ;<br />} catch (IOException e) {<br />logger . error ("JSON反序列化异常! ",e);<br />throw new RuntimeException ("天气信息解析失败") ;<br />return weather;<br />}<br />}
  其中需要注意的是:
  ●原有的RestTemplate用作REST客户端来进行天气数据的同步,这个类相关的代码都可以删除了;
  ●服务 会先从缓存中进行查询,查不到数据就抛出异常(有可能该城市的天气数据未同步,或者是数据已经过期) ;
  在执行反序列化JSON过程中也可能遭遇异常,同样将异常信息抛出。
  除上述WeatherDataServicelmpl、WeatherDataService 外,其他服务层的代码都可以删除了。
  调整控制层的代码
  除了WeatherController 外,其他控制层的代码都不需要了。
  WeatherController仍然是原有的代码保持不变。
  
  删除配置类、天气数据同步任务和工具类
  配置类RestConfiguration、QuartzConfiguration 及任务类WeatherDataSyncJob、 工具类Xml-
  Builder的代码都可以删除了。
  清理值对象
  值对象我们需要保留解析天气相关的类即可,其他值对象(如City. CityList等)都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并 删除的。
  同时,之前所编写的页面HTML、JS文件也要一并 删除。
  最后,要清理Thymeleaf在application.properties文件中的配置,以及build.gradle文件中的依赖。 查看全部

  springcloud微服务实践:天气数据API微服务的实现
  天气数据API 微服务的实现
  天气数据API微服务包含了天气数据查询组件。天气数据查询组件提供了天气数据查询的接口。
  我们的数据已经通过天气数据采集微服务集成到了Redis 存储中,天气数据API微服务只需要从Redis获取数据,而后从接口中暴露出去即可。
  在micro-weather-report应用的基础上,我们将对其进行逐步的拆分,形成一个新的微服务msa-weather-data-server应用。
  
  所需环境
  为了演示本例子,需要采用如下开发环境。
  修改天气数据服务接口及实现
  在com.waylau.spring.cloud. weather.service包下,我们之前已经定义了该应用的天气数据服务接口WeatherDataService。
  public interface WeatherDataService {<br />*★<br />*根据城市ID查询天气数据<br />@param cityId<br />@return <br />WeatherResponse getDataByCityId(String cityId);<br />/**<br />★根据城市名称查询天气数据<br />* @param cityId<br />* Creturn<br />*/<br />Wea therResponse getDataByCityName (String cityName) ;<br />}
  对于该微服务而言,我们并不需要同步天气的业务需求,所以把之前定义的syncDataByCityId方法删除了。
  WeatherDataServicelmpl是对WeatherDataService 接口的实现,也要做出相应的调整,将同步天气的代码逻辑都删除,保留以下代码。
  package com. way1au . spr ing.cloud.weather .service;<br />import java. io. IOException;<br />import org.slf4j . Logger;<br />import org.slf4j. LoggerFactory;<br />import org.springf ramework. beans. factory . annotation.Autowired;<br />import org. springfr amework. data. redis. core. StringRedisTemplate;<br />import org. springf ramework. data. redis.core. ValueOperations;<br />import org. springf ramework. stereotype. Service;<br />import com. fasterxml. jackson.databind. objectMapper;<br />import com. waylau. spring. cloud . weather . vo . Wea therResponse;<br />/**<br />k天气数据服务.<br />@since 1.0.0 2017年10月29日 <br />* @author Way Lau<br />@Service<br />public class WeatherDataServiceImpl implements WeatherDataService {<br />private final static Logger logger = LoggerFactory .getLogger (Weather<br />DataServiceImpl.class) ;<br />@Autowi red<br />private StringRedisTemplate stringRedisTemplate; <br />private final String WEATHER API = "http://wthrcdn. etouch . cn/weather_<br />mini";<br />@Override<br />public WeatherResponse getDa taByCityId(String cityId) {<br />String uri = WEATHER API + "?citykey=" + cityId;<br />return this. doGetWea therData (uri) ;<br />@Override<br />public WeatherResponse getDataByCityName (String cityName) {<br />String uri = WEATHER_ API + "?city=" + cityName ;<br />return this. doGe tWeatherData (uri) ;<br />private WeatherResponse doGetWea therData (String uri)<br />valueOperations ops = this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />/先查缓存,查不到抛出异常<br />if (!this. stringRedisTemplate . hasKey (key)) {<br />logger .error("不存在key "+ key) ;<br />throw new Runt imeException ("没有相应的天气信息") ;<br />} else {<br />logger.info("存在key"+ key + ", value=" + ops.get (key));<br />strBody = ops.get (key) ;<br />}<br />0bj ectMapper mapper = new ObjectMapper () ;<br />WeatherResponse weather = null;<br />try {<br />weather = mapper . readvalue (strBody, WeatherResponse.class) ;<br />} catch (IOException e) {<br />logger . error ("JSON反序列化异常! ",e);<br />throw new RuntimeException ("天气信息解析失败") ;<br />return weather;<br />}<br />}
  其中需要注意的是:
  ●原有的RestTemplate用作REST客户端来进行天气数据的同步,这个类相关的代码都可以删除了;
  ●服务 会先从缓存中进行查询,查不到数据就抛出异常(有可能该城市的天气数据未同步,或者是数据已经过期) ;
  在执行反序列化JSON过程中也可能遭遇异常,同样将异常信息抛出。
  除上述WeatherDataServicelmpl、WeatherDataService 外,其他服务层的代码都可以删除了。
  调整控制层的代码
  除了WeatherController 外,其他控制层的代码都不需要了。
  WeatherController仍然是原有的代码保持不变。
  
  删除配置类、天气数据同步任务和工具类
  配置类RestConfiguration、QuartzConfiguration 及任务类WeatherDataSyncJob、 工具类Xml-
  Builder的代码都可以删除了。
  清理值对象
  值对象我们需要保留解析天气相关的类即可,其他值对象(如City. CityList等)都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并 删除的。
  同时,之前所编写的页面HTML、JS文件也要一并 删除。
  最后,要清理Thymeleaf在application.properties文件中的配置,以及build.gradle文件中的依赖。

天气数据采集微服务的实现:数据采集组件、数据存储组件

采集交流优采云 发表了文章 • 0 个评论 • 117 次浏览 • 2022-05-25 13:07 • 来自相关话题

  天气数据采集微服务的实现:数据采集组件、数据存储组件
  . Spring Boot Data Redis Starter 2.0.0.M4。
  .Redis 3.2.100。
  . Spring Boot Quartz Starter 2.0.0.M4。
  . Quartz Scheduler 2.3.0。
  新增天气数据采集服务接口及实现
  在
  com.waylau.spring.cloud.weather.service包下,我们定义了该应用的天气数据采集服务接口WeatherDataCollectionService。
  public interface WeatherDataCollectionService {<br />/**<br />*根据城市工D同步天气数据<br />*<br />*@param cityId<br />*@return<br />*/<br />void syncDataByCityId(String cityId);<br />}
  WeatherDataCollectionService只有一个同步天气数据的方法。WeatherDataCollectionServicelmpl是对WeatherDataCollectionService接口的实现。
  package com.waylau.spring.cloud.weather.service;<br />import java.util.concurrent.TimeUnit;<br />import org.slf4j.Logger;<br />import org.slf4j-LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.data.redis.core.StringRedisTemplate;<br />import org.springframework.data.redis.core.ValueOperations;<br />import org.springframework.http.ResponseEntity;<br />import org.springframework.stereotype.Service;<br />import org.springframework.web.client.RestTemplate;<br />/*★<br />*天气数据采集服务.<br />*<br />*@since 1.o.0 2017年10月29日<br />* @author Way Lau<br />*/<br />@service<br />public class WeatherDataCollectionServicelmpl implements WeatherData<br />CollectionService {<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatacollectionServicelmpl.class);<br />@Autowired<br />private RestTemplate restTemplate;<br />@Autowired<br />private stringRedisTemplate stringRedisTemplate;<br />private final String WEATHER_API = "http://wthrcdn.etouch.cn/weather_mini";<br />private final Long TIME_OUT = 1800L;//缓存超时时间<br />@override<br />public void syncDataByCityId(String cityId) {<br />logger.info ("Start同步天气.cityId: "+cityId);<br />String uri = WEATHER_API +"?citykey=" +cityId;<br />this.saveweatherData (uri);<br />logger.info("End同步天气");<br />private void saveWeatherData(String uri) {<br />ValueOperations ops= this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />ResponseEntity response = restTemplate.getForEntity(uri,<br />String.class);<br />if(response.getStatusCodeValue()=-200) f<br />strBody=response.getBody(;<br />ops.set(key,strBody,TIME_OUT,TimeUnit.SECONDS);<br />}<br />}
  WeatherDataCollectionServiceImpl的实现过程,我们在之前的章节中也已经详细介绍过,大家也已经非常熟悉了。无非就是通过REST客户端去调用第三方的天气数据接口,并将返回的数据直接放入Redis存储中。
  同时,我们需要设置Redis数据的过期时间。
  修改天气数据同步任务
  对于天气数据同步任务WeatherDataSyncJob,我们要做一些调整。把之前所依赖的CityData-Service、WeatherDataService改为
  WeatherDataCollectionService。
  import java.util.ArrayList;<br />import java.util.List;<br />import org.quartz.JobExecutionContext;<br />import org.quartz.JobExecutionException;<br />import org.slf4j-Logger;<br />import org.slf4j.LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.scheduling.quartz.QuartzJobBean;<br />import com.waylau.spring.cloud.weather.service.WeatherDataCollection<br />service;<br />import com.waylau.spring.cloud.weather.vo.City;<br />*★<br />天气数据同步任务.<br />*<br />*@since 1.0.0 2017年10月29日<br />* author <a href=span style="box-sizing: border-box;border-width: 0px;border-style: initial;border-color: initial;color: rgb(0, 117, 59);""https://waylau.com"/span>Way Lau</a><br />*/<br />public class WeatherDataSyncJob extends QuartzJobBean<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatasyncJob.class);<br />@Autowired<br />private WeatherDataCollectionService weatherDataCollectionService;<br />@override<br />protected void executeInternal (JobExecutionContext context) throws<br />JobExecutionException{<br />logger.info("'Start天气数据同步任务");<br />/TODO改为由城市数据API微服务来提供数据<br />工istcityList =null;<br />trY {<br />//TODO 调用城市数据APT<br />cityList = new ArrayEist();<br />City city = new City();<br />city.setCityId("101280601");<br />cityList.add(city);<br />}catch(Exception e){<br />logger.error("获取城市信息异常!",e);<br />throw new RuntimeException("获取城市信息异常!",e);<br />}<br />for(City city : cityList){<br />String cityld = city.getCityld(;<br />logger.info("天气数据同步任务中,cityId:" +cityId);<br />//根据城市ID同步天气数据<br />weatherDataCollectionService.syncDataByCityId(cityId);<br />logger.info("End 天气数据同步任务");<br />}<br />}
  这里需要注意的是,定时器仍然对城市ID列表有依赖,只不过这个依赖最终会由其他应用(城市数据API微服务)来提供,所以这里暂时还没有办法完全写完,先用“TODO”来标识这个方法,后期还需要改进。但为了能让整个程序可以完整地走下去,我们在程序里面假设返回了一个城市ID为“101280601”的城市信息。
  配置类
  配置类仍然保留之前的RestConfiguration、QuartzConfiguration的代码不变,如下所示。
  1.RestConfiguration
  RestConfiguration用于配置REST客户端。
  import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.boot.web.client.RestTemplateBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.web.client.RestTemplate;<br />/**<br />*REST 配置类.<br />*<br />*@since 1.0.0 2017年10月18日<br />* @author Way Lau<br />*/<br />@configuration<br />public class RestConfiguration {<br />@Autowired<br />private RestTemplateBuilder builder;<br />CBean<br />public RestTemplate restTemplate(){<br />return builder.build();<br />}<br />}
  2.QuartzConfiguration
  QuartzConfiguration类用于定时任务。
  import org.quartz.JobBuilder;<br />import org.quartz.JobDetail;<br />import org.quartz.SimpleScheduleBuilder;<br />import org.quartz.Trigger;<br />import org.quartz.TriggerBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import com.waylau.spring.cloud.weather.job.WeatherDataSyncJob;<br />/*★<br />*Quartz配置类.<br />*<br />*since 1.0.0 2017年10月23日<br />* author Way Lau<br />*/<br />@configuration<br />public class QuartzConfiguration <br />private final int TIME=1800;1/更新频率<br />@Bean<br />public JobDetail weatherDataSyncJobJobDetail(){<br />return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity<br />("weatherDataSyncJob")<br />.storeDurably() .build(;<br />}<br />CBean<br />public Trigger sampleJobTrigger({<br />SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.<br />simpleschedule()<br />.withIntervalInSeconds (TIME).repeatForever();<br />return TriggerBuilder.newTrigger().forJob(weatherDataSyncJob-<br />JobDetail())<br />.withIdentity("weatherDataSyncTrigger").withSchedule<br />(scheduleBuilder).build();<br />}<br />}
  值对象
  值对象我们只需要保留City即可,其他值对象都可以删除了。需要注意的是,由于天气数据采集微服务并未涉及对XML数据的解析,所以之前在City上添加的相关的JABX注解,都是可以一并删除的。
  以下是新的City类。
  public class City {<br />private String cityId;<br />private string cityName;<br />private string cityCode;<br />private String province;<br />1/省略getter/setter方法}
  工具类
  工具类XmlBuilder的代码都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并删除的。
  同时,之前所编写的页面HTML、JS文件也要一并删除。
  最后,要清理Thymeleaf在 application.properties文件中的配置,以及build.gradle文件中的依赖。
  测试和运行
  首先,在进行测试前,需要将Redis服务器启动起来。
  而后再启动应用。启动应用之后,定时器就自动开始执行。整个同步过程可以通过以下控制台信息看到。
  2017-10-29 22:26:41.748 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.WeatherDatasyncJob<br />:Start天气数据同步任务<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.weatherDataSyncJob:天气数据同步任务中,cityId:101280601<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] s.c.w.s.Weather<br />DataCollectionServiceImpl: Start同步天气.cityId:101280601<br />2017-10-29 22:26:41.836 INFO 13956 ---[<br />main]o.s.b.w.embedded.<br />tomcat.TomcatwebServer: Tomcat started on port(s):8080 (http)<br />2017-10-29 22:26:41.840 INFO 13956 ---[<br />main]c.w.spring.<br />cloud.weather.Application:Started Application in 4.447 seconds<br />(JVM running for 4.788)<br />2017-10-29 22:26:41.919 INFO 13956---[eduler_Worker-1] S.c.w.s.eather<br />DatacollectionServiceImpl :End同步天气<br />2017-10-29 22:26:41.920 INFO 13956---[eduler Worker-1] C.W.s.c.weather.<br />job.WeatherDataSyncJob:End 天气数据同步任务
  由于我们只是在代码里面“硬编码”了一个城市ID为“101280601”的城市信息,所以,只有一条同步记录。
  当然,我们也能通过Redis Desktop Manager,来方便查看存储到Redis里面的数据,如图7-3所示。
  
  本篇内容给大家讲解的是天气数据采集微服务的实现
  下篇文章给大家讲解天气数据API微服务的实现;
  觉得文章不错的朋友可以转发此文关注小编;
  感谢大家的支持!!
  本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。 查看全部

  天气数据采集微服务的实现:数据采集组件、数据存储组件
  . Spring Boot Data Redis Starter 2.0.0.M4。
  .Redis 3.2.100。
  . Spring Boot Quartz Starter 2.0.0.M4。
  . Quartz Scheduler 2.3.0。
  新增天气数据采集服务接口及实现
  在
  com.waylau.spring.cloud.weather.service包下,我们定义了该应用的天气数据采集服务接口WeatherDataCollectionService。
  public interface WeatherDataCollectionService {<br />/**<br />*根据城市工D同步天气数据<br />*<br />*@param cityId<br />*@return<br />*/<br />void syncDataByCityId(String cityId);<br />}
  WeatherDataCollectionService只有一个同步天气数据的方法。WeatherDataCollectionServicelmpl是对WeatherDataCollectionService接口的实现。
  package com.waylau.spring.cloud.weather.service;<br />import java.util.concurrent.TimeUnit;<br />import org.slf4j.Logger;<br />import org.slf4j-LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.data.redis.core.StringRedisTemplate;<br />import org.springframework.data.redis.core.ValueOperations;<br />import org.springframework.http.ResponseEntity;<br />import org.springframework.stereotype.Service;<br />import org.springframework.web.client.RestTemplate;<br />/*★<br />*天气数据采集服务.<br />*<br />*@since 1.o.0 2017年10月29日<br />* @author Way Lau<br />*/<br />@service<br />public class WeatherDataCollectionServicelmpl implements WeatherData<br />CollectionService {<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatacollectionServicelmpl.class);<br />@Autowired<br />private RestTemplate restTemplate;<br />@Autowired<br />private stringRedisTemplate stringRedisTemplate;<br />private final String WEATHER_API = "http://wthrcdn.etouch.cn/weather_mini";<br />private final Long TIME_OUT = 1800L;//缓存超时时间<br />@override<br />public void syncDataByCityId(String cityId) {<br />logger.info ("Start同步天气.cityId: "+cityId);<br />String uri = WEATHER_API +"?citykey=" +cityId;<br />this.saveweatherData (uri);<br />logger.info("End同步天气");<br />private void saveWeatherData(String uri) {<br />ValueOperations ops= this.stringRedisTemplate.<br />opsForValue() ;<br />String key = uri;<br />String strBody = null;<br />ResponseEntity response = restTemplate.getForEntity(uri,<br />String.class);<br />if(response.getStatusCodeValue()=-200) f<br />strBody=response.getBody(;<br />ops.set(key,strBody,TIME_OUT,TimeUnit.SECONDS);<br />}<br />}
  WeatherDataCollectionServiceImpl的实现过程,我们在之前的章节中也已经详细介绍过,大家也已经非常熟悉了。无非就是通过REST客户端去调用第三方的天气数据接口,并将返回的数据直接放入Redis存储中。
  同时,我们需要设置Redis数据的过期时间。
  修改天气数据同步任务
  对于天气数据同步任务WeatherDataSyncJob,我们要做一些调整。把之前所依赖的CityData-Service、WeatherDataService改为
  WeatherDataCollectionService。
  import java.util.ArrayList;<br />import java.util.List;<br />import org.quartz.JobExecutionContext;<br />import org.quartz.JobExecutionException;<br />import org.slf4j-Logger;<br />import org.slf4j.LoggerFactory;<br />import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.scheduling.quartz.QuartzJobBean;<br />import com.waylau.spring.cloud.weather.service.WeatherDataCollection<br />service;<br />import com.waylau.spring.cloud.weather.vo.City;<br />*★<br />天气数据同步任务.<br />*<br />*@since 1.0.0 2017年10月29日<br />* author <a href=span style="box-sizing: border-box;border-width: 0px;border-style: initial;border-color: initial;color: rgb(0, 117, 59);""https://waylau.com"/span>Way Lau</a><br />*/<br />public class WeatherDataSyncJob extends QuartzJobBean<br />private final static Logger logger = LoggerFactory.getLogger(Weather<br />DatasyncJob.class);<br />@Autowired<br />private WeatherDataCollectionService weatherDataCollectionService;<br />@override<br />protected void executeInternal (JobExecutionContext context) throws<br />JobExecutionException{<br />logger.info("'Start天气数据同步任务");<br />/TODO改为由城市数据API微服务来提供数据<br />工istcityList =null;<br />trY {<br />//TODO 调用城市数据APT<br />cityList = new ArrayEist();<br />City city = new City();<br />city.setCityId("101280601");<br />cityList.add(city);<br />}catch(Exception e){<br />logger.error("获取城市信息异常!",e);<br />throw new RuntimeException("获取城市信息异常!",e);<br />}<br />for(City city : cityList){<br />String cityld = city.getCityld(;<br />logger.info("天气数据同步任务中,cityId:" +cityId);<br />//根据城市ID同步天气数据<br />weatherDataCollectionService.syncDataByCityId(cityId);<br />logger.info("End 天气数据同步任务");<br />}<br />}
  这里需要注意的是,定时器仍然对城市ID列表有依赖,只不过这个依赖最终会由其他应用(城市数据API微服务)来提供,所以这里暂时还没有办法完全写完,先用“TODO”来标识这个方法,后期还需要改进。但为了能让整个程序可以完整地走下去,我们在程序里面假设返回了一个城市ID为“101280601”的城市信息。
  配置类
  配置类仍然保留之前的RestConfiguration、QuartzConfiguration的代码不变,如下所示。
  1.RestConfiguration
  RestConfiguration用于配置REST客户端。
  import org.springframework.beans.factory.annotation.Autowired;<br />import org.springframework.boot.web.client.RestTemplateBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import org.springframework.web.client.RestTemplate;<br />/**<br />*REST 配置类.<br />*<br />*@since 1.0.0 2017年10月18日<br />* @author Way Lau<br />*/<br />@configuration<br />public class RestConfiguration {<br />@Autowired<br />private RestTemplateBuilder builder;<br />CBean<br />public RestTemplate restTemplate(){<br />return builder.build();<br />}<br />}
  2.QuartzConfiguration
  QuartzConfiguration类用于定时任务。
  import org.quartz.JobBuilder;<br />import org.quartz.JobDetail;<br />import org.quartz.SimpleScheduleBuilder;<br />import org.quartz.Trigger;<br />import org.quartz.TriggerBuilder;<br />import org.springframework.context.annotation.Bean;<br />import org.springframework.context.annotation.Configuration;<br />import com.waylau.spring.cloud.weather.job.WeatherDataSyncJob;<br />/*★<br />*Quartz配置类.<br />*<br />*since 1.0.0 2017年10月23日<br />* author Way Lau<br />*/<br />@configuration<br />public class QuartzConfiguration <br />private final int TIME=1800;1/更新频率<br />@Bean<br />public JobDetail weatherDataSyncJobJobDetail(){<br />return JobBuilder.newJob(WeatherDataSyncJob.class).withIdentity<br />("weatherDataSyncJob")<br />.storeDurably() .build(;<br />}<br />CBean<br />public Trigger sampleJobTrigger({<br />SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.<br />simpleschedule()<br />.withIntervalInSeconds (TIME).repeatForever();<br />return TriggerBuilder.newTrigger().forJob(weatherDataSyncJob-<br />JobDetail())<br />.withIdentity("weatherDataSyncTrigger").withSchedule<br />(scheduleBuilder).build();<br />}<br />}
  值对象
  值对象我们只需要保留City即可,其他值对象都可以删除了。需要注意的是,由于天气数据采集微服务并未涉及对XML数据的解析,所以之前在City上添加的相关的JABX注解,都是可以一并删除的。
  以下是新的City类。
  public class City {<br />private String cityId;<br />private string cityName;<br />private string cityCode;<br />private String province;<br />1/省略getter/setter方法}
  工具类
  工具类XmlBuilder的代码都可以删除了。
  清理前端代码、配置及测试用例
  已经删除的服务接口的相关测试用例自然也是要一并删除的。
  同时,之前所编写的页面HTML、JS文件也要一并删除。
  最后,要清理Thymeleaf在 application.properties文件中的配置,以及build.gradle文件中的依赖。
  测试和运行
  首先,在进行测试前,需要将Redis服务器启动起来。
  而后再启动应用。启动应用之后,定时器就自动开始执行。整个同步过程可以通过以下控制台信息看到。
  2017-10-29 22:26:41.748 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.WeatherDatasyncJob<br />:Start天气数据同步任务<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] c.w.s.c.weather.<br />job.weatherDataSyncJob:天气数据同步任务中,cityId:101280601<br />2017-10-29 22:26:41.749 INFO 13956---[eduler_Worker-1] s.c.w.s.Weather<br />DataCollectionServiceImpl: Start同步天气.cityId:101280601<br />2017-10-29 22:26:41.836 INFO 13956 ---[<br />main]o.s.b.w.embedded.<br />tomcat.TomcatwebServer: Tomcat started on port(s):8080 (http)<br />2017-10-29 22:26:41.840 INFO 13956 ---[<br />main]c.w.spring.<br />cloud.weather.Application:Started Application in 4.447 seconds<br />(JVM running for 4.788)<br />2017-10-29 22:26:41.919 INFO 13956---[eduler_Worker-1] S.c.w.s.eather<br />DatacollectionServiceImpl :End同步天气<br />2017-10-29 22:26:41.920 INFO 13956---[eduler Worker-1] C.W.s.c.weather.<br />job.WeatherDataSyncJob:End 天气数据同步任务
  由于我们只是在代码里面“硬编码”了一个城市ID为“101280601”的城市信息,所以,只有一条同步记录。
  当然,我们也能通过Redis Desktop Manager,来方便查看存储到Redis里面的数据,如图7-3所示。
  
  本篇内容给大家讲解的是天气数据采集微服务的实现
  下篇文章给大家讲解天气数据API微服务的实现;
  觉得文章不错的朋友可以转发此文关注小编;
  感谢大家的支持!!
  本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 119 次浏览 • 2022-05-21 18:02 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

再见笨重的ELK!这套轻量级日志收集方案要火!

采集交流优采云 发表了文章 • 0 个评论 • 68 次浏览 • 2022-05-21 01:39 • 来自相关话题

  再见笨重的ELK!这套轻量级日志收集方案要火!
  之前一直使用的日志收集方案是ELK,动辄占用几个G的内存,有些配置不好的服务器有点顶不住!最近发现一套轻量级日志收集方案:Loki+Promtail+Grafana(简称LPG), 几百M内存就够了,而且界面也挺不错的,推荐给大家!
  简介
  LPG日志收集方案内存占用很少,经济且高效!它不像ELK日志系统那样为日志建立索引,而是为每个日志流设置一组标签。下面分别介绍下它的核心组件:
  
  日志收集流程图安装
  实现这套日志收集方案需要安装Loki、Promtail、Grafana这些服务,直接使用docker-compose来安装非常方便。
  version: "3"<br /><br />services:<br />  # 日志存储和解析<br />  loki:<br />    image: grafana/loki<br />    container_name: lpg-loki<br />    volumes:<br />      - /mydata/loki/:/etc/loki/<br />    # 修改loki默认配置文件路径<br />    command: -config.file=/etc/loki/loki.yml<br />    ports:<br />      - 3100:3100<br /><br />  # 日志收集器<br />  promtail:<br />    image: grafana/promtail<br />    container_name: lpg-promtail<br />    volumes:<br />      # 将需要收集的日志所在目录挂载到promtail容器中<br />      - /mydata/app/mall-tiny-loki/logs/:/var/log/<br />      - /mydata/promtail:/etc/promtail/<br />    # 修改promtail默认配置文件路径<br />    command: -config.file=/etc/promtail/promtail.yml<br /><br />  # 日志可视化<br />  grafana:<br />    image: grafana/grafana<br />    container_name: lpg-grafana<br />    ports:<br />      - 3000:3000<br />
  auth_enabled: false<br /><br />server:<br />  http_listen_port: 3100<br /><br />ingester:<br />  lifecycler:<br />    address: 127.0.0.1<br />    ring:<br />      kvstore:<br />        store: inmemory<br />      replication_factor: 1<br />    final_sleep: 0s<br />  chunk_idle_period: 1h       # Any chunk not receiving new logs in this time will be flushed<br />  max_chunk_age: 1h           # All chunks will be flushed when they hit this age, default is 1h<br />  chunk_target_size: 1048576  # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first<br />  chunk_retain_period: 30s    # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)<br />  max_transfer_retries: 0     # Chunk transfers disabled<br /><br />schema_config:<br />  configs:<br />    - from: 2020-10-24<br />      store: boltdb-shipper<br />      object_store: filesystem<br />      schema: v11<br />      index:<br />        prefix: index_<br />        period: 24h<br /><br />storage_config:<br />  boltdb_shipper:<br />    active_index_directory: /loki/boltdb-shipper-active<br />    cache_location: /loki/boltdb-shipper-cache<br />    cache_ttl: 24h         # Can be increased for faster performance over longer query periods, uses more disk space<br />    shared_store: filesystem<br />  filesystem:<br />    directory: /loki/chunks<br /><br />compactor:<br />  working_directory: /loki/boltdb-shipper-compactor<br />  shared_store: filesystem<br /><br />limits_config:<br />  reject_old_samples: true<br />  reject_old_samples_max_age: 168h<br /><br />chunk_store_config:<br />  max_look_back_period: 0s<br /><br />table_manager:<br />  retention_deletes_enabled: false<br />  retention_period: 0s<br /><br />ruler:<br />  storage:<br />    type: local<br />    local:<br />      directory: /loki/rules<br />  rule_path: /loki/rules-temp<br />  alertmanager_url: http://localhost:9093<br />  ring:<br />    kvstore:<br />      store: inmemory<br />  enable_api: true<br />
  server:<br />  http_listen_port: 9080<br />  grpc_listen_port: 0<br /><br />positions:<br />  filename: /tmp/positions.yaml<br /><br />clients:<br />  - url: http://loki:3100/loki/api/v1/push<br /><br />scrape_configs:<br />- job_name: system<br />  static_configs:<br />  - targets:<br />      - localhost<br />    labels:<br />      job: varlogs<br />      __path__: /var/log/*log<br />
  docker-compose up -d<br />
  [root@local-linux lpg]# docker ps |grep lpg<br />64761b407423        grafana/loki                            "/usr/bin/loki -conf…"   3 minutes ago       Up 3 minutes        0.0.0.0:3100->3100/tcp                           lpg-loki<br />67f0f0912971        grafana/grafana                         "/run.sh"                3 minutes ago       Up 3 minutes        0.0.0.0:3000->3000/tcp                           lpg-grafana<br />f2d78eb188d1        grafana/promtail                        "/usr/bin/promtail -…"   3 minutes ago       Up 3 minutes                                                         lpg-promtail<br />
  使用
  接下来我们将使用LPG日志收集系统来收集SpringBoot应用的日志,SpringBoot应用基本不用做特殊配置。
  spring:<br />  application:<br />    name: mall-tiny-loki<br /><br />logging:<br />  path: /var/logs<br />  level:<br />    com.macro.mall.tiny: debug<br />
  docker run -p 8088:8088 --name mall-tiny-loki \<br />-v /etc/localtime:/etc/localtime \<br />-v /mydata/app/mall-tiny-loki/logs:/var/logs \<br />-e TZ="Asia/Shanghai" \<br />-d mall-tiny/mall-tiny-loki:1.0-SNAPSHOT<br />
  
  
  
  
  总结
  本文主要介绍了LPG日志系统的搭建及使用它收集SpringBoot应用的日志,LPG日志收集方案确实非常轻量级,性能也不错!不过如果你有对日志进行全文搜索的需求的话,还是得使用ELK系统。如果你对Grafana还不熟悉的话,可以参考下这篇文章。
  参考资料项目源码地址
  微信8.0将好友放开到了一万,小伙伴可以加我大号了,先到先得,再满就真没了
  扫描下方二维码即可加我微信啦,2021,抱团取暖,一起牛逼。
   查看全部

  再见笨重的ELK!这套轻量级日志收集方案要火!
  之前一直使用的日志收集方案是ELK,动辄占用几个G的内存,有些配置不好的服务器有点顶不住!最近发现一套轻量级日志收集方案:Loki+Promtail+Grafana(简称LPG), 几百M内存就够了,而且界面也挺不错的,推荐给大家!
  简介
  LPG日志收集方案内存占用很少,经济且高效!它不像ELK日志系统那样为日志建立索引,而是为每个日志流设置一组标签。下面分别介绍下它的核心组件:
  
  日志收集流程图安装
  实现这套日志收集方案需要安装Loki、Promtail、Grafana这些服务,直接使用docker-compose来安装非常方便。
  version: "3"<br /><br />services:<br />  # 日志存储和解析<br />  loki:<br />    image: grafana/loki<br />    container_name: lpg-loki<br />    volumes:<br />      - /mydata/loki/:/etc/loki/<br />    # 修改loki默认配置文件路径<br />    command: -config.file=/etc/loki/loki.yml<br />    ports:<br />      - 3100:3100<br /><br />  # 日志收集器<br />  promtail:<br />    image: grafana/promtail<br />    container_name: lpg-promtail<br />    volumes:<br />      # 将需要收集的日志所在目录挂载到promtail容器中<br />      - /mydata/app/mall-tiny-loki/logs/:/var/log/<br />      - /mydata/promtail:/etc/promtail/<br />    # 修改promtail默认配置文件路径<br />    command: -config.file=/etc/promtail/promtail.yml<br /><br />  # 日志可视化<br />  grafana:<br />    image: grafana/grafana<br />    container_name: lpg-grafana<br />    ports:<br />      - 3000:3000<br />
  auth_enabled: false<br /><br />server:<br />  http_listen_port: 3100<br /><br />ingester:<br />  lifecycler:<br />    address: 127.0.0.1<br />    ring:<br />      kvstore:<br />        store: inmemory<br />      replication_factor: 1<br />    final_sleep: 0s<br />  chunk_idle_period: 1h       # Any chunk not receiving new logs in this time will be flushed<br />  max_chunk_age: 1h           # All chunks will be flushed when they hit this age, default is 1h<br />  chunk_target_size: 1048576  # Loki will attempt to build chunks up to 1.5MB, flushing first if chunk_idle_period or max_chunk_age is reached first<br />  chunk_retain_period: 30s    # Must be greater than index read cache TTL if using an index cache (Default index read cache TTL is 5m)<br />  max_transfer_retries: 0     # Chunk transfers disabled<br /><br />schema_config:<br />  configs:<br />    - from: 2020-10-24<br />      store: boltdb-shipper<br />      object_store: filesystem<br />      schema: v11<br />      index:<br />        prefix: index_<br />        period: 24h<br /><br />storage_config:<br />  boltdb_shipper:<br />    active_index_directory: /loki/boltdb-shipper-active<br />    cache_location: /loki/boltdb-shipper-cache<br />    cache_ttl: 24h         # Can be increased for faster performance over longer query periods, uses more disk space<br />    shared_store: filesystem<br />  filesystem:<br />    directory: /loki/chunks<br /><br />compactor:<br />  working_directory: /loki/boltdb-shipper-compactor<br />  shared_store: filesystem<br /><br />limits_config:<br />  reject_old_samples: true<br />  reject_old_samples_max_age: 168h<br /><br />chunk_store_config:<br />  max_look_back_period: 0s<br /><br />table_manager:<br />  retention_deletes_enabled: false<br />  retention_period: 0s<br /><br />ruler:<br />  storage:<br />    type: local<br />    local:<br />      directory: /loki/rules<br />  rule_path: /loki/rules-temp<br />  alertmanager_url: http://localhost:9093<br />  ring:<br />    kvstore:<br />      store: inmemory<br />  enable_api: true<br />
  server:<br />  http_listen_port: 9080<br />  grpc_listen_port: 0<br /><br />positions:<br />  filename: /tmp/positions.yaml<br /><br />clients:<br />  - url: http://loki:3100/loki/api/v1/push<br /><br />scrape_configs:<br />- job_name: system<br />  static_configs:<br />  - targets:<br />      - localhost<br />    labels:<br />      job: varlogs<br />      __path__: /var/log/*log<br />
  docker-compose up -d<br />
  [root@local-linux lpg]# docker ps |grep lpg<br />64761b407423        grafana/loki                            "/usr/bin/loki -conf…"   3 minutes ago       Up 3 minutes        0.0.0.0:3100->3100/tcp                           lpg-loki<br />67f0f0912971        grafana/grafana                         "/run.sh"                3 minutes ago       Up 3 minutes        0.0.0.0:3000->3000/tcp                           lpg-grafana<br />f2d78eb188d1        grafana/promtail                        "/usr/bin/promtail -…"   3 minutes ago       Up 3 minutes                                                         lpg-promtail<br />
  使用
  接下来我们将使用LPG日志收集系统来收集SpringBoot应用的日志,SpringBoot应用基本不用做特殊配置。
  spring:<br />  application:<br />    name: mall-tiny-loki<br /><br />logging:<br />  path: /var/logs<br />  level:<br />    com.macro.mall.tiny: debug<br />
  docker run -p 8088:8088 --name mall-tiny-loki \<br />-v /etc/localtime:/etc/localtime \<br />-v /mydata/app/mall-tiny-loki/logs:/var/logs \<br />-e TZ="Asia/Shanghai" \<br />-d mall-tiny/mall-tiny-loki:1.0-SNAPSHOT<br />
  
  
  
  
  总结
  本文主要介绍了LPG日志系统的搭建及使用它收集SpringBoot应用的日志,LPG日志收集方案确实非常轻量级,性能也不错!不过如果你有对日志进行全文搜索的需求的话,还是得使用ELK系统。如果你对Grafana还不熟悉的话,可以参考下这篇文章。
  参考资料项目源码地址
  微信8.0将好友放开到了一万,小伙伴可以加我大号了,先到先得,再满就真没了
  扫描下方二维码即可加我微信啦,2021,抱团取暖,一起牛逼。
  

文章采集api Python 爬取人人视频

采集交流优采云 发表了文章 • 0 个评论 • 113 次浏览 • 2022-05-21 01:37 • 来自相关话题

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
   查看全部

  文章采集api Python 爬取人人视频
  hello,小伙伴们,又见面了,距离上一次发布文章的时间,也算是久别重逢了。期间也发生了很多的事情,导致博文断更,也是笔者不愿意的,但是确实是比较忙,不再过多赘述,希望大家能够体谅。
  平时不断的在后台收到小伙伴的私信,问是不是不更了,答案当然是否定的,有着这么多人的支持,小编还是要继续努力下去的,再次谢谢大家的支持。
  这次给大家带来的文章是爬取人人视频,之前多是分享一些爬取数据,图片,音乐,还没怎么分享过爬取过视频的,那么想要爬取视频的话该怎么爬取呢?
  其实不管是图片,还是音乐,或者是其他的文档,大部分都是一个文件读写的过程,当然视频也不例外,只是文件的格式不同而已。所以我们也可以试着以常规的手段去尝试下爬取视频,没错还是熟悉的套路与配方,即python常用函数 open()和 write()。
  不过这次略微不同的是我们会使用到iter_content来获取请求的原始响应数据,普通情况可以用r.raw,在初始请求中设置stream=True,来获取服务器的原始套接字响应,在这里我们使用iter_content更加方便一些,因为requests.get(url) 默认是下载在内存中的,下载完成才存到硬盘上,但Response.iter_content可以边下载边存硬盘中,所以在这视频下载方面更具有优势。
  当然说到 iter_content 的话,不得不提下chunk_size,因为流式请求就是像流水一样,不是一次过来而是一点一点“流”过来。处理流式数据也是一点一点处理。
  而chunk_size会直接返回从当前位置往后读取 chunk_size 大小的文件内容,且迭代响应数据。这避免了立即将内容读入内存以获得较大的响应。chunk_size是它应该读入内存的字节数。chunk_size的类型必须是int或None。None的值将根据流的值发挥不同的作用。
  做了引荐与讲解后,那么就开始上我们的主菜了,即目标网站:
  https://m.rr.tv/
  介于代码偏基础化,且主要知识点做过分析就直接上代码了,具体请看代码:
  单线程:
  import requestsimport jsonimport reimport osheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}ep_list =[]vod_list = []def get_vod(url): response = requests.get(url = url,headers = headers)#请求url ep= re.compile(r'sid:"(.*?)",')#提取ep链接 ep_list = re.findall(ep,response.text) vod= re.compile(r'data:\[{id:(.*?),title:"')#提取vod链接 vod_list = re.findall(vod,response.text) vod= re.compile(r',title:"(.*?)",desc:"')#提取视频标题 vod_name = re.findall(vod,response.text) ep = 1 os.mkdir('./'+vod_name[0])#创建视频保存目录 for i in ep_list: print("开始下载"+vod_name[0]+"第"+str(ep)+"集") url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers)#下载请求 r = requests.get(str(json.loads(response.text)['data']['url']), stream=True)#解析出下载链接并发起下载请求 f = open("./"+vod_name[0]+"/第"+str(ep)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk) ep = ep+1if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxxx?snum=1&episode=1'#目标视频链接 get_vod(url)
  多线程:
  import requestsimport jsonimport reimport osfrom concurrent.futures import ThreadPoolExecutorheaders = {'Referer':'https://m.rr.tv/',#全局设置'User-Agent':'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36'}vod_list = []#存储视频链接vod_name = []#存储视频标题get_down_url =[]#存储下载直链def get_vod(url): response = requests.get(url = url,headers = headers)#请求url vod_list.append(re.findall(re.compile(r'data:\[{id:(.*?),title:"'),response.text)[0])#找找视频链接 vod_name.append(re.findall(re.compile(r',title:"(.*?)",desc:"'),response.text)[0])#找找视频标题 for i in re.findall(re.compile(r'{sid:"(.*?)",key:'),response.text): url ="https://web-api.rr.tv/web/dram ... _list[0]+"&episodeId="+i+"&2-7-17xx"#拼接地址 response = requests.get(url = url,headers = headers) get_down_url.append(str(json.loads(response.text)['data']['url']))#拿下载直链进listdef down_begin(url,i): print("开始多线程下载"+vod_name[0]+"第"+str(i)+"集") r = requests.get(url = url,headers = headers)#下载请求 f = open("./"+vod_name[0]+"/第"+str(i)+"集.MP4", "wb")#保存视频 for chunk in r.iter_content(chunk_size=512): if chunk: f.write(chunk)if __name__ == '__main__': url='https://m.rr.tv/detail/xxxxx?snum=1&episode=1'#进入rr.tv自行获取 get_vod(url) os.mkdir('./'+vod_name[0])#创建视频保存目录 with ThreadPoolExecutor(10) as f:#这里写多线程参数,适合几十集的电视剧使用 for i,url in enumerate(get_down_url): i=int(i)+1 f.submit(down_begin,url = url,i=i)
  把案例里面的链接改成你想要下载的链接,然后右击运行代码,即可成功的下载你想要的视频了。代码获取后台回复:”人人视频“。
  在文章的最后给大家来一波福利,因为前一段时间小编在爬取百度相关关键词以及文章采集时,经常触发百度的验证机制,这种情况很明显要使用到代理IP,后来群里一个小伙伴推荐了品赞代理IP,小编测试了下,完美解决了爬取中存在的问题。
  
  如果大家后续有需要使用到代理方面的业务的话,可以扫码添加下方的二维码。国内外的IP都有,新用户可以免费测试。
  

官方客服QQ群

微信人工客服

QQ人工客服


线