
网页源代码抓取工具
2015数据可视化精选:35款好用工具,搞定信息图+可视化
网站优化 • 优采云 发表了文章 • 0 个评论 • 89 次浏览 • 2022-09-13 09:21
数据可视化为新闻媒体提供了生动有效的叙事方式和包装形式,也引领架构故事的思维变革。有哪些实用工具可以帮你讲好故事呢?深度君整合全年干货,为你精选35款优质可视化工具,指南+案例随你选。【点击“阅读原文”的相应链接,即可查看工具案例和更多细节哦】
1Plotly:简易交互式图表制作
数据新闻刚入门,想做简单的交互式数据图?Journalism.co.uk为我们介绍了一款适合数据分析、绘制图表的软件——Plotly,你还可以通过它连接Twitter等社交媒体。
Plotly上面有标示为“探索(Explore)”的按键,可以看到其他用户的数据可视化作品,这样你就能大致了解Plotly的图像成品。参考之后,你就可以在“工作区(Workspace)”绘制自己的图表啦。
一开始,需要点击“import”上传自己的数据文件,或者在添加“新网格(new grid)”后复制黏贴表格。这些数据栏都是自动编码的——如果你想改变它们在可视化作品中的名字,只需右键单击你想当做标题的那行,选择“使用行作为栏目标题(use row as column headers)”。
像infogram这样的软件可以根据数据表里的全部内容自动生成可视化效果,而Plotly与此不同,会另外让你选择哪一行哪一列需要在可视化作品里出现。在设计图表时,你可以在左边栏选择背景和颜色,添加注释、任意移动或伸缩尖头,随之调整文本。当图表完成,Plotly有多个隐私选项供你选择,还能生成嵌入代码,这样就能插入网站啦。更多操作细节,请查看Journalism.co.uk或者Plotly网站上的介绍吧。
2CartoDB: 不会编程,也可轻松学地图可视化
CartoDB是一款开源网络应用程序和交互式地图制作工具,以提供“一键式制图”功能闻名,也就是分析任何你上传的数据、自动制作地图以显示相关信息。起初CartoDB和数据新闻似乎相差千里,是两名西班牙研究生物多样性和自然保护的科学家为了更好地展示研究成果而建立的,没成想已经成为广大数据新闻记者的好帮手。目前CartoDB已经吸引12万用户创制40万张地图,将世界上所有有趣的主题——从全球粉丝对于Beyonce的最新专辑发布的实时反应到尼泊尔地震的损毁情况一一变成互动性强、好玩的可视化作品。
那CartoDB怎么用呢?
1注册账户
用户首先需要在CartoDB上注册账户、获得免费许可,也可以使用它提供的付费模板,有14天的有效期,可随时取消。
注册成功后,你会进入后台控制页面,刚开始是空白一片。如果上传了数据、制作好了地图,页面上会自动显示“你的数据”和“你的地图”选项,点选直接查看即可。
2创制数据表并检查
CartoDB一般以数据库模板为基础开始制图,因此登陆账户后第一步就是上传数据。可上传地理空间数据,如Shapefiles, GeoJSON等,可以设置公开或者个人可见。
用户可以传多少数据呢?仅有免费许可时,最多可上传50MB大小的数据。几乎能涵盖虽有的需求。点击“新数据集(New Dataset)”即可上传string,date,numeric和boolean众多格式。
载入数据之后,点击“连接数据集”。
如果你上传的数据集含有空间信息,就能从表格视图转换成地图视图。
上传数据后,先别急着制图,先检查检查输入的数据有没有差错,数据栏格式对不对。万一格式不统一,可以选择转换格式:
对于拖延症的小伙伴来讲,CartoDB无疑是一款治愈神器。在地图发布之前,CartoDB会提示你赶紧制作地图,而且平时定期邮件“轰炸”,告知新特性、提醒多练手。
制图之前,除了检查数据,更重要的准备是找到思路。CartoDB提供的建议固然能作为宝贵参考,但更重要的是自己先思考数据该怎么用、制作可视化的可行性有多大。先有框架,动手才快,效果才准。
3制作可视化数据上传完毕后,点击页面右上角“可视化(Visualize)”按钮即可制图。
CartoDB在屏幕右边栏贴心准备了一系列地图向导(Wizards),帮你用不同模式标注地图。例如有“简单模式(simple)”、“团簇模式(cluster)”、“等值域模式(choropleth)”(等值域地图是指根据数据的数值大小来标注地理区划的地图)等等。
点选模式后,CartoDB有众多创制图例和信息窗口的选项供你选择,也可以转换同的基础地图,看看哪一个最合适。
如果你按下右边的CSS按钮,就能自己修改可视化代码。如下图所示,可以轻松改变标注颜色,就像编辑文本一样:
4化静态为互动
是时候让地图动起来了!CartoDB的交互式数据库可运行Leaflet.js的插件和Google Maps插件来制作数据层,轻松完成这一过程。(这里还可以参考一个例子,即是依照行政区选择纽约住宅类型的可视化筛选器)【想得到手把手制作地铁动画指南和更多细节?请点击这里查看吧!】
需要更多地图制作工具/平台?深度君还推荐:
提供制图、查图、解图“一条龙”功能的ArcGIS Online Platform ,请查阅:《地图可视化制作和数据平台精选》,链接:
3NodeXL: 社会网络图制作助手
想快速学会如何将复杂的社会网络做成直观炫酷的可视化?NodeXL是你的绝佳选择:会用Excel,就会用它生成网络图。
NodeXL能计算以下这些社会网络研究中的常见指标:
程度(Degree),指每个节点拥有的关系链接数。对于指向型关系网,有内向程度(In-degree)和外向程度(Out-degree)之分,前者是指向内部的关系链接数,后者是指向外部的关系链接数。
特征向量中心性(Eigenvector centrality)所关注的不仅包括某一节点的关系链接数量,还包括与该节点相链接的是哪些节点,以及那些节点各自的关系链接数量是多少。
中介中心性(Betweenness centrality)则揭示出每个节点在不同关系网间提供“桥梁作用”的重要程度。它所特别标出的点,是一些移除后会导致整个网络崩塌的重要节点。
接近中心性(Closeness centrality)用于量度关系网中每个节点离其他节点的平均距离。它特别标出以较少连线与其他节点联系的点——这就类似于Kevin Bacon的六度理论(即在世界上任意两个陌生人之间只隔着六个人)。
在2014年计算机辅助报道协会巴尔的摩年会 (NICAR 2014) 上,BuzzFeed News 的科学与健康记者Peter Aldhous就介绍了NodeXL的用法,手把手教你分析美国共和党人中倾向于民主党的有哪些。请点击“阅读原文”里的相关链接,一起打开Excel跟着学吧!
如果觉得以上工具还不过瘾,我们还推荐:
4Analytics Vidhya:18款可视化好用工具一览
网站Analytics Vidhya整合出了一张【信息图可视化工具清单】,分为适用于一般用途和特定用途的两大类工具。一般用途的工具包括我们耳熟能详的Tableau, D3.js., R语言, Excel, Python等,也有专业人士更了解的Weka(主要用于数据挖掘,可生成简单图表,支持数据挖掘),SAS(用于数据建模),QlikView和QlikSense(用于数据整合和分享)。
在特殊用途工具中,有不少功能强大的选项:Instant Atlas(设计互动式图表,快速传达信息,提供图表和地图模板), WolframAlpha(智能呈现图表,无需配置),Cytoscape(高级分析和模型使用软件,用于社会网络或人际关系网的可视化呈现), NetworkX(是Python语言软件包,可用于生成经典图表、随机图表和综合性网络,Nodes可以包括文本、图片或者XML记录等格式), Flot(专门做线形图和柱状图,支持回调函数(Call back functions),需要jquery的相关知识才能驾驭)和Gephi(嵌入式3D生成器,以深入数据分析验证关系,普遍用于社会网络的可视化制作)。
关于更多信息,可以查看QlikSense的介绍,也可以参照Tableau和QlikView的对比。
此外,网站还推荐了诸如Dygraphs, Chart.js, Raphael, Highcharts等实用的JavaScript libraries作参考。如果你还想持续关注信息图和数据分析的新知,可以订阅Analytics Vidhya的邮件、关注它的Twitter和Facebook主页,掌握行业前沿资讯。【想查阅更多信息图制作信息,请点击“阅读原文”中的相关链接】
5软件开发师8款开源工具分享想知道灵动炫酷的可视化效果怎么做?怎么从零基础开始学?软件开发师、开源支持者Nitishi Tiwari撰文重点推荐了8款数据可视化的开源工具 (点击左边即可看到工具条目),一解众忧。如果觉得不过瘾,点击文末链接,有50款工具推荐。
Datawrapper:由欧洲的新闻学院开发,以便新闻机构做数据可视化作品。该工具基于图形用户界面(GUI),用简单四步即可绘图。
Chart JS:简洁的图表库。在生成图表之前,需要把函数库加进前端代码中,之后可以从函数库的应用程序编程接口(API)加图表,赋值。这款工具适合想要精确调整图表外观的人。它不适合想用现成工具的用户。
Charted:由Medium的产品科学组开发,是最简便的在线表格工具之一。只需粘贴谷歌表格或.csv文件,工具就会抓取数据,生成表格。Charted每三十分钟抓取数据,及时更新。
D3:是数据驱动文件(data-driven documents)的缩写,是一个JavaScript函数库。它使用数据创造并控制在网络浏览器里运行的交互图形,必须嵌入在html网页中,依赖矢量图形(SVG), 层叠式样式表(CSS3)等html的工具展示图形。需要编写代码,更适合掌握程序员技能的数据新闻人。
Dygraphs是一款基于JavaScript的函数库,十分灵活。这款工具的优势是可以处理大的数据集,并为终端用户生成互动数据。
Raw:基于网页的可视化工具。用户可以粘贴数据,在几步内生成图表。
Timeline: 用来做时间轴。按照规定格式将数据放在谷歌表格中,之后Timeline工具自动生成并发布,在网页上嵌入复制的代码即可。
Leaflet:一款轻便、适合移动端用户的JavaScript函数库,用来制作互动地图。
本文由“全球深度报道网”(the Global Investigative Journalism Network)独家奉献。若需转载,请提前联系授权,感谢支持! 查看全部
2015数据可视化精选:35款好用工具,搞定信息图+可视化
数据可视化为新闻媒体提供了生动有效的叙事方式和包装形式,也引领架构故事的思维变革。有哪些实用工具可以帮你讲好故事呢?深度君整合全年干货,为你精选35款优质可视化工具,指南+案例随你选。【点击“阅读原文”的相应链接,即可查看工具案例和更多细节哦】
1Plotly:简易交互式图表制作
数据新闻刚入门,想做简单的交互式数据图?Journalism.co.uk为我们介绍了一款适合数据分析、绘制图表的软件——Plotly,你还可以通过它连接Twitter等社交媒体。
Plotly上面有标示为“探索(Explore)”的按键,可以看到其他用户的数据可视化作品,这样你就能大致了解Plotly的图像成品。参考之后,你就可以在“工作区(Workspace)”绘制自己的图表啦。
一开始,需要点击“import”上传自己的数据文件,或者在添加“新网格(new grid)”后复制黏贴表格。这些数据栏都是自动编码的——如果你想改变它们在可视化作品中的名字,只需右键单击你想当做标题的那行,选择“使用行作为栏目标题(use row as column headers)”。
像infogram这样的软件可以根据数据表里的全部内容自动生成可视化效果,而Plotly与此不同,会另外让你选择哪一行哪一列需要在可视化作品里出现。在设计图表时,你可以在左边栏选择背景和颜色,添加注释、任意移动或伸缩尖头,随之调整文本。当图表完成,Plotly有多个隐私选项供你选择,还能生成嵌入代码,这样就能插入网站啦。更多操作细节,请查看Journalism.co.uk或者Plotly网站上的介绍吧。
2CartoDB: 不会编程,也可轻松学地图可视化
CartoDB是一款开源网络应用程序和交互式地图制作工具,以提供“一键式制图”功能闻名,也就是分析任何你上传的数据、自动制作地图以显示相关信息。起初CartoDB和数据新闻似乎相差千里,是两名西班牙研究生物多样性和自然保护的科学家为了更好地展示研究成果而建立的,没成想已经成为广大数据新闻记者的好帮手。目前CartoDB已经吸引12万用户创制40万张地图,将世界上所有有趣的主题——从全球粉丝对于Beyonce的最新专辑发布的实时反应到尼泊尔地震的损毁情况一一变成互动性强、好玩的可视化作品。
那CartoDB怎么用呢?
1注册账户
用户首先需要在CartoDB上注册账户、获得免费许可,也可以使用它提供的付费模板,有14天的有效期,可随时取消。
注册成功后,你会进入后台控制页面,刚开始是空白一片。如果上传了数据、制作好了地图,页面上会自动显示“你的数据”和“你的地图”选项,点选直接查看即可。
2创制数据表并检查
CartoDB一般以数据库模板为基础开始制图,因此登陆账户后第一步就是上传数据。可上传地理空间数据,如Shapefiles, GeoJSON等,可以设置公开或者个人可见。
用户可以传多少数据呢?仅有免费许可时,最多可上传50MB大小的数据。几乎能涵盖虽有的需求。点击“新数据集(New Dataset)”即可上传string,date,numeric和boolean众多格式。
载入数据之后,点击“连接数据集”。
如果你上传的数据集含有空间信息,就能从表格视图转换成地图视图。

上传数据后,先别急着制图,先检查检查输入的数据有没有差错,数据栏格式对不对。万一格式不统一,可以选择转换格式:
对于拖延症的小伙伴来讲,CartoDB无疑是一款治愈神器。在地图发布之前,CartoDB会提示你赶紧制作地图,而且平时定期邮件“轰炸”,告知新特性、提醒多练手。
制图之前,除了检查数据,更重要的准备是找到思路。CartoDB提供的建议固然能作为宝贵参考,但更重要的是自己先思考数据该怎么用、制作可视化的可行性有多大。先有框架,动手才快,效果才准。
3制作可视化数据上传完毕后,点击页面右上角“可视化(Visualize)”按钮即可制图。
CartoDB在屏幕右边栏贴心准备了一系列地图向导(Wizards),帮你用不同模式标注地图。例如有“简单模式(simple)”、“团簇模式(cluster)”、“等值域模式(choropleth)”(等值域地图是指根据数据的数值大小来标注地理区划的地图)等等。
点选模式后,CartoDB有众多创制图例和信息窗口的选项供你选择,也可以转换同的基础地图,看看哪一个最合适。
如果你按下右边的CSS按钮,就能自己修改可视化代码。如下图所示,可以轻松改变标注颜色,就像编辑文本一样:
4化静态为互动
是时候让地图动起来了!CartoDB的交互式数据库可运行Leaflet.js的插件和Google Maps插件来制作数据层,轻松完成这一过程。(这里还可以参考一个例子,即是依照行政区选择纽约住宅类型的可视化筛选器)【想得到手把手制作地铁动画指南和更多细节?请点击这里查看吧!】
需要更多地图制作工具/平台?深度君还推荐:
提供制图、查图、解图“一条龙”功能的ArcGIS Online Platform ,请查阅:《地图可视化制作和数据平台精选》,链接:
3NodeXL: 社会网络图制作助手
想快速学会如何将复杂的社会网络做成直观炫酷的可视化?NodeXL是你的绝佳选择:会用Excel,就会用它生成网络图。
NodeXL能计算以下这些社会网络研究中的常见指标:
程度(Degree),指每个节点拥有的关系链接数。对于指向型关系网,有内向程度(In-degree)和外向程度(Out-degree)之分,前者是指向内部的关系链接数,后者是指向外部的关系链接数。
特征向量中心性(Eigenvector centrality)所关注的不仅包括某一节点的关系链接数量,还包括与该节点相链接的是哪些节点,以及那些节点各自的关系链接数量是多少。
中介中心性(Betweenness centrality)则揭示出每个节点在不同关系网间提供“桥梁作用”的重要程度。它所特别标出的点,是一些移除后会导致整个网络崩塌的重要节点。
接近中心性(Closeness centrality)用于量度关系网中每个节点离其他节点的平均距离。它特别标出以较少连线与其他节点联系的点——这就类似于Kevin Bacon的六度理论(即在世界上任意两个陌生人之间只隔着六个人)。
在2014年计算机辅助报道协会巴尔的摩年会 (NICAR 2014) 上,BuzzFeed News 的科学与健康记者Peter Aldhous就介绍了NodeXL的用法,手把手教你分析美国共和党人中倾向于民主党的有哪些。请点击“阅读原文”里的相关链接,一起打开Excel跟着学吧!

如果觉得以上工具还不过瘾,我们还推荐:
4Analytics Vidhya:18款可视化好用工具一览
网站Analytics Vidhya整合出了一张【信息图可视化工具清单】,分为适用于一般用途和特定用途的两大类工具。一般用途的工具包括我们耳熟能详的Tableau, D3.js., R语言, Excel, Python等,也有专业人士更了解的Weka(主要用于数据挖掘,可生成简单图表,支持数据挖掘),SAS(用于数据建模),QlikView和QlikSense(用于数据整合和分享)。
在特殊用途工具中,有不少功能强大的选项:Instant Atlas(设计互动式图表,快速传达信息,提供图表和地图模板), WolframAlpha(智能呈现图表,无需配置),Cytoscape(高级分析和模型使用软件,用于社会网络或人际关系网的可视化呈现), NetworkX(是Python语言软件包,可用于生成经典图表、随机图表和综合性网络,Nodes可以包括文本、图片或者XML记录等格式), Flot(专门做线形图和柱状图,支持回调函数(Call back functions),需要jquery的相关知识才能驾驭)和Gephi(嵌入式3D生成器,以深入数据分析验证关系,普遍用于社会网络的可视化制作)。
关于更多信息,可以查看QlikSense的介绍,也可以参照Tableau和QlikView的对比。
此外,网站还推荐了诸如Dygraphs, Chart.js, Raphael, Highcharts等实用的JavaScript libraries作参考。如果你还想持续关注信息图和数据分析的新知,可以订阅Analytics Vidhya的邮件、关注它的Twitter和Facebook主页,掌握行业前沿资讯。【想查阅更多信息图制作信息,请点击“阅读原文”中的相关链接】
5软件开发师8款开源工具分享想知道灵动炫酷的可视化效果怎么做?怎么从零基础开始学?软件开发师、开源支持者Nitishi Tiwari撰文重点推荐了8款数据可视化的开源工具 (点击左边即可看到工具条目),一解众忧。如果觉得不过瘾,点击文末链接,有50款工具推荐。
Datawrapper:由欧洲的新闻学院开发,以便新闻机构做数据可视化作品。该工具基于图形用户界面(GUI),用简单四步即可绘图。
Chart JS:简洁的图表库。在生成图表之前,需要把函数库加进前端代码中,之后可以从函数库的应用程序编程接口(API)加图表,赋值。这款工具适合想要精确调整图表外观的人。它不适合想用现成工具的用户。
Charted:由Medium的产品科学组开发,是最简便的在线表格工具之一。只需粘贴谷歌表格或.csv文件,工具就会抓取数据,生成表格。Charted每三十分钟抓取数据,及时更新。
D3:是数据驱动文件(data-driven documents)的缩写,是一个JavaScript函数库。它使用数据创造并控制在网络浏览器里运行的交互图形,必须嵌入在html网页中,依赖矢量图形(SVG), 层叠式样式表(CSS3)等html的工具展示图形。需要编写代码,更适合掌握程序员技能的数据新闻人。
Dygraphs是一款基于JavaScript的函数库,十分灵活。这款工具的优势是可以处理大的数据集,并为终端用户生成互动数据。
Raw:基于网页的可视化工具。用户可以粘贴数据,在几步内生成图表。
Timeline: 用来做时间轴。按照规定格式将数据放在谷歌表格中,之后Timeline工具自动生成并发布,在网页上嵌入复制的代码即可。
Leaflet:一款轻便、适合移动端用户的JavaScript函数库,用来制作互动地图。
本文由“全球深度报道网”(the Global Investigative Journalism Network)独家奉献。若需转载,请提前联系授权,感谢支持!
网页源代码抓取工具之requests:python的一个web爬虫对象
网站优化 • 优采云 发表了文章 • 0 个评论 • 107 次浏览 • 2022-09-07 20:04
网页源代码抓取工具之requests:python的一个web爬虫对象。之前,因为web开发的规范化,爬虫爬取数据不是必须的。爬取web请求的数据(不包括网页源代码)。为什么要抓取web页面的数据?因为所有的web页面都是一样的。只要是ie浏览器就可以浏览网页。同时,它们的代码是相同的。所以,没有必要抓取。
如果不依赖浏览器,那么利用代理或者beautifulsoup库、lxml库、jquery库等都可以抓取出来。代理模式及实例。代理模式。代理模式需要配置三个东西:url,proxy,https。url是你的服务器回访问应用的地址。这个url其实并不是要寻找一个localhost或者127.0.0.1这样的地址。
实际上,服务器返回的是一个url地址,这个url和你寻找的http源地址在同一个地方。localhost就是所谓的源地址,如果你禁止请求,那么就会把url地址发送给服务器。proxy,代理的模式,让请求不能走socket,而是走get的方式。代理需要配置一些东西,如服务器ip,proxy_host等。
配置好之后,在请求里,都能返回一个通用的http头。https,端口加密技术,传输安全性等。网页页码抓取之requests_in_action看网页网页下的每一行,我们可以用requests_in_action()方法,来设置回调函数。requests_in_action()方法的返回值是response对象。
不过,我们可以传入两个参数,一个是url,一个是proxy_host。一个请求返回一个浏览器缓存相关的值,如none。另一个请求返回一个特定网站的https的值,如。这时的url就不是原来的localhost了。一个网站多个https请求,每个https请求都请求一次所在网站的https相关头,然后在后面对https相关的头加密。
这样所有的请求都设置同一个请求头,代码复用性极好。requests_in_action函数的一种返回值是https_ssl,这个值会显示请求所在网站的真实头的哈希值。https验证相关的东西,因为所有的源地址已经被加密过了,所以要验证。上面的参数,可以随意的加数据或者一些成为https相关值,比如:验证xmlhttprequest.status()的值,是否是https的validation相关,这个默认是auto。
验证sslresponse,true表示使用ssl证书。如果不是,则使用https。如果请求不用https,那么根据url,在post请求中带上一个私钥,请求一般用post方式,私钥可以保证数据安全。requests_action与其他很多的web爬虫对象一样,有一个queryset参数,我们需要知道每个请求要返回什么,这些返回数据如何保存到html文件中。这里的queryset就。 查看全部
网页源代码抓取工具之requests:python的一个web爬虫对象
网页源代码抓取工具之requests:python的一个web爬虫对象。之前,因为web开发的规范化,爬虫爬取数据不是必须的。爬取web请求的数据(不包括网页源代码)。为什么要抓取web页面的数据?因为所有的web页面都是一样的。只要是ie浏览器就可以浏览网页。同时,它们的代码是相同的。所以,没有必要抓取。
如果不依赖浏览器,那么利用代理或者beautifulsoup库、lxml库、jquery库等都可以抓取出来。代理模式及实例。代理模式。代理模式需要配置三个东西:url,proxy,https。url是你的服务器回访问应用的地址。这个url其实并不是要寻找一个localhost或者127.0.0.1这样的地址。

实际上,服务器返回的是一个url地址,这个url和你寻找的http源地址在同一个地方。localhost就是所谓的源地址,如果你禁止请求,那么就会把url地址发送给服务器。proxy,代理的模式,让请求不能走socket,而是走get的方式。代理需要配置一些东西,如服务器ip,proxy_host等。
配置好之后,在请求里,都能返回一个通用的http头。https,端口加密技术,传输安全性等。网页页码抓取之requests_in_action看网页网页下的每一行,我们可以用requests_in_action()方法,来设置回调函数。requests_in_action()方法的返回值是response对象。

不过,我们可以传入两个参数,一个是url,一个是proxy_host。一个请求返回一个浏览器缓存相关的值,如none。另一个请求返回一个特定网站的https的值,如。这时的url就不是原来的localhost了。一个网站多个https请求,每个https请求都请求一次所在网站的https相关头,然后在后面对https相关的头加密。
这样所有的请求都设置同一个请求头,代码复用性极好。requests_in_action函数的一种返回值是https_ssl,这个值会显示请求所在网站的真实头的哈希值。https验证相关的东西,因为所有的源地址已经被加密过了,所以要验证。上面的参数,可以随意的加数据或者一些成为https相关值,比如:验证xmlhttprequest.status()的值,是否是https的validation相关,这个默认是auto。
验证sslresponse,true表示使用ssl证书。如果不是,则使用https。如果请求不用https,那么根据url,在post请求中带上一个私钥,请求一般用post方式,私钥可以保证数据安全。requests_action与其他很多的web爬虫对象一样,有一个queryset参数,我们需要知道每个请求要返回什么,这些返回数据如何保存到html文件中。这里的queryset就。
网页源代码抓取工具实用(一)-一个网页工具类库
网站优化 • 优采云 发表了文章 • 0 个评论 • 143 次浏览 • 2022-09-05 08:02
网页源代码抓取工具实用(一)tinia-一个网页抓取工具类库在我们平时编程的时候,往往会遇到格式化问题,这不仅浪费时间,其实也没办法解决格式化问题。而我今天介绍的tinia就是一个可以从html网页源代码中爬取目标网页内容的工具类库。功能包括:-可以抓取html网页的内容文字,包括html代码框,方便输出在页面中的位置-抓取html格式化的图片文字等-抓取html变量声明以及对html变量声明进行相关数据转换-继承globaljsondatabasehandler类型的方法-包括获取网页url。
<p>注意:该类主要是处理一个可编程网页内容的网页源代码的变量声明及其对应的相关数据类型。使用首先我们需要了解tinia是如何进行输出html代码的。首先我们要打开其所在的目录,可以看到tinia.dll可用名称存在使用tinia程序可以通过分析本页面所有源代码生成一个新的本页面地址来提供抓取所需的内容文字,该页面有两个锚文本url,我们需要对这两个锚文本url进行抓取url添加方法如下: 查看全部
网页源代码抓取工具实用(一)-一个网页工具类库

网页源代码抓取工具实用(一)tinia-一个网页抓取工具类库在我们平时编程的时候,往往会遇到格式化问题,这不仅浪费时间,其实也没办法解决格式化问题。而我今天介绍的tinia就是一个可以从html网页源代码中爬取目标网页内容的工具类库。功能包括:-可以抓取html网页的内容文字,包括html代码框,方便输出在页面中的位置-抓取html格式化的图片文字等-抓取html变量声明以及对html变量声明进行相关数据转换-继承globaljsondatabasehandler类型的方法-包括获取网页url。

<p>注意:该类主要是处理一个可编程网页内容的网页源代码的变量声明及其对应的相关数据类型。使用首先我们需要了解tinia是如何进行输出html代码的。首先我们要打开其所在的目录,可以看到tinia.dll可用名称存在使用tinia程序可以通过分析本页面所有源代码生成一个新的本页面地址来提供抓取所需的内容文字,该页面有两个锚文本url,我们需要对这两个锚文本url进行抓取url添加方法如下:
如何安装网页源代码抓取工具?代码以及爬虫的实战总结
网站优化 • 优采云 发表了文章 • 0 个评论 • 182 次浏览 • 2022-09-02 20:00
网页源代码抓取工具,很多人都知道python以及selenium。但是还有一款免费且非常优秀的抓取工具extractor,一款最基础也是非常全面的抓取工具,而且提供丰富的python版本,功能也非常强大。强大的抓取功能是这个工具的基本功能。因为它提供的是python3的版本,那么如何安装它?详细教程请参考这篇文章python3代码抓取功能非常强大,完整的python爬虫代码以及爬虫的实战总结请参考这篇文章:首先下载安装。
下载好后,用管理员身份运行pip3installextractor。然后运行extractor.cmd。输入命令seleniumdriverseleniumdriver.chromedriver或者seleniumdriverseleniumdriver这里以seleniumdriver.chromedriver为例。
再运行pip3installextractor。pip3installextractor另外,如果你想自己把requests包安装进去,那么pip3installrequests。你也可以通过这篇文章来进行修改,最终,只需要运行命令extractor=extractor(extractor.chrome)即可安装下载好的requests包。
然后通过命令extractor=extractor(extractor.get_scrapy)即可获取你所需要的代码。接下来介绍下对源代码抓取的基本使用。1.开始使用源代码提取函数,如果你不知道该干什么,它也帮你搞定:#需要提取一段话句子,如果要快速查看抓取内容可以使用get_txt,对于爬虫系统,可以使用requests库。
#如果你已经看完上面的代码就开始使用seleniumdriver吧,因为你能够开始抓取代码内容和一些我们不需要代码显示的地方,如图片,图标。#最简单的方法是使用爬虫的自带提取代码库,在我们的requests库里,在提取自己想要内容前自带一个get方法去提取某个网页上的内容,例如刚才那个爬虫中,首先输入的源代码就是{{item.id}}get('');如果你喜欢这个代码库,你也可以修改自己爬虫的抓取代码的实现模块。
#我们需要抓取的内容是{{'id':''}},使用get_txt抓取的是{{id}}这个标识,然后使用一个方法去提取id。如下所示:#使用seleniumdriver抓取,一般做和url相关的内容的抓取,它都是使用chromedriver,其原理是让浏览器加载好页面后,返回给浏览器一个标签,它浏览器再去解析标签,看是否含有内容的url,去和页面中的id匹配,匹配成功,就返回我们要的内容。
<p>#这里chromedriver插件我们选用的是firefox,因为我们用firefox,所以一般它也会帮我们拦截掉web标准中不允许爬虫调用的东西。我们直接使用的是网页的url(id),调用get方法。#因为selenium是基于浏览器的,它当然要爬取页面中的 查看全部
如何安装网页源代码抓取工具?代码以及爬虫的实战总结
网页源代码抓取工具,很多人都知道python以及selenium。但是还有一款免费且非常优秀的抓取工具extractor,一款最基础也是非常全面的抓取工具,而且提供丰富的python版本,功能也非常强大。强大的抓取功能是这个工具的基本功能。因为它提供的是python3的版本,那么如何安装它?详细教程请参考这篇文章python3代码抓取功能非常强大,完整的python爬虫代码以及爬虫的实战总结请参考这篇文章:首先下载安装。
下载好后,用管理员身份运行pip3installextractor。然后运行extractor.cmd。输入命令seleniumdriverseleniumdriver.chromedriver或者seleniumdriverseleniumdriver这里以seleniumdriver.chromedriver为例。

再运行pip3installextractor。pip3installextractor另外,如果你想自己把requests包安装进去,那么pip3installrequests。你也可以通过这篇文章来进行修改,最终,只需要运行命令extractor=extractor(extractor.chrome)即可安装下载好的requests包。
然后通过命令extractor=extractor(extractor.get_scrapy)即可获取你所需要的代码。接下来介绍下对源代码抓取的基本使用。1.开始使用源代码提取函数,如果你不知道该干什么,它也帮你搞定:#需要提取一段话句子,如果要快速查看抓取内容可以使用get_txt,对于爬虫系统,可以使用requests库。

#如果你已经看完上面的代码就开始使用seleniumdriver吧,因为你能够开始抓取代码内容和一些我们不需要代码显示的地方,如图片,图标。#最简单的方法是使用爬虫的自带提取代码库,在我们的requests库里,在提取自己想要内容前自带一个get方法去提取某个网页上的内容,例如刚才那个爬虫中,首先输入的源代码就是{{item.id}}get('');如果你喜欢这个代码库,你也可以修改自己爬虫的抓取代码的实现模块。
#我们需要抓取的内容是{{'id':''}},使用get_txt抓取的是{{id}}这个标识,然后使用一个方法去提取id。如下所示:#使用seleniumdriver抓取,一般做和url相关的内容的抓取,它都是使用chromedriver,其原理是让浏览器加载好页面后,返回给浏览器一个标签,它浏览器再去解析标签,看是否含有内容的url,去和页面中的id匹配,匹配成功,就返回我们要的内容。
<p>#这里chromedriver插件我们选用的是firefox,因为我们用firefox,所以一般它也会帮我们拦截掉web标准中不允许爬虫调用的东西。我们直接使用的是网页的url(id),调用get方法。#因为selenium是基于浏览器的,它当然要爬取页面中的
网页代码编辑工具Dreamweaver 2018 for mac直装版Dw 20
网站优化 • 优采云 发表了文章 • 0 个评论 • 73 次浏览 • 2022-09-02 16:13
Adobe Dreamweaver 2018 for Mac 是由Adobe公司推出的一款非常实用的网页编辑软件。该款软件在功能上为用户们提供的一个相当便捷的web编辑环境,其中用户就算不具备过于专业的web编程基础,也可以通过拖拽控件的方式就可以轻松的完成整体页面的构建工作,当然如果想要设计出复杂的页面,还是要点击技术作为支撑的。而在这个版本中软件又会给我们带来怎样的闪光点呢?就让小编带大家预览几条吧,例如git的支持,有了该功能用户们将能更加方便的对版本做出管理工作;ECMAScript 6支持,代表着Web开发人员现在可以利用 ECMAScript 6的功能使用最新JavaScript语法进行编程等等,当然上面所述的只是软件更新的冰上一角,具体的功能还得用户们自己去探索挖掘。
查看全部
网页代码编辑工具Dreamweaver 2018 for mac直装版Dw 20

Adobe Dreamweaver 2018 for Mac 是由Adobe公司推出的一款非常实用的网页编辑软件。该款软件在功能上为用户们提供的一个相当便捷的web编辑环境,其中用户就算不具备过于专业的web编程基础,也可以通过拖拽控件的方式就可以轻松的完成整体页面的构建工作,当然如果想要设计出复杂的页面,还是要点击技术作为支撑的。而在这个版本中软件又会给我们带来怎样的闪光点呢?就让小编带大家预览几条吧,例如git的支持,有了该功能用户们将能更加方便的对版本做出管理工作;ECMAScript 6支持,代表着Web开发人员现在可以利用 ECMAScript 6的功能使用最新JavaScript语法进行编程等等,当然上面所述的只是软件更新的冰上一角,具体的功能还得用户们自己去探索挖掘。

学术福利弹 | N款文献管理工具,总有一款适合你
网站优化 • 优采云 发表了文章 • 0 个评论 • 83 次浏览 • 2022-08-30 18:19
学术福利弹 | N款文献管理工具,总有一款适合你
原创热爱学术的
重庆大学研究生会
cquyjsh
重庆大学研究生会是重庆大学党委领导下,党委研究生工作部、研究生院、校团委指导下的全校博士研究生、硕士研究生的群众性组织。
发表于
收录于合集
点击上方蓝字关注
作为一名热爱学术的研究生,小编每天要阅读许多篇论文,每篇论文要经历繁琐的检索、下载、阅读的过程;在读了许多文献之后,想去回顾一下以前读过的文章,却发现面对着电脑中各种文献的文件夹,怎么也找不到想要找的文章了。
这时候就需要一个文献管理工具,管理你的成千上百的文献,让你在写论文时很顺手地找到想要的文献。下面小编就来介绍几种常用的文献管理工具:
EndNote
EndNote 是SCI(Thomson Scientific 公司)的官方软件,自身具有强大的功能,是一款集文献检索、文摘及全文管理、文献共享等功能于一身的老牌软件,支持国际期刊的参考文献格式有3776种,写作模板几百种,用户可以方便的使用这些模板和格式,若准备写SCI论文,则有必要使用这款管理软件。
Endnote能直接连接上千个数据库,提高科技文献的检索效率,可以管理数十万条参考文献,不用担心文献太多放不下的问题。EndNote的好用性是有目共睹的,同时在现今的文献管理工具市场上具有较大使用率。
需要注意的是,这款软件是需要付费使用的,且对于学生来说价格较高,但在官网可以免费试用30天,大家可以体验过后再决定是否购买使用。
Citavi
Citavi来自瑞士Swiss Academic Software公司,其定位于“知识组织管理软件”,在欧洲(特别是德语区)被广泛使用。这款软件整合了知识管理、任务计划、PDF全文搜索等科研工作环节,具有强大和全面的功能。
Citavi有全功能免费版(支持每个项目插入不多于100篇文献),具有强大的参考文献编辑功能,可以从各个方面实现对参考文献的编辑需求。
Citavi很大一个特点是支持PDF阅读功能,且可以支持多种批注格式,并能管理、组织批注形成自己的知识库,同时也支持创建文献大纲,形成知识组织管理,更好的加强我们对文献的内容理解和组织构架形成。
Mendeley
Mendeley是一款Elsevier公司旗下的免费文献管理软件,所有人都可以在Mendeley上搜索到世界各地的学术文献,这些学术文件都是由用户自己上传到Mendeley“library”进行编辑管理,这款软件拥有整理组织文献、PDF标记和文件分享、网络备份的强大功能。
这款软件很大优势是强大的PDF识别和搜索功能,支持PDF标记,可以直接在PDF文档中做相应的标记和注释,突出文章中的重点内容。
拥有网页版、WINDOWS版、Mac版、Linux版,ios客户端等多个平台,用户可以根据需求选择不同平台使用。具有跨同步和云备份功能,登陆账号后,在Mendeley中导入过的PDF可实现跨平台同步,方便自己在不同平台使用、查看自己的文献,不需要重复导入。
Readcube
界面简洁漂亮,许多人正是因为它的界面而选择的这款软件。
这款软件功能性比较齐全、平台覆盖度也比较好,具有PDF文件自动识别和强化功能,免去了手工输入的繁琐,并且自带PDF阅读器,可进行内部搜索。但是也有一定的不足,比如系统不是特别稳过于依赖网速、特定项目如云同步功能需要收费等。
对于“颜控”小伙伴不妨可以试试这款软件,它的界面风格一定不会让你失望。
Zotero
是一个开放源代码的文献管理软件,可以协助我们收集、管理及引用研究资源,包括期刊、书籍等各类文献和网页、图片等。
Zotero最早是作为Firefox的插件存在的,但是现在已经发展成了一个独立的版本,但是它仍然和Firefox一样,无论是在windows、Mac还是Linux系统上都是可以跨平台使用的。
Zotero是可以免费使用的,相比于EndNotes来说,Zotero最大的特点是无限级的目录分类,一个目录下可以分为多个子目录,这样管理起来更加方面。另外zotero支持标签功能,为每个文献自动打上标签,使我们管理起文献来更加方便。
NoteEpress
NoteExpress 是一款国产软件,由北京爱琴海软件公司开发的一款专业级别的文献检索与管理系统,其核心功能涵盖“知识采集,管理,应用,挖掘”的知识管理的所有环节。
是国内专业的文献检索与管理系统,对中文文献非常友好,具有很强大的管理中文文献功能,可以实现内部搜索知网的文献库,进行批量下载,这款软件导入文献资料的速度也比国外同类软件更快。
文献资料与笔记(文章)功能协调一致,除管理参考文献资料外,还可以管理硬盘上其他文章或文件,作为个人知识管理系统。
对于中文文献较多的小伙伴不妨可以试试这款软件,说不定会爱上这款软件。
知网研学(原E-study)
是来自最大中文期刊资源中国知网官方版的研学平台软件,常用功能包括文献检索与识别、标记与添加笔记、生成参考文献、云同步等,并且可以在线写作,为受众提供与CNKI数据库密切结合的全新数字化体验。
CNKI E-study除了常规的文献管理软件的功能外,其最大的优势是与中国知网数据库同源、可以实现数据的云管理,通过同一账号在其他平台登陆可以实现数据更新,同时它能更好的和中国知网搜索引擎相结合,使得在数据管理上更加方便于受众。
另外这也是一款免费的软件,使用起来也比较方便,在市场使用率上占了不少的份额,如果大家的文献大多来源于知网的话,不妨可以考虑考虑这款软件。
Papers
是一款专业的文献管理工具,具有文档搜索、阅读和引用的功能。Papers把文献管理有关的各项活动流畅的组织在一起,主要具体功能包括文献导入、组织、阅读(注释)、自动匹配参考条目、搜索、在文档中插入引用、评点交流等。
Papers一大特色就是可以从PDF原文抓取文献信息,这样有利于更加清晰、自动化的管理文献,自动将文献进行分类,简洁明了。
Papers内部即可打开PDF文件,具有相当强大的文献阅读功能,最突出的特点是标注和笔记功能强大,有利于之后的学习和研究。
Papers是MAC系统独有的,如果电脑是MAC系统的小伙伴不妨可以尝试下这款软件。
通过以上对各软件的简单介绍
想必大家都有了一定的了解
可以选择更加适合自己的软件进行深入了解哟
小编接下来就详细介绍
其中四款软件的
基本使用方法和注意事项
Zotero
(1)文献导入
文献导入的方法有多种,包括网页抓取、抓取PDF元数据、手动输入等,小编在这里给大家介绍一下网页抓取和PDF元数据抓取的文献导入方法。
第一种是网页抓取的方法,首先需要安装叫Zotero Connector的插件,这个插件是安装在浏览器中的,在不同浏览器上搜索到的Zotero Connector插件直接安装即可安装在此浏览器中。Zotero对于英文文献的搜索是有着很好的支持的,如果想使用Google scholar、Jstor、Muse等国际主流的文献搜索引擎,只需要在相应的浏览器上下载好Zotero的插件,就可以直接在网页上进行文献抓取。
另外Zotero对中文文献也有不错的支持,下面以Firefox浏览器为例说明国内网页抓取的具体方法:
知网:Zotero可以有效的识别中国知网,大家有保存中文文献的需求可以采用这种方式(网页右上角Z字形图标)。
豆瓣:Zotero相比其他的文献管理工具来说有一个优势是可以识别豆瓣,豆瓣虽然严格来说不算一个学术网站,不过相信一个特定专业的小伙伴有时会用豆瓣进行一些书评活动。
维基百科:维基百科大家也用的非常多,Zotero也可以识别维基百科,让我们更方便的去管理我们的软件。
抓取PDF元数据的方法是指我们电脑里本身已经存在了下载好的PDF文件,需要添加到Zotero中。这时候我们只需要拖动PDF文件到Zotero中,点击右键选择:“重新抓取PDF元数据”,Zotero会根据PDF的文件信息自动进行网络搜索将文献信息补充完整,不过目前CNKI的文件不包含”元数据“,因此在CNKI中下载好的文献不能采用这种方法进行导入,但国际上几大著名的学术搜索引擎均支持这一功能。
(2)Zot-file插件
Zotero的很大一个特点是有许多的插件,我们可以根据自身需求选择不同插件,方便我们更好管理文献。其中一个很重要的插件为Zot-file插件,功能性很实用,几乎人手必备。这款插件功能很多,能够方便我们将需要的目录和文件本身链接在一起,达到有效管理PDF的目的,具体的Zot-file插件的下载和使用方法可以在网上查看相关的教程。
NoteEpress
(1)导入文献
NoteEpress可以在官方直接免费下载,下载好之后进行安装就可以导入文件了。
在导入文献前可以新建一个文件夹,将重要文献放到文件夹里,方便以后的管理。
右键点击文件夹,选择“导入文件“,把自己想要管理的文献添加到NoteEpress中,选择题录类型。
添加完成后即可对文献题录进行编辑,可以一边对照文献,一边完成表格中的内容,表格中具体完成哪些内容,可以根据自身的需求确定。
(2)文献引用
在写论文时,需要引用一些已经管理好的文献。在NoteEpress中点击要引用的文献,切换到word中,鼠标点击需要插入文献的位置,因为在下载NoteEpress完成后,插件会自动嵌入到word中(如在word中没发现相关的插件,可以在网上寻找教程),这时候只需要点击word上方的工具栏中“选择引用”即可。
Mendeley
(1)文献搜索
Mendeley有Literature Search可以搜索文献。但有一点请注意,除非使用的是Mendeley Premium版本,否则使用内置的文献搜索无法提供要查找的文章的全文。
(2)阅读界面
Mendeley提供了注释、高光、缩放等PDF阅读工具,右侧面板有文献的基本信息。
(3)引用格式切换
Mendeley的引用跟EndNote一样还是在Word里面进行插入。但Mendeley能够让用户轻松地切换引用格式,只需简单地点击style旁边的下拉框,然后选择想要的格式。如果想要的格式都不在选项中,只需点击More Styles。
图来自知乎用户@ EditSprings学术编辑
这时会有一个窗口弹出,可以下载更多的引用格式,通过点击 Get more Styles来获取更多引用风格和输入想要的格式并将其安装到Mendeley中。
图来自知乎用户@ EditSprings学术编辑
Papers 3
(1)文献搜索及导入
Papers 3有一个内置搜索引擎,其整合了大部分文献数据库,也支持各种字段的搜索。已下载的PDF文档也可以直接导入到Papers 3内,并对下载的文档进行自定义重命名。导入的文档,Papers 3会自动识别其文献信息,但是对早期一些图片PDF文档,其可能无法识别。就算识别不出来或者识别不全,还可以通过 Match 功能利用内置搜索引擎补全文献信息。
如果喜欢在浏览器直接在 Google、Google Scholar或者各个数据库搜索,也可以通过浏览器 Bookmarklet导入文献信息。在平常浏览专业杂志主页时,看到感兴趣的文献,可以用Chrome插件一键加入到Papers 3的Reading List里面以备不时之需。
(2)文献归类、管理
Papers 3 通过收藏集(Collections)来归类不同的文献信息,除此之外它还支持智能收藏集(Smart Collection),设置一定的规则,导入的文献信息就自动进入到这个收藏集里。Papers 3同时内置了功能足够的 PDF 阅读器,标注、做笔记、文字标签、颜色标签一应俱全,这一点跟PDF Expert 很相似。此外,Papers 3 支持 PDF 全文和笔记标注的搜索,在写文章的时候想起某句话、某个观点但就是想不起哪篇文献的时候就很有用了,一搜便知。
(3)文献引用
不同于 EndNote或者 Mendeley用 Word插件的方式,在写作的时候Papers 3通过快捷键唤起插入文献的小窗口,简单的搜索就可以快速插入想要引用的文献。引文格式当然是可以修改的,支持大多数主流期刊样式。
看完了这么多介绍
你是否找到属于你的文献管理工具呢?
或者你曾使用哪款软件
不妨在留言区给大家安利安利
说说你的看法~~ 查看全部
学术福利弹 | N款文献管理工具,总有一款适合你
学术福利弹 | N款文献管理工具,总有一款适合你
原创热爱学术的
重庆大学研究生会
cquyjsh
重庆大学研究生会是重庆大学党委领导下,党委研究生工作部、研究生院、校团委指导下的全校博士研究生、硕士研究生的群众性组织。
发表于
收录于合集
点击上方蓝字关注
作为一名热爱学术的研究生,小编每天要阅读许多篇论文,每篇论文要经历繁琐的检索、下载、阅读的过程;在读了许多文献之后,想去回顾一下以前读过的文章,却发现面对着电脑中各种文献的文件夹,怎么也找不到想要找的文章了。
这时候就需要一个文献管理工具,管理你的成千上百的文献,让你在写论文时很顺手地找到想要的文献。下面小编就来介绍几种常用的文献管理工具:
EndNote
EndNote 是SCI(Thomson Scientific 公司)的官方软件,自身具有强大的功能,是一款集文献检索、文摘及全文管理、文献共享等功能于一身的老牌软件,支持国际期刊的参考文献格式有3776种,写作模板几百种,用户可以方便的使用这些模板和格式,若准备写SCI论文,则有必要使用这款管理软件。
Endnote能直接连接上千个数据库,提高科技文献的检索效率,可以管理数十万条参考文献,不用担心文献太多放不下的问题。EndNote的好用性是有目共睹的,同时在现今的文献管理工具市场上具有较大使用率。
需要注意的是,这款软件是需要付费使用的,且对于学生来说价格较高,但在官网可以免费试用30天,大家可以体验过后再决定是否购买使用。
Citavi
Citavi来自瑞士Swiss Academic Software公司,其定位于“知识组织管理软件”,在欧洲(特别是德语区)被广泛使用。这款软件整合了知识管理、任务计划、PDF全文搜索等科研工作环节,具有强大和全面的功能。
Citavi有全功能免费版(支持每个项目插入不多于100篇文献),具有强大的参考文献编辑功能,可以从各个方面实现对参考文献的编辑需求。
Citavi很大一个特点是支持PDF阅读功能,且可以支持多种批注格式,并能管理、组织批注形成自己的知识库,同时也支持创建文献大纲,形成知识组织管理,更好的加强我们对文献的内容理解和组织构架形成。
Mendeley
Mendeley是一款Elsevier公司旗下的免费文献管理软件,所有人都可以在Mendeley上搜索到世界各地的学术文献,这些学术文件都是由用户自己上传到Mendeley“library”进行编辑管理,这款软件拥有整理组织文献、PDF标记和文件分享、网络备份的强大功能。
这款软件很大优势是强大的PDF识别和搜索功能,支持PDF标记,可以直接在PDF文档中做相应的标记和注释,突出文章中的重点内容。
拥有网页版、WINDOWS版、Mac版、Linux版,ios客户端等多个平台,用户可以根据需求选择不同平台使用。具有跨同步和云备份功能,登陆账号后,在Mendeley中导入过的PDF可实现跨平台同步,方便自己在不同平台使用、查看自己的文献,不需要重复导入。
Readcube
界面简洁漂亮,许多人正是因为它的界面而选择的这款软件。
这款软件功能性比较齐全、平台覆盖度也比较好,具有PDF文件自动识别和强化功能,免去了手工输入的繁琐,并且自带PDF阅读器,可进行内部搜索。但是也有一定的不足,比如系统不是特别稳过于依赖网速、特定项目如云同步功能需要收费等。
对于“颜控”小伙伴不妨可以试试这款软件,它的界面风格一定不会让你失望。
Zotero
是一个开放源代码的文献管理软件,可以协助我们收集、管理及引用研究资源,包括期刊、书籍等各类文献和网页、图片等。
Zotero最早是作为Firefox的插件存在的,但是现在已经发展成了一个独立的版本,但是它仍然和Firefox一样,无论是在windows、Mac还是Linux系统上都是可以跨平台使用的。
Zotero是可以免费使用的,相比于EndNotes来说,Zotero最大的特点是无限级的目录分类,一个目录下可以分为多个子目录,这样管理起来更加方面。另外zotero支持标签功能,为每个文献自动打上标签,使我们管理起文献来更加方便。
NoteEpress
NoteExpress 是一款国产软件,由北京爱琴海软件公司开发的一款专业级别的文献检索与管理系统,其核心功能涵盖“知识采集,管理,应用,挖掘”的知识管理的所有环节。

是国内专业的文献检索与管理系统,对中文文献非常友好,具有很强大的管理中文文献功能,可以实现内部搜索知网的文献库,进行批量下载,这款软件导入文献资料的速度也比国外同类软件更快。
文献资料与笔记(文章)功能协调一致,除管理参考文献资料外,还可以管理硬盘上其他文章或文件,作为个人知识管理系统。
对于中文文献较多的小伙伴不妨可以试试这款软件,说不定会爱上这款软件。
知网研学(原E-study)
是来自最大中文期刊资源中国知网官方版的研学平台软件,常用功能包括文献检索与识别、标记与添加笔记、生成参考文献、云同步等,并且可以在线写作,为受众提供与CNKI数据库密切结合的全新数字化体验。
CNKI E-study除了常规的文献管理软件的功能外,其最大的优势是与中国知网数据库同源、可以实现数据的云管理,通过同一账号在其他平台登陆可以实现数据更新,同时它能更好的和中国知网搜索引擎相结合,使得在数据管理上更加方便于受众。
另外这也是一款免费的软件,使用起来也比较方便,在市场使用率上占了不少的份额,如果大家的文献大多来源于知网的话,不妨可以考虑考虑这款软件。
Papers
是一款专业的文献管理工具,具有文档搜索、阅读和引用的功能。Papers把文献管理有关的各项活动流畅的组织在一起,主要具体功能包括文献导入、组织、阅读(注释)、自动匹配参考条目、搜索、在文档中插入引用、评点交流等。
Papers一大特色就是可以从PDF原文抓取文献信息,这样有利于更加清晰、自动化的管理文献,自动将文献进行分类,简洁明了。
Papers内部即可打开PDF文件,具有相当强大的文献阅读功能,最突出的特点是标注和笔记功能强大,有利于之后的学习和研究。
Papers是MAC系统独有的,如果电脑是MAC系统的小伙伴不妨可以尝试下这款软件。
通过以上对各软件的简单介绍
想必大家都有了一定的了解
可以选择更加适合自己的软件进行深入了解哟
小编接下来就详细介绍
其中四款软件的
基本使用方法和注意事项
Zotero
(1)文献导入
文献导入的方法有多种,包括网页抓取、抓取PDF元数据、手动输入等,小编在这里给大家介绍一下网页抓取和PDF元数据抓取的文献导入方法。
第一种是网页抓取的方法,首先需要安装叫Zotero Connector的插件,这个插件是安装在浏览器中的,在不同浏览器上搜索到的Zotero Connector插件直接安装即可安装在此浏览器中。Zotero对于英文文献的搜索是有着很好的支持的,如果想使用Google scholar、Jstor、Muse等国际主流的文献搜索引擎,只需要在相应的浏览器上下载好Zotero的插件,就可以直接在网页上进行文献抓取。
另外Zotero对中文文献也有不错的支持,下面以Firefox浏览器为例说明国内网页抓取的具体方法:
知网:Zotero可以有效的识别中国知网,大家有保存中文文献的需求可以采用这种方式(网页右上角Z字形图标)。
豆瓣:Zotero相比其他的文献管理工具来说有一个优势是可以识别豆瓣,豆瓣虽然严格来说不算一个学术网站,不过相信一个特定专业的小伙伴有时会用豆瓣进行一些书评活动。
维基百科:维基百科大家也用的非常多,Zotero也可以识别维基百科,让我们更方便的去管理我们的软件。
抓取PDF元数据的方法是指我们电脑里本身已经存在了下载好的PDF文件,需要添加到Zotero中。这时候我们只需要拖动PDF文件到Zotero中,点击右键选择:“重新抓取PDF元数据”,Zotero会根据PDF的文件信息自动进行网络搜索将文献信息补充完整,不过目前CNKI的文件不包含”元数据“,因此在CNKI中下载好的文献不能采用这种方法进行导入,但国际上几大著名的学术搜索引擎均支持这一功能。
(2)Zot-file插件
Zotero的很大一个特点是有许多的插件,我们可以根据自身需求选择不同插件,方便我们更好管理文献。其中一个很重要的插件为Zot-file插件,功能性很实用,几乎人手必备。这款插件功能很多,能够方便我们将需要的目录和文件本身链接在一起,达到有效管理PDF的目的,具体的Zot-file插件的下载和使用方法可以在网上查看相关的教程。
NoteEpress
(1)导入文献

NoteEpress可以在官方直接免费下载,下载好之后进行安装就可以导入文件了。
在导入文献前可以新建一个文件夹,将重要文献放到文件夹里,方便以后的管理。
右键点击文件夹,选择“导入文件“,把自己想要管理的文献添加到NoteEpress中,选择题录类型。
添加完成后即可对文献题录进行编辑,可以一边对照文献,一边完成表格中的内容,表格中具体完成哪些内容,可以根据自身的需求确定。
(2)文献引用
在写论文时,需要引用一些已经管理好的文献。在NoteEpress中点击要引用的文献,切换到word中,鼠标点击需要插入文献的位置,因为在下载NoteEpress完成后,插件会自动嵌入到word中(如在word中没发现相关的插件,可以在网上寻找教程),这时候只需要点击word上方的工具栏中“选择引用”即可。
Mendeley
(1)文献搜索
Mendeley有Literature Search可以搜索文献。但有一点请注意,除非使用的是Mendeley Premium版本,否则使用内置的文献搜索无法提供要查找的文章的全文。
(2)阅读界面
Mendeley提供了注释、高光、缩放等PDF阅读工具,右侧面板有文献的基本信息。
(3)引用格式切换
Mendeley的引用跟EndNote一样还是在Word里面进行插入。但Mendeley能够让用户轻松地切换引用格式,只需简单地点击style旁边的下拉框,然后选择想要的格式。如果想要的格式都不在选项中,只需点击More Styles。
图来自知乎用户@ EditSprings学术编辑
这时会有一个窗口弹出,可以下载更多的引用格式,通过点击 Get more Styles来获取更多引用风格和输入想要的格式并将其安装到Mendeley中。
图来自知乎用户@ EditSprings学术编辑
Papers 3
(1)文献搜索及导入
Papers 3有一个内置搜索引擎,其整合了大部分文献数据库,也支持各种字段的搜索。已下载的PDF文档也可以直接导入到Papers 3内,并对下载的文档进行自定义重命名。导入的文档,Papers 3会自动识别其文献信息,但是对早期一些图片PDF文档,其可能无法识别。就算识别不出来或者识别不全,还可以通过 Match 功能利用内置搜索引擎补全文献信息。
如果喜欢在浏览器直接在 Google、Google Scholar或者各个数据库搜索,也可以通过浏览器 Bookmarklet导入文献信息。在平常浏览专业杂志主页时,看到感兴趣的文献,可以用Chrome插件一键加入到Papers 3的Reading List里面以备不时之需。
(2)文献归类、管理
Papers 3 通过收藏集(Collections)来归类不同的文献信息,除此之外它还支持智能收藏集(Smart Collection),设置一定的规则,导入的文献信息就自动进入到这个收藏集里。Papers 3同时内置了功能足够的 PDF 阅读器,标注、做笔记、文字标签、颜色标签一应俱全,这一点跟PDF Expert 很相似。此外,Papers 3 支持 PDF 全文和笔记标注的搜索,在写文章的时候想起某句话、某个观点但就是想不起哪篇文献的时候就很有用了,一搜便知。
(3)文献引用
不同于 EndNote或者 Mendeley用 Word插件的方式,在写作的时候Papers 3通过快捷键唤起插入文献的小窗口,简单的搜索就可以快速插入想要引用的文献。引文格式当然是可以修改的,支持大多数主流期刊样式。
看完了这么多介绍
你是否找到属于你的文献管理工具呢?
或者你曾使用哪款软件
不妨在留言区给大家安利安利
说说你的看法~~
无代码爬虫,看完就会:你不需要会爬虫,你只要会爬虫软件就好了(二)
网站优化 • 优采云 发表了文章 • 0 个评论 • 71 次浏览 • 2022-07-19 03:28
上期我们说到了无代码爬虫软件优采云采集器的模板使用,也留了一个实际工作中遇到的问题没有解决,今天我们就来一起看看解决思路吧~
(戳链接回顾上期)
让我们回顾一下上期的问题:批量点击一系列相似网页的某一个按钮。
这个同学要下载的是各个市政府的报告。他是这样做的:
江苏省有13个市、98个区,每个区有五个报告要下载。
那就是要点击98*5=490次,刷新切换98次页面……点是不可能点的。
打开网站链接,按F12,观察网页源代码:
我知道有朋友看到这里已经头大了,心里已经在骂看不懂别演示了。
学习嘛,你不痛苦这几分钟,那就等着痛苦手动做吧。
这里先补充一个小背景知识:
网页是由代码写的,我们访问看到的图片、文字,都是网站管理员早就写好的代码,因此,打开任一网页,你看到的东西都是代码呈现的。我们要做的就是把代码给扒下来。
任意浏览器,按F12都会调出开发者调试工具,点控制台选项,不需要看懂啥,只用点调出来的页面的那个长得像“鼠标”的按钮。
鼠标滑动到网页上我们原本需要人工点击的位置,点击。右边的窗口就自动帮我们定位到了相应的代码位置。
就是它!搞它!盘它!就是它害得我们有这么多工作!
理论上来说,在网页页面上点击链接和点击代码窗口的链接,效果会是一样的,但是这次我们点了没有反应,那一定是哪里出了问题,别着急。我先去点击原页面看看是咋回事。
刚刚动画里演示的虽然很快,但是我们能看到,在点击下载按钮后,先是有一个页面弹了出来。幸好我眼疾手快把这个页面给截了下来。长这样:
有没有发现什么!
虽然我看不懂代码,但是在网页源代码那张图里,那个标红了的超链接,好像是上图网址里面的uuid!
大胆猜测,是不是只要我能够拿到所有市区的uuid,然后把网址链接补充完整,那就能实现批量下载报告了?!
说干就干啊!
不要着急~让我们来复习一下今天学的~
1
网页都是由代码写的,按F12可以查看源代码。
2
需要抓取的内容在源代码里,按F12上像个小箭头一样的标识就可以定位了。
3
网页地址似乎有一种特殊的结构,有规律可循。
下期预告
abxshdhsdicidsihciohivhi占位占位
如何利用优采云自定义任务
如何批量访问一系列链接
……
快来一起学习~! 查看全部
无代码爬虫,看完就会:你不需要会爬虫,你只要会爬虫软件就好了(二)
上期我们说到了无代码爬虫软件优采云采集器的模板使用,也留了一个实际工作中遇到的问题没有解决,今天我们就来一起看看解决思路吧~
(戳链接回顾上期)
让我们回顾一下上期的问题:批量点击一系列相似网页的某一个按钮。
这个同学要下载的是各个市政府的报告。他是这样做的:
江苏省有13个市、98个区,每个区有五个报告要下载。
那就是要点击98*5=490次,刷新切换98次页面……点是不可能点的。
打开网站链接,按F12,观察网页源代码:
我知道有朋友看到这里已经头大了,心里已经在骂看不懂别演示了。
学习嘛,你不痛苦这几分钟,那就等着痛苦手动做吧。
这里先补充一个小背景知识:

网页是由代码写的,我们访问看到的图片、文字,都是网站管理员早就写好的代码,因此,打开任一网页,你看到的东西都是代码呈现的。我们要做的就是把代码给扒下来。
任意浏览器,按F12都会调出开发者调试工具,点控制台选项,不需要看懂啥,只用点调出来的页面的那个长得像“鼠标”的按钮。
鼠标滑动到网页上我们原本需要人工点击的位置,点击。右边的窗口就自动帮我们定位到了相应的代码位置。
就是它!搞它!盘它!就是它害得我们有这么多工作!
理论上来说,在网页页面上点击链接和点击代码窗口的链接,效果会是一样的,但是这次我们点了没有反应,那一定是哪里出了问题,别着急。我先去点击原页面看看是咋回事。
刚刚动画里演示的虽然很快,但是我们能看到,在点击下载按钮后,先是有一个页面弹了出来。幸好我眼疾手快把这个页面给截了下来。长这样:
有没有发现什么!
虽然我看不懂代码,但是在网页源代码那张图里,那个标红了的超链接,好像是上图网址里面的uuid!
大胆猜测,是不是只要我能够拿到所有市区的uuid,然后把网址链接补充完整,那就能实现批量下载报告了?!
说干就干啊!
不要着急~让我们来复习一下今天学的~

1
网页都是由代码写的,按F12可以查看源代码。
2
需要抓取的内容在源代码里,按F12上像个小箭头一样的标识就可以定位了。
3
网页地址似乎有一种特殊的结构,有规律可循。
下期预告
abxshdhsdicidsihciohivhi占位占位
如何利用优采云自定义任务
如何批量访问一系列链接
……
快来一起学习~!
隐藏新闻广告前阵子看手机视频被恶意植入广告
网站优化 • 优采云 发表了文章 • 0 个评论 • 58 次浏览 • 2022-07-10 10:01
网页源代码抓取工具专门支持各种网站并提供实时抓取效果:unblockyoukudock加速中心idacast,中国科技网新闻源抓取工具--隐藏新闻广告前阵子看手机视频被恶意植入广告,从此对浏览器广告青睐有加,douyinmedialibraryindouyinmedialibrary-majority-freeavailabledirectlyforjavascriptads.手机视频app不能免流,于是玩了ncsoftsafariautoplayer。
装完safarispacecornercapture后,发现新闻视频之类的可以自动加速:用过mediapipe等工具发现都不太友好,官方网站有一个可用按钮:chromeautoplay:onlinechromegamesforwindowsandmacnewsstand编辑器推荐官方的livecopyvideo工具evernote中的播放列表及手机输入法的弹幕功能youtube视频助手-youtubeampisanaccessoriesdeviceworkingwithyourolderpcandmobilephones视频工具dokuwikibookbox视频加速工具vlc插件vlcintroduction解码工具nekopara网页缓存助手ffplayer解码器-ie8.0的浏览器兼容性工具nvidiayoutubetagsystemtagmessagecodebingplayernestedscript解码器ffplayer-tvwebstreamingupdateplayertagsystemcodebingcodebingcodebingfreematrixobjectgametagtagsforchinesestandstone-opengamesforchineseinstrumentalstreamingupdateplayerpocket快捷输入工具youdao解码器-youdaouiportabletracking,speedtracking,calculator,limits,inspections,storeandconverter-tamer密码管理工具密码管理器火狐extensionjsbrowser,比如小贝idfapijsbrowser-youdao只是浏览器账号登录就能对他进行第三方应用或者在浏览器上下载游戏等等,也可以集成googlechrome应用商店,体验类似safari中的chrome应用程序enableextensionkeyboardintegrationandnewkeyboardextensionsplayersmtp二维码生成器官方新出的二维码生成器,手机端浏览速度很快可以生成flash的密码浏览器,还支持抓取外部网页的二维码我的推荐者fythobzid25没说起来的/这里上官网看一下:。 查看全部
隐藏新闻广告前阵子看手机视频被恶意植入广告

网页源代码抓取工具专门支持各种网站并提供实时抓取效果:unblockyoukudock加速中心idacast,中国科技网新闻源抓取工具--隐藏新闻广告前阵子看手机视频被恶意植入广告,从此对浏览器广告青睐有加,douyinmedialibraryindouyinmedialibrary-majority-freeavailabledirectlyforjavascriptads.手机视频app不能免流,于是玩了ncsoftsafariautoplayer。

装完safarispacecornercapture后,发现新闻视频之类的可以自动加速:用过mediapipe等工具发现都不太友好,官方网站有一个可用按钮:chromeautoplay:onlinechromegamesforwindowsandmacnewsstand编辑器推荐官方的livecopyvideo工具evernote中的播放列表及手机输入法的弹幕功能youtube视频助手-youtubeampisanaccessoriesdeviceworkingwithyourolderpcandmobilephones视频工具dokuwikibookbox视频加速工具vlc插件vlcintroduction解码工具nekopara网页缓存助手ffplayer解码器-ie8.0的浏览器兼容性工具nvidiayoutubetagsystemtagmessagecodebingplayernestedscript解码器ffplayer-tvwebstreamingupdateplayertagsystemcodebingcodebingcodebingfreematrixobjectgametagtagsforchinesestandstone-opengamesforchineseinstrumentalstreamingupdateplayerpocket快捷输入工具youdao解码器-youdaouiportabletracking,speedtracking,calculator,limits,inspections,storeandconverter-tamer密码管理工具密码管理器火狐extensionjsbrowser,比如小贝idfapijsbrowser-youdao只是浏览器账号登录就能对他进行第三方应用或者在浏览器上下载游戏等等,也可以集成googlechrome应用商店,体验类似safari中的chrome应用程序enableextensionkeyboardintegrationandnewkeyboardextensionsplayersmtp二维码生成器官方新出的二维码生成器,手机端浏览速度很快可以生成flash的密码浏览器,还支持抓取外部网页的二维码我的推荐者fythobzid25没说起来的/这里上官网看一下:。
Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 68 次浏览 • 2022-07-09 08:33
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获 查看全部
Node.js 实现抢票小工具&短信通知提醒
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。

我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj

let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获
专业网站优化公司网站优化软件,旺道网站优化软件?
网站优化 • 优采云 发表了文章 • 0 个评论 • 71 次浏览 • 2022-06-25 08:53
加:sum932,备注: 霸屏,免费领取100种推广获客方法。
就在昨天我们操作万词霸屏的网站,使用新方法,站长平台百度权重升级至7:
专业网站优化公司
专业网站优化公司
使用新方法后,网站流量也从原来每天3000多IP,直接提升到每天30000多IP,流量直接提升了10倍。
专业网站优化公司
使用新方法关键词排名数量也增加至12.69万个:
seo软件优化工具软件,为什么要用seo软件优化工具软件,因为seo软件优化工具软件集可以快速让网站收录以及关键词排名,今天给大家分享一款万能的SEO工具多个功能集合。一键建站+内容以及资源采集+伪原创+主动推送给搜索引擎收录以及各种内容处理等下会以图片的形式给大家展示。大家注意看图。
假设你想应用你中文网站他们的文原本捕捉和留住运用者,这种他们就能透过阅读器来找到他们想的东西,所以你必需有好的文本和运用者体验。假设好的外部链接能进步公交站点的 pr 值和预览速度,所以提供高效率的文本是让运用者留下来,这种阅读器蜘蛛就能Murviel更快。
特别是随着许多“伪创译者”该文的呈现,互联网平台上的许多文本高度反复,优质创译者该文稀缺。因而,假设你的中文网站有非常好的创译者文本,并且预览频率很高,所以它能更好地被阅读器收录下来。一段时间后,你的中文网站会被阅读器辨认为有用的中文网站,接着权重会逐步增加,从而增加中文网站在搜索结果中的高度关注度。
2、统计数据发掘
中文网站SEO还有这种两个企业方面产业开展也是一种非常具有重要,那是中文网站的统计数据技术预测,只要引见了小学生他们学习中文网站的技术细节,才可能去更精确的抓住强化的方向,做到补漏伯粉。这个时分,中文网站的笔记管理工作是由于他们要运用的两个国度非常重要的辅助工具了,透过重要信息计算机网络笔记他们能对中文网站的意向展开深居简出的引见。
同时,还需要对各品种类型的相关统计数据展开预测,如 cnss 统计预测、腾讯名列词大小和24小时腾讯收录于等。那些统计数据不只能让你发现中文网站上的难题,还能让你发现名列规则的变化,并快速修正以处理那些难题。
关于新手朋友来说,会经常听他人提及到网站的三大标签,其实网站的三大标签就是title、keywords、description。当我们在搜索引擎中搜索一个关键词的时分,在搜索结果页中我们首先看到的就是网站的标题和描绘,标题就是标签中的title,描绘就是标签中的description,keywords需求在源代码里面才干看到。下面我们一同来细致地理解一下这三大标签。
网站三大标签
1、title标签。
Title标签是网站的重中之重,从用户的角度来看,用户搜索关键词之后,首先看到的就是你网站的标题,这个时分就要看你的标题能否能处理用户的需求,能否能吸收用户点击了。
从搜索引擎的角度来看,蜘蛛来到我们网站的时分,首先会找到网站的title标签,从上到下看代码,从上到下抓取,看title标签的时分,能够晓得整个页面的主题是什么。优化是以title为准的,是整个网站的中心,在搜索引擎中占有重要位置,是参与排名的重中之重。
2、keywords标签。
很多站长在keywords中堆砌关键词,因而搜索引擎曾经不是很注重了,搜索引擎蜘蛛是能够抓取的keywords,但是关于关键词排名没有任何提升的协助,没有任何权重上的协助,它不参与排名。那么能否就不需求设置keywords标签了呢?设置Keywords标签是为了页面的完好水平,还是有间接参与排名的要素的,也能够让我们在运用工具查询关键词排名的时分,更便当看排在第几位。
3、description标签。
搜索引擎蜘蛛也是可以抓取的,也是不参与排名的,但是写好描绘也是很重要的,当我们在搜索结果页中寻觅我们需求的东西的时分,在看完标题之后,更多的会是去看描绘,若你的描绘写得有吸收力,那么就会吸收到用户点击。在前20名当中,若描绘写得好,吸收了用户的点击量,排名也就被推进了。假如不写描绘,那么搜索引擎就会从你的内容中调用一段来做描绘,因而还是写比拟好一些。 查看全部
专业网站优化公司网站优化软件,旺道网站优化软件?
加:sum932,备注: 霸屏,免费领取100种推广获客方法。
就在昨天我们操作万词霸屏的网站,使用新方法,站长平台百度权重升级至7:
专业网站优化公司
专业网站优化公司
使用新方法后,网站流量也从原来每天3000多IP,直接提升到每天30000多IP,流量直接提升了10倍。
专业网站优化公司
使用新方法关键词排名数量也增加至12.69万个:
seo软件优化工具软件,为什么要用seo软件优化工具软件,因为seo软件优化工具软件集可以快速让网站收录以及关键词排名,今天给大家分享一款万能的SEO工具多个功能集合。一键建站+内容以及资源采集+伪原创+主动推送给搜索引擎收录以及各种内容处理等下会以图片的形式给大家展示。大家注意看图。
假设你想应用你中文网站他们的文原本捕捉和留住运用者,这种他们就能透过阅读器来找到他们想的东西,所以你必需有好的文本和运用者体验。假设好的外部链接能进步公交站点的 pr 值和预览速度,所以提供高效率的文本是让运用者留下来,这种阅读器蜘蛛就能Murviel更快。
特别是随着许多“伪创译者”该文的呈现,互联网平台上的许多文本高度反复,优质创译者该文稀缺。因而,假设你的中文网站有非常好的创译者文本,并且预览频率很高,所以它能更好地被阅读器收录下来。一段时间后,你的中文网站会被阅读器辨认为有用的中文网站,接着权重会逐步增加,从而增加中文网站在搜索结果中的高度关注度。
2、统计数据发掘
中文网站SEO还有这种两个企业方面产业开展也是一种非常具有重要,那是中文网站的统计数据技术预测,只要引见了小学生他们学习中文网站的技术细节,才可能去更精确的抓住强化的方向,做到补漏伯粉。这个时分,中文网站的笔记管理工作是由于他们要运用的两个国度非常重要的辅助工具了,透过重要信息计算机网络笔记他们能对中文网站的意向展开深居简出的引见。
同时,还需要对各品种类型的相关统计数据展开预测,如 cnss 统计预测、腾讯名列词大小和24小时腾讯收录于等。那些统计数据不只能让你发现中文网站上的难题,还能让你发现名列规则的变化,并快速修正以处理那些难题。
关于新手朋友来说,会经常听他人提及到网站的三大标签,其实网站的三大标签就是title、keywords、description。当我们在搜索引擎中搜索一个关键词的时分,在搜索结果页中我们首先看到的就是网站的标题和描绘,标题就是标签中的title,描绘就是标签中的description,keywords需求在源代码里面才干看到。下面我们一同来细致地理解一下这三大标签。
网站三大标签
1、title标签。
Title标签是网站的重中之重,从用户的角度来看,用户搜索关键词之后,首先看到的就是你网站的标题,这个时分就要看你的标题能否能处理用户的需求,能否能吸收用户点击了。
从搜索引擎的角度来看,蜘蛛来到我们网站的时分,首先会找到网站的title标签,从上到下看代码,从上到下抓取,看title标签的时分,能够晓得整个页面的主题是什么。优化是以title为准的,是整个网站的中心,在搜索引擎中占有重要位置,是参与排名的重中之重。
2、keywords标签。
很多站长在keywords中堆砌关键词,因而搜索引擎曾经不是很注重了,搜索引擎蜘蛛是能够抓取的keywords,但是关于关键词排名没有任何提升的协助,没有任何权重上的协助,它不参与排名。那么能否就不需求设置keywords标签了呢?设置Keywords标签是为了页面的完好水平,还是有间接参与排名的要素的,也能够让我们在运用工具查询关键词排名的时分,更便当看排在第几位。
3、description标签。
搜索引擎蜘蛛也是可以抓取的,也是不参与排名的,但是写好描绘也是很重要的,当我们在搜索结果页中寻觅我们需求的东西的时分,在看完标题之后,更多的会是去看描绘,若你的描绘写得有吸收力,那么就会吸收到用户点击。在前20名当中,若描绘写得好,吸收了用户的点击量,排名也就被推进了。假如不写描绘,那么搜索引擎就会从你的内容中调用一段来做描绘,因而还是写比拟好一些。
(实战)Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 51 次浏览 • 2022-06-25 08:35
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/ProxySetting看下,哦原来我之前设置成了8888
然后找到Charles的help/LocalIPAddress,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开Moretools/Networkconditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOSProxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为breakpoints,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
<p>这里为日期
如果有余票则显示余票数量</p>
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
<p>cnpm install axios --save</p>
定时任务node-schedule
<p>cnpm install node-schedule --save</p>
node端选择dom节点工具cheerio
<p>cnpm install cheerio --save</p>
腾讯发短信的依赖包qcloudsms_js
<p>cnpm install qcloudsms_js</p>
热更新包,诺豆的妈妈,nodemon(其实不用也可以)
<p>cnpm install nodemon --save-dev</p>
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<p>const axios = require('axios')
const querystring = require("querystring"); //序列化对象,用qs也行,都一样
let QcloudSms = require("qcloudsms_js");
let cheerio = require('cheerio');
let schedule = require('node-schedule');</p>
然后我们先定义请求参数,来一个obj
<p>let obj = {
data: {
lineId: 111130, //路线id
vehTime: 0722, //发车时间,
startTime: 0751, //预计上车时间
onStationId: 564492, //预定的站点id
offStationId: 17990,//到站id
onStationName: '宝安交通运输局③', //预定的站点名称
offStationName: "深港产学研基地",//预定到站名称
tradePrice: 0,//总金额
saleDates: '17',//车票日期
beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据
},
phoneNumber: 123123123, //用户手机号,接收短信的手机号
cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie
day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票
}</p>
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票的操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
<p>class QueryTicket{
/**
*Creates an instance of QueryTicket.
* @param {Object} { data, phoneNumber, cookie, day }
* @param data {Object} 请求余票接口的requery参数
* @param phoneNumber {Number} 用户手机号,短信需要用到
* @param cookie {String} cookie信息
* @params day {String} 某日的票,如'18'
* @memberof QueryTicket 请求余票接口
*/
constructor({ data, phoneNumber, cookie, day }) {
this.data = data
this.cookie = cookie
this.day = day
this.phoneNumber = phoneNumber
this.postData = querystring.stringify(data)
this.times = 0; //记录次数
let stop = false //通过特定接口才能修改stop值,防止外部随意串改
this.getStop = function () { //获取是否停止
return stop
}
this.setStop = function (ifStop) { //设置是否停止
stop = ifStop
}
}
}</p>
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
init(){}//初始化
handleQueryTicket(){}//查询余票的逻辑
requestTicket(){} //调用查询余票接口
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化,因为涉及到异步请求,所以我们使用`async await`
async init(){
let ticketList = await this.handleQueryTicket() //返回查询到的余票数组
}
//查询余票的逻辑
handleQueryTicket(){
let ticketList = [] //余票数组
let res = await this.requestTicket()
this.times++ //计数器,记录请求查询多少次
let str = res.data.replace(/\\/g, "") //格式化返回值
let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据
let list = $(".main").find(".b") //查找是否有余票的dom节点
// 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码
if (!list.length) {
console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)
return
}
<br />
// 如果有余票
list.each((idx, item) => {
let str = $(item).html() //str这时格式是21&$x4F59;0
//最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已
//因此要在下一步对其进行格式化
let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true)
let data = {
day: arr[0],
ticketLeft: arr[1]
}
<br />
//如果是要抢指定日期的票
if (this.day) {
//如果有指定日期的余票
if (parseInt(data.day) === parseInt(data.day)) {
ticketList.push(data)
}
} else {
//如果不是,则返回查询到的所有余票
ticketList.push(data)
}
})
return ticketList
}
//调用查询余票接口
requestTicket(){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
let year = new Date().getFullYear() //年份,
let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号
let {
onStationName,//起始站点名
offStationName,//结束站点名
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站台id
offStationId //到站的站台id
} = this.data // 初始化的数据
<br />
let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"
let dateStr = ""; //车票日期
let tickAmount = "" //总张数
ticketList.forEach(item => {
dateStr = dateStr + `${year}-${month}-${item.day},`
tickAmount = tickAmount + `${item.ticketLeft}张,`
})
<br />
let buyTicket = {
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站点id
offStationId,//目标站点id
tradePrice: '5', //金额
saleDates: dateStr.slice(0, -1),
payType: '2' //支付方式,微信支付
}
<br />
// 调用购票接口
let data = querystring.stringify(buyTicket)
let res = await this.requestOrder(data) //返回json数据,是否购票成功等等
//把发短信所需要数据都要传入
return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })
}//购票相关逻辑
//调用购票接口
requestOrder(obj){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
//执行通知逻辑
this.handleInfoUser(resParse)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
//购票代码...
}
//调用购票接口
requestOrder(obj){
//购票接口请求代码...
}
//通知用户的逻辑
async handleInfoUser(parseData){
//获取上一步购票的response数据和我们拼接的数据
let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData
//如果购票成功,则返回500
if (returnCode === "500") {
let res = await this.sendMsg({
dateStr, //日期
tickAmount: tickAmount.slice(0, -1), //总张数
station, //站点
lineName, //巴士名称/路线名称
tradePrice,//总价
startTime,//出发时间
phoneNumber: this.phoneNumber,//手机号
})
//如果发信成功,则不再进行抢票操作
if (res.result === 0 && res.errmsg === "OK") {
this.setStop(true)
} else {
//失败不做任何操作
console.log(res.errmsg)
}
} else {
//失败不做任何操作
console.log(resParse['returnInfo'])
}
}
//发短信接口
sendMSg(){
let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
let appid = 140034324; // SDK AppID 以1400开头
// 短信应用 SDK AppKey
let appkey = "asdfdsvajwienin23493nadsnzxc";
// 短信模板 ID,需要在短信控制台中申请
let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
// 签名
let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请
// 实例化 QcloudSms
let qcloudsms = QcloudSms(appid, appkey);
let ssender = qcloudsms.SmsSingleSender();
// 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容
let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];
//用promise来封装下异步操作
return new Promise((resolve, reject) => {
ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {
if (err) {
reject(err)
} else {
resolve(resData)
}
});
})
}
}</p>
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
<p>// 定时任务
class SetInter {
constructor({ timer, fn }) {
this.timer = timer // 每几秒执行
this.fn = fn //执行的回调
this.rule = new schedule.RecurrenceRule(); //实例化一个对象
this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已
this.init()
}
setRule() {
let rule = [];
let i = 1;
while (i {
this.fn() // 定时调用传入的回调方法
});
}
}</p>
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
<p> data: { //用户1
lineId: 111130,
vehTime: 0722,
startTime: 0751,
onStationId: 564492,
offStationId: 17990,
onStationName: '宝安交通运输局③',
offStationName: "深港产学研基地",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 123123123,
cookie: 'JSESSIONID=TESTCOOKIE',
day: "17"
}
let obj2 = { //用户2
data: {
lineId: 134423,
vehTime: 1820,
startTime: 1855,
onStationId: 4322,
offStationId: 53231,
onStationName: '百度国际大厦',
offStationName: "裕安路口",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 175932123124,
cookie: 'JSESSIONID=TESTCOOKIE',
day: ""
}
let ticket = new QueryTicket(obj) //用户1
let ticket2 = new QueryTicket(obj2) //用户2
<br />
new SetInter({
timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图
fn: function () {
[ticket,ticket2].map(item => { //同时进行两个用户的抢票
if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢
item.init()
} else { // 如果抢到票了,则不继续抢票
console.log('stop')
}
})
}
})</p>
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
- END -
<p style="letter-spacing: 0.544px;text-align: right;">分享前端好文,点亮 <strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;">在看</strong><strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;"> </strong></p> 查看全部
(实战)Node.js 实现抢票小工具&短信通知提醒
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/ProxySetting看下,哦原来我之前设置成了8888
然后找到Charles的help/LocalIPAddress,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开Moretools/Networkconditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOSProxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为breakpoints,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
<p>这里为日期
如果有余票则显示余票数量</p>
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
<p>cnpm install axios --save</p>
定时任务node-schedule
<p>cnpm install node-schedule --save</p>
node端选择dom节点工具cheerio
<p>cnpm install cheerio --save</p>
腾讯发短信的依赖包qcloudsms_js
<p>cnpm install qcloudsms_js</p>
热更新包,诺豆的妈妈,nodemon(其实不用也可以)
<p>cnpm install nodemon --save-dev</p>
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<p>const axios = require('axios')
const querystring = require("querystring"); //序列化对象,用qs也行,都一样
let QcloudSms = require("qcloudsms_js");
let cheerio = require('cheerio');
let schedule = require('node-schedule');</p>
然后我们先定义请求参数,来一个obj
<p>let obj = {
data: {
lineId: 111130, //路线id
vehTime: 0722, //发车时间,
startTime: 0751, //预计上车时间
onStationId: 564492, //预定的站点id
offStationId: 17990,//到站id
onStationName: '宝安交通运输局③', //预定的站点名称
offStationName: "深港产学研基地",//预定到站名称
tradePrice: 0,//总金额
saleDates: '17',//车票日期
beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据
},
phoneNumber: 123123123, //用户手机号,接收短信的手机号
cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie
day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票
}</p>
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票的操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
<p>class QueryTicket{
/**
*Creates an instance of QueryTicket.
* @param {Object} { data, phoneNumber, cookie, day }
* @param data {Object} 请求余票接口的requery参数
* @param phoneNumber {Number} 用户手机号,短信需要用到
* @param cookie {String} cookie信息
* @params day {String} 某日的票,如'18'
* @memberof QueryTicket 请求余票接口
*/
constructor({ data, phoneNumber, cookie, day }) {
this.data = data
this.cookie = cookie
this.day = day
this.phoneNumber = phoneNumber
this.postData = querystring.stringify(data)
this.times = 0; //记录次数
let stop = false //通过特定接口才能修改stop值,防止外部随意串改
this.getStop = function () { //获取是否停止
return stop
}
this.setStop = function (ifStop) { //设置是否停止
stop = ifStop
}
}
}</p>
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
init(){}//初始化
handleQueryTicket(){}//查询余票的逻辑
requestTicket(){} //调用查询余票接口
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化,因为涉及到异步请求,所以我们使用`async await`
async init(){
let ticketList = await this.handleQueryTicket() //返回查询到的余票数组
}
//查询余票的逻辑
handleQueryTicket(){
let ticketList = [] //余票数组
let res = await this.requestTicket()
this.times++ //计数器,记录请求查询多少次
let str = res.data.replace(/\\/g, "") //格式化返回值
let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据
let list = $(".main").find(".b") //查找是否有余票的dom节点
// 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码
if (!list.length) {
console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)
return
}
<br />
// 如果有余票
list.each((idx, item) => {
let str = $(item).html() //str这时格式是21&$x4F59;0
//最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已
//因此要在下一步对其进行格式化
let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true)
let data = {
day: arr[0],
ticketLeft: arr[1]
}
<br />
//如果是要抢指定日期的票
if (this.day) {
//如果有指定日期的余票
if (parseInt(data.day) === parseInt(data.day)) {
ticketList.push(data)
}
} else {
//如果不是,则返回查询到的所有余票
ticketList.push(data)
}
})
return ticketList
}
//调用查询余票接口
requestTicket(){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
let year = new Date().getFullYear() //年份,
let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号
let {
onStationName,//起始站点名
offStationName,//结束站点名
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站台id
offStationId //到站的站台id
} = this.data // 初始化的数据
<br />
let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"
let dateStr = ""; //车票日期
let tickAmount = "" //总张数
ticketList.forEach(item => {
dateStr = dateStr + `${year}-${month}-${item.day},`
tickAmount = tickAmount + `${item.ticketLeft}张,`
})
<br />
let buyTicket = {
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站点id
offStationId,//目标站点id
tradePrice: '5', //金额
saleDates: dateStr.slice(0, -1),
payType: '2' //支付方式,微信支付
}
<br />
// 调用购票接口
let data = querystring.stringify(buyTicket)
let res = await this.requestOrder(data) //返回json数据,是否购票成功等等
//把发短信所需要数据都要传入
return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })
}//购票相关逻辑
//调用购票接口
requestOrder(obj){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
//执行通知逻辑
this.handleInfoUser(resParse)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
//购票代码...
}
//调用购票接口
requestOrder(obj){
//购票接口请求代码...
}
//通知用户的逻辑
async handleInfoUser(parseData){
//获取上一步购票的response数据和我们拼接的数据
let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData
//如果购票成功,则返回500
if (returnCode === "500") {
let res = await this.sendMsg({
dateStr, //日期
tickAmount: tickAmount.slice(0, -1), //总张数
station, //站点
lineName, //巴士名称/路线名称
tradePrice,//总价
startTime,//出发时间
phoneNumber: this.phoneNumber,//手机号
})
//如果发信成功,则不再进行抢票操作
if (res.result === 0 && res.errmsg === "OK") {
this.setStop(true)
} else {
//失败不做任何操作
console.log(res.errmsg)
}
} else {
//失败不做任何操作
console.log(resParse['returnInfo'])
}
}
//发短信接口
sendMSg(){
let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
let appid = 140034324; // SDK AppID 以1400开头
// 短信应用 SDK AppKey
let appkey = "asdfdsvajwienin23493nadsnzxc";
// 短信模板 ID,需要在短信控制台中申请
let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
// 签名
let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请
// 实例化 QcloudSms
let qcloudsms = QcloudSms(appid, appkey);
let ssender = qcloudsms.SmsSingleSender();
// 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容
let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];
//用promise来封装下异步操作
return new Promise((resolve, reject) => {
ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {
if (err) {
reject(err)
} else {
resolve(resData)
}
});
})
}
}</p>
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
<p>// 定时任务
class SetInter {
constructor({ timer, fn }) {
this.timer = timer // 每几秒执行
this.fn = fn //执行的回调
this.rule = new schedule.RecurrenceRule(); //实例化一个对象
this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已
this.init()
}
setRule() {
let rule = [];
let i = 1;
while (i {
this.fn() // 定时调用传入的回调方法
});
}
}</p>
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
<p> data: { //用户1
lineId: 111130,
vehTime: 0722,
startTime: 0751,
onStationId: 564492,
offStationId: 17990,
onStationName: '宝安交通运输局③',
offStationName: "深港产学研基地",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 123123123,
cookie: 'JSESSIONID=TESTCOOKIE',
day: "17"
}
let obj2 = { //用户2
data: {
lineId: 134423,
vehTime: 1820,
startTime: 1855,
onStationId: 4322,
offStationId: 53231,
onStationName: '百度国际大厦',
offStationName: "裕安路口",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 175932123124,
cookie: 'JSESSIONID=TESTCOOKIE',
day: ""
}
let ticket = new QueryTicket(obj) //用户1
let ticket2 = new QueryTicket(obj2) //用户2
<br />
new SetInter({
timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图
fn: function () {
[ticket,ticket2].map(item => { //同时进行两个用户的抢票
if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢
item.init()
} else { // 如果抢到票了,则不继续抢票
console.log('stop')
}
})
}
})</p>
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
- END -
<p style="letter-spacing: 0.544px;text-align: right;">分享前端好文,点亮 <strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;">在看</strong><strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;"> </strong></p>
不会写代码,如何抓取网页里的信息?
网站优化 • 优采云 发表了文章 • 0 个评论 • 61 次浏览 • 2022-06-19 03:02
竞价QQ交流群:850434915
信息流QQ交流群:1070089747
当我们在网站上看到一系列地址信息、商品信息甚至天气、新闻等真实信息,但因为数量庞大难以依靠手动复制黏贴来完整获取时,爬虫就能代替你完成所有工作。
——“如何向完全没有背景知识的人解释爬虫为何物?”
——“爬虫就是按一定规则替你浏览网页并复制黏贴东西下来的程序。”
是的,听起来很很高级,是不是要写代码啊?!网上一搜什么Python+Scrapy啊,功能强大到爆。
但即使这样对于某些结构简单且只用于江湖救急的网站数据获取未免显得有些小题大做了,而且普通用户很可能折在装python+scrapy包这件事上。
WebScraper登场
这时候Chrome的一款爬虫插件就脱颖而出了!()名字就叫Web Scraper,web既可以指网络爬虫也能说明在线爬虫的意思,一语双关(也可能是我想多了……)
如何安装插件此处略过,并且网站上有非常友好的introduce video,总之,安装好以后在Chrome下按F12就可以开始
举一个椰子
话不多说,来看椰子!哦不,栗子!
带大家来爬一下天猫上「椰青」的价格吧!
1、打开页面
先来看一下我们感兴趣的「椰青及价格」页面
没错!是我感兴趣的椰青!
2、喊出爬虫页面
于是我们按提示打开在线爬虫的界面
最右侧的Web Scraper tag就是我们之前安装的爬虫插件,从现在开始我们就要为爬虫建立一个复制黏贴数据的规则,以防获得一些不该获得的不想要的数据。
3、建立规则
前面说过,爬虫是替你浏览网页并复制黏贴东西下来的东西,那么它就应该模拟你的行为。首先你打开这个界面,知道了这个网页是“我想要的数据起点”,那么对于爬虫来说,这就是他的root。所以我们来新建一个爬虫并告诉他:
我们点击Create new sitemap来创建一个爬虫并给它起个名字~顺便告诉它起点(当前浏览器里的网址)。之后我们就会进入这个爬虫(taobao)的根目录下:
4、选择元素
然后我们开始获取每一个商品的集合,单击add new selector,新增一个筛选器,选出所有的「椰青商品」元素:
同样的取个名字,选择type为element,选择商品元素,当选择2个相同属性元素时插件会自动勾选上页面中所有该属性元素。
点击done selecting完成选择,并勾上multiple。Save selector!
此时我们只需要从之前筛选出来的item元素中获取需要的字段就可以啦。同样的我们在item目录下新建一些selector,由于需要获得的是文本信息,所以type需要变为text。
此时一个简易的单页爬虫就做好了,在sitemap的下拉菜单中还可以选择graph来查看爬虫的结构。
5、点击Scrape开始爬
6、下载数据
结束后数据会自动生成在视窗中,插件自带了导出为CSV的功能,可以一键下载。不小心关了也没关系,browse中可以看到上一次抓取的数据。
翻页怎么办?
如果要翻页的话就会困难一些,大概给个思路:正如item中的element会被遍历获取,那么同样的在root目录下新建一个翻页的link selector来实现「下一页」功能。
将item链接到link selector下,并且将link selector和之前创建的item selector链接到自己来实现一个死循环知道下一页不存在或者下一页unable
循环建立好以后就可以成了下面这个样子:
So What?
你可能要问:So What?
我用这个工具抓了瓜子二手车全国几百台在售的二手宝马3系的价格,看一下不同车龄的宝马3系轿车在使用了若干年后的价格跌幅吧~
如果本文对你有帮助或启发,也请分享给你身边的人。记得顺手点赞哦,感谢。
- END -
查看全部
不会写代码,如何抓取网页里的信息?
竞价QQ交流群:850434915
信息流QQ交流群:1070089747
当我们在网站上看到一系列地址信息、商品信息甚至天气、新闻等真实信息,但因为数量庞大难以依靠手动复制黏贴来完整获取时,爬虫就能代替你完成所有工作。
——“如何向完全没有背景知识的人解释爬虫为何物?”
——“爬虫就是按一定规则替你浏览网页并复制黏贴东西下来的程序。”
是的,听起来很很高级,是不是要写代码啊?!网上一搜什么Python+Scrapy啊,功能强大到爆。
但即使这样对于某些结构简单且只用于江湖救急的网站数据获取未免显得有些小题大做了,而且普通用户很可能折在装python+scrapy包这件事上。
WebScraper登场
这时候Chrome的一款爬虫插件就脱颖而出了!()名字就叫Web Scraper,web既可以指网络爬虫也能说明在线爬虫的意思,一语双关(也可能是我想多了……)
如何安装插件此处略过,并且网站上有非常友好的introduce video,总之,安装好以后在Chrome下按F12就可以开始
举一个椰子
话不多说,来看椰子!哦不,栗子!
带大家来爬一下天猫上「椰青」的价格吧!
1、打开页面
先来看一下我们感兴趣的「椰青及价格」页面
没错!是我感兴趣的椰青!
2、喊出爬虫页面
于是我们按提示打开在线爬虫的界面
最右侧的Web Scraper tag就是我们之前安装的爬虫插件,从现在开始我们就要为爬虫建立一个复制黏贴数据的规则,以防获得一些不该获得的不想要的数据。
3、建立规则
前面说过,爬虫是替你浏览网页并复制黏贴东西下来的东西,那么它就应该模拟你的行为。首先你打开这个界面,知道了这个网页是“我想要的数据起点”,那么对于爬虫来说,这就是他的root。所以我们来新建一个爬虫并告诉他:
我们点击Create new sitemap来创建一个爬虫并给它起个名字~顺便告诉它起点(当前浏览器里的网址)。之后我们就会进入这个爬虫(taobao)的根目录下:
4、选择元素
然后我们开始获取每一个商品的集合,单击add new selector,新增一个筛选器,选出所有的「椰青商品」元素:
同样的取个名字,选择type为element,选择商品元素,当选择2个相同属性元素时插件会自动勾选上页面中所有该属性元素。
点击done selecting完成选择,并勾上multiple。Save selector!
此时我们只需要从之前筛选出来的item元素中获取需要的字段就可以啦。同样的我们在item目录下新建一些selector,由于需要获得的是文本信息,所以type需要变为text。
此时一个简易的单页爬虫就做好了,在sitemap的下拉菜单中还可以选择graph来查看爬虫的结构。
5、点击Scrape开始爬
6、下载数据
结束后数据会自动生成在视窗中,插件自带了导出为CSV的功能,可以一键下载。不小心关了也没关系,browse中可以看到上一次抓取的数据。
翻页怎么办?
如果要翻页的话就会困难一些,大概给个思路:正如item中的element会被遍历获取,那么同样的在root目录下新建一个翻页的link selector来实现「下一页」功能。
将item链接到link selector下,并且将link selector和之前创建的item selector链接到自己来实现一个死循环知道下一页不存在或者下一页unable
循环建立好以后就可以成了下面这个样子:
So What?
你可能要问:So What?
我用这个工具抓了瓜子二手车全国几百台在售的二手宝马3系的价格,看一下不同车龄的宝马3系轿车在使用了若干年后的价格跌幅吧~
如果本文对你有帮助或启发,也请分享给你身边的人。记得顺手点赞哦,感谢。
- END -
爬虫干货 | 网页源码解析模块介绍及实战
网站优化 • 优采云 发表了文章 • 0 个评论 • 87 次浏览 • 2022-06-19 03:00
皮卡丘联合爬虫业界大牛FastGets整理,介绍python爬虫基础知识、大牛多年反爬经验,有难度网站抓取实战、爬虫系统架构设计、安卓app逆向分析。帮助小白入门,初级工程师成为资深工程师,大神回顾。大家有任何问题可留言交流,欢迎转发分享。
上一篇主要介绍了爬虫抓取的模块,抓取下来的文本内容主要是json字符串形式或html源码形式,前者解析比较方便,直接使用python3的json模块的loads方法,即可将json字符串形式转变成python常用的列表或字典形式;后者比较麻烦,所以本文主要介绍后者的解析。这里主要介绍xpath和正则表达式来解析html源码并提取所需信息。
一、xpath介绍
xpath是XML路径语言,它是一种用来确定XML文档中某部分位置的语言。xpath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力。这里主要使用实战例子来介绍笔者在爬虫源码解析中的几种常用方法。
1. 首先我们使用lxml模块将html源码字符串转换成HtmlElement对象的树形结构。Python 标准库中自带的 xml 模块与第三方库BeautifulSoup,性能都不够好,而且缺乏一些人性化的 api,相比之下,第三方库lxml,速度比较快,对用户比较友好。代码如下:
2. 下面我们用xpath的路径表达式来定位树形结构上的元素,提取我们所需要的信息。
(1) 定位html标签节点
(2) 定位到标签的属性
(3) 定位到标签里面的文本
(4) 根据特定的属性选择节点
(5) 获取节点的父亲节点
(6) 获取节点的后续同级节点
(7) 节点对象一些常用的方法
3. 下面我们来解析36kr新闻详情页面的数据
(1) 在Chrome里面安装xpath helper插件,在工具栏,更多工具,扩展程序,里面搜索xpath helper,然后安装。
(2) 下面我们来看看这篇新闻的标题的xpath。
打开页面后,点击红色箭头标记处的小图标,就会出现xpath helper插件,我们在xpath helper里面写xpath,右边就会展示我们xpath路径的结果,下面我们运行代码看看结果。
可以看到,输出的结果是空列表,但是我们的xpath又没有写错,怎么会这样呢?出现这种原因,很大可能是html源码与浏览器的开发者工具里面显示的不一样,因为浏览器加载源码会执行一些js代码,致使它们不一样。这个时候,我们点击显示网页源码,我们从源码里面查找不到class='mobile_article'。但是笔者发现我们所需要的数据在props变量的值里面,这是一个json字符串,如下图。
我们可以用正则表达式,将json字符串提取出来,然后用json模块来解析,代码如下。
以上是从爬虫解析实战中介绍了xpath的基础用法,如果要系统的学习可以查看以下链接的文章
二、正则表达式介绍
正则表达式简单来讲就是字符串规则,用这个规则,我们从html源码字符串里面提取出符合我们规则的信息,python中主要使用re模块来做正则表达式。爬虫中使用的正则表达式并不复杂,都是一些比较简单的。由于我们篇幅有限,我们就在实战中介绍一些常用的。
笔者经常使用re.findall(pattern, string)方法来提取string中的所需要的信息,pattern即是正则表达式。如下示例:
以下是常用正则表达式字符集
字符
说明
\w
匹配字母数字及下划线
\W
匹配非字母数字及下划线
\s
匹配任意空白字符,等价于 [\t\n\r\f]
\S
匹配任意非空字符
\d
匹配任意数字,等价于 [0-9]
\D
匹配任意非数字
\n
匹配一个换行符
\t
匹配一个制表符
^
匹配字符串的开头
$
匹配字符串的末尾
.
匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...]
用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...]
不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符
*
匹配0个或多个的表达式
+
匹配1个或多个的表达式
匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
{n}
精确匹配n个前面表达式
{n, m}
匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b
匹配a或b
( )
匹配括号内的表达式,也表示一个组
熟练掌握正则表达式是每一个程序员必须具备的技能。python正则表达式详细介绍可以看这篇文章
三、爬虫抓取解析实战
抓取虎嗅网()新闻,分析虎嗅网的网页结构可知,虎嗅网的新闻资讯分为不同的类别,我们分别抓取这些类别,然后再进入类别下面的新闻列表页进行抓取,如下图。
点击电商消费类别,出现电商消费类的新闻列表,如下图。
抓取思路:我们先从虎嗅网首页抓取新闻资讯类别,然后进入每个类别的列表页抓取新闻列表,然后再抓取新闻的详情页。笔者提供如下代码:
以后每篇文章的代码都可以在github的CrawlArticles仓库查看:
喜欢的话,点个赞呗!
查看全部
爬虫干货 | 网页源码解析模块介绍及实战
皮卡丘联合爬虫业界大牛FastGets整理,介绍python爬虫基础知识、大牛多年反爬经验,有难度网站抓取实战、爬虫系统架构设计、安卓app逆向分析。帮助小白入门,初级工程师成为资深工程师,大神回顾。大家有任何问题可留言交流,欢迎转发分享。
上一篇主要介绍了爬虫抓取的模块,抓取下来的文本内容主要是json字符串形式或html源码形式,前者解析比较方便,直接使用python3的json模块的loads方法,即可将json字符串形式转变成python常用的列表或字典形式;后者比较麻烦,所以本文主要介绍后者的解析。这里主要介绍xpath和正则表达式来解析html源码并提取所需信息。
一、xpath介绍
xpath是XML路径语言,它是一种用来确定XML文档中某部分位置的语言。xpath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力。这里主要使用实战例子来介绍笔者在爬虫源码解析中的几种常用方法。
1. 首先我们使用lxml模块将html源码字符串转换成HtmlElement对象的树形结构。Python 标准库中自带的 xml 模块与第三方库BeautifulSoup,性能都不够好,而且缺乏一些人性化的 api,相比之下,第三方库lxml,速度比较快,对用户比较友好。代码如下:
2. 下面我们用xpath的路径表达式来定位树形结构上的元素,提取我们所需要的信息。
(1) 定位html标签节点
(2) 定位到标签的属性
(3) 定位到标签里面的文本
(4) 根据特定的属性选择节点
(5) 获取节点的父亲节点
(6) 获取节点的后续同级节点
(7) 节点对象一些常用的方法
3. 下面我们来解析36kr新闻详情页面的数据
(1) 在Chrome里面安装xpath helper插件,在工具栏,更多工具,扩展程序,里面搜索xpath helper,然后安装。
(2) 下面我们来看看这篇新闻的标题的xpath。
打开页面后,点击红色箭头标记处的小图标,就会出现xpath helper插件,我们在xpath helper里面写xpath,右边就会展示我们xpath路径的结果,下面我们运行代码看看结果。
可以看到,输出的结果是空列表,但是我们的xpath又没有写错,怎么会这样呢?出现这种原因,很大可能是html源码与浏览器的开发者工具里面显示的不一样,因为浏览器加载源码会执行一些js代码,致使它们不一样。这个时候,我们点击显示网页源码,我们从源码里面查找不到class='mobile_article'。但是笔者发现我们所需要的数据在props变量的值里面,这是一个json字符串,如下图。
我们可以用正则表达式,将json字符串提取出来,然后用json模块来解析,代码如下。
以上是从爬虫解析实战中介绍了xpath的基础用法,如果要系统的学习可以查看以下链接的文章
二、正则表达式介绍
正则表达式简单来讲就是字符串规则,用这个规则,我们从html源码字符串里面提取出符合我们规则的信息,python中主要使用re模块来做正则表达式。爬虫中使用的正则表达式并不复杂,都是一些比较简单的。由于我们篇幅有限,我们就在实战中介绍一些常用的。
笔者经常使用re.findall(pattern, string)方法来提取string中的所需要的信息,pattern即是正则表达式。如下示例:
以下是常用正则表达式字符集
字符
说明
\w
匹配字母数字及下划线
\W
匹配非字母数字及下划线
\s
匹配任意空白字符,等价于 [\t\n\r\f]
\S
匹配任意非空字符
\d
匹配任意数字,等价于 [0-9]
\D
匹配任意非数字
\n
匹配一个换行符
\t
匹配一个制表符
^
匹配字符串的开头
$
匹配字符串的末尾
.
匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...]
用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...]
不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符
*
匹配0个或多个的表达式
+
匹配1个或多个的表达式
匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
{n}
精确匹配n个前面表达式
{n, m}
匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b
匹配a或b
( )
匹配括号内的表达式,也表示一个组
熟练掌握正则表达式是每一个程序员必须具备的技能。python正则表达式详细介绍可以看这篇文章
三、爬虫抓取解析实战
抓取虎嗅网()新闻,分析虎嗅网的网页结构可知,虎嗅网的新闻资讯分为不同的类别,我们分别抓取这些类别,然后再进入类别下面的新闻列表页进行抓取,如下图。
点击电商消费类别,出现电商消费类的新闻列表,如下图。
抓取思路:我们先从虎嗅网首页抓取新闻资讯类别,然后进入每个类别的列表页抓取新闻列表,然后再抓取新闻的详情页。笔者提供如下代码:
以后每篇文章的代码都可以在github的CrawlArticles仓库查看:
喜欢的话,点个赞呗!
Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 50 次浏览 • 2022-06-19 00:29
在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获 查看全部
Node.js 实现抢票小工具&短信通知提醒
在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获
Node.js 实现抢票小工具&amp;短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 55 次浏览 • 2022-06-19 00:24
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获 查看全部
Node.js 实现抢票小工具&amp;短信通知提醒
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获
(实战)Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 79 次浏览 • 2022-06-17 16:46
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【xxxxxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口设置代理手机上网依次执行上面两步获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【xxxx】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容: 整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址cookie信息各自的request参数字段user-Agent信息各自的response返回内容设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
**小方块的结构为:**<br />
<br />这里为日期<br />如果有余票则显示余票数量<br /><br /><br />
td的样式名为a代表不可选样式名为e代表已满样式名为d代表已购样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口定时请求任务有余票则自动请求购票接口下订单调用腾讯云短信api接口发送短信通知多个用户抢票功能抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。 下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的`http.request`,我这里选择用的是`axios`,毕竟`axios`在node端底层也是调用`http.request`<br />
cnpm install axios --save<br /><br />
定时任务 `node-schedule`<br />
cnpm install node-schedule --save<br /><br />
node端选择dom节点工具 `cheerio`<br />
cnpm install cheerio --save<br /><br />
腾讯发短信的依赖包 `qcloudsms_js`<br />
cnpm install qcloudsms_js <br /><br />
热更新包,诺豆的妈妈,`nodemon` (其实不用也可以)<br />
cnpm install nodemon --save-dev<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<br />const axios = require('axios')<br />const querystring = require("querystring"); //序列化对象,用qs也行,都一样<br />let QcloudSms = require("qcloudsms_js");<br />let cheerio = require('cheerio');<br />let schedule = require('node-schedule');<br /><br /><br />
然后我们先定义请求参数,来一个obj
let obj = {<br /> data: {<br /> lineId: 111130, //路线id<br /> vehTime: 0722, //发车时间,<br /> startTime: 0751, //预计上车时间<br /> onStationId: 564492, //预定的站点id<br /> offStationId: 17990,//到站id<br /> onStationName: '宝安交通运输局③', //预定的站点名称<br /> offStationName: "深港产学研基地",//预定到站名称<br /> tradePrice: 0,//总金额<br /> saleDates: '17',//车票日期<br /> beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据<br /> },<br /> phoneNumber: 123123123, //用户手机号,接收短信的手机号<br /> cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie<br /> day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票<br />}<br /><br /><br />
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{<br /> /**<br /> *Creates an instance of QueryTicket.<br /> * @param {Object} { data, phoneNumber, cookie, day }<br /> * @param data {Object} 请求余票接口的requery参数<br /> * @param phoneNumber {Number} 用户手机号,短信需要用到<br /> * @param cookie {String} cookie信息<br /> * @params day {String} 某日的票,如'18'<br /> * @memberof QueryTicket 请求余票接口<br /> */<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> this.data = data <br /> this.cookie = cookie<br /> this.day = day<br /> this.phoneNumber = phoneNumber<br /> this.postData = querystring.stringify(data)<br /> this.times = 0; //记录次数<br /> let stop = false //通过特定接口才能修改stop值,防止外部随意串改<br /> this.getStop = function () { //获取是否停止<br /> return stop <br /> }<br /> this.setStop = function (ifStop) { //设置是否停止<br /> stop = ifStop<br /> }<br /> }<br />}<br /><br /><br />
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> init(){}//初始化<br /> handleQueryTicket(){}//查询余票的逻辑<br /> requestTicket(){} //调用查询余票接口<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化,因为涉及到异步请求,所以我们使用`async await`<br /> async init(){<br /> let ticketList = await this.handleQueryTicket() //返回查询到的余票数组<br /> }<br /> //查询余票的逻辑<br /> handleQueryTicket(){ <br /> let ticketList = [] //余票数组<br /> let res = await this.requestTicket()<br /> this.times++ //计数器,记录请求查询多少次<br /> let str = res.data.replace(/\\/g, "") //格式化返回值<br /> let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据<br /> let list = $(".main").find(".b") //查找是否有余票的dom节点<br /> // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码<br /> if (!list.length) {<br /> console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)<br /> return<br /> }<br /><br /> // 如果有余票<br /> list.each((idx, item) => {<br /> let str = $(item).html() //str这时格式是21&$x4F59;0<br /> //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已<br /> //因此要在下一步对其进行格式化<br /> let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) <br /> let data = {<br /> day: arr[0],<br /> ticketLeft: arr[1]<br /> }<br /> <br /> //如果是要抢指定日期的票<br /> if (this.day) {<br /> //如果有指定日期的余票<br /> if (parseInt(data.day) === parseInt(data.day)) {<br /> ticketList.push(data)<br /> }<br /> } else {<br /> //如果不是,则返回查询到的所有余票<br /> ticketList.push(data)<br /> }<br /> })<br /> return ticketList<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> }) <br /> }<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> let year = new Date().getFullYear() //年份,<br /> let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号<br /> let {<br /> onStationName,//起始站点名<br /> offStationName,//结束站点名<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站台id<br /> offStationId //到站的站台id<br /> } = this.data // 初始化的数据<br /><br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"<br /> let dateStr = ""; //车票日期<br /> let tickAmount = "" //总张数<br /> ticketList.forEach(item => {<br /> dateStr = dateStr + `${year}-${month}-${item.day},`<br /> tickAmount = tickAmount + `${item.ticketLeft}张,`<br /> })<br /><br /> let buyTicket = {<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站点id<br /> offStationId,//目标站点id<br /> tradePrice: '5', //金额<br /> saleDates: dateStr.slice(0, -1),<br /> payType: '2' //支付方式,微信支付<br /> }<br /><br /> // 调用购票接口<br /> let data = querystring.stringify(buyTicket)<br /> let res = await this.requestOrder(data) //返回json数据,是否购票成功等等<br /> //把发短信所需要数据都要传入<br /> return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })<br /> }//购票相关逻辑<br /> //调用购票接口<br /> requestOrder(obj){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> })<br /> }<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> //执行通知逻辑<br /> this.handleInfoUser(resParse)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> //购票代码...<br /> }<br /> //调用购票接口<br /> requestOrder(obj){<br /> //购票接口请求代码...<br /> }<br /> //通知用户的逻辑<br /> async handleInfoUser(parseData){<br /> //获取上一步购票的response数据和我们拼接的数据<br /> let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData<br /> //如果购票成功,则返回500<br /> if (returnCode === "500") {<br /> let res = await this.sendMsg({<br /> dateStr, //日期<br /> tickAmount: tickAmount.slice(0, -1), //总张数<br /> station, //站点<br /> lineName, //巴士名称/路线名称<br /> tradePrice,//总价<br /> startTime,//出发时间<br /> phoneNumber: this.phoneNumber,//手机号<br /> })<br /> //如果发信成功,则不再进行抢票操作<br /> if (res.result === 0 && res.errmsg === "OK") {<br /> this.setStop(true)<br /> } else {<br /> //失败不做任何操作<br /> console.log(res.errmsg)<br /> }<br /> } else {<br /> //失败不做任何操作<br /> console.log(resParse['returnInfo'])<br /> } <br /> }<br /> //发短信接口<br /> sendMSg(){<br /> let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj<br /> let appid = 140034324; // SDK AppID 以1400开头<br /> // 短信应用 SDK AppKey<br /> let appkey = "asdfdsvajwienin23493nadsnzxc";<br /> // 短信模板 ID,需要在短信控制台中申请<br /> let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请<br /> // 签名<br /> let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请<br /> // 实例化 QcloudSms<br /> let qcloudsms = QcloudSms(appid, appkey);<br /> let ssender = qcloudsms.SmsSingleSender();<br /> // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容<br /> let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];<br /> //用promise来封装下异步操作<br /> return new Promise((resolve, reject) => {<br /> ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {<br /> if (err) {<br /> reject(err)<br /> } else {<br /> resolve(resData)<br /> }<br /> });<br /> })<br /> } <br />}<br /><br />
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务<br />class SetInter {<br /> constructor({ timer, fn }) {<br /> this.timer = timer // 每几秒执行<br /> this.fn = fn //执行的回调<br /> this.rule = new schedule.RecurrenceRule(); //实例化一个对象<br /> this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已<br /> this.init()<br /> }<br /> setRule() {<br /> let rule = [];<br /> let i = 1;<br /> while (i {<br /> this.fn() // 定时调用传入的回调方法<br /> });<br /> }<br />}<br /><br /><br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1<br /> lineId: 111130,<br /> vehTime: 0722,<br /> startTime: 0751,<br /> onStationId: 564492,<br /> offStationId: 17990,<br /> onStationName: '宝安交通运输局③',<br /> offStationName: "深港产学研基地",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 123123123,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "17"<br />}<br />let obj2 = { //用户2<br /> data: {<br /> lineId: 134423,<br /> vehTime: 1820,<br /> startTime: 1855,<br /> onStationId: 4322,<br /> offStationId: 53231,<br /> onStationName: '百度国际大厦',<br /> offStationName: "裕安路口",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 175932123124,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "" <br />}<br />let ticket = new QueryTicket(obj) //用户1<br />let ticket2 = new QueryTicket(obj2) //用户2<br /><br />new SetInter({<br /> timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图<br /> fn: function () {<br /> [ticket,ticket2].map(item => { //同时进行两个用户的抢票<br /> if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢<br /> item.init()<br /> } else { // 如果抢到票了,则不继续抢票<br /> console.log('stop')<br /> }<br /> })<br /> }<br />})<br /><br /><br />
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
最后
欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!
回复「交流」,吹吹水、聊聊技术、吐吐槽!回复「阅读」,每日刷刷高质量好文!如果这篇文章对你有帮助,「在看」是最大的支持
》》面试官也在看的算法资料《《“在看和转发”就是最大的支持 查看全部
(实战)Node.js 实现抢票小工具&短信通知提醒
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【xxxxxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口设置代理手机上网依次执行上面两步获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【xxxx】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容: 整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址cookie信息各自的request参数字段user-Agent信息各自的response返回内容设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
**小方块的结构为:**<br />
<br />这里为日期<br />如果有余票则显示余票数量<br /><br /><br />
td的样式名为a代表不可选样式名为e代表已满样式名为d代表已购样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口定时请求任务有余票则自动请求购票接口下订单调用腾讯云短信api接口发送短信通知多个用户抢票功能抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。 下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的`http.request`,我这里选择用的是`axios`,毕竟`axios`在node端底层也是调用`http.request`<br />
cnpm install axios --save<br /><br />
定时任务 `node-schedule`<br />
cnpm install node-schedule --save<br /><br />
node端选择dom节点工具 `cheerio`<br />
cnpm install cheerio --save<br /><br />
腾讯发短信的依赖包 `qcloudsms_js`<br />
cnpm install qcloudsms_js <br /><br />
热更新包,诺豆的妈妈,`nodemon` (其实不用也可以)<br />
cnpm install nodemon --save-dev<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<br />const axios = require('axios')<br />const querystring = require("querystring"); //序列化对象,用qs也行,都一样<br />let QcloudSms = require("qcloudsms_js");<br />let cheerio = require('cheerio');<br />let schedule = require('node-schedule');<br /><br /><br />
然后我们先定义请求参数,来一个obj
let obj = {<br /> data: {<br /> lineId: 111130, //路线id<br /> vehTime: 0722, //发车时间,<br /> startTime: 0751, //预计上车时间<br /> onStationId: 564492, //预定的站点id<br /> offStationId: 17990,//到站id<br /> onStationName: '宝安交通运输局③', //预定的站点名称<br /> offStationName: "深港产学研基地",//预定到站名称<br /> tradePrice: 0,//总金额<br /> saleDates: '17',//车票日期<br /> beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据<br /> },<br /> phoneNumber: 123123123, //用户手机号,接收短信的手机号<br /> cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie<br /> day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票<br />}<br /><br /><br />
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{<br /> /**<br /> *Creates an instance of QueryTicket.<br /> * @param {Object} { data, phoneNumber, cookie, day }<br /> * @param data {Object} 请求余票接口的requery参数<br /> * @param phoneNumber {Number} 用户手机号,短信需要用到<br /> * @param cookie {String} cookie信息<br /> * @params day {String} 某日的票,如'18'<br /> * @memberof QueryTicket 请求余票接口<br /> */<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> this.data = data <br /> this.cookie = cookie<br /> this.day = day<br /> this.phoneNumber = phoneNumber<br /> this.postData = querystring.stringify(data)<br /> this.times = 0; //记录次数<br /> let stop = false //通过特定接口才能修改stop值,防止外部随意串改<br /> this.getStop = function () { //获取是否停止<br /> return stop <br /> }<br /> this.setStop = function (ifStop) { //设置是否停止<br /> stop = ifStop<br /> }<br /> }<br />}<br /><br /><br />
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> init(){}//初始化<br /> handleQueryTicket(){}//查询余票的逻辑<br /> requestTicket(){} //调用查询余票接口<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化,因为涉及到异步请求,所以我们使用`async await`<br /> async init(){<br /> let ticketList = await this.handleQueryTicket() //返回查询到的余票数组<br /> }<br /> //查询余票的逻辑<br /> handleQueryTicket(){ <br /> let ticketList = [] //余票数组<br /> let res = await this.requestTicket()<br /> this.times++ //计数器,记录请求查询多少次<br /> let str = res.data.replace(/\\/g, "") //格式化返回值<br /> let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据<br /> let list = $(".main").find(".b") //查找是否有余票的dom节点<br /> // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码<br /> if (!list.length) {<br /> console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)<br /> return<br /> }<br /><br /> // 如果有余票<br /> list.each((idx, item) => {<br /> let str = $(item).html() //str这时格式是21&$x4F59;0<br /> //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已<br /> //因此要在下一步对其进行格式化<br /> let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) <br /> let data = {<br /> day: arr[0],<br /> ticketLeft: arr[1]<br /> }<br /> <br /> //如果是要抢指定日期的票<br /> if (this.day) {<br /> //如果有指定日期的余票<br /> if (parseInt(data.day) === parseInt(data.day)) {<br /> ticketList.push(data)<br /> }<br /> } else {<br /> //如果不是,则返回查询到的所有余票<br /> ticketList.push(data)<br /> }<br /> })<br /> return ticketList<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> }) <br /> }<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> let year = new Date().getFullYear() //年份,<br /> let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号<br /> let {<br /> onStationName,//起始站点名<br /> offStationName,//结束站点名<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站台id<br /> offStationId //到站的站台id<br /> } = this.data // 初始化的数据<br /><br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"<br /> let dateStr = ""; //车票日期<br /> let tickAmount = "" //总张数<br /> ticketList.forEach(item => {<br /> dateStr = dateStr + `${year}-${month}-${item.day},`<br /> tickAmount = tickAmount + `${item.ticketLeft}张,`<br /> })<br /><br /> let buyTicket = {<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站点id<br /> offStationId,//目标站点id<br /> tradePrice: '5', //金额<br /> saleDates: dateStr.slice(0, -1),<br /> payType: '2' //支付方式,微信支付<br /> }<br /><br /> // 调用购票接口<br /> let data = querystring.stringify(buyTicket)<br /> let res = await this.requestOrder(data) //返回json数据,是否购票成功等等<br /> //把发短信所需要数据都要传入<br /> return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })<br /> }//购票相关逻辑<br /> //调用购票接口<br /> requestOrder(obj){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> })<br /> }<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> //执行通知逻辑<br /> this.handleInfoUser(resParse)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> //购票代码...<br /> }<br /> //调用购票接口<br /> requestOrder(obj){<br /> //购票接口请求代码...<br /> }<br /> //通知用户的逻辑<br /> async handleInfoUser(parseData){<br /> //获取上一步购票的response数据和我们拼接的数据<br /> let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData<br /> //如果购票成功,则返回500<br /> if (returnCode === "500") {<br /> let res = await this.sendMsg({<br /> dateStr, //日期<br /> tickAmount: tickAmount.slice(0, -1), //总张数<br /> station, //站点<br /> lineName, //巴士名称/路线名称<br /> tradePrice,//总价<br /> startTime,//出发时间<br /> phoneNumber: this.phoneNumber,//手机号<br /> })<br /> //如果发信成功,则不再进行抢票操作<br /> if (res.result === 0 && res.errmsg === "OK") {<br /> this.setStop(true)<br /> } else {<br /> //失败不做任何操作<br /> console.log(res.errmsg)<br /> }<br /> } else {<br /> //失败不做任何操作<br /> console.log(resParse['returnInfo'])<br /> } <br /> }<br /> //发短信接口<br /> sendMSg(){<br /> let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj<br /> let appid = 140034324; // SDK AppID 以1400开头<br /> // 短信应用 SDK AppKey<br /> let appkey = "asdfdsvajwienin23493nadsnzxc";<br /> // 短信模板 ID,需要在短信控制台中申请<br /> let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请<br /> // 签名<br /> let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请<br /> // 实例化 QcloudSms<br /> let qcloudsms = QcloudSms(appid, appkey);<br /> let ssender = qcloudsms.SmsSingleSender();<br /> // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容<br /> let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];<br /> //用promise来封装下异步操作<br /> return new Promise((resolve, reject) => {<br /> ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {<br /> if (err) {<br /> reject(err)<br /> } else {<br /> resolve(resData)<br /> }<br /> });<br /> })<br /> } <br />}<br /><br />
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务<br />class SetInter {<br /> constructor({ timer, fn }) {<br /> this.timer = timer // 每几秒执行<br /> this.fn = fn //执行的回调<br /> this.rule = new schedule.RecurrenceRule(); //实例化一个对象<br /> this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已<br /> this.init()<br /> }<br /> setRule() {<br /> let rule = [];<br /> let i = 1;<br /> while (i {<br /> this.fn() // 定时调用传入的回调方法<br /> });<br /> }<br />}<br /><br /><br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1<br /> lineId: 111130,<br /> vehTime: 0722,<br /> startTime: 0751,<br /> onStationId: 564492,<br /> offStationId: 17990,<br /> onStationName: '宝安交通运输局③',<br /> offStationName: "深港产学研基地",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 123123123,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "17"<br />}<br />let obj2 = { //用户2<br /> data: {<br /> lineId: 134423,<br /> vehTime: 1820,<br /> startTime: 1855,<br /> onStationId: 4322,<br /> offStationId: 53231,<br /> onStationName: '百度国际大厦',<br /> offStationName: "裕安路口",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 175932123124,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "" <br />}<br />let ticket = new QueryTicket(obj) //用户1<br />let ticket2 = new QueryTicket(obj2) //用户2<br /><br />new SetInter({<br /> timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图<br /> fn: function () {<br /> [ticket,ticket2].map(item => { //同时进行两个用户的抢票<br /> if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢<br /> item.init()<br /> } else { // 如果抢到票了,则不继续抢票<br /> console.log('stop')<br /> }<br /> })<br /> }<br />})<br /><br /><br />
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
最后
欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!
回复「交流」,吹吹水、聊聊技术、吐吐槽!回复「阅读」,每日刷刷高质量好文!如果这篇文章对你有帮助,「在看」是最大的支持
》》面试官也在看的算法资料《《“在看和转发”就是最大的支持
外贸网站常用的辅助工具
网站优化 • 优采云 发表了文章 • 0 个评论 • 75 次浏览 • 2022-06-14 06:38
1Google Webmaster Tools
Google Webmaster Tools提供非常多的实用工具,总而言之它可以告诉你Google是如何看你的网站的,比如索引了哪些页面,有没有死链接,搜索关键词的排名, 内部链接和外部链接等等。
2Google Analytics
除了提供其它统计网站的统计项目外,Google Analytics最大的特色在于提供Pay-Per-Click数据,只是没在网页中显示任何图标。
3Summit Media's Spider Simulator
搜索引擎蜘蛛模拟器,看看你的网站在蜘蛛眼里是什么样子,优化页面的好工具。
4Mike's Marketing Tools
提供N多免费的Marketing Tools,比如Search Engine Rankings可以同时测试你网站的多个关键词,在多个搜索引擎中的排名,不再需要挨个查看,极大地节省了你的时间。Link Popularity Tool帮你查看各大搜索引擎中有多少反向链接,反向链接对SEO非常重要。
5SEOmoz's Page Strength Tool
为你的网站提供10多项SEO数据,你可以根据这些数据进行SEO。
's SEO Analyzer
同样是一个SEO分析工具,但是与SEOmoz's Page Strength Tool不同,它从网站内部数据得出分析结果,比如页面结构、网页源代码、内部连接、页面尺寸等等。这个工具需要注册后才能用。
7Dead Links Checker
死链检查器,含有死链接不但会让你的访客扫兴,还会被搜索引擎降级的,所以还是定期检查一下为好。
一个关键词排名分析工具,可惜的是需要SOAP API,而Google已经停止发放SOAP API,所以必须已经申请过,才能使用这个工具。
9Feed Burner
全方位的Feed服务、托管、优化、统计、广告等等,还用多说么. 其实国内的Feedsky也不错,只是不知道它能不能抓取国外的站点。
10Self SEO Page Speed Checker
网站测速,可以同时比较几个网站,当然如果你的网站放在国内,这个工具就没什么参考价值了,因为速度太慢了。
(版权说明:感谢原作者的辛苦创作,如转载涉及版权等问题,请作者与我们联系,我们将在第一时间删除,谢谢!)
查看全部
外贸网站常用的辅助工具
1Google Webmaster Tools
Google Webmaster Tools提供非常多的实用工具,总而言之它可以告诉你Google是如何看你的网站的,比如索引了哪些页面,有没有死链接,搜索关键词的排名, 内部链接和外部链接等等。
2Google Analytics
除了提供其它统计网站的统计项目外,Google Analytics最大的特色在于提供Pay-Per-Click数据,只是没在网页中显示任何图标。
3Summit Media's Spider Simulator
搜索引擎蜘蛛模拟器,看看你的网站在蜘蛛眼里是什么样子,优化页面的好工具。
4Mike's Marketing Tools
提供N多免费的Marketing Tools,比如Search Engine Rankings可以同时测试你网站的多个关键词,在多个搜索引擎中的排名,不再需要挨个查看,极大地节省了你的时间。Link Popularity Tool帮你查看各大搜索引擎中有多少反向链接,反向链接对SEO非常重要。
5SEOmoz's Page Strength Tool
为你的网站提供10多项SEO数据,你可以根据这些数据进行SEO。
's SEO Analyzer
同样是一个SEO分析工具,但是与SEOmoz's Page Strength Tool不同,它从网站内部数据得出分析结果,比如页面结构、网页源代码、内部连接、页面尺寸等等。这个工具需要注册后才能用。
7Dead Links Checker
死链检查器,含有死链接不但会让你的访客扫兴,还会被搜索引擎降级的,所以还是定期检查一下为好。
一个关键词排名分析工具,可惜的是需要SOAP API,而Google已经停止发放SOAP API,所以必须已经申请过,才能使用这个工具。
9Feed Burner
全方位的Feed服务、托管、优化、统计、广告等等,还用多说么. 其实国内的Feedsky也不错,只是不知道它能不能抓取国外的站点。
10Self SEO Page Speed Checker
网站测速,可以同时比较几个网站,当然如果你的网站放在国内,这个工具就没什么参考价值了,因为速度太慢了。
(版权说明:感谢原作者的辛苦创作,如转载涉及版权等问题,请作者与我们联系,我们将在第一时间删除,谢谢!)
这些Python代码技巧,你可能还不知道!
网站优化 • 优采云 发表了文章 • 0 个评论 • 162 次浏览 • 2022-06-03 11:00
它可以帮助你从大量顶级国际出版物中检索到新闻文章和相关元数据。你可以检索图像、文本和作者名。
它还有一些内置的 NLP 功能。
地址:#performing-nlp-on-an-article
如果你想在下一个项目中使用 BeautifulSoup 或其它 DIY 网页抓取库,那么不如使用$ pip install newspaper3k,既省时又省事,何乐而不为呢?
运算符重载(Operator overloading)
Python 支持运算符重载。
它实际上是一个简单的概念。你有没有想过为什么 Python 允许用户使用 + 运算符来将数字相加,并级联字符串?这就是运算符重载在发挥作用。
你可以使用 Python 的标准运算符号来定义对象,这样你可以在与这些对象相关的语境中使用它们。
class Thing:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __init__(self, value):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> self.__value = value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __gt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value > other.__value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __lt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value nothing<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># False<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />something >> file = open('file.txt', 'r')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />>>> print(file)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />
这使代码 debug 变得简单很多。将字符串添加到类别定义,如下所示:
class someClass:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __repr__(self):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return ""<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />someInstance = someClass()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># prints <br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(someInstance)
sh
Python 是一种伟大的脚本语言,不过有时使用标准 os 和 subprocess 库会有点棘手。
sh 库提供了一种不错的替代方案。
sh 库:
该库允许用户像使用普通函数一样调用任意程序,这对自动化工作流和任务非常有用。
from sh import *<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.pwd()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.mkdir('new_folder')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.touch('new_file.txt')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.whoami()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.echo('This is great!')
类型提示(Type hints)
Python 是动态语言。在定义变量、函数、类别等时无需指定数据类型。
这有利于缩短开发周期。但是,简单的类型错误(typing issue)导致的运行时错误真的太烦了。
从 Python 3.5 版本开始,用户可以选择在定义函数时开启类型提示。
def addTwo(x : Int) -> Int:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return x + 2
你还可以定义类型别名:
from typing import List<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Vector = List[float]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Matrix = List[Vector]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />def addMatrix(a : Matrix, b : Matrix) -> Matrix:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result = []<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for i,row in enumerate(a):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row =[]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for j, col in enumerate(row):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row += [a[i][j] + b[i][j]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result += [result_row]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return result<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />x = [[1.0, 0.0], [0.0, 1.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />y = [[2.0, 1.0], [0.0, -2.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />z = addMatrix(x, y)
尽管非强制,但类型注释可以使代码更易理解。
它们还允许你在运行之前使用类型检查工具捕捉 TypeError。在进行大型复杂项目时执行此类操作是值得的。
uuid
生成通用唯一标识符(Universally Unique ID,UUID)的一种快速简单方法就是使用 Python 标准库的 uuid 模块。
uuid 模块:
import uuid<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />user_id = uuid.uuid4()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(user_id)
这创建了一个随机化后的 128 比特数字,该数字几乎必然是唯一的。
事实上,可以生成 2¹²²可能的 UUID。这个数字超过了 5,000,000,000,000,000,000,000,000,000,000,000,000。
在给定集合中找出重复数字的可能性极低。即使有一万亿 UUID,重复数字存在的概率也远远低于十亿分之一。
虚拟环境(Virtual environment)
这可能是 Python 中我最喜欢的事物了。
你可能同时处理多个 Python 项目。不幸的是,有时候两个项目依赖于相同依赖项的不同版本。那你要安装哪个版本呢?
幸运的是,Python 支持虚拟环境,这使得用户能够充分利用两种环境。见下列行:
python -m venv my-project<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />source my-project/bin/activate<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />pip install all-the-modules
现在你在一台机器上具备独立的多个 Python 版本了。问题解决!
wikipedia
Wikipedia 拥有一个很棒的 API,允许用户以编程方式访问巨大体量的免费知识和信息。
wikipedia 模块使得访问该 API 非常便捷。
Wikipedia 模块:
import wikipedia<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />result = wikipedia.page('freeCodeCamp')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(result.summary)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />for link in result.links:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> print(link)
和真实的维基百科网站类似,该模块支持多种语言、页面消歧、随机页面检索,甚至还具备 donate() 方法。
xkcd
humour 是 Python 语言的一个关键特征,其名称来自英国喜剧片《蒙提·派森的飞行马戏团》(Monty Python and the Flying Circus)。Python 的很多官方文档引用了该喜剧片最著名的剧情。
幽默感并不限于文档。试着运行下列行:
import antigravity
将打开 xkcd 画的 Python 漫画。不要改变这一点,Python。不要改变。
YAML
YAML 代表 『YAML Ain』t Markup Language』。它是一种数据格式语言,是 JSON 的超集。
与 JSON 不同,它可以存储更复杂的对象并引用自己的元素。你还可以编写注释,使其尤其适用于编写配置文件。
PyYAML 模块()可以让你在 Python 中使用 YAML。安装:
$ pip install pyyaml
然后导入到项目中:
import yaml
PyYAML 使你能够存储任何数据类型的 Python 对象,以及任何用户定义类别的实例。
zip
给你支最后一招,非常酷。还在用两个列表来组成一部词典吗?
keys = ['a', 'b', 'c']<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />vals = [1, 2, 3]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />zipped = dict(zip(keys, vals))
zip() 内置函数使用多个可迭代对象作为输入并返回元组列表。每个元组按位置索引对输入对象的元素进行分组。
你也可以通过调用*zip() 来「解压」对象。
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。 查看全部
这些Python代码技巧,你可能还不知道!
它可以帮助你从大量顶级国际出版物中检索到新闻文章和相关元数据。你可以检索图像、文本和作者名。
它还有一些内置的 NLP 功能。
地址:#performing-nlp-on-an-article
如果你想在下一个项目中使用 BeautifulSoup 或其它 DIY 网页抓取库,那么不如使用$ pip install newspaper3k,既省时又省事,何乐而不为呢?
运算符重载(Operator overloading)
Python 支持运算符重载。
它实际上是一个简单的概念。你有没有想过为什么 Python 允许用户使用 + 运算符来将数字相加,并级联字符串?这就是运算符重载在发挥作用。
你可以使用 Python 的标准运算符号来定义对象,这样你可以在与这些对象相关的语境中使用它们。
class Thing:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __init__(self, value):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> self.__value = value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __gt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value > other.__value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __lt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value nothing<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># False<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />something >> file = open('file.txt', 'r')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />>>> print(file)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />
这使代码 debug 变得简单很多。将字符串添加到类别定义,如下所示:
class someClass:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __repr__(self):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return ""<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />someInstance = someClass()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># prints <br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(someInstance)
sh
Python 是一种伟大的脚本语言,不过有时使用标准 os 和 subprocess 库会有点棘手。
sh 库提供了一种不错的替代方案。
sh 库:
该库允许用户像使用普通函数一样调用任意程序,这对自动化工作流和任务非常有用。
from sh import *<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.pwd()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.mkdir('new_folder')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.touch('new_file.txt')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.whoami()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.echo('This is great!')
类型提示(Type hints)
Python 是动态语言。在定义变量、函数、类别等时无需指定数据类型。
这有利于缩短开发周期。但是,简单的类型错误(typing issue)导致的运行时错误真的太烦了。
从 Python 3.5 版本开始,用户可以选择在定义函数时开启类型提示。
def addTwo(x : Int) -> Int:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return x + 2
你还可以定义类型别名:
from typing import List<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Vector = List[float]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Matrix = List[Vector]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />def addMatrix(a : Matrix, b : Matrix) -> Matrix:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result = []<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for i,row in enumerate(a):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row =[]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for j, col in enumerate(row):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row += [a[i][j] + b[i][j]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result += [result_row]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return result<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />x = [[1.0, 0.0], [0.0, 1.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />y = [[2.0, 1.0], [0.0, -2.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />z = addMatrix(x, y)
尽管非强制,但类型注释可以使代码更易理解。
它们还允许你在运行之前使用类型检查工具捕捉 TypeError。在进行大型复杂项目时执行此类操作是值得的。
uuid
生成通用唯一标识符(Universally Unique ID,UUID)的一种快速简单方法就是使用 Python 标准库的 uuid 模块。
uuid 模块:
import uuid<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />user_id = uuid.uuid4()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(user_id)
这创建了一个随机化后的 128 比特数字,该数字几乎必然是唯一的。
事实上,可以生成 2¹²²可能的 UUID。这个数字超过了 5,000,000,000,000,000,000,000,000,000,000,000,000。
在给定集合中找出重复数字的可能性极低。即使有一万亿 UUID,重复数字存在的概率也远远低于十亿分之一。
虚拟环境(Virtual environment)
这可能是 Python 中我最喜欢的事物了。
你可能同时处理多个 Python 项目。不幸的是,有时候两个项目依赖于相同依赖项的不同版本。那你要安装哪个版本呢?
幸运的是,Python 支持虚拟环境,这使得用户能够充分利用两种环境。见下列行:
python -m venv my-project<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />source my-project/bin/activate<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />pip install all-the-modules
现在你在一台机器上具备独立的多个 Python 版本了。问题解决!
wikipedia
Wikipedia 拥有一个很棒的 API,允许用户以编程方式访问巨大体量的免费知识和信息。
wikipedia 模块使得访问该 API 非常便捷。
Wikipedia 模块:
import wikipedia<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />result = wikipedia.page('freeCodeCamp')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(result.summary)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />for link in result.links:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> print(link)
和真实的维基百科网站类似,该模块支持多种语言、页面消歧、随机页面检索,甚至还具备 donate() 方法。
xkcd
humour 是 Python 语言的一个关键特征,其名称来自英国喜剧片《蒙提·派森的飞行马戏团》(Monty Python and the Flying Circus)。Python 的很多官方文档引用了该喜剧片最著名的剧情。
幽默感并不限于文档。试着运行下列行:
import antigravity
将打开 xkcd 画的 Python 漫画。不要改变这一点,Python。不要改变。
YAML
YAML 代表 『YAML Ain』t Markup Language』。它是一种数据格式语言,是 JSON 的超集。
与 JSON 不同,它可以存储更复杂的对象并引用自己的元素。你还可以编写注释,使其尤其适用于编写配置文件。
PyYAML 模块()可以让你在 Python 中使用 YAML。安装:
$ pip install pyyaml
然后导入到项目中:
import yaml
PyYAML 使你能够存储任何数据类型的 Python 对象,以及任何用户定义类别的实例。
zip
给你支最后一招,非常酷。还在用两个列表来组成一部词典吗?
keys = ['a', 'b', 'c']<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />vals = [1, 2, 3]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />zipped = dict(zip(keys, vals))
zip() 内置函数使用多个可迭代对象作为输入并返回元组列表。每个元组按位置索引对输入对象的元素进行分组。
你也可以通过调用*zip() 来「解压」对象。
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。
30 行 Python 代码实现 Twitch 主播上线实时通知
网站优化 • 优采云 发表了文章 • 0 个评论 • 83 次浏览 • 2022-06-03 10:27
大家好,今天我将开始写作一个新的文章系列,特别面向 Python 初学者。简言之,我将会尝试更多新的工具,编写尽可能少的代码,来完成一个有趣的项目。
例如,我们今天学习使用 Twilio API、Twitch API、在 Heroku 上发布项目。我将会教你用 30 行代码写一个 “Twitch 直播”短信通知工具,而且一个月只需要花费 12 美分。
前提
你只需要懂得怎么运行 Python 程序,以及会操作基本的 git 命令行(commit & push)。如果你需要学习一下这些知识点,可以看看下面两篇文章:
《Python 3 安装与设置指南》
《Git 最佳入门教程》fromAdrian Hajdin
将会学到的知识将要构建的项目
要求很简单:我们想要在某个主播正在直播的时候接收到一条短信通知,我们想要知道主播何时上线以及何时退出直播,并且这个通知程序全天都在自动运行。
我们将把整个项目分成 3 个部分。首先,我们看看如何通过程序知晓一个特定主播上线了,然后看看如何接收一条主播上线的通知短信,最后我们需要让这段代码每隔 X 分钟执行一次,这样我们就不会错过喜欢的主播的动态啦。
主播正在直播吗?
我们可以这样了解主播是否正在直播:访问主播的 URL,看看是否有 Live 徽章。
主播直播时候的截图
这个过程涉及到网络爬虫,而且这个功能不是 20 行左右的 Python 代码能完成的。Twitch 使用了非常多的 JS 脚本代码,一个简单的 request.get() 是不足以达到我们要求的。
对于使用爬虫去爬取直播信息,我们将会借助 Chrome 浏览器抓取这个网页的内容,如截图所示。这种方式是可行的,但是需要 30 行以上代码。如果你想要了解更多,可以参考我最近的文章《网页抓取指南》。
除了抓取 Twitch 网页这种方式外,我们还可以使用 Twitch 的 API。有的读者可能不了解 API 这个术语,这里我们解释一下:API 是应用程序编程接口,网站通过 API 向任何人(主要是开发者)公开特性和数据。Twitch 的 API 是通过 HTTP 协议对外开放,也就是说我们可以通过一个简单的 HTTP 请求去获取到大量信息以及做许多事情。
获取你的 API KEY
首先,你需要去创建一个 Twitch 的 API Key。许多 API 服务需要对访问者进行身份认证,以避免有人滥用 API,或者以限制某些人访问某些功能。
请按照以下步骤获取你的 API Key:
在屏幕底端,你可以看到你的 client-id,将它保存好,稍后会使用。
确认主播是否正在直播
我们手上有了 API key,现在就可以查询 Twitch 的 API 获取我们想要的信息。下面的代码给 Twitch 的 API 传递了正确的参数并且打印响应信息。
# requests is the go to package in python to make http request<br /># https://2.python-requests.org/en/master/<br />import requests<br /><br /># This is one of the route where Twich expose data, <br /># They have many more: https://dev.twitch.tv/docs<br />endpoint = "https://api.twitch.tv/helix/streams?"<br /><br /># In order to authenticate we need to pass our api key through header<br />headers = {"Client-ID": ""}<br /><br /># The previously set endpoint needs some parameter, here, the Twitcher we want to follow<br /># Disclaimer, I don't even know who this is, but he was the first one on Twich to have a live stream so I could have nice examples<br />params = {"user_login": "Solary"}<br /><br /># It is now time to make the actual request<br />response = request.get(endpoint, params=params, headers=headers)<br />print(response.json())<br />
输出信息就像下面这样:
{<br /> 'data':[<br /> {<br /> 'id':'35289543872',<br /> 'user_id':'174955366',<br /> 'user_name':'Solary',<br /> 'game_id':'21779',<br /> 'type':'live',<br /> 'title':"Wakz duoQ w/ Tioo - GM 400LP - On récupère le chall après les -250LP d'inactivité !",<br /> 'viewer_count':4073,<br /> 'started_at':'2019-08-14T07:01:59Z',<br /> 'language':'fr',<br /> 'thumbnail_url':'https://static-cdn.jtvnw.net/previews-ttv/live_user_solary-{width}x{height}.jpg',<br /> 'tag_ids':[<br /> '6f655045-9989-4ef7-8f85-1edcec42d648'<br /> ]<br /> }<br /> ],<br /> 'pagination':{<br /> 'cursor':'eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6MX19'<br /> }<br />}<br />
这个数据格式是一种易于阅读的 JSON 格式。data是一个包含所有当前直播的数组对象。type键表示这个直播间正在直播,此外type的值还可以为空(比如,在报错的时候)。
因此如果我们想要在 Python 里创建一个表示当前主播是否正在直播的布尔变量,我们需要去加上如下代码:
json_response = response.json()<br /><br /># We get only streams<br />streams = json_response.get('data', [])<br /><br /># We create a small function, (a lambda), that tests if a stream is live or not<br />is_active = lambda stream: stream.get('type') == 'live'<br /># We filter our array of streams with this function so we only keep streams that are active<br />streams_active = filter(is_active, streams)<br /><br /># any returns True if streams_active has at least one element, else False<br />at_least_one_stream_active = any(streams_active)<br /><br />print(at_least_one_stream_active)<br />
此时,at_least_one_stream_active变量是 True 的时候表示你喜欢的主播正在直播。
让我们现在看看如何获得短信通知。
现在给自己发一条短信!
那么我们将使用 Twilio API 自己发送一条短信。访问此处并且创建一个账号。当需要你手机验证的时候,填入你想要在此项目中接受短信的手机号码,这样你就可以使用 Twilio 为新用户提供的 15 美元的免费信用额度。一条短信 1 美分,足以支撑你的机器运行一年了。
访问console,你将会看到自己的 Account SID 和 Auth Token。请保留好它们以备后用。同时点击红色按钮“获得试用账号”,进行下一步,将试用账号也保存好以备后用。
使用 Python API 发送短信很简单,有软件包帮你一系列事情。使用pip install Twilio导入相应的包并且执行下面的代码:
from twilio.rest import Client<br />client = Client(, )<br />client.messages.create(<br /> body='Test MSG',from_=,to=)<br />
只需要这么点代码,你就可以给自己发一条通知短信了,是不是很棒?
整合所有代码
现在我们来整合所有代码,压缩到不到 30 行 Python 代码。
import requests<br />from twilio.rest import Client<br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br />if at_least_one_stream_active:<br /> client = Client(, )<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />
只留下了 10 多行代码!
避免重复通知
这段代码的效果很好,但是如果这段代码在服务器上每分钟执行一次,我们喜欢的主播一开启直播,我们就会每分钟都收到一条短信。
我们需要让程序知道它已经给我们发了主播上线直播的短信通知,别再重复发短信了。好消息是 Twilio API 提供检索历史消息的方法,因此我们仅仅需要检索发送的历史消息中是否包含已经发送过的主播正在直播的消息。
如下是我们要做的伪代码:
if favorite_twitcher_live and last_sent_sms is not live_notification:<br /> send_live_notification()<br />if not favorite_twitcher_live and last_sent_sms is live_notification:<br /> send_live_is_over_notification()<br />
使用这种方法,我们将会在直播开始和结束后都接收到短信。这样我们就不会收到重复信息了。现在完美了么?让我们继续吧。
# reusing our Twilio client<br />last_messages_sent = client.messages.list(limit=1)<br />last_message_id = last_messages_sent[0].sid<br />last_message_data = client.messages(last_message_id).fetch()<br />last_message_content = last_message_data.body<br />
现在我们再一次将代码合起来:
import requests<br />from twilio.rest import Client<br />client = Client(, )<br /><br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br /><br />last_messages_sent = client.messages.list(limit=1)<br />if last_messages_sent:<br /> last_message_id = last_messages_sent[0].sid<br /> last_message_data = client.messages(last_message_id).fetch()<br /> last_message_content = last_message_data.body<br /> online_notified = "LIVE" in last_message_content<br /> offline_notified = not online_notified<br />else:<br /> online_notified, offline_notified = False, False<br /><br />if at_least_one_stream_active and not online_notified:<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />if not at_least_one_stream_active and not offline_notified:<br /> client.messages.create(body='OFFLINE !!!',from_=,to=)<br />
完成了!
你现在拥有一段不到 30 行的 Python 代码,可以在你喜欢的主播上线或者离线的时候发送短信通知给你,而且不会重复发送信息。
我们现在需要一种方法去托管代码,并且每 X 分钟执行一次这个程序。
托管代码
我们将使用 Heroku 去托管、执行该代码。Heroku 是一种简便的托管 app 到 Web 的方式。它的缺点是,比起其他的解决方案,价格会贵一些。幸运的是,他们有一个慷慨的免费计划允许我们做所有想做的事。
如果你之前没有Heroku 账户,那就创建一个吧。你同时也需要下载并且安装 Heroku 客户端。
现在你需要将你的 Python 脚本放到自己的文件夹内,记得加一个requirements.txt文件在里面。文件内容的开头如下:
requests<br />twilio<br />
这样可以确保 Heroku 下载正确的依赖程序
cd进入到该文件夹内同时执行heroku create --app <app name>。如果你进入到app 看板页,你将会看到你的新 app。
我们现在需要去初始化一个 git 仓库并且 push 代码到 Heroku:
git init<br />heroku git:remote -a <br />git add .<br />git commit -am 'Deploy breakthrough script'<br />git push heroku master<br />
现在你的 app 已经传到 Heroku,但是它还不可以干任何事。由于这个小脚本无法接受 HTTP 请求,访问.没法做任何事,但是这并不是一个问题。
为了让这个脚本全天候执行,我们需要使用一个简单的 Heroku 插件 Heroku Scheduler。在你的 app 看板页点击 Configure Add-ons 来安装插件。
接下来在搜索框输入 Heroku Scheduler:
点击搜索结果,并且按下 Provision 按钮。
如果你返回到你的 app 看板页,你将会看到 Heroku Scheduler:
点击 Heroku Scheduler链接去配置一个任务,点击 Create Job 按钮,在这里选择 10 minutes 的选项,之后选择执行命令python <name_of_your_script>.py,最后点击 Save job 按钮。
虽然到目前为止我们在 Heroku 上使用的所有东西都是免费的,但是 Heroku Scheduler 将会花费 25 美元每个月。而我们的程序是要秒级执行的。因为该脚本需要每 3 秒执行一次,所以每 10 分钟运行该项目,一个月下来将会花费 12 美分。
建议
我希望你喜欢这个项目,并且喜欢自己动手操作的过程。我们通过这不到 30 行代码实现了很多功能。不过这个项目还不够完美,这里我有一些改善的建议:
如果你有其他好主意,欢迎留言告诉我。
总结
我希望你喜欢上这篇文章并且通过这篇文章学到东西。我相信这样的项目是学习新工具和新概念的最好方式。最近我做了 Web scraping API,在做的过程中我也学到很多。如果你喜欢这种学习方式并且你想要做更多的事情,请在评论区留言。
我有许多别的想法,希望你将会喜欢上它们。如果你使用这段代码实现了别的东西,请一定分享给我啊。我相信这段代码有很多可能性。
Happy Coding!
非营利组织自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
你也想成为 freeCodeCamp 社区的贡献者吗?欢迎了解。 查看全部
30 行 Python 代码实现 Twitch 主播上线实时通知
大家好,今天我将开始写作一个新的文章系列,特别面向 Python 初学者。简言之,我将会尝试更多新的工具,编写尽可能少的代码,来完成一个有趣的项目。
例如,我们今天学习使用 Twilio API、Twitch API、在 Heroku 上发布项目。我将会教你用 30 行代码写一个 “Twitch 直播”短信通知工具,而且一个月只需要花费 12 美分。
前提
你只需要懂得怎么运行 Python 程序,以及会操作基本的 git 命令行(commit & push)。如果你需要学习一下这些知识点,可以看看下面两篇文章:
《Python 3 安装与设置指南》
《Git 最佳入门教程》fromAdrian Hajdin
将会学到的知识将要构建的项目
要求很简单:我们想要在某个主播正在直播的时候接收到一条短信通知,我们想要知道主播何时上线以及何时退出直播,并且这个通知程序全天都在自动运行。
我们将把整个项目分成 3 个部分。首先,我们看看如何通过程序知晓一个特定主播上线了,然后看看如何接收一条主播上线的通知短信,最后我们需要让这段代码每隔 X 分钟执行一次,这样我们就不会错过喜欢的主播的动态啦。
主播正在直播吗?
我们可以这样了解主播是否正在直播:访问主播的 URL,看看是否有 Live 徽章。
主播直播时候的截图
这个过程涉及到网络爬虫,而且这个功能不是 20 行左右的 Python 代码能完成的。Twitch 使用了非常多的 JS 脚本代码,一个简单的 request.get() 是不足以达到我们要求的。
对于使用爬虫去爬取直播信息,我们将会借助 Chrome 浏览器抓取这个网页的内容,如截图所示。这种方式是可行的,但是需要 30 行以上代码。如果你想要了解更多,可以参考我最近的文章《网页抓取指南》。
除了抓取 Twitch 网页这种方式外,我们还可以使用 Twitch 的 API。有的读者可能不了解 API 这个术语,这里我们解释一下:API 是应用程序编程接口,网站通过 API 向任何人(主要是开发者)公开特性和数据。Twitch 的 API 是通过 HTTP 协议对外开放,也就是说我们可以通过一个简单的 HTTP 请求去获取到大量信息以及做许多事情。
获取你的 API KEY
首先,你需要去创建一个 Twitch 的 API Key。许多 API 服务需要对访问者进行身份认证,以避免有人滥用 API,或者以限制某些人访问某些功能。
请按照以下步骤获取你的 API Key:
在屏幕底端,你可以看到你的 client-id,将它保存好,稍后会使用。
确认主播是否正在直播
我们手上有了 API key,现在就可以查询 Twitch 的 API 获取我们想要的信息。下面的代码给 Twitch 的 API 传递了正确的参数并且打印响应信息。
# requests is the go to package in python to make http request<br /># https://2.python-requests.org/en/master/<br />import requests<br /><br /># This is one of the route where Twich expose data, <br /># They have many more: https://dev.twitch.tv/docs<br />endpoint = "https://api.twitch.tv/helix/streams?"<br /><br /># In order to authenticate we need to pass our api key through header<br />headers = {"Client-ID": ""}<br /><br /># The previously set endpoint needs some parameter, here, the Twitcher we want to follow<br /># Disclaimer, I don't even know who this is, but he was the first one on Twich to have a live stream so I could have nice examples<br />params = {"user_login": "Solary"}<br /><br /># It is now time to make the actual request<br />response = request.get(endpoint, params=params, headers=headers)<br />print(response.json())<br />
输出信息就像下面这样:
{<br /> 'data':[<br /> {<br /> 'id':'35289543872',<br /> 'user_id':'174955366',<br /> 'user_name':'Solary',<br /> 'game_id':'21779',<br /> 'type':'live',<br /> 'title':"Wakz duoQ w/ Tioo - GM 400LP - On récupère le chall après les -250LP d'inactivité !",<br /> 'viewer_count':4073,<br /> 'started_at':'2019-08-14T07:01:59Z',<br /> 'language':'fr',<br /> 'thumbnail_url':'https://static-cdn.jtvnw.net/previews-ttv/live_user_solary-{width}x{height}.jpg',<br /> 'tag_ids':[<br /> '6f655045-9989-4ef7-8f85-1edcec42d648'<br /> ]<br /> }<br /> ],<br /> 'pagination':{<br /> 'cursor':'eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6MX19'<br /> }<br />}<br />
这个数据格式是一种易于阅读的 JSON 格式。data是一个包含所有当前直播的数组对象。type键表示这个直播间正在直播,此外type的值还可以为空(比如,在报错的时候)。
因此如果我们想要在 Python 里创建一个表示当前主播是否正在直播的布尔变量,我们需要去加上如下代码:
json_response = response.json()<br /><br /># We get only streams<br />streams = json_response.get('data', [])<br /><br /># We create a small function, (a lambda), that tests if a stream is live or not<br />is_active = lambda stream: stream.get('type') == 'live'<br /># We filter our array of streams with this function so we only keep streams that are active<br />streams_active = filter(is_active, streams)<br /><br /># any returns True if streams_active has at least one element, else False<br />at_least_one_stream_active = any(streams_active)<br /><br />print(at_least_one_stream_active)<br />
此时,at_least_one_stream_active变量是 True 的时候表示你喜欢的主播正在直播。
让我们现在看看如何获得短信通知。
现在给自己发一条短信!
那么我们将使用 Twilio API 自己发送一条短信。访问此处并且创建一个账号。当需要你手机验证的时候,填入你想要在此项目中接受短信的手机号码,这样你就可以使用 Twilio 为新用户提供的 15 美元的免费信用额度。一条短信 1 美分,足以支撑你的机器运行一年了。
访问console,你将会看到自己的 Account SID 和 Auth Token。请保留好它们以备后用。同时点击红色按钮“获得试用账号”,进行下一步,将试用账号也保存好以备后用。
使用 Python API 发送短信很简单,有软件包帮你一系列事情。使用pip install Twilio导入相应的包并且执行下面的代码:
from twilio.rest import Client<br />client = Client(, )<br />client.messages.create(<br /> body='Test MSG',from_=,to=)<br />
只需要这么点代码,你就可以给自己发一条通知短信了,是不是很棒?
整合所有代码
现在我们来整合所有代码,压缩到不到 30 行 Python 代码。
import requests<br />from twilio.rest import Client<br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br />if at_least_one_stream_active:<br /> client = Client(, )<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />
只留下了 10 多行代码!
避免重复通知
这段代码的效果很好,但是如果这段代码在服务器上每分钟执行一次,我们喜欢的主播一开启直播,我们就会每分钟都收到一条短信。
我们需要让程序知道它已经给我们发了主播上线直播的短信通知,别再重复发短信了。好消息是 Twilio API 提供检索历史消息的方法,因此我们仅仅需要检索发送的历史消息中是否包含已经发送过的主播正在直播的消息。
如下是我们要做的伪代码:
if favorite_twitcher_live and last_sent_sms is not live_notification:<br /> send_live_notification()<br />if not favorite_twitcher_live and last_sent_sms is live_notification:<br /> send_live_is_over_notification()<br />
使用这种方法,我们将会在直播开始和结束后都接收到短信。这样我们就不会收到重复信息了。现在完美了么?让我们继续吧。
# reusing our Twilio client<br />last_messages_sent = client.messages.list(limit=1)<br />last_message_id = last_messages_sent[0].sid<br />last_message_data = client.messages(last_message_id).fetch()<br />last_message_content = last_message_data.body<br />
现在我们再一次将代码合起来:
import requests<br />from twilio.rest import Client<br />client = Client(, )<br /><br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br /><br />last_messages_sent = client.messages.list(limit=1)<br />if last_messages_sent:<br /> last_message_id = last_messages_sent[0].sid<br /> last_message_data = client.messages(last_message_id).fetch()<br /> last_message_content = last_message_data.body<br /> online_notified = "LIVE" in last_message_content<br /> offline_notified = not online_notified<br />else:<br /> online_notified, offline_notified = False, False<br /><br />if at_least_one_stream_active and not online_notified:<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />if not at_least_one_stream_active and not offline_notified:<br /> client.messages.create(body='OFFLINE !!!',from_=,to=)<br />
完成了!
你现在拥有一段不到 30 行的 Python 代码,可以在你喜欢的主播上线或者离线的时候发送短信通知给你,而且不会重复发送信息。
我们现在需要一种方法去托管代码,并且每 X 分钟执行一次这个程序。
托管代码
我们将使用 Heroku 去托管、执行该代码。Heroku 是一种简便的托管 app 到 Web 的方式。它的缺点是,比起其他的解决方案,价格会贵一些。幸运的是,他们有一个慷慨的免费计划允许我们做所有想做的事。
如果你之前没有Heroku 账户,那就创建一个吧。你同时也需要下载并且安装 Heroku 客户端。
现在你需要将你的 Python 脚本放到自己的文件夹内,记得加一个requirements.txt文件在里面。文件内容的开头如下:
requests<br />twilio<br />
这样可以确保 Heroku 下载正确的依赖程序
cd进入到该文件夹内同时执行heroku create --app <app name>。如果你进入到app 看板页,你将会看到你的新 app。
我们现在需要去初始化一个 git 仓库并且 push 代码到 Heroku:
git init<br />heroku git:remote -a <br />git add .<br />git commit -am 'Deploy breakthrough script'<br />git push heroku master<br />
现在你的 app 已经传到 Heroku,但是它还不可以干任何事。由于这个小脚本无法接受 HTTP 请求,访问.没法做任何事,但是这并不是一个问题。
为了让这个脚本全天候执行,我们需要使用一个简单的 Heroku 插件 Heroku Scheduler。在你的 app 看板页点击 Configure Add-ons 来安装插件。
接下来在搜索框输入 Heroku Scheduler:
点击搜索结果,并且按下 Provision 按钮。
如果你返回到你的 app 看板页,你将会看到 Heroku Scheduler:
点击 Heroku Scheduler链接去配置一个任务,点击 Create Job 按钮,在这里选择 10 minutes 的选项,之后选择执行命令python <name_of_your_script>.py,最后点击 Save job 按钮。
虽然到目前为止我们在 Heroku 上使用的所有东西都是免费的,但是 Heroku Scheduler 将会花费 25 美元每个月。而我们的程序是要秒级执行的。因为该脚本需要每 3 秒执行一次,所以每 10 分钟运行该项目,一个月下来将会花费 12 美分。
建议
我希望你喜欢这个项目,并且喜欢自己动手操作的过程。我们通过这不到 30 行代码实现了很多功能。不过这个项目还不够完美,这里我有一些改善的建议:
如果你有其他好主意,欢迎留言告诉我。
总结
我希望你喜欢上这篇文章并且通过这篇文章学到东西。我相信这样的项目是学习新工具和新概念的最好方式。最近我做了 Web scraping API,在做的过程中我也学到很多。如果你喜欢这种学习方式并且你想要做更多的事情,请在评论区留言。
我有许多别的想法,希望你将会喜欢上它们。如果你使用这段代码实现了别的东西,请一定分享给我啊。我相信这段代码有很多可能性。
Happy Coding!
非营利组织自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
你也想成为 freeCodeCamp 社区的贡献者吗?欢迎了解。
不会写代码也能实现赏金自动化
网站优化 • 优采云 发表了文章 • 0 个评论 • 58 次浏览 • 2022-06-01 14:41
最近一直在研究自动化漏洞发现的技术,github 也有非常多优秀的集成工具,本着学习研究的心态,对这些工具进行了学习,今天来分享其中的一个,通过 bash 脚本将各种工具集成到一起,实现无需自己实现相关功能也能自动化漏洞发现。项目地址:
reconFTW 是一款优秀的信息收集工具,可以尽可能的收集子域名以及各种漏洞的检查,说的这么溜,到底是如何实现的?这是我比较感兴趣的,首先看一下安装脚本,看看都用到了哪些工具,如图:
数了数,大概用了三十个工具集成在一起,接下来我想知道这些工具都是干什么用的,那么就需要一个一个的看,为了节省大家的时间,我来给大家一一介绍一下。
0x01 gf (替代 grep,更方便提取结果中的关键内容)
项目地址:
其实 grep 能做非常多的事儿,但是由于参数复杂,每次使用可能都要去查询怎么用,如何组合参数,而这个工具就是为了能够在其他工具输出结果之后,通过这个工具来对结果进行整理,从而输出不同工具所需的参数内容,实现不同工具之间的数据共享来实现自动化的流程。
0x02 qsreplace (url 去重、参数替换)
项目地址:
在抓取到大量 URL 时,需要对这些 URL 进行去重,去掉相同接口、相同参数但是参数值不同的 URL,该工具可以替换 URL 中的参数值为某个指定字符串,替换之后在进行整体去重,就能实现相同接口、相同参数名的 URL 只保留一个,减少测试的目标,从而提升测试效率,实现效果:
0x03 Amass(子域名收集工具)
项目地址:
子域名收集的方式无非集中方式:dns 枚举、网络空间搜索、dns 反解析、搜索引擎、历史 dns 记录等,这款工具集成非常多的数据源,一个图就能一目了然:
0x04 ffuf (网页 fuzz 工具)
项目地址:
FUZZ 的目的就通过大量测试来发现隐藏的问题,比如隐藏的功能,隐藏的漏洞等,这个工具可以对网站的请求进行模糊测试,无论是 GET 参数还是 POST 参数,还能测试 header 中的关键字段以及目录等。比如 fuzz 目录:
0x05 github-subdomains(从 github 上发现子域名)
项目地址:
github 是程序员的聚集地,程序员的共享精神是一直存在的,他们也会时不时把自己在企业写的代码分享出去,从而给了我们一个信息收集的途径,那么这个工具就是通过 github 的代码搜索功能来实现子域名的收集。
0x06 waybackurls (从第三方平台获取目标网页内容)
项目地址:
这个工具之前的文章介绍过,主要从多个第三方平台来获取目标网页内容,有些平台通过自己实现爬虫的方式,抓取全网网站内容,然后提供给一些人使用,从而省去了直接访问目标的操作,在不接触目标的情况下也能获取目标网页内容。该工具主要获取的网页中的 URL 列表,为后续的漏洞测试做准备。
0x07 nuclei (poc 扫描工具)
项目地址:
nuclei 是国外知名的 POC 检测工具,它所收录的 POC 非常丰富,而且更新非常快,网络上一旦出现任何漏洞,其 POC 可能几个小时就能获得更新,而且完全免费开源,用过的都说好。
0x08 anew (内容去重工具)
项目地址:
类似于tee -a去掉文件中的重复行,没啥可介绍的,直接看案例吧:
0x09 notify (自动通报工具)
项目地址:
该工具可以将消息推送到多个平台(Slack / Discord / Telegram / Pushover / Email / Teams 等),主要方便在发现漏洞时,能够及时得知,然后提交,防止发现漏洞而被他人捷足先登的情况,当然怎么用,还要看自己的需求。
0x0A unfurl(快速提取 url 中的关键字段)
项目地址:
当我们获取到大量的 URL 时,我们需要提取 URL 中的主域名或者子域名输出列表,怎么办?你可以自己编写脚本实现,当然也可以使用这个工具,直接看效果吧:
0x0B 阶段性总结
这个项目集成了三十个工具,今天先分享十个吧,不知道大家是否有兴趣了解更多,如果还想要了解剩下的二十个工具分别是什么,有什么用,以及项目流程如何将所有工具集成自动化的,还请你点个再看,根据大家的意愿来决定是否更新下一篇完整介绍以及程序的流程讲解。
查看全部
不会写代码也能实现赏金自动化
最近一直在研究自动化漏洞发现的技术,github 也有非常多优秀的集成工具,本着学习研究的心态,对这些工具进行了学习,今天来分享其中的一个,通过 bash 脚本将各种工具集成到一起,实现无需自己实现相关功能也能自动化漏洞发现。项目地址:
reconFTW 是一款优秀的信息收集工具,可以尽可能的收集子域名以及各种漏洞的检查,说的这么溜,到底是如何实现的?这是我比较感兴趣的,首先看一下安装脚本,看看都用到了哪些工具,如图:
数了数,大概用了三十个工具集成在一起,接下来我想知道这些工具都是干什么用的,那么就需要一个一个的看,为了节省大家的时间,我来给大家一一介绍一下。
0x01 gf (替代 grep,更方便提取结果中的关键内容)
项目地址:
其实 grep 能做非常多的事儿,但是由于参数复杂,每次使用可能都要去查询怎么用,如何组合参数,而这个工具就是为了能够在其他工具输出结果之后,通过这个工具来对结果进行整理,从而输出不同工具所需的参数内容,实现不同工具之间的数据共享来实现自动化的流程。
0x02 qsreplace (url 去重、参数替换)
项目地址:
在抓取到大量 URL 时,需要对这些 URL 进行去重,去掉相同接口、相同参数但是参数值不同的 URL,该工具可以替换 URL 中的参数值为某个指定字符串,替换之后在进行整体去重,就能实现相同接口、相同参数名的 URL 只保留一个,减少测试的目标,从而提升测试效率,实现效果:
0x03 Amass(子域名收集工具)
项目地址:
子域名收集的方式无非集中方式:dns 枚举、网络空间搜索、dns 反解析、搜索引擎、历史 dns 记录等,这款工具集成非常多的数据源,一个图就能一目了然:
0x04 ffuf (网页 fuzz 工具)
项目地址:
FUZZ 的目的就通过大量测试来发现隐藏的问题,比如隐藏的功能,隐藏的漏洞等,这个工具可以对网站的请求进行模糊测试,无论是 GET 参数还是 POST 参数,还能测试 header 中的关键字段以及目录等。比如 fuzz 目录:
0x05 github-subdomains(从 github 上发现子域名)
项目地址:
github 是程序员的聚集地,程序员的共享精神是一直存在的,他们也会时不时把自己在企业写的代码分享出去,从而给了我们一个信息收集的途径,那么这个工具就是通过 github 的代码搜索功能来实现子域名的收集。
0x06 waybackurls (从第三方平台获取目标网页内容)
项目地址:
这个工具之前的文章介绍过,主要从多个第三方平台来获取目标网页内容,有些平台通过自己实现爬虫的方式,抓取全网网站内容,然后提供给一些人使用,从而省去了直接访问目标的操作,在不接触目标的情况下也能获取目标网页内容。该工具主要获取的网页中的 URL 列表,为后续的漏洞测试做准备。
0x07 nuclei (poc 扫描工具)
项目地址:
nuclei 是国外知名的 POC 检测工具,它所收录的 POC 非常丰富,而且更新非常快,网络上一旦出现任何漏洞,其 POC 可能几个小时就能获得更新,而且完全免费开源,用过的都说好。
0x08 anew (内容去重工具)
项目地址:
类似于tee -a去掉文件中的重复行,没啥可介绍的,直接看案例吧:
0x09 notify (自动通报工具)
项目地址:
该工具可以将消息推送到多个平台(Slack / Discord / Telegram / Pushover / Email / Teams 等),主要方便在发现漏洞时,能够及时得知,然后提交,防止发现漏洞而被他人捷足先登的情况,当然怎么用,还要看自己的需求。
0x0A unfurl(快速提取 url 中的关键字段)
项目地址:
当我们获取到大量的 URL 时,我们需要提取 URL 中的主域名或者子域名输出列表,怎么办?你可以自己编写脚本实现,当然也可以使用这个工具,直接看效果吧:
0x0B 阶段性总结
这个项目集成了三十个工具,今天先分享十个吧,不知道大家是否有兴趣了解更多,如果还想要了解剩下的二十个工具分别是什么,有什么用,以及项目流程如何将所有工具集成自动化的,还请你点个再看,根据大家的意愿来决定是否更新下一篇完整介绍以及程序的流程讲解。
2015数据可视化精选:35款好用工具,搞定信息图+可视化
网站优化 • 优采云 发表了文章 • 0 个评论 • 89 次浏览 • 2022-09-13 09:21
数据可视化为新闻媒体提供了生动有效的叙事方式和包装形式,也引领架构故事的思维变革。有哪些实用工具可以帮你讲好故事呢?深度君整合全年干货,为你精选35款优质可视化工具,指南+案例随你选。【点击“阅读原文”的相应链接,即可查看工具案例和更多细节哦】
1Plotly:简易交互式图表制作
数据新闻刚入门,想做简单的交互式数据图?Journalism.co.uk为我们介绍了一款适合数据分析、绘制图表的软件——Plotly,你还可以通过它连接Twitter等社交媒体。
Plotly上面有标示为“探索(Explore)”的按键,可以看到其他用户的数据可视化作品,这样你就能大致了解Plotly的图像成品。参考之后,你就可以在“工作区(Workspace)”绘制自己的图表啦。
一开始,需要点击“import”上传自己的数据文件,或者在添加“新网格(new grid)”后复制黏贴表格。这些数据栏都是自动编码的——如果你想改变它们在可视化作品中的名字,只需右键单击你想当做标题的那行,选择“使用行作为栏目标题(use row as column headers)”。
像infogram这样的软件可以根据数据表里的全部内容自动生成可视化效果,而Plotly与此不同,会另外让你选择哪一行哪一列需要在可视化作品里出现。在设计图表时,你可以在左边栏选择背景和颜色,添加注释、任意移动或伸缩尖头,随之调整文本。当图表完成,Plotly有多个隐私选项供你选择,还能生成嵌入代码,这样就能插入网站啦。更多操作细节,请查看Journalism.co.uk或者Plotly网站上的介绍吧。
2CartoDB: 不会编程,也可轻松学地图可视化
CartoDB是一款开源网络应用程序和交互式地图制作工具,以提供“一键式制图”功能闻名,也就是分析任何你上传的数据、自动制作地图以显示相关信息。起初CartoDB和数据新闻似乎相差千里,是两名西班牙研究生物多样性和自然保护的科学家为了更好地展示研究成果而建立的,没成想已经成为广大数据新闻记者的好帮手。目前CartoDB已经吸引12万用户创制40万张地图,将世界上所有有趣的主题——从全球粉丝对于Beyonce的最新专辑发布的实时反应到尼泊尔地震的损毁情况一一变成互动性强、好玩的可视化作品。
那CartoDB怎么用呢?
1注册账户
用户首先需要在CartoDB上注册账户、获得免费许可,也可以使用它提供的付费模板,有14天的有效期,可随时取消。
注册成功后,你会进入后台控制页面,刚开始是空白一片。如果上传了数据、制作好了地图,页面上会自动显示“你的数据”和“你的地图”选项,点选直接查看即可。
2创制数据表并检查
CartoDB一般以数据库模板为基础开始制图,因此登陆账户后第一步就是上传数据。可上传地理空间数据,如Shapefiles, GeoJSON等,可以设置公开或者个人可见。
用户可以传多少数据呢?仅有免费许可时,最多可上传50MB大小的数据。几乎能涵盖虽有的需求。点击“新数据集(New Dataset)”即可上传string,date,numeric和boolean众多格式。
载入数据之后,点击“连接数据集”。
如果你上传的数据集含有空间信息,就能从表格视图转换成地图视图。
上传数据后,先别急着制图,先检查检查输入的数据有没有差错,数据栏格式对不对。万一格式不统一,可以选择转换格式:
对于拖延症的小伙伴来讲,CartoDB无疑是一款治愈神器。在地图发布之前,CartoDB会提示你赶紧制作地图,而且平时定期邮件“轰炸”,告知新特性、提醒多练手。
制图之前,除了检查数据,更重要的准备是找到思路。CartoDB提供的建议固然能作为宝贵参考,但更重要的是自己先思考数据该怎么用、制作可视化的可行性有多大。先有框架,动手才快,效果才准。
3制作可视化数据上传完毕后,点击页面右上角“可视化(Visualize)”按钮即可制图。
CartoDB在屏幕右边栏贴心准备了一系列地图向导(Wizards),帮你用不同模式标注地图。例如有“简单模式(simple)”、“团簇模式(cluster)”、“等值域模式(choropleth)”(等值域地图是指根据数据的数值大小来标注地理区划的地图)等等。
点选模式后,CartoDB有众多创制图例和信息窗口的选项供你选择,也可以转换同的基础地图,看看哪一个最合适。
如果你按下右边的CSS按钮,就能自己修改可视化代码。如下图所示,可以轻松改变标注颜色,就像编辑文本一样:
4化静态为互动
是时候让地图动起来了!CartoDB的交互式数据库可运行Leaflet.js的插件和Google Maps插件来制作数据层,轻松完成这一过程。(这里还可以参考一个例子,即是依照行政区选择纽约住宅类型的可视化筛选器)【想得到手把手制作地铁动画指南和更多细节?请点击这里查看吧!】
需要更多地图制作工具/平台?深度君还推荐:
提供制图、查图、解图“一条龙”功能的ArcGIS Online Platform ,请查阅:《地图可视化制作和数据平台精选》,链接:
3NodeXL: 社会网络图制作助手
想快速学会如何将复杂的社会网络做成直观炫酷的可视化?NodeXL是你的绝佳选择:会用Excel,就会用它生成网络图。
NodeXL能计算以下这些社会网络研究中的常见指标:
程度(Degree),指每个节点拥有的关系链接数。对于指向型关系网,有内向程度(In-degree)和外向程度(Out-degree)之分,前者是指向内部的关系链接数,后者是指向外部的关系链接数。
特征向量中心性(Eigenvector centrality)所关注的不仅包括某一节点的关系链接数量,还包括与该节点相链接的是哪些节点,以及那些节点各自的关系链接数量是多少。
中介中心性(Betweenness centrality)则揭示出每个节点在不同关系网间提供“桥梁作用”的重要程度。它所特别标出的点,是一些移除后会导致整个网络崩塌的重要节点。
接近中心性(Closeness centrality)用于量度关系网中每个节点离其他节点的平均距离。它特别标出以较少连线与其他节点联系的点——这就类似于Kevin Bacon的六度理论(即在世界上任意两个陌生人之间只隔着六个人)。
在2014年计算机辅助报道协会巴尔的摩年会 (NICAR 2014) 上,BuzzFeed News 的科学与健康记者Peter Aldhous就介绍了NodeXL的用法,手把手教你分析美国共和党人中倾向于民主党的有哪些。请点击“阅读原文”里的相关链接,一起打开Excel跟着学吧!
如果觉得以上工具还不过瘾,我们还推荐:
4Analytics Vidhya:18款可视化好用工具一览
网站Analytics Vidhya整合出了一张【信息图可视化工具清单】,分为适用于一般用途和特定用途的两大类工具。一般用途的工具包括我们耳熟能详的Tableau, D3.js., R语言, Excel, Python等,也有专业人士更了解的Weka(主要用于数据挖掘,可生成简单图表,支持数据挖掘),SAS(用于数据建模),QlikView和QlikSense(用于数据整合和分享)。
在特殊用途工具中,有不少功能强大的选项:Instant Atlas(设计互动式图表,快速传达信息,提供图表和地图模板), WolframAlpha(智能呈现图表,无需配置),Cytoscape(高级分析和模型使用软件,用于社会网络或人际关系网的可视化呈现), NetworkX(是Python语言软件包,可用于生成经典图表、随机图表和综合性网络,Nodes可以包括文本、图片或者XML记录等格式), Flot(专门做线形图和柱状图,支持回调函数(Call back functions),需要jquery的相关知识才能驾驭)和Gephi(嵌入式3D生成器,以深入数据分析验证关系,普遍用于社会网络的可视化制作)。
关于更多信息,可以查看QlikSense的介绍,也可以参照Tableau和QlikView的对比。
此外,网站还推荐了诸如Dygraphs, Chart.js, Raphael, Highcharts等实用的JavaScript libraries作参考。如果你还想持续关注信息图和数据分析的新知,可以订阅Analytics Vidhya的邮件、关注它的Twitter和Facebook主页,掌握行业前沿资讯。【想查阅更多信息图制作信息,请点击“阅读原文”中的相关链接】
5软件开发师8款开源工具分享想知道灵动炫酷的可视化效果怎么做?怎么从零基础开始学?软件开发师、开源支持者Nitishi Tiwari撰文重点推荐了8款数据可视化的开源工具 (点击左边即可看到工具条目),一解众忧。如果觉得不过瘾,点击文末链接,有50款工具推荐。
Datawrapper:由欧洲的新闻学院开发,以便新闻机构做数据可视化作品。该工具基于图形用户界面(GUI),用简单四步即可绘图。
Chart JS:简洁的图表库。在生成图表之前,需要把函数库加进前端代码中,之后可以从函数库的应用程序编程接口(API)加图表,赋值。这款工具适合想要精确调整图表外观的人。它不适合想用现成工具的用户。
Charted:由Medium的产品科学组开发,是最简便的在线表格工具之一。只需粘贴谷歌表格或.csv文件,工具就会抓取数据,生成表格。Charted每三十分钟抓取数据,及时更新。
D3:是数据驱动文件(data-driven documents)的缩写,是一个JavaScript函数库。它使用数据创造并控制在网络浏览器里运行的交互图形,必须嵌入在html网页中,依赖矢量图形(SVG), 层叠式样式表(CSS3)等html的工具展示图形。需要编写代码,更适合掌握程序员技能的数据新闻人。
Dygraphs是一款基于JavaScript的函数库,十分灵活。这款工具的优势是可以处理大的数据集,并为终端用户生成互动数据。
Raw:基于网页的可视化工具。用户可以粘贴数据,在几步内生成图表。
Timeline: 用来做时间轴。按照规定格式将数据放在谷歌表格中,之后Timeline工具自动生成并发布,在网页上嵌入复制的代码即可。
Leaflet:一款轻便、适合移动端用户的JavaScript函数库,用来制作互动地图。
本文由“全球深度报道网”(the Global Investigative Journalism Network)独家奉献。若需转载,请提前联系授权,感谢支持! 查看全部
2015数据可视化精选:35款好用工具,搞定信息图+可视化
数据可视化为新闻媒体提供了生动有效的叙事方式和包装形式,也引领架构故事的思维变革。有哪些实用工具可以帮你讲好故事呢?深度君整合全年干货,为你精选35款优质可视化工具,指南+案例随你选。【点击“阅读原文”的相应链接,即可查看工具案例和更多细节哦】
1Plotly:简易交互式图表制作
数据新闻刚入门,想做简单的交互式数据图?Journalism.co.uk为我们介绍了一款适合数据分析、绘制图表的软件——Plotly,你还可以通过它连接Twitter等社交媒体。
Plotly上面有标示为“探索(Explore)”的按键,可以看到其他用户的数据可视化作品,这样你就能大致了解Plotly的图像成品。参考之后,你就可以在“工作区(Workspace)”绘制自己的图表啦。
一开始,需要点击“import”上传自己的数据文件,或者在添加“新网格(new grid)”后复制黏贴表格。这些数据栏都是自动编码的——如果你想改变它们在可视化作品中的名字,只需右键单击你想当做标题的那行,选择“使用行作为栏目标题(use row as column headers)”。
像infogram这样的软件可以根据数据表里的全部内容自动生成可视化效果,而Plotly与此不同,会另外让你选择哪一行哪一列需要在可视化作品里出现。在设计图表时,你可以在左边栏选择背景和颜色,添加注释、任意移动或伸缩尖头,随之调整文本。当图表完成,Plotly有多个隐私选项供你选择,还能生成嵌入代码,这样就能插入网站啦。更多操作细节,请查看Journalism.co.uk或者Plotly网站上的介绍吧。
2CartoDB: 不会编程,也可轻松学地图可视化
CartoDB是一款开源网络应用程序和交互式地图制作工具,以提供“一键式制图”功能闻名,也就是分析任何你上传的数据、自动制作地图以显示相关信息。起初CartoDB和数据新闻似乎相差千里,是两名西班牙研究生物多样性和自然保护的科学家为了更好地展示研究成果而建立的,没成想已经成为广大数据新闻记者的好帮手。目前CartoDB已经吸引12万用户创制40万张地图,将世界上所有有趣的主题——从全球粉丝对于Beyonce的最新专辑发布的实时反应到尼泊尔地震的损毁情况一一变成互动性强、好玩的可视化作品。
那CartoDB怎么用呢?
1注册账户
用户首先需要在CartoDB上注册账户、获得免费许可,也可以使用它提供的付费模板,有14天的有效期,可随时取消。
注册成功后,你会进入后台控制页面,刚开始是空白一片。如果上传了数据、制作好了地图,页面上会自动显示“你的数据”和“你的地图”选项,点选直接查看即可。
2创制数据表并检查
CartoDB一般以数据库模板为基础开始制图,因此登陆账户后第一步就是上传数据。可上传地理空间数据,如Shapefiles, GeoJSON等,可以设置公开或者个人可见。
用户可以传多少数据呢?仅有免费许可时,最多可上传50MB大小的数据。几乎能涵盖虽有的需求。点击“新数据集(New Dataset)”即可上传string,date,numeric和boolean众多格式。
载入数据之后,点击“连接数据集”。
如果你上传的数据集含有空间信息,就能从表格视图转换成地图视图。

上传数据后,先别急着制图,先检查检查输入的数据有没有差错,数据栏格式对不对。万一格式不统一,可以选择转换格式:
对于拖延症的小伙伴来讲,CartoDB无疑是一款治愈神器。在地图发布之前,CartoDB会提示你赶紧制作地图,而且平时定期邮件“轰炸”,告知新特性、提醒多练手。
制图之前,除了检查数据,更重要的准备是找到思路。CartoDB提供的建议固然能作为宝贵参考,但更重要的是自己先思考数据该怎么用、制作可视化的可行性有多大。先有框架,动手才快,效果才准。
3制作可视化数据上传完毕后,点击页面右上角“可视化(Visualize)”按钮即可制图。
CartoDB在屏幕右边栏贴心准备了一系列地图向导(Wizards),帮你用不同模式标注地图。例如有“简单模式(simple)”、“团簇模式(cluster)”、“等值域模式(choropleth)”(等值域地图是指根据数据的数值大小来标注地理区划的地图)等等。
点选模式后,CartoDB有众多创制图例和信息窗口的选项供你选择,也可以转换同的基础地图,看看哪一个最合适。
如果你按下右边的CSS按钮,就能自己修改可视化代码。如下图所示,可以轻松改变标注颜色,就像编辑文本一样:
4化静态为互动
是时候让地图动起来了!CartoDB的交互式数据库可运行Leaflet.js的插件和Google Maps插件来制作数据层,轻松完成这一过程。(这里还可以参考一个例子,即是依照行政区选择纽约住宅类型的可视化筛选器)【想得到手把手制作地铁动画指南和更多细节?请点击这里查看吧!】
需要更多地图制作工具/平台?深度君还推荐:
提供制图、查图、解图“一条龙”功能的ArcGIS Online Platform ,请查阅:《地图可视化制作和数据平台精选》,链接:
3NodeXL: 社会网络图制作助手
想快速学会如何将复杂的社会网络做成直观炫酷的可视化?NodeXL是你的绝佳选择:会用Excel,就会用它生成网络图。
NodeXL能计算以下这些社会网络研究中的常见指标:
程度(Degree),指每个节点拥有的关系链接数。对于指向型关系网,有内向程度(In-degree)和外向程度(Out-degree)之分,前者是指向内部的关系链接数,后者是指向外部的关系链接数。
特征向量中心性(Eigenvector centrality)所关注的不仅包括某一节点的关系链接数量,还包括与该节点相链接的是哪些节点,以及那些节点各自的关系链接数量是多少。
中介中心性(Betweenness centrality)则揭示出每个节点在不同关系网间提供“桥梁作用”的重要程度。它所特别标出的点,是一些移除后会导致整个网络崩塌的重要节点。
接近中心性(Closeness centrality)用于量度关系网中每个节点离其他节点的平均距离。它特别标出以较少连线与其他节点联系的点——这就类似于Kevin Bacon的六度理论(即在世界上任意两个陌生人之间只隔着六个人)。
在2014年计算机辅助报道协会巴尔的摩年会 (NICAR 2014) 上,BuzzFeed News 的科学与健康记者Peter Aldhous就介绍了NodeXL的用法,手把手教你分析美国共和党人中倾向于民主党的有哪些。请点击“阅读原文”里的相关链接,一起打开Excel跟着学吧!

如果觉得以上工具还不过瘾,我们还推荐:
4Analytics Vidhya:18款可视化好用工具一览
网站Analytics Vidhya整合出了一张【信息图可视化工具清单】,分为适用于一般用途和特定用途的两大类工具。一般用途的工具包括我们耳熟能详的Tableau, D3.js., R语言, Excel, Python等,也有专业人士更了解的Weka(主要用于数据挖掘,可生成简单图表,支持数据挖掘),SAS(用于数据建模),QlikView和QlikSense(用于数据整合和分享)。
在特殊用途工具中,有不少功能强大的选项:Instant Atlas(设计互动式图表,快速传达信息,提供图表和地图模板), WolframAlpha(智能呈现图表,无需配置),Cytoscape(高级分析和模型使用软件,用于社会网络或人际关系网的可视化呈现), NetworkX(是Python语言软件包,可用于生成经典图表、随机图表和综合性网络,Nodes可以包括文本、图片或者XML记录等格式), Flot(专门做线形图和柱状图,支持回调函数(Call back functions),需要jquery的相关知识才能驾驭)和Gephi(嵌入式3D生成器,以深入数据分析验证关系,普遍用于社会网络的可视化制作)。
关于更多信息,可以查看QlikSense的介绍,也可以参照Tableau和QlikView的对比。
此外,网站还推荐了诸如Dygraphs, Chart.js, Raphael, Highcharts等实用的JavaScript libraries作参考。如果你还想持续关注信息图和数据分析的新知,可以订阅Analytics Vidhya的邮件、关注它的Twitter和Facebook主页,掌握行业前沿资讯。【想查阅更多信息图制作信息,请点击“阅读原文”中的相关链接】
5软件开发师8款开源工具分享想知道灵动炫酷的可视化效果怎么做?怎么从零基础开始学?软件开发师、开源支持者Nitishi Tiwari撰文重点推荐了8款数据可视化的开源工具 (点击左边即可看到工具条目),一解众忧。如果觉得不过瘾,点击文末链接,有50款工具推荐。
Datawrapper:由欧洲的新闻学院开发,以便新闻机构做数据可视化作品。该工具基于图形用户界面(GUI),用简单四步即可绘图。
Chart JS:简洁的图表库。在生成图表之前,需要把函数库加进前端代码中,之后可以从函数库的应用程序编程接口(API)加图表,赋值。这款工具适合想要精确调整图表外观的人。它不适合想用现成工具的用户。
Charted:由Medium的产品科学组开发,是最简便的在线表格工具之一。只需粘贴谷歌表格或.csv文件,工具就会抓取数据,生成表格。Charted每三十分钟抓取数据,及时更新。
D3:是数据驱动文件(data-driven documents)的缩写,是一个JavaScript函数库。它使用数据创造并控制在网络浏览器里运行的交互图形,必须嵌入在html网页中,依赖矢量图形(SVG), 层叠式样式表(CSS3)等html的工具展示图形。需要编写代码,更适合掌握程序员技能的数据新闻人。
Dygraphs是一款基于JavaScript的函数库,十分灵活。这款工具的优势是可以处理大的数据集,并为终端用户生成互动数据。
Raw:基于网页的可视化工具。用户可以粘贴数据,在几步内生成图表。
Timeline: 用来做时间轴。按照规定格式将数据放在谷歌表格中,之后Timeline工具自动生成并发布,在网页上嵌入复制的代码即可。
Leaflet:一款轻便、适合移动端用户的JavaScript函数库,用来制作互动地图。
本文由“全球深度报道网”(the Global Investigative Journalism Network)独家奉献。若需转载,请提前联系授权,感谢支持!
网页源代码抓取工具之requests:python的一个web爬虫对象
网站优化 • 优采云 发表了文章 • 0 个评论 • 107 次浏览 • 2022-09-07 20:04
网页源代码抓取工具之requests:python的一个web爬虫对象。之前,因为web开发的规范化,爬虫爬取数据不是必须的。爬取web请求的数据(不包括网页源代码)。为什么要抓取web页面的数据?因为所有的web页面都是一样的。只要是ie浏览器就可以浏览网页。同时,它们的代码是相同的。所以,没有必要抓取。
如果不依赖浏览器,那么利用代理或者beautifulsoup库、lxml库、jquery库等都可以抓取出来。代理模式及实例。代理模式。代理模式需要配置三个东西:url,proxy,https。url是你的服务器回访问应用的地址。这个url其实并不是要寻找一个localhost或者127.0.0.1这样的地址。
实际上,服务器返回的是一个url地址,这个url和你寻找的http源地址在同一个地方。localhost就是所谓的源地址,如果你禁止请求,那么就会把url地址发送给服务器。proxy,代理的模式,让请求不能走socket,而是走get的方式。代理需要配置一些东西,如服务器ip,proxy_host等。
配置好之后,在请求里,都能返回一个通用的http头。https,端口加密技术,传输安全性等。网页页码抓取之requests_in_action看网页网页下的每一行,我们可以用requests_in_action()方法,来设置回调函数。requests_in_action()方法的返回值是response对象。
不过,我们可以传入两个参数,一个是url,一个是proxy_host。一个请求返回一个浏览器缓存相关的值,如none。另一个请求返回一个特定网站的https的值,如。这时的url就不是原来的localhost了。一个网站多个https请求,每个https请求都请求一次所在网站的https相关头,然后在后面对https相关的头加密。
这样所有的请求都设置同一个请求头,代码复用性极好。requests_in_action函数的一种返回值是https_ssl,这个值会显示请求所在网站的真实头的哈希值。https验证相关的东西,因为所有的源地址已经被加密过了,所以要验证。上面的参数,可以随意的加数据或者一些成为https相关值,比如:验证xmlhttprequest.status()的值,是否是https的validation相关,这个默认是auto。
验证sslresponse,true表示使用ssl证书。如果不是,则使用https。如果请求不用https,那么根据url,在post请求中带上一个私钥,请求一般用post方式,私钥可以保证数据安全。requests_action与其他很多的web爬虫对象一样,有一个queryset参数,我们需要知道每个请求要返回什么,这些返回数据如何保存到html文件中。这里的queryset就。 查看全部
网页源代码抓取工具之requests:python的一个web爬虫对象
网页源代码抓取工具之requests:python的一个web爬虫对象。之前,因为web开发的规范化,爬虫爬取数据不是必须的。爬取web请求的数据(不包括网页源代码)。为什么要抓取web页面的数据?因为所有的web页面都是一样的。只要是ie浏览器就可以浏览网页。同时,它们的代码是相同的。所以,没有必要抓取。
如果不依赖浏览器,那么利用代理或者beautifulsoup库、lxml库、jquery库等都可以抓取出来。代理模式及实例。代理模式。代理模式需要配置三个东西:url,proxy,https。url是你的服务器回访问应用的地址。这个url其实并不是要寻找一个localhost或者127.0.0.1这样的地址。

实际上,服务器返回的是一个url地址,这个url和你寻找的http源地址在同一个地方。localhost就是所谓的源地址,如果你禁止请求,那么就会把url地址发送给服务器。proxy,代理的模式,让请求不能走socket,而是走get的方式。代理需要配置一些东西,如服务器ip,proxy_host等。
配置好之后,在请求里,都能返回一个通用的http头。https,端口加密技术,传输安全性等。网页页码抓取之requests_in_action看网页网页下的每一行,我们可以用requests_in_action()方法,来设置回调函数。requests_in_action()方法的返回值是response对象。

不过,我们可以传入两个参数,一个是url,一个是proxy_host。一个请求返回一个浏览器缓存相关的值,如none。另一个请求返回一个特定网站的https的值,如。这时的url就不是原来的localhost了。一个网站多个https请求,每个https请求都请求一次所在网站的https相关头,然后在后面对https相关的头加密。
这样所有的请求都设置同一个请求头,代码复用性极好。requests_in_action函数的一种返回值是https_ssl,这个值会显示请求所在网站的真实头的哈希值。https验证相关的东西,因为所有的源地址已经被加密过了,所以要验证。上面的参数,可以随意的加数据或者一些成为https相关值,比如:验证xmlhttprequest.status()的值,是否是https的validation相关,这个默认是auto。
验证sslresponse,true表示使用ssl证书。如果不是,则使用https。如果请求不用https,那么根据url,在post请求中带上一个私钥,请求一般用post方式,私钥可以保证数据安全。requests_action与其他很多的web爬虫对象一样,有一个queryset参数,我们需要知道每个请求要返回什么,这些返回数据如何保存到html文件中。这里的queryset就。
网页源代码抓取工具实用(一)-一个网页工具类库
网站优化 • 优采云 发表了文章 • 0 个评论 • 143 次浏览 • 2022-09-05 08:02
网页源代码抓取工具实用(一)tinia-一个网页抓取工具类库在我们平时编程的时候,往往会遇到格式化问题,这不仅浪费时间,其实也没办法解决格式化问题。而我今天介绍的tinia就是一个可以从html网页源代码中爬取目标网页内容的工具类库。功能包括:-可以抓取html网页的内容文字,包括html代码框,方便输出在页面中的位置-抓取html格式化的图片文字等-抓取html变量声明以及对html变量声明进行相关数据转换-继承globaljsondatabasehandler类型的方法-包括获取网页url。
<p>注意:该类主要是处理一个可编程网页内容的网页源代码的变量声明及其对应的相关数据类型。使用首先我们需要了解tinia是如何进行输出html代码的。首先我们要打开其所在的目录,可以看到tinia.dll可用名称存在使用tinia程序可以通过分析本页面所有源代码生成一个新的本页面地址来提供抓取所需的内容文字,该页面有两个锚文本url,我们需要对这两个锚文本url进行抓取url添加方法如下: 查看全部
网页源代码抓取工具实用(一)-一个网页工具类库

网页源代码抓取工具实用(一)tinia-一个网页抓取工具类库在我们平时编程的时候,往往会遇到格式化问题,这不仅浪费时间,其实也没办法解决格式化问题。而我今天介绍的tinia就是一个可以从html网页源代码中爬取目标网页内容的工具类库。功能包括:-可以抓取html网页的内容文字,包括html代码框,方便输出在页面中的位置-抓取html格式化的图片文字等-抓取html变量声明以及对html变量声明进行相关数据转换-继承globaljsondatabasehandler类型的方法-包括获取网页url。

<p>注意:该类主要是处理一个可编程网页内容的网页源代码的变量声明及其对应的相关数据类型。使用首先我们需要了解tinia是如何进行输出html代码的。首先我们要打开其所在的目录,可以看到tinia.dll可用名称存在使用tinia程序可以通过分析本页面所有源代码生成一个新的本页面地址来提供抓取所需的内容文字,该页面有两个锚文本url,我们需要对这两个锚文本url进行抓取url添加方法如下:
如何安装网页源代码抓取工具?代码以及爬虫的实战总结
网站优化 • 优采云 发表了文章 • 0 个评论 • 182 次浏览 • 2022-09-02 20:00
网页源代码抓取工具,很多人都知道python以及selenium。但是还有一款免费且非常优秀的抓取工具extractor,一款最基础也是非常全面的抓取工具,而且提供丰富的python版本,功能也非常强大。强大的抓取功能是这个工具的基本功能。因为它提供的是python3的版本,那么如何安装它?详细教程请参考这篇文章python3代码抓取功能非常强大,完整的python爬虫代码以及爬虫的实战总结请参考这篇文章:首先下载安装。
下载好后,用管理员身份运行pip3installextractor。然后运行extractor.cmd。输入命令seleniumdriverseleniumdriver.chromedriver或者seleniumdriverseleniumdriver这里以seleniumdriver.chromedriver为例。
再运行pip3installextractor。pip3installextractor另外,如果你想自己把requests包安装进去,那么pip3installrequests。你也可以通过这篇文章来进行修改,最终,只需要运行命令extractor=extractor(extractor.chrome)即可安装下载好的requests包。
然后通过命令extractor=extractor(extractor.get_scrapy)即可获取你所需要的代码。接下来介绍下对源代码抓取的基本使用。1.开始使用源代码提取函数,如果你不知道该干什么,它也帮你搞定:#需要提取一段话句子,如果要快速查看抓取内容可以使用get_txt,对于爬虫系统,可以使用requests库。
#如果你已经看完上面的代码就开始使用seleniumdriver吧,因为你能够开始抓取代码内容和一些我们不需要代码显示的地方,如图片,图标。#最简单的方法是使用爬虫的自带提取代码库,在我们的requests库里,在提取自己想要内容前自带一个get方法去提取某个网页上的内容,例如刚才那个爬虫中,首先输入的源代码就是{{item.id}}get('');如果你喜欢这个代码库,你也可以修改自己爬虫的抓取代码的实现模块。
#我们需要抓取的内容是{{'id':''}},使用get_txt抓取的是{{id}}这个标识,然后使用一个方法去提取id。如下所示:#使用seleniumdriver抓取,一般做和url相关的内容的抓取,它都是使用chromedriver,其原理是让浏览器加载好页面后,返回给浏览器一个标签,它浏览器再去解析标签,看是否含有内容的url,去和页面中的id匹配,匹配成功,就返回我们要的内容。
<p>#这里chromedriver插件我们选用的是firefox,因为我们用firefox,所以一般它也会帮我们拦截掉web标准中不允许爬虫调用的东西。我们直接使用的是网页的url(id),调用get方法。#因为selenium是基于浏览器的,它当然要爬取页面中的 查看全部
如何安装网页源代码抓取工具?代码以及爬虫的实战总结
网页源代码抓取工具,很多人都知道python以及selenium。但是还有一款免费且非常优秀的抓取工具extractor,一款最基础也是非常全面的抓取工具,而且提供丰富的python版本,功能也非常强大。强大的抓取功能是这个工具的基本功能。因为它提供的是python3的版本,那么如何安装它?详细教程请参考这篇文章python3代码抓取功能非常强大,完整的python爬虫代码以及爬虫的实战总结请参考这篇文章:首先下载安装。
下载好后,用管理员身份运行pip3installextractor。然后运行extractor.cmd。输入命令seleniumdriverseleniumdriver.chromedriver或者seleniumdriverseleniumdriver这里以seleniumdriver.chromedriver为例。

再运行pip3installextractor。pip3installextractor另外,如果你想自己把requests包安装进去,那么pip3installrequests。你也可以通过这篇文章来进行修改,最终,只需要运行命令extractor=extractor(extractor.chrome)即可安装下载好的requests包。
然后通过命令extractor=extractor(extractor.get_scrapy)即可获取你所需要的代码。接下来介绍下对源代码抓取的基本使用。1.开始使用源代码提取函数,如果你不知道该干什么,它也帮你搞定:#需要提取一段话句子,如果要快速查看抓取内容可以使用get_txt,对于爬虫系统,可以使用requests库。

#如果你已经看完上面的代码就开始使用seleniumdriver吧,因为你能够开始抓取代码内容和一些我们不需要代码显示的地方,如图片,图标。#最简单的方法是使用爬虫的自带提取代码库,在我们的requests库里,在提取自己想要内容前自带一个get方法去提取某个网页上的内容,例如刚才那个爬虫中,首先输入的源代码就是{{item.id}}get('');如果你喜欢这个代码库,你也可以修改自己爬虫的抓取代码的实现模块。
#我们需要抓取的内容是{{'id':''}},使用get_txt抓取的是{{id}}这个标识,然后使用一个方法去提取id。如下所示:#使用seleniumdriver抓取,一般做和url相关的内容的抓取,它都是使用chromedriver,其原理是让浏览器加载好页面后,返回给浏览器一个标签,它浏览器再去解析标签,看是否含有内容的url,去和页面中的id匹配,匹配成功,就返回我们要的内容。
<p>#这里chromedriver插件我们选用的是firefox,因为我们用firefox,所以一般它也会帮我们拦截掉web标准中不允许爬虫调用的东西。我们直接使用的是网页的url(id),调用get方法。#因为selenium是基于浏览器的,它当然要爬取页面中的
网页代码编辑工具Dreamweaver 2018 for mac直装版Dw 20
网站优化 • 优采云 发表了文章 • 0 个评论 • 73 次浏览 • 2022-09-02 16:13
Adobe Dreamweaver 2018 for Mac 是由Adobe公司推出的一款非常实用的网页编辑软件。该款软件在功能上为用户们提供的一个相当便捷的web编辑环境,其中用户就算不具备过于专业的web编程基础,也可以通过拖拽控件的方式就可以轻松的完成整体页面的构建工作,当然如果想要设计出复杂的页面,还是要点击技术作为支撑的。而在这个版本中软件又会给我们带来怎样的闪光点呢?就让小编带大家预览几条吧,例如git的支持,有了该功能用户们将能更加方便的对版本做出管理工作;ECMAScript 6支持,代表着Web开发人员现在可以利用 ECMAScript 6的功能使用最新JavaScript语法进行编程等等,当然上面所述的只是软件更新的冰上一角,具体的功能还得用户们自己去探索挖掘。
查看全部
网页代码编辑工具Dreamweaver 2018 for mac直装版Dw 20

Adobe Dreamweaver 2018 for Mac 是由Adobe公司推出的一款非常实用的网页编辑软件。该款软件在功能上为用户们提供的一个相当便捷的web编辑环境,其中用户就算不具备过于专业的web编程基础,也可以通过拖拽控件的方式就可以轻松的完成整体页面的构建工作,当然如果想要设计出复杂的页面,还是要点击技术作为支撑的。而在这个版本中软件又会给我们带来怎样的闪光点呢?就让小编带大家预览几条吧,例如git的支持,有了该功能用户们将能更加方便的对版本做出管理工作;ECMAScript 6支持,代表着Web开发人员现在可以利用 ECMAScript 6的功能使用最新JavaScript语法进行编程等等,当然上面所述的只是软件更新的冰上一角,具体的功能还得用户们自己去探索挖掘。

学术福利弹 | N款文献管理工具,总有一款适合你
网站优化 • 优采云 发表了文章 • 0 个评论 • 83 次浏览 • 2022-08-30 18:19
学术福利弹 | N款文献管理工具,总有一款适合你
原创热爱学术的
重庆大学研究生会
cquyjsh
重庆大学研究生会是重庆大学党委领导下,党委研究生工作部、研究生院、校团委指导下的全校博士研究生、硕士研究生的群众性组织。
发表于
收录于合集
点击上方蓝字关注
作为一名热爱学术的研究生,小编每天要阅读许多篇论文,每篇论文要经历繁琐的检索、下载、阅读的过程;在读了许多文献之后,想去回顾一下以前读过的文章,却发现面对着电脑中各种文献的文件夹,怎么也找不到想要找的文章了。
这时候就需要一个文献管理工具,管理你的成千上百的文献,让你在写论文时很顺手地找到想要的文献。下面小编就来介绍几种常用的文献管理工具:
EndNote
EndNote 是SCI(Thomson Scientific 公司)的官方软件,自身具有强大的功能,是一款集文献检索、文摘及全文管理、文献共享等功能于一身的老牌软件,支持国际期刊的参考文献格式有3776种,写作模板几百种,用户可以方便的使用这些模板和格式,若准备写SCI论文,则有必要使用这款管理软件。
Endnote能直接连接上千个数据库,提高科技文献的检索效率,可以管理数十万条参考文献,不用担心文献太多放不下的问题。EndNote的好用性是有目共睹的,同时在现今的文献管理工具市场上具有较大使用率。
需要注意的是,这款软件是需要付费使用的,且对于学生来说价格较高,但在官网可以免费试用30天,大家可以体验过后再决定是否购买使用。
Citavi
Citavi来自瑞士Swiss Academic Software公司,其定位于“知识组织管理软件”,在欧洲(特别是德语区)被广泛使用。这款软件整合了知识管理、任务计划、PDF全文搜索等科研工作环节,具有强大和全面的功能。
Citavi有全功能免费版(支持每个项目插入不多于100篇文献),具有强大的参考文献编辑功能,可以从各个方面实现对参考文献的编辑需求。
Citavi很大一个特点是支持PDF阅读功能,且可以支持多种批注格式,并能管理、组织批注形成自己的知识库,同时也支持创建文献大纲,形成知识组织管理,更好的加强我们对文献的内容理解和组织构架形成。
Mendeley
Mendeley是一款Elsevier公司旗下的免费文献管理软件,所有人都可以在Mendeley上搜索到世界各地的学术文献,这些学术文件都是由用户自己上传到Mendeley“library”进行编辑管理,这款软件拥有整理组织文献、PDF标记和文件分享、网络备份的强大功能。
这款软件很大优势是强大的PDF识别和搜索功能,支持PDF标记,可以直接在PDF文档中做相应的标记和注释,突出文章中的重点内容。
拥有网页版、WINDOWS版、Mac版、Linux版,ios客户端等多个平台,用户可以根据需求选择不同平台使用。具有跨同步和云备份功能,登陆账号后,在Mendeley中导入过的PDF可实现跨平台同步,方便自己在不同平台使用、查看自己的文献,不需要重复导入。
Readcube
界面简洁漂亮,许多人正是因为它的界面而选择的这款软件。
这款软件功能性比较齐全、平台覆盖度也比较好,具有PDF文件自动识别和强化功能,免去了手工输入的繁琐,并且自带PDF阅读器,可进行内部搜索。但是也有一定的不足,比如系统不是特别稳过于依赖网速、特定项目如云同步功能需要收费等。
对于“颜控”小伙伴不妨可以试试这款软件,它的界面风格一定不会让你失望。
Zotero
是一个开放源代码的文献管理软件,可以协助我们收集、管理及引用研究资源,包括期刊、书籍等各类文献和网页、图片等。
Zotero最早是作为Firefox的插件存在的,但是现在已经发展成了一个独立的版本,但是它仍然和Firefox一样,无论是在windows、Mac还是Linux系统上都是可以跨平台使用的。
Zotero是可以免费使用的,相比于EndNotes来说,Zotero最大的特点是无限级的目录分类,一个目录下可以分为多个子目录,这样管理起来更加方面。另外zotero支持标签功能,为每个文献自动打上标签,使我们管理起文献来更加方便。
NoteEpress
NoteExpress 是一款国产软件,由北京爱琴海软件公司开发的一款专业级别的文献检索与管理系统,其核心功能涵盖“知识采集,管理,应用,挖掘”的知识管理的所有环节。
是国内专业的文献检索与管理系统,对中文文献非常友好,具有很强大的管理中文文献功能,可以实现内部搜索知网的文献库,进行批量下载,这款软件导入文献资料的速度也比国外同类软件更快。
文献资料与笔记(文章)功能协调一致,除管理参考文献资料外,还可以管理硬盘上其他文章或文件,作为个人知识管理系统。
对于中文文献较多的小伙伴不妨可以试试这款软件,说不定会爱上这款软件。
知网研学(原E-study)
是来自最大中文期刊资源中国知网官方版的研学平台软件,常用功能包括文献检索与识别、标记与添加笔记、生成参考文献、云同步等,并且可以在线写作,为受众提供与CNKI数据库密切结合的全新数字化体验。
CNKI E-study除了常规的文献管理软件的功能外,其最大的优势是与中国知网数据库同源、可以实现数据的云管理,通过同一账号在其他平台登陆可以实现数据更新,同时它能更好的和中国知网搜索引擎相结合,使得在数据管理上更加方便于受众。
另外这也是一款免费的软件,使用起来也比较方便,在市场使用率上占了不少的份额,如果大家的文献大多来源于知网的话,不妨可以考虑考虑这款软件。
Papers
是一款专业的文献管理工具,具有文档搜索、阅读和引用的功能。Papers把文献管理有关的各项活动流畅的组织在一起,主要具体功能包括文献导入、组织、阅读(注释)、自动匹配参考条目、搜索、在文档中插入引用、评点交流等。
Papers一大特色就是可以从PDF原文抓取文献信息,这样有利于更加清晰、自动化的管理文献,自动将文献进行分类,简洁明了。
Papers内部即可打开PDF文件,具有相当强大的文献阅读功能,最突出的特点是标注和笔记功能强大,有利于之后的学习和研究。
Papers是MAC系统独有的,如果电脑是MAC系统的小伙伴不妨可以尝试下这款软件。
通过以上对各软件的简单介绍
想必大家都有了一定的了解
可以选择更加适合自己的软件进行深入了解哟
小编接下来就详细介绍
其中四款软件的
基本使用方法和注意事项
Zotero
(1)文献导入
文献导入的方法有多种,包括网页抓取、抓取PDF元数据、手动输入等,小编在这里给大家介绍一下网页抓取和PDF元数据抓取的文献导入方法。
第一种是网页抓取的方法,首先需要安装叫Zotero Connector的插件,这个插件是安装在浏览器中的,在不同浏览器上搜索到的Zotero Connector插件直接安装即可安装在此浏览器中。Zotero对于英文文献的搜索是有着很好的支持的,如果想使用Google scholar、Jstor、Muse等国际主流的文献搜索引擎,只需要在相应的浏览器上下载好Zotero的插件,就可以直接在网页上进行文献抓取。
另外Zotero对中文文献也有不错的支持,下面以Firefox浏览器为例说明国内网页抓取的具体方法:
知网:Zotero可以有效的识别中国知网,大家有保存中文文献的需求可以采用这种方式(网页右上角Z字形图标)。
豆瓣:Zotero相比其他的文献管理工具来说有一个优势是可以识别豆瓣,豆瓣虽然严格来说不算一个学术网站,不过相信一个特定专业的小伙伴有时会用豆瓣进行一些书评活动。
维基百科:维基百科大家也用的非常多,Zotero也可以识别维基百科,让我们更方便的去管理我们的软件。
抓取PDF元数据的方法是指我们电脑里本身已经存在了下载好的PDF文件,需要添加到Zotero中。这时候我们只需要拖动PDF文件到Zotero中,点击右键选择:“重新抓取PDF元数据”,Zotero会根据PDF的文件信息自动进行网络搜索将文献信息补充完整,不过目前CNKI的文件不包含”元数据“,因此在CNKI中下载好的文献不能采用这种方法进行导入,但国际上几大著名的学术搜索引擎均支持这一功能。
(2)Zot-file插件
Zotero的很大一个特点是有许多的插件,我们可以根据自身需求选择不同插件,方便我们更好管理文献。其中一个很重要的插件为Zot-file插件,功能性很实用,几乎人手必备。这款插件功能很多,能够方便我们将需要的目录和文件本身链接在一起,达到有效管理PDF的目的,具体的Zot-file插件的下载和使用方法可以在网上查看相关的教程。
NoteEpress
(1)导入文献
NoteEpress可以在官方直接免费下载,下载好之后进行安装就可以导入文件了。
在导入文献前可以新建一个文件夹,将重要文献放到文件夹里,方便以后的管理。
右键点击文件夹,选择“导入文件“,把自己想要管理的文献添加到NoteEpress中,选择题录类型。
添加完成后即可对文献题录进行编辑,可以一边对照文献,一边完成表格中的内容,表格中具体完成哪些内容,可以根据自身的需求确定。
(2)文献引用
在写论文时,需要引用一些已经管理好的文献。在NoteEpress中点击要引用的文献,切换到word中,鼠标点击需要插入文献的位置,因为在下载NoteEpress完成后,插件会自动嵌入到word中(如在word中没发现相关的插件,可以在网上寻找教程),这时候只需要点击word上方的工具栏中“选择引用”即可。
Mendeley
(1)文献搜索
Mendeley有Literature Search可以搜索文献。但有一点请注意,除非使用的是Mendeley Premium版本,否则使用内置的文献搜索无法提供要查找的文章的全文。
(2)阅读界面
Mendeley提供了注释、高光、缩放等PDF阅读工具,右侧面板有文献的基本信息。
(3)引用格式切换
Mendeley的引用跟EndNote一样还是在Word里面进行插入。但Mendeley能够让用户轻松地切换引用格式,只需简单地点击style旁边的下拉框,然后选择想要的格式。如果想要的格式都不在选项中,只需点击More Styles。
图来自知乎用户@ EditSprings学术编辑
这时会有一个窗口弹出,可以下载更多的引用格式,通过点击 Get more Styles来获取更多引用风格和输入想要的格式并将其安装到Mendeley中。
图来自知乎用户@ EditSprings学术编辑
Papers 3
(1)文献搜索及导入
Papers 3有一个内置搜索引擎,其整合了大部分文献数据库,也支持各种字段的搜索。已下载的PDF文档也可以直接导入到Papers 3内,并对下载的文档进行自定义重命名。导入的文档,Papers 3会自动识别其文献信息,但是对早期一些图片PDF文档,其可能无法识别。就算识别不出来或者识别不全,还可以通过 Match 功能利用内置搜索引擎补全文献信息。
如果喜欢在浏览器直接在 Google、Google Scholar或者各个数据库搜索,也可以通过浏览器 Bookmarklet导入文献信息。在平常浏览专业杂志主页时,看到感兴趣的文献,可以用Chrome插件一键加入到Papers 3的Reading List里面以备不时之需。
(2)文献归类、管理
Papers 3 通过收藏集(Collections)来归类不同的文献信息,除此之外它还支持智能收藏集(Smart Collection),设置一定的规则,导入的文献信息就自动进入到这个收藏集里。Papers 3同时内置了功能足够的 PDF 阅读器,标注、做笔记、文字标签、颜色标签一应俱全,这一点跟PDF Expert 很相似。此外,Papers 3 支持 PDF 全文和笔记标注的搜索,在写文章的时候想起某句话、某个观点但就是想不起哪篇文献的时候就很有用了,一搜便知。
(3)文献引用
不同于 EndNote或者 Mendeley用 Word插件的方式,在写作的时候Papers 3通过快捷键唤起插入文献的小窗口,简单的搜索就可以快速插入想要引用的文献。引文格式当然是可以修改的,支持大多数主流期刊样式。
看完了这么多介绍
你是否找到属于你的文献管理工具呢?
或者你曾使用哪款软件
不妨在留言区给大家安利安利
说说你的看法~~ 查看全部
学术福利弹 | N款文献管理工具,总有一款适合你
学术福利弹 | N款文献管理工具,总有一款适合你
原创热爱学术的
重庆大学研究生会
cquyjsh
重庆大学研究生会是重庆大学党委领导下,党委研究生工作部、研究生院、校团委指导下的全校博士研究生、硕士研究生的群众性组织。
发表于
收录于合集
点击上方蓝字关注
作为一名热爱学术的研究生,小编每天要阅读许多篇论文,每篇论文要经历繁琐的检索、下载、阅读的过程;在读了许多文献之后,想去回顾一下以前读过的文章,却发现面对着电脑中各种文献的文件夹,怎么也找不到想要找的文章了。
这时候就需要一个文献管理工具,管理你的成千上百的文献,让你在写论文时很顺手地找到想要的文献。下面小编就来介绍几种常用的文献管理工具:
EndNote
EndNote 是SCI(Thomson Scientific 公司)的官方软件,自身具有强大的功能,是一款集文献检索、文摘及全文管理、文献共享等功能于一身的老牌软件,支持国际期刊的参考文献格式有3776种,写作模板几百种,用户可以方便的使用这些模板和格式,若准备写SCI论文,则有必要使用这款管理软件。
Endnote能直接连接上千个数据库,提高科技文献的检索效率,可以管理数十万条参考文献,不用担心文献太多放不下的问题。EndNote的好用性是有目共睹的,同时在现今的文献管理工具市场上具有较大使用率。
需要注意的是,这款软件是需要付费使用的,且对于学生来说价格较高,但在官网可以免费试用30天,大家可以体验过后再决定是否购买使用。
Citavi
Citavi来自瑞士Swiss Academic Software公司,其定位于“知识组织管理软件”,在欧洲(特别是德语区)被广泛使用。这款软件整合了知识管理、任务计划、PDF全文搜索等科研工作环节,具有强大和全面的功能。
Citavi有全功能免费版(支持每个项目插入不多于100篇文献),具有强大的参考文献编辑功能,可以从各个方面实现对参考文献的编辑需求。
Citavi很大一个特点是支持PDF阅读功能,且可以支持多种批注格式,并能管理、组织批注形成自己的知识库,同时也支持创建文献大纲,形成知识组织管理,更好的加强我们对文献的内容理解和组织构架形成。
Mendeley
Mendeley是一款Elsevier公司旗下的免费文献管理软件,所有人都可以在Mendeley上搜索到世界各地的学术文献,这些学术文件都是由用户自己上传到Mendeley“library”进行编辑管理,这款软件拥有整理组织文献、PDF标记和文件分享、网络备份的强大功能。
这款软件很大优势是强大的PDF识别和搜索功能,支持PDF标记,可以直接在PDF文档中做相应的标记和注释,突出文章中的重点内容。
拥有网页版、WINDOWS版、Mac版、Linux版,ios客户端等多个平台,用户可以根据需求选择不同平台使用。具有跨同步和云备份功能,登陆账号后,在Mendeley中导入过的PDF可实现跨平台同步,方便自己在不同平台使用、查看自己的文献,不需要重复导入。
Readcube
界面简洁漂亮,许多人正是因为它的界面而选择的这款软件。
这款软件功能性比较齐全、平台覆盖度也比较好,具有PDF文件自动识别和强化功能,免去了手工输入的繁琐,并且自带PDF阅读器,可进行内部搜索。但是也有一定的不足,比如系统不是特别稳过于依赖网速、特定项目如云同步功能需要收费等。
对于“颜控”小伙伴不妨可以试试这款软件,它的界面风格一定不会让你失望。
Zotero
是一个开放源代码的文献管理软件,可以协助我们收集、管理及引用研究资源,包括期刊、书籍等各类文献和网页、图片等。
Zotero最早是作为Firefox的插件存在的,但是现在已经发展成了一个独立的版本,但是它仍然和Firefox一样,无论是在windows、Mac还是Linux系统上都是可以跨平台使用的。
Zotero是可以免费使用的,相比于EndNotes来说,Zotero最大的特点是无限级的目录分类,一个目录下可以分为多个子目录,这样管理起来更加方面。另外zotero支持标签功能,为每个文献自动打上标签,使我们管理起文献来更加方便。
NoteEpress
NoteExpress 是一款国产软件,由北京爱琴海软件公司开发的一款专业级别的文献检索与管理系统,其核心功能涵盖“知识采集,管理,应用,挖掘”的知识管理的所有环节。

是国内专业的文献检索与管理系统,对中文文献非常友好,具有很强大的管理中文文献功能,可以实现内部搜索知网的文献库,进行批量下载,这款软件导入文献资料的速度也比国外同类软件更快。
文献资料与笔记(文章)功能协调一致,除管理参考文献资料外,还可以管理硬盘上其他文章或文件,作为个人知识管理系统。
对于中文文献较多的小伙伴不妨可以试试这款软件,说不定会爱上这款软件。
知网研学(原E-study)
是来自最大中文期刊资源中国知网官方版的研学平台软件,常用功能包括文献检索与识别、标记与添加笔记、生成参考文献、云同步等,并且可以在线写作,为受众提供与CNKI数据库密切结合的全新数字化体验。
CNKI E-study除了常规的文献管理软件的功能外,其最大的优势是与中国知网数据库同源、可以实现数据的云管理,通过同一账号在其他平台登陆可以实现数据更新,同时它能更好的和中国知网搜索引擎相结合,使得在数据管理上更加方便于受众。
另外这也是一款免费的软件,使用起来也比较方便,在市场使用率上占了不少的份额,如果大家的文献大多来源于知网的话,不妨可以考虑考虑这款软件。
Papers
是一款专业的文献管理工具,具有文档搜索、阅读和引用的功能。Papers把文献管理有关的各项活动流畅的组织在一起,主要具体功能包括文献导入、组织、阅读(注释)、自动匹配参考条目、搜索、在文档中插入引用、评点交流等。
Papers一大特色就是可以从PDF原文抓取文献信息,这样有利于更加清晰、自动化的管理文献,自动将文献进行分类,简洁明了。
Papers内部即可打开PDF文件,具有相当强大的文献阅读功能,最突出的特点是标注和笔记功能强大,有利于之后的学习和研究。
Papers是MAC系统独有的,如果电脑是MAC系统的小伙伴不妨可以尝试下这款软件。
通过以上对各软件的简单介绍
想必大家都有了一定的了解
可以选择更加适合自己的软件进行深入了解哟
小编接下来就详细介绍
其中四款软件的
基本使用方法和注意事项
Zotero
(1)文献导入
文献导入的方法有多种,包括网页抓取、抓取PDF元数据、手动输入等,小编在这里给大家介绍一下网页抓取和PDF元数据抓取的文献导入方法。
第一种是网页抓取的方法,首先需要安装叫Zotero Connector的插件,这个插件是安装在浏览器中的,在不同浏览器上搜索到的Zotero Connector插件直接安装即可安装在此浏览器中。Zotero对于英文文献的搜索是有着很好的支持的,如果想使用Google scholar、Jstor、Muse等国际主流的文献搜索引擎,只需要在相应的浏览器上下载好Zotero的插件,就可以直接在网页上进行文献抓取。
另外Zotero对中文文献也有不错的支持,下面以Firefox浏览器为例说明国内网页抓取的具体方法:
知网:Zotero可以有效的识别中国知网,大家有保存中文文献的需求可以采用这种方式(网页右上角Z字形图标)。
豆瓣:Zotero相比其他的文献管理工具来说有一个优势是可以识别豆瓣,豆瓣虽然严格来说不算一个学术网站,不过相信一个特定专业的小伙伴有时会用豆瓣进行一些书评活动。
维基百科:维基百科大家也用的非常多,Zotero也可以识别维基百科,让我们更方便的去管理我们的软件。
抓取PDF元数据的方法是指我们电脑里本身已经存在了下载好的PDF文件,需要添加到Zotero中。这时候我们只需要拖动PDF文件到Zotero中,点击右键选择:“重新抓取PDF元数据”,Zotero会根据PDF的文件信息自动进行网络搜索将文献信息补充完整,不过目前CNKI的文件不包含”元数据“,因此在CNKI中下载好的文献不能采用这种方法进行导入,但国际上几大著名的学术搜索引擎均支持这一功能。
(2)Zot-file插件
Zotero的很大一个特点是有许多的插件,我们可以根据自身需求选择不同插件,方便我们更好管理文献。其中一个很重要的插件为Zot-file插件,功能性很实用,几乎人手必备。这款插件功能很多,能够方便我们将需要的目录和文件本身链接在一起,达到有效管理PDF的目的,具体的Zot-file插件的下载和使用方法可以在网上查看相关的教程。
NoteEpress
(1)导入文献

NoteEpress可以在官方直接免费下载,下载好之后进行安装就可以导入文件了。
在导入文献前可以新建一个文件夹,将重要文献放到文件夹里,方便以后的管理。
右键点击文件夹,选择“导入文件“,把自己想要管理的文献添加到NoteEpress中,选择题录类型。
添加完成后即可对文献题录进行编辑,可以一边对照文献,一边完成表格中的内容,表格中具体完成哪些内容,可以根据自身的需求确定。
(2)文献引用
在写论文时,需要引用一些已经管理好的文献。在NoteEpress中点击要引用的文献,切换到word中,鼠标点击需要插入文献的位置,因为在下载NoteEpress完成后,插件会自动嵌入到word中(如在word中没发现相关的插件,可以在网上寻找教程),这时候只需要点击word上方的工具栏中“选择引用”即可。
Mendeley
(1)文献搜索
Mendeley有Literature Search可以搜索文献。但有一点请注意,除非使用的是Mendeley Premium版本,否则使用内置的文献搜索无法提供要查找的文章的全文。
(2)阅读界面
Mendeley提供了注释、高光、缩放等PDF阅读工具,右侧面板有文献的基本信息。
(3)引用格式切换
Mendeley的引用跟EndNote一样还是在Word里面进行插入。但Mendeley能够让用户轻松地切换引用格式,只需简单地点击style旁边的下拉框,然后选择想要的格式。如果想要的格式都不在选项中,只需点击More Styles。
图来自知乎用户@ EditSprings学术编辑
这时会有一个窗口弹出,可以下载更多的引用格式,通过点击 Get more Styles来获取更多引用风格和输入想要的格式并将其安装到Mendeley中。
图来自知乎用户@ EditSprings学术编辑
Papers 3
(1)文献搜索及导入
Papers 3有一个内置搜索引擎,其整合了大部分文献数据库,也支持各种字段的搜索。已下载的PDF文档也可以直接导入到Papers 3内,并对下载的文档进行自定义重命名。导入的文档,Papers 3会自动识别其文献信息,但是对早期一些图片PDF文档,其可能无法识别。就算识别不出来或者识别不全,还可以通过 Match 功能利用内置搜索引擎补全文献信息。
如果喜欢在浏览器直接在 Google、Google Scholar或者各个数据库搜索,也可以通过浏览器 Bookmarklet导入文献信息。在平常浏览专业杂志主页时,看到感兴趣的文献,可以用Chrome插件一键加入到Papers 3的Reading List里面以备不时之需。
(2)文献归类、管理
Papers 3 通过收藏集(Collections)来归类不同的文献信息,除此之外它还支持智能收藏集(Smart Collection),设置一定的规则,导入的文献信息就自动进入到这个收藏集里。Papers 3同时内置了功能足够的 PDF 阅读器,标注、做笔记、文字标签、颜色标签一应俱全,这一点跟PDF Expert 很相似。此外,Papers 3 支持 PDF 全文和笔记标注的搜索,在写文章的时候想起某句话、某个观点但就是想不起哪篇文献的时候就很有用了,一搜便知。
(3)文献引用
不同于 EndNote或者 Mendeley用 Word插件的方式,在写作的时候Papers 3通过快捷键唤起插入文献的小窗口,简单的搜索就可以快速插入想要引用的文献。引文格式当然是可以修改的,支持大多数主流期刊样式。
看完了这么多介绍
你是否找到属于你的文献管理工具呢?
或者你曾使用哪款软件
不妨在留言区给大家安利安利
说说你的看法~~
无代码爬虫,看完就会:你不需要会爬虫,你只要会爬虫软件就好了(二)
网站优化 • 优采云 发表了文章 • 0 个评论 • 71 次浏览 • 2022-07-19 03:28
上期我们说到了无代码爬虫软件优采云采集器的模板使用,也留了一个实际工作中遇到的问题没有解决,今天我们就来一起看看解决思路吧~
(戳链接回顾上期)
让我们回顾一下上期的问题:批量点击一系列相似网页的某一个按钮。
这个同学要下载的是各个市政府的报告。他是这样做的:
江苏省有13个市、98个区,每个区有五个报告要下载。
那就是要点击98*5=490次,刷新切换98次页面……点是不可能点的。
打开网站链接,按F12,观察网页源代码:
我知道有朋友看到这里已经头大了,心里已经在骂看不懂别演示了。
学习嘛,你不痛苦这几分钟,那就等着痛苦手动做吧。
这里先补充一个小背景知识:
网页是由代码写的,我们访问看到的图片、文字,都是网站管理员早就写好的代码,因此,打开任一网页,你看到的东西都是代码呈现的。我们要做的就是把代码给扒下来。
任意浏览器,按F12都会调出开发者调试工具,点控制台选项,不需要看懂啥,只用点调出来的页面的那个长得像“鼠标”的按钮。
鼠标滑动到网页上我们原本需要人工点击的位置,点击。右边的窗口就自动帮我们定位到了相应的代码位置。
就是它!搞它!盘它!就是它害得我们有这么多工作!
理论上来说,在网页页面上点击链接和点击代码窗口的链接,效果会是一样的,但是这次我们点了没有反应,那一定是哪里出了问题,别着急。我先去点击原页面看看是咋回事。
刚刚动画里演示的虽然很快,但是我们能看到,在点击下载按钮后,先是有一个页面弹了出来。幸好我眼疾手快把这个页面给截了下来。长这样:
有没有发现什么!
虽然我看不懂代码,但是在网页源代码那张图里,那个标红了的超链接,好像是上图网址里面的uuid!
大胆猜测,是不是只要我能够拿到所有市区的uuid,然后把网址链接补充完整,那就能实现批量下载报告了?!
说干就干啊!
不要着急~让我们来复习一下今天学的~
1
网页都是由代码写的,按F12可以查看源代码。
2
需要抓取的内容在源代码里,按F12上像个小箭头一样的标识就可以定位了。
3
网页地址似乎有一种特殊的结构,有规律可循。
下期预告
abxshdhsdicidsihciohivhi占位占位
如何利用优采云自定义任务
如何批量访问一系列链接
……
快来一起学习~! 查看全部
无代码爬虫,看完就会:你不需要会爬虫,你只要会爬虫软件就好了(二)
上期我们说到了无代码爬虫软件优采云采集器的模板使用,也留了一个实际工作中遇到的问题没有解决,今天我们就来一起看看解决思路吧~
(戳链接回顾上期)
让我们回顾一下上期的问题:批量点击一系列相似网页的某一个按钮。
这个同学要下载的是各个市政府的报告。他是这样做的:
江苏省有13个市、98个区,每个区有五个报告要下载。
那就是要点击98*5=490次,刷新切换98次页面……点是不可能点的。
打开网站链接,按F12,观察网页源代码:
我知道有朋友看到这里已经头大了,心里已经在骂看不懂别演示了。
学习嘛,你不痛苦这几分钟,那就等着痛苦手动做吧。
这里先补充一个小背景知识:

网页是由代码写的,我们访问看到的图片、文字,都是网站管理员早就写好的代码,因此,打开任一网页,你看到的东西都是代码呈现的。我们要做的就是把代码给扒下来。
任意浏览器,按F12都会调出开发者调试工具,点控制台选项,不需要看懂啥,只用点调出来的页面的那个长得像“鼠标”的按钮。
鼠标滑动到网页上我们原本需要人工点击的位置,点击。右边的窗口就自动帮我们定位到了相应的代码位置。
就是它!搞它!盘它!就是它害得我们有这么多工作!
理论上来说,在网页页面上点击链接和点击代码窗口的链接,效果会是一样的,但是这次我们点了没有反应,那一定是哪里出了问题,别着急。我先去点击原页面看看是咋回事。
刚刚动画里演示的虽然很快,但是我们能看到,在点击下载按钮后,先是有一个页面弹了出来。幸好我眼疾手快把这个页面给截了下来。长这样:
有没有发现什么!
虽然我看不懂代码,但是在网页源代码那张图里,那个标红了的超链接,好像是上图网址里面的uuid!
大胆猜测,是不是只要我能够拿到所有市区的uuid,然后把网址链接补充完整,那就能实现批量下载报告了?!
说干就干啊!
不要着急~让我们来复习一下今天学的~

1
网页都是由代码写的,按F12可以查看源代码。
2
需要抓取的内容在源代码里,按F12上像个小箭头一样的标识就可以定位了。
3
网页地址似乎有一种特殊的结构,有规律可循。
下期预告
abxshdhsdicidsihciohivhi占位占位
如何利用优采云自定义任务
如何批量访问一系列链接
……
快来一起学习~!
隐藏新闻广告前阵子看手机视频被恶意植入广告
网站优化 • 优采云 发表了文章 • 0 个评论 • 58 次浏览 • 2022-07-10 10:01
网页源代码抓取工具专门支持各种网站并提供实时抓取效果:unblockyoukudock加速中心idacast,中国科技网新闻源抓取工具--隐藏新闻广告前阵子看手机视频被恶意植入广告,从此对浏览器广告青睐有加,douyinmedialibraryindouyinmedialibrary-majority-freeavailabledirectlyforjavascriptads.手机视频app不能免流,于是玩了ncsoftsafariautoplayer。
装完safarispacecornercapture后,发现新闻视频之类的可以自动加速:用过mediapipe等工具发现都不太友好,官方网站有一个可用按钮:chromeautoplay:onlinechromegamesforwindowsandmacnewsstand编辑器推荐官方的livecopyvideo工具evernote中的播放列表及手机输入法的弹幕功能youtube视频助手-youtubeampisanaccessoriesdeviceworkingwithyourolderpcandmobilephones视频工具dokuwikibookbox视频加速工具vlc插件vlcintroduction解码工具nekopara网页缓存助手ffplayer解码器-ie8.0的浏览器兼容性工具nvidiayoutubetagsystemtagmessagecodebingplayernestedscript解码器ffplayer-tvwebstreamingupdateplayertagsystemcodebingcodebingcodebingfreematrixobjectgametagtagsforchinesestandstone-opengamesforchineseinstrumentalstreamingupdateplayerpocket快捷输入工具youdao解码器-youdaouiportabletracking,speedtracking,calculator,limits,inspections,storeandconverter-tamer密码管理工具密码管理器火狐extensionjsbrowser,比如小贝idfapijsbrowser-youdao只是浏览器账号登录就能对他进行第三方应用或者在浏览器上下载游戏等等,也可以集成googlechrome应用商店,体验类似safari中的chrome应用程序enableextensionkeyboardintegrationandnewkeyboardextensionsplayersmtp二维码生成器官方新出的二维码生成器,手机端浏览速度很快可以生成flash的密码浏览器,还支持抓取外部网页的二维码我的推荐者fythobzid25没说起来的/这里上官网看一下:。 查看全部
隐藏新闻广告前阵子看手机视频被恶意植入广告

网页源代码抓取工具专门支持各种网站并提供实时抓取效果:unblockyoukudock加速中心idacast,中国科技网新闻源抓取工具--隐藏新闻广告前阵子看手机视频被恶意植入广告,从此对浏览器广告青睐有加,douyinmedialibraryindouyinmedialibrary-majority-freeavailabledirectlyforjavascriptads.手机视频app不能免流,于是玩了ncsoftsafariautoplayer。

装完safarispacecornercapture后,发现新闻视频之类的可以自动加速:用过mediapipe等工具发现都不太友好,官方网站有一个可用按钮:chromeautoplay:onlinechromegamesforwindowsandmacnewsstand编辑器推荐官方的livecopyvideo工具evernote中的播放列表及手机输入法的弹幕功能youtube视频助手-youtubeampisanaccessoriesdeviceworkingwithyourolderpcandmobilephones视频工具dokuwikibookbox视频加速工具vlc插件vlcintroduction解码工具nekopara网页缓存助手ffplayer解码器-ie8.0的浏览器兼容性工具nvidiayoutubetagsystemtagmessagecodebingplayernestedscript解码器ffplayer-tvwebstreamingupdateplayertagsystemcodebingcodebingcodebingfreematrixobjectgametagtagsforchinesestandstone-opengamesforchineseinstrumentalstreamingupdateplayerpocket快捷输入工具youdao解码器-youdaouiportabletracking,speedtracking,calculator,limits,inspections,storeandconverter-tamer密码管理工具密码管理器火狐extensionjsbrowser,比如小贝idfapijsbrowser-youdao只是浏览器账号登录就能对他进行第三方应用或者在浏览器上下载游戏等等,也可以集成googlechrome应用商店,体验类似safari中的chrome应用程序enableextensionkeyboardintegrationandnewkeyboardextensionsplayersmtp二维码生成器官方新出的二维码生成器,手机端浏览速度很快可以生成flash的密码浏览器,还支持抓取外部网页的二维码我的推荐者fythobzid25没说起来的/这里上官网看一下:。
Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 68 次浏览 • 2022-07-09 08:33
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获 查看全部
Node.js 实现抢票小工具&短信通知提醒
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。

我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj

let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获
专业网站优化公司网站优化软件,旺道网站优化软件?
网站优化 • 优采云 发表了文章 • 0 个评论 • 71 次浏览 • 2022-06-25 08:53
加:sum932,备注: 霸屏,免费领取100种推广获客方法。
就在昨天我们操作万词霸屏的网站,使用新方法,站长平台百度权重升级至7:
专业网站优化公司
专业网站优化公司
使用新方法后,网站流量也从原来每天3000多IP,直接提升到每天30000多IP,流量直接提升了10倍。
专业网站优化公司
使用新方法关键词排名数量也增加至12.69万个:
seo软件优化工具软件,为什么要用seo软件优化工具软件,因为seo软件优化工具软件集可以快速让网站收录以及关键词排名,今天给大家分享一款万能的SEO工具多个功能集合。一键建站+内容以及资源采集+伪原创+主动推送给搜索引擎收录以及各种内容处理等下会以图片的形式给大家展示。大家注意看图。
假设你想应用你中文网站他们的文原本捕捉和留住运用者,这种他们就能透过阅读器来找到他们想的东西,所以你必需有好的文本和运用者体验。假设好的外部链接能进步公交站点的 pr 值和预览速度,所以提供高效率的文本是让运用者留下来,这种阅读器蜘蛛就能Murviel更快。
特别是随着许多“伪创译者”该文的呈现,互联网平台上的许多文本高度反复,优质创译者该文稀缺。因而,假设你的中文网站有非常好的创译者文本,并且预览频率很高,所以它能更好地被阅读器收录下来。一段时间后,你的中文网站会被阅读器辨认为有用的中文网站,接着权重会逐步增加,从而增加中文网站在搜索结果中的高度关注度。
2、统计数据发掘
中文网站SEO还有这种两个企业方面产业开展也是一种非常具有重要,那是中文网站的统计数据技术预测,只要引见了小学生他们学习中文网站的技术细节,才可能去更精确的抓住强化的方向,做到补漏伯粉。这个时分,中文网站的笔记管理工作是由于他们要运用的两个国度非常重要的辅助工具了,透过重要信息计算机网络笔记他们能对中文网站的意向展开深居简出的引见。
同时,还需要对各品种类型的相关统计数据展开预测,如 cnss 统计预测、腾讯名列词大小和24小时腾讯收录于等。那些统计数据不只能让你发现中文网站上的难题,还能让你发现名列规则的变化,并快速修正以处理那些难题。
关于新手朋友来说,会经常听他人提及到网站的三大标签,其实网站的三大标签就是title、keywords、description。当我们在搜索引擎中搜索一个关键词的时分,在搜索结果页中我们首先看到的就是网站的标题和描绘,标题就是标签中的title,描绘就是标签中的description,keywords需求在源代码里面才干看到。下面我们一同来细致地理解一下这三大标签。
网站三大标签
1、title标签。
Title标签是网站的重中之重,从用户的角度来看,用户搜索关键词之后,首先看到的就是你网站的标题,这个时分就要看你的标题能否能处理用户的需求,能否能吸收用户点击了。
从搜索引擎的角度来看,蜘蛛来到我们网站的时分,首先会找到网站的title标签,从上到下看代码,从上到下抓取,看title标签的时分,能够晓得整个页面的主题是什么。优化是以title为准的,是整个网站的中心,在搜索引擎中占有重要位置,是参与排名的重中之重。
2、keywords标签。
很多站长在keywords中堆砌关键词,因而搜索引擎曾经不是很注重了,搜索引擎蜘蛛是能够抓取的keywords,但是关于关键词排名没有任何提升的协助,没有任何权重上的协助,它不参与排名。那么能否就不需求设置keywords标签了呢?设置Keywords标签是为了页面的完好水平,还是有间接参与排名的要素的,也能够让我们在运用工具查询关键词排名的时分,更便当看排在第几位。
3、description标签。
搜索引擎蜘蛛也是可以抓取的,也是不参与排名的,但是写好描绘也是很重要的,当我们在搜索结果页中寻觅我们需求的东西的时分,在看完标题之后,更多的会是去看描绘,若你的描绘写得有吸收力,那么就会吸收到用户点击。在前20名当中,若描绘写得好,吸收了用户的点击量,排名也就被推进了。假如不写描绘,那么搜索引擎就会从你的内容中调用一段来做描绘,因而还是写比拟好一些。 查看全部
专业网站优化公司网站优化软件,旺道网站优化软件?
加:sum932,备注: 霸屏,免费领取100种推广获客方法。
就在昨天我们操作万词霸屏的网站,使用新方法,站长平台百度权重升级至7:
专业网站优化公司
专业网站优化公司
使用新方法后,网站流量也从原来每天3000多IP,直接提升到每天30000多IP,流量直接提升了10倍。
专业网站优化公司
使用新方法关键词排名数量也增加至12.69万个:
seo软件优化工具软件,为什么要用seo软件优化工具软件,因为seo软件优化工具软件集可以快速让网站收录以及关键词排名,今天给大家分享一款万能的SEO工具多个功能集合。一键建站+内容以及资源采集+伪原创+主动推送给搜索引擎收录以及各种内容处理等下会以图片的形式给大家展示。大家注意看图。
假设你想应用你中文网站他们的文原本捕捉和留住运用者,这种他们就能透过阅读器来找到他们想的东西,所以你必需有好的文本和运用者体验。假设好的外部链接能进步公交站点的 pr 值和预览速度,所以提供高效率的文本是让运用者留下来,这种阅读器蜘蛛就能Murviel更快。
特别是随着许多“伪创译者”该文的呈现,互联网平台上的许多文本高度反复,优质创译者该文稀缺。因而,假设你的中文网站有非常好的创译者文本,并且预览频率很高,所以它能更好地被阅读器收录下来。一段时间后,你的中文网站会被阅读器辨认为有用的中文网站,接着权重会逐步增加,从而增加中文网站在搜索结果中的高度关注度。
2、统计数据发掘
中文网站SEO还有这种两个企业方面产业开展也是一种非常具有重要,那是中文网站的统计数据技术预测,只要引见了小学生他们学习中文网站的技术细节,才可能去更精确的抓住强化的方向,做到补漏伯粉。这个时分,中文网站的笔记管理工作是由于他们要运用的两个国度非常重要的辅助工具了,透过重要信息计算机网络笔记他们能对中文网站的意向展开深居简出的引见。
同时,还需要对各品种类型的相关统计数据展开预测,如 cnss 统计预测、腾讯名列词大小和24小时腾讯收录于等。那些统计数据不只能让你发现中文网站上的难题,还能让你发现名列规则的变化,并快速修正以处理那些难题。
关于新手朋友来说,会经常听他人提及到网站的三大标签,其实网站的三大标签就是title、keywords、description。当我们在搜索引擎中搜索一个关键词的时分,在搜索结果页中我们首先看到的就是网站的标题和描绘,标题就是标签中的title,描绘就是标签中的description,keywords需求在源代码里面才干看到。下面我们一同来细致地理解一下这三大标签。
网站三大标签
1、title标签。
Title标签是网站的重中之重,从用户的角度来看,用户搜索关键词之后,首先看到的就是你网站的标题,这个时分就要看你的标题能否能处理用户的需求,能否能吸收用户点击了。
从搜索引擎的角度来看,蜘蛛来到我们网站的时分,首先会找到网站的title标签,从上到下看代码,从上到下抓取,看title标签的时分,能够晓得整个页面的主题是什么。优化是以title为准的,是整个网站的中心,在搜索引擎中占有重要位置,是参与排名的重中之重。
2、keywords标签。
很多站长在keywords中堆砌关键词,因而搜索引擎曾经不是很注重了,搜索引擎蜘蛛是能够抓取的keywords,但是关于关键词排名没有任何提升的协助,没有任何权重上的协助,它不参与排名。那么能否就不需求设置keywords标签了呢?设置Keywords标签是为了页面的完好水平,还是有间接参与排名的要素的,也能够让我们在运用工具查询关键词排名的时分,更便当看排在第几位。
3、description标签。
搜索引擎蜘蛛也是可以抓取的,也是不参与排名的,但是写好描绘也是很重要的,当我们在搜索结果页中寻觅我们需求的东西的时分,在看完标题之后,更多的会是去看描绘,若你的描绘写得有吸收力,那么就会吸收到用户点击。在前20名当中,若描绘写得好,吸收了用户的点击量,排名也就被推进了。假如不写描绘,那么搜索引擎就会从你的内容中调用一段来做描绘,因而还是写比拟好一些。
(实战)Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 51 次浏览 • 2022-06-25 08:35
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/ProxySetting看下,哦原来我之前设置成了8888
然后找到Charles的help/LocalIPAddress,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开Moretools/Networkconditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOSProxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为breakpoints,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
<p>这里为日期
如果有余票则显示余票数量</p>
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
<p>cnpm install axios --save</p>
定时任务node-schedule
<p>cnpm install node-schedule --save</p>
node端选择dom节点工具cheerio
<p>cnpm install cheerio --save</p>
腾讯发短信的依赖包qcloudsms_js
<p>cnpm install qcloudsms_js</p>
热更新包,诺豆的妈妈,nodemon(其实不用也可以)
<p>cnpm install nodemon --save-dev</p>
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<p>const axios = require('axios')
const querystring = require("querystring"); //序列化对象,用qs也行,都一样
let QcloudSms = require("qcloudsms_js");
let cheerio = require('cheerio');
let schedule = require('node-schedule');</p>
然后我们先定义请求参数,来一个obj
<p>let obj = {
data: {
lineId: 111130, //路线id
vehTime: 0722, //发车时间,
startTime: 0751, //预计上车时间
onStationId: 564492, //预定的站点id
offStationId: 17990,//到站id
onStationName: '宝安交通运输局③', //预定的站点名称
offStationName: "深港产学研基地",//预定到站名称
tradePrice: 0,//总金额
saleDates: '17',//车票日期
beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据
},
phoneNumber: 123123123, //用户手机号,接收短信的手机号
cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie
day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票
}</p>
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票的操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
<p>class QueryTicket{
/**
*Creates an instance of QueryTicket.
* @param {Object} { data, phoneNumber, cookie, day }
* @param data {Object} 请求余票接口的requery参数
* @param phoneNumber {Number} 用户手机号,短信需要用到
* @param cookie {String} cookie信息
* @params day {String} 某日的票,如'18'
* @memberof QueryTicket 请求余票接口
*/
constructor({ data, phoneNumber, cookie, day }) {
this.data = data
this.cookie = cookie
this.day = day
this.phoneNumber = phoneNumber
this.postData = querystring.stringify(data)
this.times = 0; //记录次数
let stop = false //通过特定接口才能修改stop值,防止外部随意串改
this.getStop = function () { //获取是否停止
return stop
}
this.setStop = function (ifStop) { //设置是否停止
stop = ifStop
}
}
}</p>
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
init(){}//初始化
handleQueryTicket(){}//查询余票的逻辑
requestTicket(){} //调用查询余票接口
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化,因为涉及到异步请求,所以我们使用`async await`
async init(){
let ticketList = await this.handleQueryTicket() //返回查询到的余票数组
}
//查询余票的逻辑
handleQueryTicket(){
let ticketList = [] //余票数组
let res = await this.requestTicket()
this.times++ //计数器,记录请求查询多少次
let str = res.data.replace(/\\/g, "") //格式化返回值
let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据
let list = $(".main").find(".b") //查找是否有余票的dom节点
// 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码
if (!list.length) {
console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)
return
}
<br />
// 如果有余票
list.each((idx, item) => {
let str = $(item).html() //str这时格式是21&$x4F59;0
//最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已
//因此要在下一步对其进行格式化
let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true)
let data = {
day: arr[0],
ticketLeft: arr[1]
}
<br />
//如果是要抢指定日期的票
if (this.day) {
//如果有指定日期的余票
if (parseInt(data.day) === parseInt(data.day)) {
ticketList.push(data)
}
} else {
//如果不是,则返回查询到的所有余票
ticketList.push(data)
}
})
return ticketList
}
//调用查询余票接口
requestTicket(){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
let year = new Date().getFullYear() //年份,
let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号
let {
onStationName,//起始站点名
offStationName,//结束站点名
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站台id
offStationId //到站的站台id
} = this.data // 初始化的数据
<br />
let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"
let dateStr = ""; //车票日期
let tickAmount = "" //总张数
ticketList.forEach(item => {
dateStr = dateStr + `${year}-${month}-${item.day},`
tickAmount = tickAmount + `${item.ticketLeft}张,`
})
<br />
let buyTicket = {
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站点id
offStationId,//目标站点id
tradePrice: '5', //金额
saleDates: dateStr.slice(0, -1),
payType: '2' //支付方式,微信支付
}
<br />
// 调用购票接口
let data = querystring.stringify(buyTicket)
let res = await this.requestOrder(data) //返回json数据,是否购票成功等等
//把发短信所需要数据都要传入
return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })
}//购票相关逻辑
//调用购票接口
requestOrder(obj){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
//执行通知逻辑
this.handleInfoUser(resParse)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
//购票代码...
}
//调用购票接口
requestOrder(obj){
//购票接口请求代码...
}
//通知用户的逻辑
async handleInfoUser(parseData){
//获取上一步购票的response数据和我们拼接的数据
let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData
//如果购票成功,则返回500
if (returnCode === "500") {
let res = await this.sendMsg({
dateStr, //日期
tickAmount: tickAmount.slice(0, -1), //总张数
station, //站点
lineName, //巴士名称/路线名称
tradePrice,//总价
startTime,//出发时间
phoneNumber: this.phoneNumber,//手机号
})
//如果发信成功,则不再进行抢票操作
if (res.result === 0 && res.errmsg === "OK") {
this.setStop(true)
} else {
//失败不做任何操作
console.log(res.errmsg)
}
} else {
//失败不做任何操作
console.log(resParse['returnInfo'])
}
}
//发短信接口
sendMSg(){
let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
let appid = 140034324; // SDK AppID 以1400开头
// 短信应用 SDK AppKey
let appkey = "asdfdsvajwienin23493nadsnzxc";
// 短信模板 ID,需要在短信控制台中申请
let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
// 签名
let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请
// 实例化 QcloudSms
let qcloudsms = QcloudSms(appid, appkey);
let ssender = qcloudsms.SmsSingleSender();
// 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容
let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];
//用promise来封装下异步操作
return new Promise((resolve, reject) => {
ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {
if (err) {
reject(err)
} else {
resolve(resData)
}
});
})
}
}</p>
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
<p>// 定时任务
class SetInter {
constructor({ timer, fn }) {
this.timer = timer // 每几秒执行
this.fn = fn //执行的回调
this.rule = new schedule.RecurrenceRule(); //实例化一个对象
this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已
this.init()
}
setRule() {
let rule = [];
let i = 1;
while (i {
this.fn() // 定时调用传入的回调方法
});
}
}</p>
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
<p> data: { //用户1
lineId: 111130,
vehTime: 0722,
startTime: 0751,
onStationId: 564492,
offStationId: 17990,
onStationName: '宝安交通运输局③',
offStationName: "深港产学研基地",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 123123123,
cookie: 'JSESSIONID=TESTCOOKIE',
day: "17"
}
let obj2 = { //用户2
data: {
lineId: 134423,
vehTime: 1820,
startTime: 1855,
onStationId: 4322,
offStationId: 53231,
onStationName: '百度国际大厦',
offStationName: "裕安路口",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 175932123124,
cookie: 'JSESSIONID=TESTCOOKIE',
day: ""
}
let ticket = new QueryTicket(obj) //用户1
let ticket2 = new QueryTicket(obj2) //用户2
<br />
new SetInter({
timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图
fn: function () {
[ticket,ticket2].map(item => { //同时进行两个用户的抢票
if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢
item.init()
} else { // 如果抢到票了,则不继续抢票
console.log('stop')
}
})
}
})</p>
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
- END -
<p style="letter-spacing: 0.544px;text-align: right;">分享前端好文,点亮 <strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;">在看</strong><strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;"> </strong></p> 查看全部
(实战)Node.js 实现抢票小工具&短信通知提醒
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/ProxySetting看下,哦原来我之前设置成了8888
然后找到Charles的help/LocalIPAddress,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开Moretools/Networkconditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOSProxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为breakpoints,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
<p>这里为日期
如果有余票则显示余票数量</p>
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
<p>cnpm install axios --save</p>
定时任务node-schedule
<p>cnpm install node-schedule --save</p>
node端选择dom节点工具cheerio
<p>cnpm install cheerio --save</p>
腾讯发短信的依赖包qcloudsms_js
<p>cnpm install qcloudsms_js</p>
热更新包,诺豆的妈妈,nodemon(其实不用也可以)
<p>cnpm install nodemon --save-dev</p>
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<p>const axios = require('axios')
const querystring = require("querystring"); //序列化对象,用qs也行,都一样
let QcloudSms = require("qcloudsms_js");
let cheerio = require('cheerio');
let schedule = require('node-schedule');</p>
然后我们先定义请求参数,来一个obj
<p>let obj = {
data: {
lineId: 111130, //路线id
vehTime: 0722, //发车时间,
startTime: 0751, //预计上车时间
onStationId: 564492, //预定的站点id
offStationId: 17990,//到站id
onStationName: '宝安交通运输局③', //预定的站点名称
offStationName: "深港产学研基地",//预定到站名称
tradePrice: 0,//总金额
saleDates: '17',//车票日期
beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据
},
phoneNumber: 123123123, //用户手机号,接收短信的手机号
cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie
day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票
}</p>
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票的操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
<p>class QueryTicket{
/**
*Creates an instance of QueryTicket.
* @param {Object} { data, phoneNumber, cookie, day }
* @param data {Object} 请求余票接口的requery参数
* @param phoneNumber {Number} 用户手机号,短信需要用到
* @param cookie {String} cookie信息
* @params day {String} 某日的票,如'18'
* @memberof QueryTicket 请求余票接口
*/
constructor({ data, phoneNumber, cookie, day }) {
this.data = data
this.cookie = cookie
this.day = day
this.phoneNumber = phoneNumber
this.postData = querystring.stringify(data)
this.times = 0; //记录次数
let stop = false //通过特定接口才能修改stop值,防止外部随意串改
this.getStop = function () { //获取是否停止
return stop
}
this.setStop = function (ifStop) { //设置是否停止
stop = ifStop
}
}
}</p>
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
init(){}//初始化
handleQueryTicket(){}//查询余票的逻辑
requestTicket(){} //调用查询余票接口
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化,因为涉及到异步请求,所以我们使用`async await`
async init(){
let ticketList = await this.handleQueryTicket() //返回查询到的余票数组
}
//查询余票的逻辑
handleQueryTicket(){
let ticketList = [] //余票数组
let res = await this.requestTicket()
this.times++ //计数器,记录请求查询多少次
let str = res.data.replace(/\\/g, "") //格式化返回值
let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据
let list = $(".main").find(".b") //查找是否有余票的dom节点
// 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码
if (!list.length) {
console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)
return
}
<br />
// 如果有余票
list.each((idx, item) => {
let str = $(item).html() //str这时格式是21&$x4F59;0
//最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已
//因此要在下一步对其进行格式化
let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true)
let data = {
day: arr[0],
ticketLeft: arr[1]
}
<br />
//如果是要抢指定日期的票
if (this.day) {
//如果有指定日期的余票
if (parseInt(data.day) === parseInt(data.day)) {
ticketList.push(data)
}
} else {
//如果不是,则返回查询到的所有余票
ticketList.push(data)
}
})
return ticketList
}
//调用查询余票接口
requestTicket(){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleBuyTicket(){} //购票相关逻辑
requestOrder(){}//调用购票接口
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
let year = new Date().getFullYear() //年份,
let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号
let {
onStationName,//起始站点名
offStationName,//结束站点名
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站台id
offStationId //到站的站台id
} = this.data // 初始化的数据
<br />
let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"
let dateStr = ""; //车票日期
let tickAmount = "" //总张数
ticketList.forEach(item => {
dateStr = dateStr + `${year}-${month}-${item.day},`
tickAmount = tickAmount + `${item.ticketLeft}张,`
})
<br />
let buyTicket = {
lineId,//线路id
vehTime,//发车时间
startTime,//预计上车时间
onStationId,//上车的站点id
offStationId,//目标站点id
tradePrice: '5', //金额
saleDates: dateStr.slice(0, -1),
payType: '2' //支付方式,微信支付
}
<br />
// 调用购票接口
let data = querystring.stringify(buyTicket)
let res = await this.requestOrder(data) //返回json数据,是否购票成功等等
//把发短信所需要数据都要传入
return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })
}//购票相关逻辑
//调用购票接口
requestOrder(obj){
return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
"Cookie": this.cookie
}
})
}
handleInfoUser(){}//通知用户的逻辑
sendMSg(){} //发短信接口
}</p>
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
<p>class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代码...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票传入购票逻辑方法,返回短信通知所需要的数据
let resParse = await this.handleBuyTicket(ticketList)
//执行通知逻辑
this.handleInfoUser(resParse)
}
}
<br />
//查询余票的逻辑
async handleQueryTicket(){
// 查询余票代码...
}
//调用查询余票接口
requestTicket(){
//调用查询余票接口代码...
}
//购票相关逻辑
async handleBuyTicket(ticketList){
//购票代码...
}
//调用购票接口
requestOrder(obj){
//购票接口请求代码...
}
//通知用户的逻辑
async handleInfoUser(parseData){
//获取上一步购票的response数据和我们拼接的数据
let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData
//如果购票成功,则返回500
if (returnCode === "500") {
let res = await this.sendMsg({
dateStr, //日期
tickAmount: tickAmount.slice(0, -1), //总张数
station, //站点
lineName, //巴士名称/路线名称
tradePrice,//总价
startTime,//出发时间
phoneNumber: this.phoneNumber,//手机号
})
//如果发信成功,则不再进行抢票操作
if (res.result === 0 && res.errmsg === "OK") {
this.setStop(true)
} else {
//失败不做任何操作
console.log(res.errmsg)
}
} else {
//失败不做任何操作
console.log(resParse['returnInfo'])
}
}
//发短信接口
sendMSg(){
let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
let appid = 140034324; // SDK AppID 以1400开头
// 短信应用 SDK AppKey
let appkey = "asdfdsvajwienin23493nadsnzxc";
// 短信模板 ID,需要在短信控制台中申请
let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
// 签名
let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请
// 实例化 QcloudSms
let qcloudsms = QcloudSms(appid, appkey);
let ssender = qcloudsms.SmsSingleSender();
// 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容
let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];
//用promise来封装下异步操作
return new Promise((resolve, reject) => {
ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {
if (err) {
reject(err)
} else {
resolve(resData)
}
});
})
}
}</p>
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
<p>// 定时任务
class SetInter {
constructor({ timer, fn }) {
this.timer = timer // 每几秒执行
this.fn = fn //执行的回调
this.rule = new schedule.RecurrenceRule(); //实例化一个对象
this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已
this.init()
}
setRule() {
let rule = [];
let i = 1;
while (i {
this.fn() // 定时调用传入的回调方法
});
}
}</p>
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
<p> data: { //用户1
lineId: 111130,
vehTime: 0722,
startTime: 0751,
onStationId: 564492,
offStationId: 17990,
onStationName: '宝安交通运输局③',
offStationName: "深港产学研基地",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 123123123,
cookie: 'JSESSIONID=TESTCOOKIE',
day: "17"
}
let obj2 = { //用户2
data: {
lineId: 134423,
vehTime: 1820,
startTime: 1855,
onStationId: 4322,
offStationId: 53231,
onStationName: '百度国际大厦',
offStationName: "裕安路口",
tradePrice: 0,
saleDates: '',
beginDate: '',
},
phoneNumber: 175932123124,
cookie: 'JSESSIONID=TESTCOOKIE',
day: ""
}
let ticket = new QueryTicket(obj) //用户1
let ticket2 = new QueryTicket(obj2) //用户2
<br />
new SetInter({
timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图
fn: function () {
[ticket,ticket2].map(item => { //同时进行两个用户的抢票
if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢
item.init()
} else { // 如果抢到票了,则不继续抢票
console.log('stop')
}
})
}
})</p>
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
- END -
<p style="letter-spacing: 0.544px;text-align: right;">分享前端好文,点亮 <strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;">在看</strong><strong mpa-from-tpl="t" style="color: rgb(245, 197, 67);font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;font-size: 14px;letter-spacing: 0.544px;"> </strong></p>
不会写代码,如何抓取网页里的信息?
网站优化 • 优采云 发表了文章 • 0 个评论 • 61 次浏览 • 2022-06-19 03:02
竞价QQ交流群:850434915
信息流QQ交流群:1070089747
当我们在网站上看到一系列地址信息、商品信息甚至天气、新闻等真实信息,但因为数量庞大难以依靠手动复制黏贴来完整获取时,爬虫就能代替你完成所有工作。
——“如何向完全没有背景知识的人解释爬虫为何物?”
——“爬虫就是按一定规则替你浏览网页并复制黏贴东西下来的程序。”
是的,听起来很很高级,是不是要写代码啊?!网上一搜什么Python+Scrapy啊,功能强大到爆。
但即使这样对于某些结构简单且只用于江湖救急的网站数据获取未免显得有些小题大做了,而且普通用户很可能折在装python+scrapy包这件事上。
WebScraper登场
这时候Chrome的一款爬虫插件就脱颖而出了!()名字就叫Web Scraper,web既可以指网络爬虫也能说明在线爬虫的意思,一语双关(也可能是我想多了……)
如何安装插件此处略过,并且网站上有非常友好的introduce video,总之,安装好以后在Chrome下按F12就可以开始
举一个椰子
话不多说,来看椰子!哦不,栗子!
带大家来爬一下天猫上「椰青」的价格吧!
1、打开页面
先来看一下我们感兴趣的「椰青及价格」页面
没错!是我感兴趣的椰青!
2、喊出爬虫页面
于是我们按提示打开在线爬虫的界面
最右侧的Web Scraper tag就是我们之前安装的爬虫插件,从现在开始我们就要为爬虫建立一个复制黏贴数据的规则,以防获得一些不该获得的不想要的数据。
3、建立规则
前面说过,爬虫是替你浏览网页并复制黏贴东西下来的东西,那么它就应该模拟你的行为。首先你打开这个界面,知道了这个网页是“我想要的数据起点”,那么对于爬虫来说,这就是他的root。所以我们来新建一个爬虫并告诉他:
我们点击Create new sitemap来创建一个爬虫并给它起个名字~顺便告诉它起点(当前浏览器里的网址)。之后我们就会进入这个爬虫(taobao)的根目录下:
4、选择元素
然后我们开始获取每一个商品的集合,单击add new selector,新增一个筛选器,选出所有的「椰青商品」元素:
同样的取个名字,选择type为element,选择商品元素,当选择2个相同属性元素时插件会自动勾选上页面中所有该属性元素。
点击done selecting完成选择,并勾上multiple。Save selector!
此时我们只需要从之前筛选出来的item元素中获取需要的字段就可以啦。同样的我们在item目录下新建一些selector,由于需要获得的是文本信息,所以type需要变为text。
此时一个简易的单页爬虫就做好了,在sitemap的下拉菜单中还可以选择graph来查看爬虫的结构。
5、点击Scrape开始爬
6、下载数据
结束后数据会自动生成在视窗中,插件自带了导出为CSV的功能,可以一键下载。不小心关了也没关系,browse中可以看到上一次抓取的数据。
翻页怎么办?
如果要翻页的话就会困难一些,大概给个思路:正如item中的element会被遍历获取,那么同样的在root目录下新建一个翻页的link selector来实现「下一页」功能。
将item链接到link selector下,并且将link selector和之前创建的item selector链接到自己来实现一个死循环知道下一页不存在或者下一页unable
循环建立好以后就可以成了下面这个样子:
So What?
你可能要问:So What?
我用这个工具抓了瓜子二手车全国几百台在售的二手宝马3系的价格,看一下不同车龄的宝马3系轿车在使用了若干年后的价格跌幅吧~
如果本文对你有帮助或启发,也请分享给你身边的人。记得顺手点赞哦,感谢。
- END -
查看全部
不会写代码,如何抓取网页里的信息?
竞价QQ交流群:850434915
信息流QQ交流群:1070089747
当我们在网站上看到一系列地址信息、商品信息甚至天气、新闻等真实信息,但因为数量庞大难以依靠手动复制黏贴来完整获取时,爬虫就能代替你完成所有工作。
——“如何向完全没有背景知识的人解释爬虫为何物?”
——“爬虫就是按一定规则替你浏览网页并复制黏贴东西下来的程序。”
是的,听起来很很高级,是不是要写代码啊?!网上一搜什么Python+Scrapy啊,功能强大到爆。
但即使这样对于某些结构简单且只用于江湖救急的网站数据获取未免显得有些小题大做了,而且普通用户很可能折在装python+scrapy包这件事上。
WebScraper登场
这时候Chrome的一款爬虫插件就脱颖而出了!()名字就叫Web Scraper,web既可以指网络爬虫也能说明在线爬虫的意思,一语双关(也可能是我想多了……)
如何安装插件此处略过,并且网站上有非常友好的introduce video,总之,安装好以后在Chrome下按F12就可以开始
举一个椰子
话不多说,来看椰子!哦不,栗子!
带大家来爬一下天猫上「椰青」的价格吧!
1、打开页面
先来看一下我们感兴趣的「椰青及价格」页面
没错!是我感兴趣的椰青!
2、喊出爬虫页面
于是我们按提示打开在线爬虫的界面
最右侧的Web Scraper tag就是我们之前安装的爬虫插件,从现在开始我们就要为爬虫建立一个复制黏贴数据的规则,以防获得一些不该获得的不想要的数据。
3、建立规则
前面说过,爬虫是替你浏览网页并复制黏贴东西下来的东西,那么它就应该模拟你的行为。首先你打开这个界面,知道了这个网页是“我想要的数据起点”,那么对于爬虫来说,这就是他的root。所以我们来新建一个爬虫并告诉他:
我们点击Create new sitemap来创建一个爬虫并给它起个名字~顺便告诉它起点(当前浏览器里的网址)。之后我们就会进入这个爬虫(taobao)的根目录下:
4、选择元素
然后我们开始获取每一个商品的集合,单击add new selector,新增一个筛选器,选出所有的「椰青商品」元素:
同样的取个名字,选择type为element,选择商品元素,当选择2个相同属性元素时插件会自动勾选上页面中所有该属性元素。
点击done selecting完成选择,并勾上multiple。Save selector!
此时我们只需要从之前筛选出来的item元素中获取需要的字段就可以啦。同样的我们在item目录下新建一些selector,由于需要获得的是文本信息,所以type需要变为text。
此时一个简易的单页爬虫就做好了,在sitemap的下拉菜单中还可以选择graph来查看爬虫的结构。
5、点击Scrape开始爬
6、下载数据
结束后数据会自动生成在视窗中,插件自带了导出为CSV的功能,可以一键下载。不小心关了也没关系,browse中可以看到上一次抓取的数据。
翻页怎么办?
如果要翻页的话就会困难一些,大概给个思路:正如item中的element会被遍历获取,那么同样的在root目录下新建一个翻页的link selector来实现「下一页」功能。
将item链接到link selector下,并且将link selector和之前创建的item selector链接到自己来实现一个死循环知道下一页不存在或者下一页unable
循环建立好以后就可以成了下面这个样子:
So What?
你可能要问:So What?
我用这个工具抓了瓜子二手车全国几百台在售的二手宝马3系的价格,看一下不同车龄的宝马3系轿车在使用了若干年后的价格跌幅吧~
如果本文对你有帮助或启发,也请分享给你身边的人。记得顺手点赞哦,感谢。
- END -
爬虫干货 | 网页源码解析模块介绍及实战
网站优化 • 优采云 发表了文章 • 0 个评论 • 87 次浏览 • 2022-06-19 03:00
皮卡丘联合爬虫业界大牛FastGets整理,介绍python爬虫基础知识、大牛多年反爬经验,有难度网站抓取实战、爬虫系统架构设计、安卓app逆向分析。帮助小白入门,初级工程师成为资深工程师,大神回顾。大家有任何问题可留言交流,欢迎转发分享。
上一篇主要介绍了爬虫抓取的模块,抓取下来的文本内容主要是json字符串形式或html源码形式,前者解析比较方便,直接使用python3的json模块的loads方法,即可将json字符串形式转变成python常用的列表或字典形式;后者比较麻烦,所以本文主要介绍后者的解析。这里主要介绍xpath和正则表达式来解析html源码并提取所需信息。
一、xpath介绍
xpath是XML路径语言,它是一种用来确定XML文档中某部分位置的语言。xpath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力。这里主要使用实战例子来介绍笔者在爬虫源码解析中的几种常用方法。
1. 首先我们使用lxml模块将html源码字符串转换成HtmlElement对象的树形结构。Python 标准库中自带的 xml 模块与第三方库BeautifulSoup,性能都不够好,而且缺乏一些人性化的 api,相比之下,第三方库lxml,速度比较快,对用户比较友好。代码如下:
2. 下面我们用xpath的路径表达式来定位树形结构上的元素,提取我们所需要的信息。
(1) 定位html标签节点
(2) 定位到标签的属性
(3) 定位到标签里面的文本
(4) 根据特定的属性选择节点
(5) 获取节点的父亲节点
(6) 获取节点的后续同级节点
(7) 节点对象一些常用的方法
3. 下面我们来解析36kr新闻详情页面的数据
(1) 在Chrome里面安装xpath helper插件,在工具栏,更多工具,扩展程序,里面搜索xpath helper,然后安装。
(2) 下面我们来看看这篇新闻的标题的xpath。
打开页面后,点击红色箭头标记处的小图标,就会出现xpath helper插件,我们在xpath helper里面写xpath,右边就会展示我们xpath路径的结果,下面我们运行代码看看结果。
可以看到,输出的结果是空列表,但是我们的xpath又没有写错,怎么会这样呢?出现这种原因,很大可能是html源码与浏览器的开发者工具里面显示的不一样,因为浏览器加载源码会执行一些js代码,致使它们不一样。这个时候,我们点击显示网页源码,我们从源码里面查找不到class='mobile_article'。但是笔者发现我们所需要的数据在props变量的值里面,这是一个json字符串,如下图。
我们可以用正则表达式,将json字符串提取出来,然后用json模块来解析,代码如下。
以上是从爬虫解析实战中介绍了xpath的基础用法,如果要系统的学习可以查看以下链接的文章
二、正则表达式介绍
正则表达式简单来讲就是字符串规则,用这个规则,我们从html源码字符串里面提取出符合我们规则的信息,python中主要使用re模块来做正则表达式。爬虫中使用的正则表达式并不复杂,都是一些比较简单的。由于我们篇幅有限,我们就在实战中介绍一些常用的。
笔者经常使用re.findall(pattern, string)方法来提取string中的所需要的信息,pattern即是正则表达式。如下示例:
以下是常用正则表达式字符集
字符
说明
\w
匹配字母数字及下划线
\W
匹配非字母数字及下划线
\s
匹配任意空白字符,等价于 [\t\n\r\f]
\S
匹配任意非空字符
\d
匹配任意数字,等价于 [0-9]
\D
匹配任意非数字
\n
匹配一个换行符
\t
匹配一个制表符
^
匹配字符串的开头
$
匹配字符串的末尾
.
匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...]
用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...]
不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符
*
匹配0个或多个的表达式
+
匹配1个或多个的表达式
匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
{n}
精确匹配n个前面表达式
{n, m}
匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b
匹配a或b
( )
匹配括号内的表达式,也表示一个组
熟练掌握正则表达式是每一个程序员必须具备的技能。python正则表达式详细介绍可以看这篇文章
三、爬虫抓取解析实战
抓取虎嗅网()新闻,分析虎嗅网的网页结构可知,虎嗅网的新闻资讯分为不同的类别,我们分别抓取这些类别,然后再进入类别下面的新闻列表页进行抓取,如下图。
点击电商消费类别,出现电商消费类的新闻列表,如下图。
抓取思路:我们先从虎嗅网首页抓取新闻资讯类别,然后进入每个类别的列表页抓取新闻列表,然后再抓取新闻的详情页。笔者提供如下代码:
以后每篇文章的代码都可以在github的CrawlArticles仓库查看:
喜欢的话,点个赞呗!
查看全部
爬虫干货 | 网页源码解析模块介绍及实战
皮卡丘联合爬虫业界大牛FastGets整理,介绍python爬虫基础知识、大牛多年反爬经验,有难度网站抓取实战、爬虫系统架构设计、安卓app逆向分析。帮助小白入门,初级工程师成为资深工程师,大神回顾。大家有任何问题可留言交流,欢迎转发分享。
上一篇主要介绍了爬虫抓取的模块,抓取下来的文本内容主要是json字符串形式或html源码形式,前者解析比较方便,直接使用python3的json模块的loads方法,即可将json字符串形式转变成python常用的列表或字典形式;后者比较麻烦,所以本文主要介绍后者的解析。这里主要介绍xpath和正则表达式来解析html源码并提取所需信息。
一、xpath介绍
xpath是XML路径语言,它是一种用来确定XML文档中某部分位置的语言。xpath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力。这里主要使用实战例子来介绍笔者在爬虫源码解析中的几种常用方法。
1. 首先我们使用lxml模块将html源码字符串转换成HtmlElement对象的树形结构。Python 标准库中自带的 xml 模块与第三方库BeautifulSoup,性能都不够好,而且缺乏一些人性化的 api,相比之下,第三方库lxml,速度比较快,对用户比较友好。代码如下:
2. 下面我们用xpath的路径表达式来定位树形结构上的元素,提取我们所需要的信息。
(1) 定位html标签节点
(2) 定位到标签的属性
(3) 定位到标签里面的文本
(4) 根据特定的属性选择节点
(5) 获取节点的父亲节点
(6) 获取节点的后续同级节点
(7) 节点对象一些常用的方法
3. 下面我们来解析36kr新闻详情页面的数据
(1) 在Chrome里面安装xpath helper插件,在工具栏,更多工具,扩展程序,里面搜索xpath helper,然后安装。
(2) 下面我们来看看这篇新闻的标题的xpath。
打开页面后,点击红色箭头标记处的小图标,就会出现xpath helper插件,我们在xpath helper里面写xpath,右边就会展示我们xpath路径的结果,下面我们运行代码看看结果。
可以看到,输出的结果是空列表,但是我们的xpath又没有写错,怎么会这样呢?出现这种原因,很大可能是html源码与浏览器的开发者工具里面显示的不一样,因为浏览器加载源码会执行一些js代码,致使它们不一样。这个时候,我们点击显示网页源码,我们从源码里面查找不到class='mobile_article'。但是笔者发现我们所需要的数据在props变量的值里面,这是一个json字符串,如下图。
我们可以用正则表达式,将json字符串提取出来,然后用json模块来解析,代码如下。
以上是从爬虫解析实战中介绍了xpath的基础用法,如果要系统的学习可以查看以下链接的文章
二、正则表达式介绍
正则表达式简单来讲就是字符串规则,用这个规则,我们从html源码字符串里面提取出符合我们规则的信息,python中主要使用re模块来做正则表达式。爬虫中使用的正则表达式并不复杂,都是一些比较简单的。由于我们篇幅有限,我们就在实战中介绍一些常用的。
笔者经常使用re.findall(pattern, string)方法来提取string中的所需要的信息,pattern即是正则表达式。如下示例:
以下是常用正则表达式字符集
字符
说明
\w
匹配字母数字及下划线
\W
匹配非字母数字及下划线
\s
匹配任意空白字符,等价于 [\t\n\r\f]
\S
匹配任意非空字符
\d
匹配任意数字,等价于 [0-9]
\D
匹配任意非数字
\n
匹配一个换行符
\t
匹配一个制表符
^
匹配字符串的开头
$
匹配字符串的末尾
.
匹配任意字符,除了换行符,当re.DOTALL标记被指定时,则可以匹配包括换行符的任意字符
[...]
用来表示一组字符,单独列出:[amk] 匹配 'a','m'或'k'
[^...]
不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符
*
匹配0个或多个的表达式
+
匹配1个或多个的表达式
匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式
{n}
精确匹配n个前面表达式
{n, m}
匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式
a|b
匹配a或b
( )
匹配括号内的表达式,也表示一个组
熟练掌握正则表达式是每一个程序员必须具备的技能。python正则表达式详细介绍可以看这篇文章
三、爬虫抓取解析实战
抓取虎嗅网()新闻,分析虎嗅网的网页结构可知,虎嗅网的新闻资讯分为不同的类别,我们分别抓取这些类别,然后再进入类别下面的新闻列表页进行抓取,如下图。
点击电商消费类别,出现电商消费类的新闻列表,如下图。
抓取思路:我们先从虎嗅网首页抓取新闻资讯类别,然后进入每个类别的列表页抓取新闻列表,然后再抓取新闻的详情页。笔者提供如下代码:
以后每篇文章的代码都可以在github的CrawlArticles仓库查看:
喜欢的话,点个赞呗!
Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 50 次浏览 • 2022-06-19 00:29
在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获 查看全部
Node.js 实现抢票小工具&短信通知提醒
在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获
Node.js 实现抢票小工具&amp;短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 55 次浏览 • 2022-06-19 00:24
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获 查看全部
Node.js 实现抢票小工具&amp;短信通知提醒
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【深圳xxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息
根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口
设置代理手机上网
依次执行上面两步
获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【深圳x出行】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档,也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址
cookie信息
各自的request参数字段
user-Agent信息
各自的response返回内容
设置chrome
有以上信息后,我们就可以开始用chrome调试了,首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启 macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
小方块的结构为:
这里为日期如果有余票则显示余票数量
td的样式名为a代表不可选
样式名为e代表已满
样式名为d代表已购
样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口
定时请求任务
有余票则自动请求购票接口下订单
调用腾讯云短信api接口发送短信通知
多个用户抢票功能
抢某个日期的票
首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的http.request,我这里选择用的是axios,毕竟axios在node端底层也是调用http.request
cnpm install axios --save
定时任务 node-schedule
cnpm install node-schedule --save
node端选择dom节点工具 cheerio
cnpm install cheerio --save
腾讯发短信的依赖包 qcloudsms_js
cnpm install qcloudsms_js
热更新包,诺豆的妈妈,nodemon (其实不用也可以)
cnpm install nodemon --save-dev
<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
const axios = require('axios')const querystring = require("querystring"); //序列化对象,用qs也行,都一样let QcloudSms = require("qcloudsms_js");let cheerio = require('cheerio');let schedule = require('node-schedule');
然后我们先定义请求参数,来一个obj
let obj = { data: { lineId: 111130, //路线id vehTime: 0722, //发车时间, startTime: 0751, //预计上车时间 onStationId: 564492, //预定的站点id offStationId: 17990,//到站id onStationName: '宝安交通运输局③', //预定的站点名称 offStationName: "深港产学研基地",//预定到站名称 tradePrice: 0,//总金额 saleDates: '17',//车票日期 beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据 }, phoneNumber: 123123123, //用户手机号,接收短信的手机号 cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票}
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{ /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, day } * @param data {Object} 请求余票接口的requery参数 * @param phoneNumber {Number} 用户手机号,短信需要用到 * @param cookie {String} cookie信息 * @params day {String} 某日的票,如'18' * @memberof QueryTicket 请求余票接口 */ constructor({ data, phoneNumber, cookie, day }) { this.data = data this.cookie = cookie this.day = day this.phoneNumber = phoneNumber this.postData = querystring.stringify(data) this.times = 0; //记录次数 let stop = false //通过特定接口才能修改stop值,防止外部随意串改 this.getStop = function () { //获取是否停止 return stop } this.setStop = function (ifStop) { //设置是否停止 stop = ifStop } }}
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } init(){}//初始化 handleQueryTicket(){}//查询余票的逻辑 requestTicket(){} //调用查询余票接口 handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化,因为涉及到异步请求,所以我们使用`async await` async init(){ let ticketList = await this.handleQueryTicket() //返回查询到的余票数组 } //查询余票的逻辑 handleQueryTicket(){ let ticketList = [] //余票数组 let res = await this.requestTicket() this.times++ //计数器,记录请求查询多少次 let str = res.data.replace(/\\/g, "") //格式化返回值 let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据 let list = $(".main").find(".b") //查找是否有余票的dom节点 // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码 if (!list.length) { console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`) return }<br /> // 如果有余票 list.each((idx, item) => { let str = $(item).html() //str这时格式是21&$x4F59;0 //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已 //因此要在下一步对其进行格式化 let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) let data = { day: arr[0], ticketLeft: arr[1] }<br /> //如果是要抢指定日期的票 if (this.day) { //如果有指定日期的余票 if (parseInt(data.day) === parseInt(data.day)) { ticketList.push(data) } } else { //如果不是,则返回查询到的所有余票 ticketList.push(data) } }) return ticketList } //调用查询余票接口 requestTicket(){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleBuyTicket(){} //购票相关逻辑 requestOrder(){}//调用购票接口 handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ let year = new Date().getFullYear() //年份, let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号 let { onStationName,//起始站点名 offStationName,//结束站点名 lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站台id offStationId //到站的站台id } = this.data // 初始化的数据<br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地" let dateStr = ""; //车票日期 let tickAmount = "" //总张数 ticketList.forEach(item => { dateStr = dateStr + `${year}-${month}-${item.day},` tickAmount = tickAmount + `${item.ticketLeft}张,` })<br /> let buyTicket = { lineId,//线路id vehTime,//发车时间 startTime,//预计上车时间 onStationId,//上车的站点id offStationId,//目标站点id tradePrice: '5', //金额 saleDates: dateStr.slice(0, -1), payType: '2' //支付方式,微信支付 }<br /> // 调用购票接口 let data = querystring.stringify(buyTicket) let res = await this.requestOrder(data) //返回json数据,是否购票成功等等 //把发短信所需要数据都要传入 return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } }) }//购票相关逻辑 //调用购票接口 requestOrder(obj){ return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI", "Cookie": this.cookie } }) } handleInfoUser(){}//通知用户的逻辑 sendMSg(){} //发短信接口}
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{ constructor({ data, phoneNumber, cookie, day }) { //constructor代码... } //初始化 async init(){ let ticketList = await this.handleQueryTicket() //如果有余票 if (ticketList.length) { //把余票传入购票逻辑方法,返回短信通知所需要的数据 let resParse = await this.handleBuyTicket(ticketList) //执行通知逻辑 this.handleInfoUser(resParse) } }<br /> //查询余票的逻辑 async handleQueryTicket(){ // 查询余票代码... } //调用查询余票接口 requestTicket(){ //调用查询余票接口代码... } //购票相关逻辑 async handleBuyTicket(ticketList){ //购票代码... } //调用购票接口 requestOrder(obj){ //购票接口请求代码... } //通知用户的逻辑 async handleInfoUser(parseData){ //获取上一步购票的response数据和我们拼接的数据 let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData //如果购票成功,则返回500 if (returnCode === "500") { let res = await this.sendMsg({ dateStr, //日期 tickAmount: tickAmount.slice(0, -1), //总张数 station, //站点 lineName, //巴士名称/路线名称 tradePrice,//总价 startTime,//出发时间 phoneNumber: this.phoneNumber,//手机号 }) //如果发信成功,则不再进行抢票操作 if (res.result === 0 && res.errmsg === "OK") { this.setStop(true) } else { //失败不做任何操作 console.log(res.errmsg) } } else { //失败不做任何操作 console.log(resParse['returnInfo']) } } //发短信接口 sendMSg(){ let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj let appid = 140034324; // SDK AppID 以1400开头 // 短信应用 SDK AppKey let appkey = "asdfdsvajwienin23493nadsnzxc"; // 短信模板 ID,需要在短信控制台中申请 let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 // 签名 let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请 // 实例化 QcloudSms let qcloudsms = QcloudSms(appid, appkey); let ssender = qcloudsms.SmsSingleSender(); // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容 let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice]; //用promise来封装下异步操作 return new Promise((resolve, reject) => { ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) { if (err) { reject(err) } else { resolve(resData) } }); }) }}
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务class SetInter { constructor({ timer, fn }) { this.timer = timer // 每几秒执行 this.fn = fn //执行的回调 this.rule = new schedule.RecurrenceRule(); //实例化一个对象 this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已 this.init() } setRule() { let rule = []; let i = 1; while (i < 60) { rule.push(i) i += this.timer } return rule //假设传入的timer为5,则表示定时任务每5秒执行一次 // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] } init() { schedule.scheduleJob(this.rule, () => { this.fn() // 定时调用传入的回调方法 }); }}<br />
<br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1 lineId: 111130, vehTime: 0722, startTime: 0751, onStationId: 564492, offStationId: 17990, onStationName: '宝安交通运输局③', offStationName: "深港产学研基地", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 123123123, cookie: 'JSESSIONID=TESTCOOKIE', day: "17"}let obj2 = { //用户2 data: { lineId: 134423, vehTime: 1820, startTime: 1855, onStationId: 4322, offStationId: 53231, onStationName: '百度国际大厦', offStationName: "裕安路口", tradePrice: 0, saleDates: '', beginDate: '', }, phoneNumber: 175932123124, cookie: 'JSESSIONID=TESTCOOKIE', day: ""}let ticket = new QueryTicket(obj) //用户1let ticket2 = new QueryTicket(obj2) //用户2<br />new SetInter({ timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图 fn: function () { [ticket,ticket2].map(item => { //同时进行两个用户的抢票 if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢 item.init() } else { // 如果抢到票了,则不继续抢票 console.log('stop') } }) }})<br />
node index.js 运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持,关于ts我推荐阅读这篇JD前端写的文章
juejin.im/post/5d8efe…
希望各位能有所收获
(实战)Node.js 实现抢票小工具&短信通知提醒
网站优化 • 优采云 发表了文章 • 0 个评论 • 79 次浏览 • 2022-06-17 16:46
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【xxxxxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口设置代理手机上网依次执行上面两步获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【xxxx】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容: 整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址cookie信息各自的request参数字段user-Agent信息各自的response返回内容设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
**小方块的结构为:**<br />
<br />这里为日期<br />如果有余票则显示余票数量<br /><br /><br />
td的样式名为a代表不可选样式名为e代表已满样式名为d代表已购样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口定时请求任务有余票则自动请求购票接口下订单调用腾讯云短信api接口发送短信通知多个用户抢票功能抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。 下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的`http.request`,我这里选择用的是`axios`,毕竟`axios`在node端底层也是调用`http.request`<br />
cnpm install axios --save<br /><br />
定时任务 `node-schedule`<br />
cnpm install node-schedule --save<br /><br />
node端选择dom节点工具 `cheerio`<br />
cnpm install cheerio --save<br /><br />
腾讯发短信的依赖包 `qcloudsms_js`<br />
cnpm install qcloudsms_js <br /><br />
热更新包,诺豆的妈妈,`nodemon` (其实不用也可以)<br />
cnpm install nodemon --save-dev<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<br />const axios = require('axios')<br />const querystring = require("querystring"); //序列化对象,用qs也行,都一样<br />let QcloudSms = require("qcloudsms_js");<br />let cheerio = require('cheerio');<br />let schedule = require('node-schedule');<br /><br /><br />
然后我们先定义请求参数,来一个obj
let obj = {<br /> data: {<br /> lineId: 111130, //路线id<br /> vehTime: 0722, //发车时间,<br /> startTime: 0751, //预计上车时间<br /> onStationId: 564492, //预定的站点id<br /> offStationId: 17990,//到站id<br /> onStationName: '宝安交通运输局③', //预定的站点名称<br /> offStationName: "深港产学研基地",//预定到站名称<br /> tradePrice: 0,//总金额<br /> saleDates: '17',//车票日期<br /> beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据<br /> },<br /> phoneNumber: 123123123, //用户手机号,接收短信的手机号<br /> cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie<br /> day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票<br />}<br /><br /><br />
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{<br /> /**<br /> *Creates an instance of QueryTicket.<br /> * @param {Object} { data, phoneNumber, cookie, day }<br /> * @param data {Object} 请求余票接口的requery参数<br /> * @param phoneNumber {Number} 用户手机号,短信需要用到<br /> * @param cookie {String} cookie信息<br /> * @params day {String} 某日的票,如'18'<br /> * @memberof QueryTicket 请求余票接口<br /> */<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> this.data = data <br /> this.cookie = cookie<br /> this.day = day<br /> this.phoneNumber = phoneNumber<br /> this.postData = querystring.stringify(data)<br /> this.times = 0; //记录次数<br /> let stop = false //通过特定接口才能修改stop值,防止外部随意串改<br /> this.getStop = function () { //获取是否停止<br /> return stop <br /> }<br /> this.setStop = function (ifStop) { //设置是否停止<br /> stop = ifStop<br /> }<br /> }<br />}<br /><br /><br />
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> init(){}//初始化<br /> handleQueryTicket(){}//查询余票的逻辑<br /> requestTicket(){} //调用查询余票接口<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化,因为涉及到异步请求,所以我们使用`async await`<br /> async init(){<br /> let ticketList = await this.handleQueryTicket() //返回查询到的余票数组<br /> }<br /> //查询余票的逻辑<br /> handleQueryTicket(){ <br /> let ticketList = [] //余票数组<br /> let res = await this.requestTicket()<br /> this.times++ //计数器,记录请求查询多少次<br /> let str = res.data.replace(/\\/g, "") //格式化返回值<br /> let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据<br /> let list = $(".main").find(".b") //查找是否有余票的dom节点<br /> // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码<br /> if (!list.length) {<br /> console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)<br /> return<br /> }<br /><br /> // 如果有余票<br /> list.each((idx, item) => {<br /> let str = $(item).html() //str这时格式是21&$x4F59;0<br /> //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已<br /> //因此要在下一步对其进行格式化<br /> let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) <br /> let data = {<br /> day: arr[0],<br /> ticketLeft: arr[1]<br /> }<br /> <br /> //如果是要抢指定日期的票<br /> if (this.day) {<br /> //如果有指定日期的余票<br /> if (parseInt(data.day) === parseInt(data.day)) {<br /> ticketList.push(data)<br /> }<br /> } else {<br /> //如果不是,则返回查询到的所有余票<br /> ticketList.push(data)<br /> }<br /> })<br /> return ticketList<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> }) <br /> }<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> let year = new Date().getFullYear() //年份,<br /> let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号<br /> let {<br /> onStationName,//起始站点名<br /> offStationName,//结束站点名<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站台id<br /> offStationId //到站的站台id<br /> } = this.data // 初始化的数据<br /><br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"<br /> let dateStr = ""; //车票日期<br /> let tickAmount = "" //总张数<br /> ticketList.forEach(item => {<br /> dateStr = dateStr + `${year}-${month}-${item.day},`<br /> tickAmount = tickAmount + `${item.ticketLeft}张,`<br /> })<br /><br /> let buyTicket = {<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站点id<br /> offStationId,//目标站点id<br /> tradePrice: '5', //金额<br /> saleDates: dateStr.slice(0, -1),<br /> payType: '2' //支付方式,微信支付<br /> }<br /><br /> // 调用购票接口<br /> let data = querystring.stringify(buyTicket)<br /> let res = await this.requestOrder(data) //返回json数据,是否购票成功等等<br /> //把发短信所需要数据都要传入<br /> return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })<br /> }//购票相关逻辑<br /> //调用购票接口<br /> requestOrder(obj){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> })<br /> }<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> //执行通知逻辑<br /> this.handleInfoUser(resParse)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> //购票代码...<br /> }<br /> //调用购票接口<br /> requestOrder(obj){<br /> //购票接口请求代码...<br /> }<br /> //通知用户的逻辑<br /> async handleInfoUser(parseData){<br /> //获取上一步购票的response数据和我们拼接的数据<br /> let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData<br /> //如果购票成功,则返回500<br /> if (returnCode === "500") {<br /> let res = await this.sendMsg({<br /> dateStr, //日期<br /> tickAmount: tickAmount.slice(0, -1), //总张数<br /> station, //站点<br /> lineName, //巴士名称/路线名称<br /> tradePrice,//总价<br /> startTime,//出发时间<br /> phoneNumber: this.phoneNumber,//手机号<br /> })<br /> //如果发信成功,则不再进行抢票操作<br /> if (res.result === 0 && res.errmsg === "OK") {<br /> this.setStop(true)<br /> } else {<br /> //失败不做任何操作<br /> console.log(res.errmsg)<br /> }<br /> } else {<br /> //失败不做任何操作<br /> console.log(resParse['returnInfo'])<br /> } <br /> }<br /> //发短信接口<br /> sendMSg(){<br /> let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj<br /> let appid = 140034324; // SDK AppID 以1400开头<br /> // 短信应用 SDK AppKey<br /> let appkey = "asdfdsvajwienin23493nadsnzxc";<br /> // 短信模板 ID,需要在短信控制台中申请<br /> let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请<br /> // 签名<br /> let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请<br /> // 实例化 QcloudSms<br /> let qcloudsms = QcloudSms(appid, appkey);<br /> let ssender = qcloudsms.SmsSingleSender();<br /> // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容<br /> let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];<br /> //用promise来封装下异步操作<br /> return new Promise((resolve, reject) => {<br /> ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {<br /> if (err) {<br /> reject(err)<br /> } else {<br /> resolve(resData)<br /> }<br /> });<br /> })<br /> } <br />}<br /><br />
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务<br />class SetInter {<br /> constructor({ timer, fn }) {<br /> this.timer = timer // 每几秒执行<br /> this.fn = fn //执行的回调<br /> this.rule = new schedule.RecurrenceRule(); //实例化一个对象<br /> this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已<br /> this.init()<br /> }<br /> setRule() {<br /> let rule = [];<br /> let i = 1;<br /> while (i {<br /> this.fn() // 定时调用传入的回调方法<br /> });<br /> }<br />}<br /><br /><br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1<br /> lineId: 111130,<br /> vehTime: 0722,<br /> startTime: 0751,<br /> onStationId: 564492,<br /> offStationId: 17990,<br /> onStationName: '宝安交通运输局③',<br /> offStationName: "深港产学研基地",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 123123123,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "17"<br />}<br />let obj2 = { //用户2<br /> data: {<br /> lineId: 134423,<br /> vehTime: 1820,<br /> startTime: 1855,<br /> onStationId: 4322,<br /> offStationId: 53231,<br /> onStationName: '百度国际大厦',<br /> offStationName: "裕安路口",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 175932123124,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "" <br />}<br />let ticket = new QueryTicket(obj) //用户1<br />let ticket2 = new QueryTicket(obj2) //用户2<br /><br />new SetInter({<br /> timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图<br /> fn: function () {<br /> [ticket,ticket2].map(item => { //同时进行两个用户的抢票<br /> if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢<br /> item.init()<br /> } else { // 如果抢到票了,则不继续抢票<br /> console.log('stop')<br /> }<br /> })<br /> }<br />})<br /><br /><br />
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
最后
欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!
回复「交流」,吹吹水、聊聊技术、吐吐槽!回复「阅读」,每日刷刷高质量好文!如果这篇文章对你有帮助,「在看」是最大的支持
》》面试官也在看的算法资料《《“在看和转发”就是最大的支持 查看全部
(实战)Node.js 实现抢票小工具&短信通知提醒
写在前言
要知道在深圳上班是非常痛苦的事情,特别是我上班的科兴科技园这一块,去的人非常多,每天上班跟春运一样,如果我能换到以前的大冲上班那就幸福了,可惜,换不得。
尤其是我这个站等车的多的一笔,上班公交挤的不行,车满的时候只有少部分人能硬挤上去。通常我只会用两个字来形容这种人:“公交怪”
想当年我朋友瘦的像只猴还能上去,老子身高182体重72kg挤个公交,不成问题,反手一个阻挡,闷声发大财,前面的阿姨你快点阿姨,别磨磨唧唧的,快上去啊阿姨,嗯?你还想挤掉我?你能挤掉我?你能挤掉我!我当场!把车吃了!
....
咳咳,挤公交是不可能挤公交滴,因为今天我发现了一个可以定制路线的网约巴士公众号【xxxxxx】
但是呢,票经常会被抢光,同时我还我发现,有时候会有人退票,这时候就有空余票了,关键是我不可能时时都在公众号上盯着,于是,我就写了一个抢票+短信通知的小工具
获取接口信息查看页面结构
这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选
如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:
我们打算这么做,
定时抓取返回的接口信息根据接口返回值判断是否有余票
好,审查下源代码看下接口信息,等等,微信浏览器没办法审查源代码,于是
使用chrome 调试微信公众号网页页面
首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去
所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,因为我电脑是Mac,用不了Fiddler,我用的是Charles花瓶,就是下面这位仁兄
借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:
获取本机IP地址和端口设置代理手机上网依次执行上面两步获取本机IP地址和端口
第一步,找到端口号,一般默认是8088,但是为了确认可以打开Proxy/Proxy Setting看下,哦原来我之前设置成了8888
然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步
设置代理手机上网
首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息,比如我的猴米...不对,小米9手机!设置如下:
输入上一步获取主机名,端口号就ok了
输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。
用手机访问目标网页
我们用手机访问微信公众号【xxxx】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。
仔细分析了下,大概明白了业务逻辑:
整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。
当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块,
而我们想要的核心内容:日历表,一开始是不显示的
因为还要在请求一次
第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容
下面这个是请求当月票务信息,然而发现他返回的是一堆html节点
好吧...估计是获取到之后直接append到div里面的,然后渲染生成日历表内容
接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:
看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式
在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息
记录所需要的信息内容
根据上面的分析,总结下内容: 整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚,唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的
所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。
我们找个记事本,记录下信息,记录的内容有:
请求余票接口和购票接口的url地址cookie信息各自的request参数字段user-Agent信息各自的response返回内容设置chrome
有以上信息后,我们就可以开始用chrome调试了, 首先打开More tools/Network conditions
把user-Agent填入到Custom里面
Charles抓包本地请求
因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie
首先我们需要开启macOS Proxy,抓包我们的http请求
打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试
然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute
这时候,能够正确跳到目标页面了。
大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块
审查了下元素发现:
**小方块的结构为:**<br />
<br />这里为日期<br />如果有余票则显示余票数量<br /><br /><br />
td的样式名为a代表不可选样式名为e代表已满样式名为d代表已购样式名为b则是我们要找的,代表可选,也就是有余票
到这一步,整个购票流程就清楚了
到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b,有余票的话,在获取div里面的余票数量内容就Ok了
Node.js 请求目标接口分析需要开发的功能点
写代码之前我们需要想好功能点,我们需要什么功能:
请求余票接口定时请求任务有余票则自动请求购票接口下订单调用腾讯云短信api接口发送短信通知多个用户抢票功能抢某个日期的票
首先mkdir ticket创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。 下面开始安装依赖,根据上面的功能需求,我们大概需要:
请求工具,这里看个人习惯,你也可以使用原生的`http.request`,我这里选择用的是`axios`,毕竟`axios`在node端底层也是调用`http.request`<br />
cnpm install axios --save<br /><br />
定时任务 `node-schedule`<br />
cnpm install node-schedule --save<br /><br />
node端选择dom节点工具 `cheerio`<br />
cnpm install cheerio --save<br /><br />
腾讯发短信的依赖包 `qcloudsms_js`<br />
cnpm install qcloudsms_js <br /><br />
热更新包,诺豆的妈妈,`nodemon` (其实不用也可以)<br />
cnpm install nodemon --save-dev<br /><br />
开发请求余票接口
接着touch index.js创建核心js文件,开始编码:
首先引入所有依赖
<br />const axios = require('axios')<br />const querystring = require("querystring"); //序列化对象,用qs也行,都一样<br />let QcloudSms = require("qcloudsms_js");<br />let cheerio = require('cheerio');<br />let schedule = require('node-schedule');<br /><br /><br />
然后我们先定义请求参数,来一个obj
let obj = {<br /> data: {<br /> lineId: 111130, //路线id<br /> vehTime: 0722, //发车时间,<br /> startTime: 0751, //预计上车时间<br /> onStationId: 564492, //预定的站点id<br /> offStationId: 17990,//到站id<br /> onStationName: '宝安交通运输局③', //预定的站点名称<br /> offStationName: "深港产学研基地",//预定到站名称<br /> tradePrice: 0,//总金额<br /> saleDates: '17',//车票日期<br /> beginDate: '',//订票时间,滞空,用于抓取到余票后填入数据<br /> },<br /> phoneNumber: 123123123, //用户手机号,接收短信的手机号<br /> cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie<br /> day: "17" //定17号的票,这个主要是用于抢指定日期的票,滞空则为抢当月所有余票<br />}<br /><br /><br />
接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,
同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop
编写代码:
class QueryTicket{<br /> /**<br /> *Creates an instance of QueryTicket.<br /> * @param {Object} { data, phoneNumber, cookie, day }<br /> * @param data {Object} 请求余票接口的requery参数<br /> * @param phoneNumber {Number} 用户手机号,短信需要用到<br /> * @param cookie {String} cookie信息<br /> * @params day {String} 某日的票,如'18'<br /> * @memberof QueryTicket 请求余票接口<br /> */<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> this.data = data <br /> this.cookie = cookie<br /> this.day = day<br /> this.phoneNumber = phoneNumber<br /> this.postData = querystring.stringify(data)<br /> this.times = 0; //记录次数<br /> let stop = false //通过特定接口才能修改stop值,防止外部随意串改<br /> this.getStop = function () { //获取是否停止<br /> return stop <br /> }<br /> this.setStop = function (ifStop) { //设置是否停止<br /> stop = ifStop<br /> }<br /> }<br />}<br /><br /><br />
下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> init(){}//初始化<br /> handleQueryTicket(){}//查询余票的逻辑<br /> requestTicket(){} //调用查询余票接口<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
所有数据都是基于查询余票的操作,因此我们先开发这部分功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化,因为涉及到异步请求,所以我们使用`async await`<br /> async init(){<br /> let ticketList = await this.handleQueryTicket() //返回查询到的余票数组<br /> }<br /> //查询余票的逻辑<br /> handleQueryTicket(){ <br /> let ticketList = [] //余票数组<br /> let res = await this.requestTicket()<br /> this.times++ //计数器,记录请求查询多少次<br /> let str = res.data.replace(/\\/g, "") //格式化返回值<br /> let $ = cheerio.load(`${str}`) // cheerio载入查询接口response的html节点数据<br /> let list = $(".main").find(".b") //查找是否有余票的dom节点<br /> // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码<br /> if (!list.length) {<br /> console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)<br /> return<br /> }<br /><br /> // 如果有余票<br /> list.each((idx, item) => {<br /> let str = $(item).html() //str这时格式是21&$x4F59;0<br /> //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已<br /> //因此要在下一步对其进行格式化<br /> let arr = str.split(/||\&\#x4F59\;/).filter(item => !!item === true) <br /> let data = {<br /> day: arr[0],<br /> ticketLeft: arr[1]<br /> }<br /> <br /> //如果是要抢指定日期的票<br /> if (this.day) {<br /> //如果有指定日期的余票<br /> if (parseInt(data.day) === parseInt(data.day)) {<br /> ticketList.push(data)<br /> }<br /> } else {<br /> //如果不是,则返回查询到的所有余票<br /> ticketList.push(data)<br /> }<br /> })<br /> return ticketList<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> }) <br /> }<br /> handleBuyTicket(){} //购票相关逻辑<br /> requestOrder(){}//调用购票接口<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量
所以我们要把它格式化变成这种数组,也就是ticketList
开发购票功能
首先我们在init方法里做个判断,如果有余票才去购票,没有余票购个毛
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> let year = new Date().getFullYear() //年份,<br /> let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号<br /> let {<br /> onStationName,//起始站点名<br /> offStationName,//结束站点名<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站台id<br /> offStationId //到站的站台id<br /> } = this.data // 初始化的数据<br /><br /> let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"<br /> let dateStr = ""; //车票日期<br /> let tickAmount = "" //总张数<br /> ticketList.forEach(item => {<br /> dateStr = dateStr + `${year}-${month}-${item.day},`<br /> tickAmount = tickAmount + `${item.ticketLeft}张,`<br /> })<br /><br /> let buyTicket = {<br /> lineId,//线路id<br /> vehTime,//发车时间<br /> startTime,//预计上车时间<br /> onStationId,//上车的站点id<br /> offStationId,//目标站点id<br /> tradePrice: '5', //金额<br /> saleDates: dateStr.slice(0, -1),<br /> payType: '2' //支付方式,微信支付<br /> }<br /><br /> // 调用购票接口<br /> let data = querystring.stringify(buyTicket)<br /> let res = await this.requestOrder(data) //返回json数据,是否购票成功等等<br /> //把发短信所需要数据都要传入<br /> return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })<br /> }//购票相关逻辑<br /> //调用购票接口<br /> requestOrder(obj){<br /> return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {<br /> headers: {<br /> 'Content-Type': 'application/x-www-form-urlencoded',<br /> 'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",<br /> "Cookie": this.cookie<br /> }<br /> })<br /> }<br /> handleInfoUser(){}//通知用户的逻辑<br /> sendMSg(){} //发短信接口<br />}<br /><br /><br />
到这里,查询余票,购票这两个核心操作已经完成。
目前还剩下,如何通知用户是否购票成功。
之前我尝试过使用qq邮箱的smtp服务,抢票成功后发送邮件通知,但是我觉得吧,并不好用,主要是我没有打开邮箱的习惯,没网也收不到,所以,并没有采纳这个方案。
加上之前我注册过企业认证的公众号,腾讯云免费送了我1000条短信通知,而且短信也比较直观,所以我这里就安装腾讯云的SDK,部署了一套发短信的功能。
腾讯云短信的相关内容
其实看看文档就行了,我也是copy文档,注意看短信单发那部分
/document/pr…
如果跟我一样有企业认证的话,看快速入门这里就行了,一步步跟着操作
看下短信正文,{Number}这些里面的数字是变量。
就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义
调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推
提交审核,审核一般很快就通过,也就是几十万毫秒吧
开发通知功能
class QueryTicket{<br /> constructor({ data, phoneNumber, cookie, day }) {<br /> //constructor代码... <br /> }<br /> //初始化<br /> async init(){<br /> let ticketList = await this.handleQueryTicket()<br /> //如果有余票<br /> if (ticketList.length) {<br /> //把余票传入购票逻辑方法,返回短信通知所需要的数据<br /> let resParse = await this.handleBuyTicket(ticketList)<br /> //执行通知逻辑<br /> this.handleInfoUser(resParse)<br /> }<br /> }<br /> <br /> //查询余票的逻辑<br /> async handleQueryTicket(){<br /> // 查询余票代码...<br /> }<br /> //调用查询余票接口<br /> requestTicket(){<br /> //调用查询余票接口代码... <br /> } <br /> //购票相关逻辑<br /> async handleBuyTicket(ticketList){<br /> //购票代码...<br /> }<br /> //调用购票接口<br /> requestOrder(obj){<br /> //购票接口请求代码...<br /> }<br /> //通知用户的逻辑<br /> async handleInfoUser(parseData){<br /> //获取上一步购票的response数据和我们拼接的数据<br /> let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData<br /> //如果购票成功,则返回500<br /> if (returnCode === "500") {<br /> let res = await this.sendMsg({<br /> dateStr, //日期<br /> tickAmount: tickAmount.slice(0, -1), //总张数<br /> station, //站点<br /> lineName, //巴士名称/路线名称<br /> tradePrice,//总价<br /> startTime,//出发时间<br /> phoneNumber: this.phoneNumber,//手机号<br /> })<br /> //如果发信成功,则不再进行抢票操作<br /> if (res.result === 0 && res.errmsg === "OK") {<br /> this.setStop(true)<br /> } else {<br /> //失败不做任何操作<br /> console.log(res.errmsg)<br /> }<br /> } else {<br /> //失败不做任何操作<br /> console.log(resParse['returnInfo'])<br /> } <br /> }<br /> //发短信接口<br /> sendMSg(){<br /> let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj<br /> let appid = 140034324; // SDK AppID 以1400开头<br /> // 短信应用 SDK AppKey<br /> let appkey = "asdfdsvajwienin23493nadsnzxc";<br /> // 短信模板 ID,需要在短信控制台中申请<br /> let templateId = 7839; // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请<br /> // 签名<br /> let smsSign = "测试短信"; // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请<br /> // 实例化 QcloudSms<br /> let qcloudsms = QcloudSms(appid, appkey);<br /> let ssender = qcloudsms.SmsSingleSender();<br /> // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容<br /> let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];<br /> //用promise来封装下异步操作<br /> return new Promise((resolve, reject) => {<br /> ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {<br /> if (err) {<br /> reject(err)<br /> } else {<br /> resolve(resData)<br /> }<br /> });<br /> })<br /> } <br />}<br /><br />
如果发信成功,返回result:0
到这里,大部分需求已经完成了,还剩下一个定时任务
定时任务
也声明一个类,这里我们用到的是schedule
// 定时任务<br />class SetInter {<br /> constructor({ timer, fn }) {<br /> this.timer = timer // 每几秒执行<br /> this.fn = fn //执行的回调<br /> this.rule = new schedule.RecurrenceRule(); //实例化一个对象<br /> this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已<br /> this.init()<br /> }<br /> setRule() {<br /> let rule = [];<br /> let i = 1;<br /> while (i {<br /> this.fn() // 定时调用传入的回调方法<br /> });<br /> }<br />}<br /><br /><br />
多个用户抢票
假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类
data: { //用户1<br /> lineId: 111130,<br /> vehTime: 0722,<br /> startTime: 0751,<br /> onStationId: 564492,<br /> offStationId: 17990,<br /> onStationName: '宝安交通运输局③',<br /> offStationName: "深港产学研基地",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 123123123,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "17"<br />}<br />let obj2 = { //用户2<br /> data: {<br /> lineId: 134423,<br /> vehTime: 1820,<br /> startTime: 1855,<br /> onStationId: 4322,<br /> offStationId: 53231,<br /> onStationName: '百度国际大厦',<br /> offStationName: "裕安路口",<br /> tradePrice: 0,<br /> saleDates: '',<br /> beginDate: '',<br /> },<br /> phoneNumber: 175932123124,<br /> cookie: 'JSESSIONID=TESTCOOKIE',<br /> day: "" <br />}<br />let ticket = new QueryTicket(obj) //用户1<br />let ticket2 = new QueryTicket(obj2) //用户2<br /><br />new SetInter({<br /> timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图<br /> fn: function () {<br /> [ticket,ticket2].map(item => { //同时进行两个用户的抢票<br /> if (!item.getStop()) { //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢<br /> item.init()<br /> } else { // 如果抢到票了,则不继续抢票<br /> console.log('stop')<br /> }<br /> })<br /> }<br />})<br /><br /><br />
node index.js运行下,跑起来了
如果他抢到票的话,我就会收到短信通知:
打开手机,看下订单信息
搞定,收工
写在最后
其实可以在此基础上还能添加更多功能,比如直接抓取登录接口获取cookie,指定路线抢票,还有错误处理啊啥的
值得注意的是,请求接口不能太频繁,最好控制在5秒一次的频率,不然会给别人造成困扰,也容易被ip拉黑
如果想把它做成一个完整的项目,建议使用ts加持 ,关于ts我推荐阅读这篇JD前端写的文章
希望各位能有所收获
最后
欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
欢迎关注「前端瓶子君」,回复「算法」,加入前端算法源码编程群,每日一刷(工作日),每题瓶子君都会很认真的解答哟!
回复「交流」,吹吹水、聊聊技术、吐吐槽!回复「阅读」,每日刷刷高质量好文!如果这篇文章对你有帮助,「在看」是最大的支持
》》面试官也在看的算法资料《《“在看和转发”就是最大的支持
外贸网站常用的辅助工具
网站优化 • 优采云 发表了文章 • 0 个评论 • 75 次浏览 • 2022-06-14 06:38
1Google Webmaster Tools
Google Webmaster Tools提供非常多的实用工具,总而言之它可以告诉你Google是如何看你的网站的,比如索引了哪些页面,有没有死链接,搜索关键词的排名, 内部链接和外部链接等等。
2Google Analytics
除了提供其它统计网站的统计项目外,Google Analytics最大的特色在于提供Pay-Per-Click数据,只是没在网页中显示任何图标。
3Summit Media's Spider Simulator
搜索引擎蜘蛛模拟器,看看你的网站在蜘蛛眼里是什么样子,优化页面的好工具。
4Mike's Marketing Tools
提供N多免费的Marketing Tools,比如Search Engine Rankings可以同时测试你网站的多个关键词,在多个搜索引擎中的排名,不再需要挨个查看,极大地节省了你的时间。Link Popularity Tool帮你查看各大搜索引擎中有多少反向链接,反向链接对SEO非常重要。
5SEOmoz's Page Strength Tool
为你的网站提供10多项SEO数据,你可以根据这些数据进行SEO。
's SEO Analyzer
同样是一个SEO分析工具,但是与SEOmoz's Page Strength Tool不同,它从网站内部数据得出分析结果,比如页面结构、网页源代码、内部连接、页面尺寸等等。这个工具需要注册后才能用。
7Dead Links Checker
死链检查器,含有死链接不但会让你的访客扫兴,还会被搜索引擎降级的,所以还是定期检查一下为好。
一个关键词排名分析工具,可惜的是需要SOAP API,而Google已经停止发放SOAP API,所以必须已经申请过,才能使用这个工具。
9Feed Burner
全方位的Feed服务、托管、优化、统计、广告等等,还用多说么. 其实国内的Feedsky也不错,只是不知道它能不能抓取国外的站点。
10Self SEO Page Speed Checker
网站测速,可以同时比较几个网站,当然如果你的网站放在国内,这个工具就没什么参考价值了,因为速度太慢了。
(版权说明:感谢原作者的辛苦创作,如转载涉及版权等问题,请作者与我们联系,我们将在第一时间删除,谢谢!)
查看全部
外贸网站常用的辅助工具
1Google Webmaster Tools
Google Webmaster Tools提供非常多的实用工具,总而言之它可以告诉你Google是如何看你的网站的,比如索引了哪些页面,有没有死链接,搜索关键词的排名, 内部链接和外部链接等等。
2Google Analytics
除了提供其它统计网站的统计项目外,Google Analytics最大的特色在于提供Pay-Per-Click数据,只是没在网页中显示任何图标。
3Summit Media's Spider Simulator
搜索引擎蜘蛛模拟器,看看你的网站在蜘蛛眼里是什么样子,优化页面的好工具。
4Mike's Marketing Tools
提供N多免费的Marketing Tools,比如Search Engine Rankings可以同时测试你网站的多个关键词,在多个搜索引擎中的排名,不再需要挨个查看,极大地节省了你的时间。Link Popularity Tool帮你查看各大搜索引擎中有多少反向链接,反向链接对SEO非常重要。
5SEOmoz's Page Strength Tool
为你的网站提供10多项SEO数据,你可以根据这些数据进行SEO。
's SEO Analyzer
同样是一个SEO分析工具,但是与SEOmoz's Page Strength Tool不同,它从网站内部数据得出分析结果,比如页面结构、网页源代码、内部连接、页面尺寸等等。这个工具需要注册后才能用。
7Dead Links Checker
死链检查器,含有死链接不但会让你的访客扫兴,还会被搜索引擎降级的,所以还是定期检查一下为好。
一个关键词排名分析工具,可惜的是需要SOAP API,而Google已经停止发放SOAP API,所以必须已经申请过,才能使用这个工具。
9Feed Burner
全方位的Feed服务、托管、优化、统计、广告等等,还用多说么. 其实国内的Feedsky也不错,只是不知道它能不能抓取国外的站点。
10Self SEO Page Speed Checker
网站测速,可以同时比较几个网站,当然如果你的网站放在国内,这个工具就没什么参考价值了,因为速度太慢了。
(版权说明:感谢原作者的辛苦创作,如转载涉及版权等问题,请作者与我们联系,我们将在第一时间删除,谢谢!)
这些Python代码技巧,你可能还不知道!
网站优化 • 优采云 发表了文章 • 0 个评论 • 162 次浏览 • 2022-06-03 11:00
它可以帮助你从大量顶级国际出版物中检索到新闻文章和相关元数据。你可以检索图像、文本和作者名。
它还有一些内置的 NLP 功能。
地址:#performing-nlp-on-an-article
如果你想在下一个项目中使用 BeautifulSoup 或其它 DIY 网页抓取库,那么不如使用$ pip install newspaper3k,既省时又省事,何乐而不为呢?
运算符重载(Operator overloading)
Python 支持运算符重载。
它实际上是一个简单的概念。你有没有想过为什么 Python 允许用户使用 + 运算符来将数字相加,并级联字符串?这就是运算符重载在发挥作用。
你可以使用 Python 的标准运算符号来定义对象,这样你可以在与这些对象相关的语境中使用它们。
class Thing:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __init__(self, value):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> self.__value = value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __gt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value > other.__value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __lt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value nothing<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># False<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />something >> file = open('file.txt', 'r')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />>>> print(file)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />
这使代码 debug 变得简单很多。将字符串添加到类别定义,如下所示:
class someClass:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __repr__(self):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return ""<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />someInstance = someClass()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># prints <br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(someInstance)
sh
Python 是一种伟大的脚本语言,不过有时使用标准 os 和 subprocess 库会有点棘手。
sh 库提供了一种不错的替代方案。
sh 库:
该库允许用户像使用普通函数一样调用任意程序,这对自动化工作流和任务非常有用。
from sh import *<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.pwd()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.mkdir('new_folder')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.touch('new_file.txt')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.whoami()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.echo('This is great!')
类型提示(Type hints)
Python 是动态语言。在定义变量、函数、类别等时无需指定数据类型。
这有利于缩短开发周期。但是,简单的类型错误(typing issue)导致的运行时错误真的太烦了。
从 Python 3.5 版本开始,用户可以选择在定义函数时开启类型提示。
def addTwo(x : Int) -> Int:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return x + 2
你还可以定义类型别名:
from typing import List<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Vector = List[float]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Matrix = List[Vector]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />def addMatrix(a : Matrix, b : Matrix) -> Matrix:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result = []<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for i,row in enumerate(a):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row =[]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for j, col in enumerate(row):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row += [a[i][j] + b[i][j]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result += [result_row]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return result<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />x = [[1.0, 0.0], [0.0, 1.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />y = [[2.0, 1.0], [0.0, -2.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />z = addMatrix(x, y)
尽管非强制,但类型注释可以使代码更易理解。
它们还允许你在运行之前使用类型检查工具捕捉 TypeError。在进行大型复杂项目时执行此类操作是值得的。
uuid
生成通用唯一标识符(Universally Unique ID,UUID)的一种快速简单方法就是使用 Python 标准库的 uuid 模块。
uuid 模块:
import uuid<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />user_id = uuid.uuid4()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(user_id)
这创建了一个随机化后的 128 比特数字,该数字几乎必然是唯一的。
事实上,可以生成 2¹²²可能的 UUID。这个数字超过了 5,000,000,000,000,000,000,000,000,000,000,000,000。
在给定集合中找出重复数字的可能性极低。即使有一万亿 UUID,重复数字存在的概率也远远低于十亿分之一。
虚拟环境(Virtual environment)
这可能是 Python 中我最喜欢的事物了。
你可能同时处理多个 Python 项目。不幸的是,有时候两个项目依赖于相同依赖项的不同版本。那你要安装哪个版本呢?
幸运的是,Python 支持虚拟环境,这使得用户能够充分利用两种环境。见下列行:
python -m venv my-project<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />source my-project/bin/activate<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />pip install all-the-modules
现在你在一台机器上具备独立的多个 Python 版本了。问题解决!
wikipedia
Wikipedia 拥有一个很棒的 API,允许用户以编程方式访问巨大体量的免费知识和信息。
wikipedia 模块使得访问该 API 非常便捷。
Wikipedia 模块:
import wikipedia<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />result = wikipedia.page('freeCodeCamp')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(result.summary)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />for link in result.links:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> print(link)
和真实的维基百科网站类似,该模块支持多种语言、页面消歧、随机页面检索,甚至还具备 donate() 方法。
xkcd
humour 是 Python 语言的一个关键特征,其名称来自英国喜剧片《蒙提·派森的飞行马戏团》(Monty Python and the Flying Circus)。Python 的很多官方文档引用了该喜剧片最著名的剧情。
幽默感并不限于文档。试着运行下列行:
import antigravity
将打开 xkcd 画的 Python 漫画。不要改变这一点,Python。不要改变。
YAML
YAML 代表 『YAML Ain』t Markup Language』。它是一种数据格式语言,是 JSON 的超集。
与 JSON 不同,它可以存储更复杂的对象并引用自己的元素。你还可以编写注释,使其尤其适用于编写配置文件。
PyYAML 模块()可以让你在 Python 中使用 YAML。安装:
$ pip install pyyaml
然后导入到项目中:
import yaml
PyYAML 使你能够存储任何数据类型的 Python 对象,以及任何用户定义类别的实例。
zip
给你支最后一招,非常酷。还在用两个列表来组成一部词典吗?
keys = ['a', 'b', 'c']<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />vals = [1, 2, 3]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />zipped = dict(zip(keys, vals))
zip() 内置函数使用多个可迭代对象作为输入并返回元组列表。每个元组按位置索引对输入对象的元素进行分组。
你也可以通过调用*zip() 来「解压」对象。
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。 查看全部
这些Python代码技巧,你可能还不知道!
它可以帮助你从大量顶级国际出版物中检索到新闻文章和相关元数据。你可以检索图像、文本和作者名。
它还有一些内置的 NLP 功能。
地址:#performing-nlp-on-an-article
如果你想在下一个项目中使用 BeautifulSoup 或其它 DIY 网页抓取库,那么不如使用$ pip install newspaper3k,既省时又省事,何乐而不为呢?
运算符重载(Operator overloading)
Python 支持运算符重载。
它实际上是一个简单的概念。你有没有想过为什么 Python 允许用户使用 + 运算符来将数字相加,并级联字符串?这就是运算符重载在发挥作用。
你可以使用 Python 的标准运算符号来定义对象,这样你可以在与这些对象相关的语境中使用它们。
class Thing:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __init__(self, value):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> self.__value = value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __gt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value > other.__value<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __lt__(self, other):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return self.__value nothing<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># False<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />something >> file = open('file.txt', 'r')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />>>> print(file)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />
这使代码 debug 变得简单很多。将字符串添加到类别定义,如下所示:
class someClass:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> def __repr__(self):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return ""<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />someInstance = someClass()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /># prints <br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(someInstance)
sh
Python 是一种伟大的脚本语言,不过有时使用标准 os 和 subprocess 库会有点棘手。
sh 库提供了一种不错的替代方案。
sh 库:
该库允许用户像使用普通函数一样调用任意程序,这对自动化工作流和任务非常有用。
from sh import *<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.pwd()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.mkdir('new_folder')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.touch('new_file.txt')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.whoami()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />sh.echo('This is great!')
类型提示(Type hints)
Python 是动态语言。在定义变量、函数、类别等时无需指定数据类型。
这有利于缩短开发周期。但是,简单的类型错误(typing issue)导致的运行时错误真的太烦了。
从 Python 3.5 版本开始,用户可以选择在定义函数时开启类型提示。
def addTwo(x : Int) -> Int:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return x + 2
你还可以定义类型别名:
from typing import List<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Vector = List[float]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />Matrix = List[Vector]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />def addMatrix(a : Matrix, b : Matrix) -> Matrix:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result = []<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for i,row in enumerate(a):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row =[]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> for j, col in enumerate(row):<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result_row += [a[i][j] + b[i][j]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> result += [result_row]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> return result<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />x = [[1.0, 0.0], [0.0, 1.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />y = [[2.0, 1.0], [0.0, -2.0]]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />z = addMatrix(x, y)
尽管非强制,但类型注释可以使代码更易理解。
它们还允许你在运行之前使用类型检查工具捕捉 TypeError。在进行大型复杂项目时执行此类操作是值得的。
uuid
生成通用唯一标识符(Universally Unique ID,UUID)的一种快速简单方法就是使用 Python 标准库的 uuid 模块。
uuid 模块:
import uuid<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />user_id = uuid.uuid4()<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(user_id)
这创建了一个随机化后的 128 比特数字,该数字几乎必然是唯一的。
事实上,可以生成 2¹²²可能的 UUID。这个数字超过了 5,000,000,000,000,000,000,000,000,000,000,000,000。
在给定集合中找出重复数字的可能性极低。即使有一万亿 UUID,重复数字存在的概率也远远低于十亿分之一。
虚拟环境(Virtual environment)
这可能是 Python 中我最喜欢的事物了。
你可能同时处理多个 Python 项目。不幸的是,有时候两个项目依赖于相同依赖项的不同版本。那你要安装哪个版本呢?
幸运的是,Python 支持虚拟环境,这使得用户能够充分利用两种环境。见下列行:
python -m venv my-project<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />source my-project/bin/activate<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />pip install all-the-modules
现在你在一台机器上具备独立的多个 Python 版本了。问题解决!
wikipedia
Wikipedia 拥有一个很棒的 API,允许用户以编程方式访问巨大体量的免费知识和信息。
wikipedia 模块使得访问该 API 非常便捷。
Wikipedia 模块:
import wikipedia<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />result = wikipedia.page('freeCodeCamp')<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />print(result.summary)<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />for link in result.links:<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" /> print(link)
和真实的维基百科网站类似,该模块支持多种语言、页面消歧、随机页面检索,甚至还具备 donate() 方法。
xkcd
humour 是 Python 语言的一个关键特征,其名称来自英国喜剧片《蒙提·派森的飞行马戏团》(Monty Python and the Flying Circus)。Python 的很多官方文档引用了该喜剧片最著名的剧情。
幽默感并不限于文档。试着运行下列行:
import antigravity
将打开 xkcd 画的 Python 漫画。不要改变这一点,Python。不要改变。
YAML
YAML 代表 『YAML Ain』t Markup Language』。它是一种数据格式语言,是 JSON 的超集。
与 JSON 不同,它可以存储更复杂的对象并引用自己的元素。你还可以编写注释,使其尤其适用于编写配置文件。
PyYAML 模块()可以让你在 Python 中使用 YAML。安装:
$ pip install pyyaml
然后导入到项目中:
import yaml
PyYAML 使你能够存储任何数据类型的 Python 对象,以及任何用户定义类别的实例。
zip
给你支最后一招,非常酷。还在用两个列表来组成一部词典吗?
keys = ['a', 'b', 'c']<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />vals = [1, 2, 3]<br style="max-width: 100%;box-sizing: border-box !important;word-wrap: break-word !important;" />zipped = dict(zip(keys, vals))
zip() 内置函数使用多个可迭代对象作为输入并返回元组列表。每个元组按位置索引对输入对象的元素进行分组。
你也可以通过调用*zip() 来「解压」对象。
免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。
30 行 Python 代码实现 Twitch 主播上线实时通知
网站优化 • 优采云 发表了文章 • 0 个评论 • 83 次浏览 • 2022-06-03 10:27
大家好,今天我将开始写作一个新的文章系列,特别面向 Python 初学者。简言之,我将会尝试更多新的工具,编写尽可能少的代码,来完成一个有趣的项目。
例如,我们今天学习使用 Twilio API、Twitch API、在 Heroku 上发布项目。我将会教你用 30 行代码写一个 “Twitch 直播”短信通知工具,而且一个月只需要花费 12 美分。
前提
你只需要懂得怎么运行 Python 程序,以及会操作基本的 git 命令行(commit & push)。如果你需要学习一下这些知识点,可以看看下面两篇文章:
《Python 3 安装与设置指南》
《Git 最佳入门教程》fromAdrian Hajdin
将会学到的知识将要构建的项目
要求很简单:我们想要在某个主播正在直播的时候接收到一条短信通知,我们想要知道主播何时上线以及何时退出直播,并且这个通知程序全天都在自动运行。
我们将把整个项目分成 3 个部分。首先,我们看看如何通过程序知晓一个特定主播上线了,然后看看如何接收一条主播上线的通知短信,最后我们需要让这段代码每隔 X 分钟执行一次,这样我们就不会错过喜欢的主播的动态啦。
主播正在直播吗?
我们可以这样了解主播是否正在直播:访问主播的 URL,看看是否有 Live 徽章。
主播直播时候的截图
这个过程涉及到网络爬虫,而且这个功能不是 20 行左右的 Python 代码能完成的。Twitch 使用了非常多的 JS 脚本代码,一个简单的 request.get() 是不足以达到我们要求的。
对于使用爬虫去爬取直播信息,我们将会借助 Chrome 浏览器抓取这个网页的内容,如截图所示。这种方式是可行的,但是需要 30 行以上代码。如果你想要了解更多,可以参考我最近的文章《网页抓取指南》。
除了抓取 Twitch 网页这种方式外,我们还可以使用 Twitch 的 API。有的读者可能不了解 API 这个术语,这里我们解释一下:API 是应用程序编程接口,网站通过 API 向任何人(主要是开发者)公开特性和数据。Twitch 的 API 是通过 HTTP 协议对外开放,也就是说我们可以通过一个简单的 HTTP 请求去获取到大量信息以及做许多事情。
获取你的 API KEY
首先,你需要去创建一个 Twitch 的 API Key。许多 API 服务需要对访问者进行身份认证,以避免有人滥用 API,或者以限制某些人访问某些功能。
请按照以下步骤获取你的 API Key:
在屏幕底端,你可以看到你的 client-id,将它保存好,稍后会使用。
确认主播是否正在直播
我们手上有了 API key,现在就可以查询 Twitch 的 API 获取我们想要的信息。下面的代码给 Twitch 的 API 传递了正确的参数并且打印响应信息。
# requests is the go to package in python to make http request<br /># https://2.python-requests.org/en/master/<br />import requests<br /><br /># This is one of the route where Twich expose data, <br /># They have many more: https://dev.twitch.tv/docs<br />endpoint = "https://api.twitch.tv/helix/streams?"<br /><br /># In order to authenticate we need to pass our api key through header<br />headers = {"Client-ID": ""}<br /><br /># The previously set endpoint needs some parameter, here, the Twitcher we want to follow<br /># Disclaimer, I don't even know who this is, but he was the first one on Twich to have a live stream so I could have nice examples<br />params = {"user_login": "Solary"}<br /><br /># It is now time to make the actual request<br />response = request.get(endpoint, params=params, headers=headers)<br />print(response.json())<br />
输出信息就像下面这样:
{<br /> 'data':[<br /> {<br /> 'id':'35289543872',<br /> 'user_id':'174955366',<br /> 'user_name':'Solary',<br /> 'game_id':'21779',<br /> 'type':'live',<br /> 'title':"Wakz duoQ w/ Tioo - GM 400LP - On récupère le chall après les -250LP d'inactivité !",<br /> 'viewer_count':4073,<br /> 'started_at':'2019-08-14T07:01:59Z',<br /> 'language':'fr',<br /> 'thumbnail_url':'https://static-cdn.jtvnw.net/previews-ttv/live_user_solary-{width}x{height}.jpg',<br /> 'tag_ids':[<br /> '6f655045-9989-4ef7-8f85-1edcec42d648'<br /> ]<br /> }<br /> ],<br /> 'pagination':{<br /> 'cursor':'eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6MX19'<br /> }<br />}<br />
这个数据格式是一种易于阅读的 JSON 格式。data是一个包含所有当前直播的数组对象。type键表示这个直播间正在直播,此外type的值还可以为空(比如,在报错的时候)。
因此如果我们想要在 Python 里创建一个表示当前主播是否正在直播的布尔变量,我们需要去加上如下代码:
json_response = response.json()<br /><br /># We get only streams<br />streams = json_response.get('data', [])<br /><br /># We create a small function, (a lambda), that tests if a stream is live or not<br />is_active = lambda stream: stream.get('type') == 'live'<br /># We filter our array of streams with this function so we only keep streams that are active<br />streams_active = filter(is_active, streams)<br /><br /># any returns True if streams_active has at least one element, else False<br />at_least_one_stream_active = any(streams_active)<br /><br />print(at_least_one_stream_active)<br />
此时,at_least_one_stream_active变量是 True 的时候表示你喜欢的主播正在直播。
让我们现在看看如何获得短信通知。
现在给自己发一条短信!
那么我们将使用 Twilio API 自己发送一条短信。访问此处并且创建一个账号。当需要你手机验证的时候,填入你想要在此项目中接受短信的手机号码,这样你就可以使用 Twilio 为新用户提供的 15 美元的免费信用额度。一条短信 1 美分,足以支撑你的机器运行一年了。
访问console,你将会看到自己的 Account SID 和 Auth Token。请保留好它们以备后用。同时点击红色按钮“获得试用账号”,进行下一步,将试用账号也保存好以备后用。
使用 Python API 发送短信很简单,有软件包帮你一系列事情。使用pip install Twilio导入相应的包并且执行下面的代码:
from twilio.rest import Client<br />client = Client(, )<br />client.messages.create(<br /> body='Test MSG',from_=,to=)<br />
只需要这么点代码,你就可以给自己发一条通知短信了,是不是很棒?
整合所有代码
现在我们来整合所有代码,压缩到不到 30 行 Python 代码。
import requests<br />from twilio.rest import Client<br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br />if at_least_one_stream_active:<br /> client = Client(, )<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />
只留下了 10 多行代码!
避免重复通知
这段代码的效果很好,但是如果这段代码在服务器上每分钟执行一次,我们喜欢的主播一开启直播,我们就会每分钟都收到一条短信。
我们需要让程序知道它已经给我们发了主播上线直播的短信通知,别再重复发短信了。好消息是 Twilio API 提供检索历史消息的方法,因此我们仅仅需要检索发送的历史消息中是否包含已经发送过的主播正在直播的消息。
如下是我们要做的伪代码:
if favorite_twitcher_live and last_sent_sms is not live_notification:<br /> send_live_notification()<br />if not favorite_twitcher_live and last_sent_sms is live_notification:<br /> send_live_is_over_notification()<br />
使用这种方法,我们将会在直播开始和结束后都接收到短信。这样我们就不会收到重复信息了。现在完美了么?让我们继续吧。
# reusing our Twilio client<br />last_messages_sent = client.messages.list(limit=1)<br />last_message_id = last_messages_sent[0].sid<br />last_message_data = client.messages(last_message_id).fetch()<br />last_message_content = last_message_data.body<br />
现在我们再一次将代码合起来:
import requests<br />from twilio.rest import Client<br />client = Client(, )<br /><br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br /><br />last_messages_sent = client.messages.list(limit=1)<br />if last_messages_sent:<br /> last_message_id = last_messages_sent[0].sid<br /> last_message_data = client.messages(last_message_id).fetch()<br /> last_message_content = last_message_data.body<br /> online_notified = "LIVE" in last_message_content<br /> offline_notified = not online_notified<br />else:<br /> online_notified, offline_notified = False, False<br /><br />if at_least_one_stream_active and not online_notified:<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />if not at_least_one_stream_active and not offline_notified:<br /> client.messages.create(body='OFFLINE !!!',from_=,to=)<br />
完成了!
你现在拥有一段不到 30 行的 Python 代码,可以在你喜欢的主播上线或者离线的时候发送短信通知给你,而且不会重复发送信息。
我们现在需要一种方法去托管代码,并且每 X 分钟执行一次这个程序。
托管代码
我们将使用 Heroku 去托管、执行该代码。Heroku 是一种简便的托管 app 到 Web 的方式。它的缺点是,比起其他的解决方案,价格会贵一些。幸运的是,他们有一个慷慨的免费计划允许我们做所有想做的事。
如果你之前没有Heroku 账户,那就创建一个吧。你同时也需要下载并且安装 Heroku 客户端。
现在你需要将你的 Python 脚本放到自己的文件夹内,记得加一个requirements.txt文件在里面。文件内容的开头如下:
requests<br />twilio<br />
这样可以确保 Heroku 下载正确的依赖程序
cd进入到该文件夹内同时执行heroku create --app <app name>。如果你进入到app 看板页,你将会看到你的新 app。
我们现在需要去初始化一个 git 仓库并且 push 代码到 Heroku:
git init<br />heroku git:remote -a <br />git add .<br />git commit -am 'Deploy breakthrough script'<br />git push heroku master<br />
现在你的 app 已经传到 Heroku,但是它还不可以干任何事。由于这个小脚本无法接受 HTTP 请求,访问.没法做任何事,但是这并不是一个问题。
为了让这个脚本全天候执行,我们需要使用一个简单的 Heroku 插件 Heroku Scheduler。在你的 app 看板页点击 Configure Add-ons 来安装插件。
接下来在搜索框输入 Heroku Scheduler:
点击搜索结果,并且按下 Provision 按钮。
如果你返回到你的 app 看板页,你将会看到 Heroku Scheduler:
点击 Heroku Scheduler链接去配置一个任务,点击 Create Job 按钮,在这里选择 10 minutes 的选项,之后选择执行命令python <name_of_your_script>.py,最后点击 Save job 按钮。
虽然到目前为止我们在 Heroku 上使用的所有东西都是免费的,但是 Heroku Scheduler 将会花费 25 美元每个月。而我们的程序是要秒级执行的。因为该脚本需要每 3 秒执行一次,所以每 10 分钟运行该项目,一个月下来将会花费 12 美分。
建议
我希望你喜欢这个项目,并且喜欢自己动手操作的过程。我们通过这不到 30 行代码实现了很多功能。不过这个项目还不够完美,这里我有一些改善的建议:
如果你有其他好主意,欢迎留言告诉我。
总结
我希望你喜欢上这篇文章并且通过这篇文章学到东西。我相信这样的项目是学习新工具和新概念的最好方式。最近我做了 Web scraping API,在做的过程中我也学到很多。如果你喜欢这种学习方式并且你想要做更多的事情,请在评论区留言。
我有许多别的想法,希望你将会喜欢上它们。如果你使用这段代码实现了别的东西,请一定分享给我啊。我相信这段代码有很多可能性。
Happy Coding!
非营利组织自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
你也想成为 freeCodeCamp 社区的贡献者吗?欢迎了解。 查看全部
30 行 Python 代码实现 Twitch 主播上线实时通知
大家好,今天我将开始写作一个新的文章系列,特别面向 Python 初学者。简言之,我将会尝试更多新的工具,编写尽可能少的代码,来完成一个有趣的项目。
例如,我们今天学习使用 Twilio API、Twitch API、在 Heroku 上发布项目。我将会教你用 30 行代码写一个 “Twitch 直播”短信通知工具,而且一个月只需要花费 12 美分。
前提
你只需要懂得怎么运行 Python 程序,以及会操作基本的 git 命令行(commit & push)。如果你需要学习一下这些知识点,可以看看下面两篇文章:
《Python 3 安装与设置指南》
《Git 最佳入门教程》fromAdrian Hajdin
将会学到的知识将要构建的项目
要求很简单:我们想要在某个主播正在直播的时候接收到一条短信通知,我们想要知道主播何时上线以及何时退出直播,并且这个通知程序全天都在自动运行。
我们将把整个项目分成 3 个部分。首先,我们看看如何通过程序知晓一个特定主播上线了,然后看看如何接收一条主播上线的通知短信,最后我们需要让这段代码每隔 X 分钟执行一次,这样我们就不会错过喜欢的主播的动态啦。
主播正在直播吗?
我们可以这样了解主播是否正在直播:访问主播的 URL,看看是否有 Live 徽章。
主播直播时候的截图
这个过程涉及到网络爬虫,而且这个功能不是 20 行左右的 Python 代码能完成的。Twitch 使用了非常多的 JS 脚本代码,一个简单的 request.get() 是不足以达到我们要求的。
对于使用爬虫去爬取直播信息,我们将会借助 Chrome 浏览器抓取这个网页的内容,如截图所示。这种方式是可行的,但是需要 30 行以上代码。如果你想要了解更多,可以参考我最近的文章《网页抓取指南》。
除了抓取 Twitch 网页这种方式外,我们还可以使用 Twitch 的 API。有的读者可能不了解 API 这个术语,这里我们解释一下:API 是应用程序编程接口,网站通过 API 向任何人(主要是开发者)公开特性和数据。Twitch 的 API 是通过 HTTP 协议对外开放,也就是说我们可以通过一个简单的 HTTP 请求去获取到大量信息以及做许多事情。
获取你的 API KEY
首先,你需要去创建一个 Twitch 的 API Key。许多 API 服务需要对访问者进行身份认证,以避免有人滥用 API,或者以限制某些人访问某些功能。
请按照以下步骤获取你的 API Key:
在屏幕底端,你可以看到你的 client-id,将它保存好,稍后会使用。
确认主播是否正在直播
我们手上有了 API key,现在就可以查询 Twitch 的 API 获取我们想要的信息。下面的代码给 Twitch 的 API 传递了正确的参数并且打印响应信息。
# requests is the go to package in python to make http request<br /># https://2.python-requests.org/en/master/<br />import requests<br /><br /># This is one of the route where Twich expose data, <br /># They have many more: https://dev.twitch.tv/docs<br />endpoint = "https://api.twitch.tv/helix/streams?"<br /><br /># In order to authenticate we need to pass our api key through header<br />headers = {"Client-ID": ""}<br /><br /># The previously set endpoint needs some parameter, here, the Twitcher we want to follow<br /># Disclaimer, I don't even know who this is, but he was the first one on Twich to have a live stream so I could have nice examples<br />params = {"user_login": "Solary"}<br /><br /># It is now time to make the actual request<br />response = request.get(endpoint, params=params, headers=headers)<br />print(response.json())<br />
输出信息就像下面这样:
{<br /> 'data':[<br /> {<br /> 'id':'35289543872',<br /> 'user_id':'174955366',<br /> 'user_name':'Solary',<br /> 'game_id':'21779',<br /> 'type':'live',<br /> 'title':"Wakz duoQ w/ Tioo - GM 400LP - On récupère le chall après les -250LP d'inactivité !",<br /> 'viewer_count':4073,<br /> 'started_at':'2019-08-14T07:01:59Z',<br /> 'language':'fr',<br /> 'thumbnail_url':'https://static-cdn.jtvnw.net/previews-ttv/live_user_solary-{width}x{height}.jpg',<br /> 'tag_ids':[<br /> '6f655045-9989-4ef7-8f85-1edcec42d648'<br /> ]<br /> }<br /> ],<br /> 'pagination':{<br /> 'cursor':'eyJiIjpudWxsLCJhIjp7Ik9mZnNldCI6MX19'<br /> }<br />}<br />
这个数据格式是一种易于阅读的 JSON 格式。data是一个包含所有当前直播的数组对象。type键表示这个直播间正在直播,此外type的值还可以为空(比如,在报错的时候)。
因此如果我们想要在 Python 里创建一个表示当前主播是否正在直播的布尔变量,我们需要去加上如下代码:
json_response = response.json()<br /><br /># We get only streams<br />streams = json_response.get('data', [])<br /><br /># We create a small function, (a lambda), that tests if a stream is live or not<br />is_active = lambda stream: stream.get('type') == 'live'<br /># We filter our array of streams with this function so we only keep streams that are active<br />streams_active = filter(is_active, streams)<br /><br /># any returns True if streams_active has at least one element, else False<br />at_least_one_stream_active = any(streams_active)<br /><br />print(at_least_one_stream_active)<br />
此时,at_least_one_stream_active变量是 True 的时候表示你喜欢的主播正在直播。
让我们现在看看如何获得短信通知。
现在给自己发一条短信!
那么我们将使用 Twilio API 自己发送一条短信。访问此处并且创建一个账号。当需要你手机验证的时候,填入你想要在此项目中接受短信的手机号码,这样你就可以使用 Twilio 为新用户提供的 15 美元的免费信用额度。一条短信 1 美分,足以支撑你的机器运行一年了。
访问console,你将会看到自己的 Account SID 和 Auth Token。请保留好它们以备后用。同时点击红色按钮“获得试用账号”,进行下一步,将试用账号也保存好以备后用。
使用 Python API 发送短信很简单,有软件包帮你一系列事情。使用pip install Twilio导入相应的包并且执行下面的代码:
from twilio.rest import Client<br />client = Client(, )<br />client.messages.create(<br /> body='Test MSG',from_=,to=)<br />
只需要这么点代码,你就可以给自己发一条通知短信了,是不是很棒?
整合所有代码
现在我们来整合所有代码,压缩到不到 30 行 Python 代码。
import requests<br />from twilio.rest import Client<br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br />if at_least_one_stream_active:<br /> client = Client(, )<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />
只留下了 10 多行代码!
避免重复通知
这段代码的效果很好,但是如果这段代码在服务器上每分钟执行一次,我们喜欢的主播一开启直播,我们就会每分钟都收到一条短信。
我们需要让程序知道它已经给我们发了主播上线直播的短信通知,别再重复发短信了。好消息是 Twilio API 提供检索历史消息的方法,因此我们仅仅需要检索发送的历史消息中是否包含已经发送过的主播正在直播的消息。
如下是我们要做的伪代码:
if favorite_twitcher_live and last_sent_sms is not live_notification:<br /> send_live_notification()<br />if not favorite_twitcher_live and last_sent_sms is live_notification:<br /> send_live_is_over_notification()<br />
使用这种方法,我们将会在直播开始和结束后都接收到短信。这样我们就不会收到重复信息了。现在完美了么?让我们继续吧。
# reusing our Twilio client<br />last_messages_sent = client.messages.list(limit=1)<br />last_message_id = last_messages_sent[0].sid<br />last_message_data = client.messages(last_message_id).fetch()<br />last_message_content = last_message_data.body<br />
现在我们再一次将代码合起来:
import requests<br />from twilio.rest import Client<br />client = Client(, )<br /><br />endpoint = "https://api.twitch.tv/helix/streams?"<br />headers = {"Client-ID": ""}<br />params = {"user_login": "Solary"}<br />response = request.get(endpoint, params=params, headers=headers)<br />json_response = response.json()<br />streams = json_response.get('data', [])<br />is_active = lambda stream:stream.get('type') == 'live'<br />streams_active = filter(is_active, streams)<br />at_least_one_stream_active = any(streams_active)<br /><br />last_messages_sent = client.messages.list(limit=1)<br />if last_messages_sent:<br /> last_message_id = last_messages_sent[0].sid<br /> last_message_data = client.messages(last_message_id).fetch()<br /> last_message_content = last_message_data.body<br /> online_notified = "LIVE" in last_message_content<br /> offline_notified = not online_notified<br />else:<br /> online_notified, offline_notified = False, False<br /><br />if at_least_one_stream_active and not online_notified:<br /> client.messages.create(body='LIVE !!!',from_=,to=)<br />if not at_least_one_stream_active and not offline_notified:<br /> client.messages.create(body='OFFLINE !!!',from_=,to=)<br />
完成了!
你现在拥有一段不到 30 行的 Python 代码,可以在你喜欢的主播上线或者离线的时候发送短信通知给你,而且不会重复发送信息。
我们现在需要一种方法去托管代码,并且每 X 分钟执行一次这个程序。
托管代码
我们将使用 Heroku 去托管、执行该代码。Heroku 是一种简便的托管 app 到 Web 的方式。它的缺点是,比起其他的解决方案,价格会贵一些。幸运的是,他们有一个慷慨的免费计划允许我们做所有想做的事。
如果你之前没有Heroku 账户,那就创建一个吧。你同时也需要下载并且安装 Heroku 客户端。
现在你需要将你的 Python 脚本放到自己的文件夹内,记得加一个requirements.txt文件在里面。文件内容的开头如下:
requests<br />twilio<br />
这样可以确保 Heroku 下载正确的依赖程序
cd进入到该文件夹内同时执行heroku create --app <app name>。如果你进入到app 看板页,你将会看到你的新 app。
我们现在需要去初始化一个 git 仓库并且 push 代码到 Heroku:
git init<br />heroku git:remote -a <br />git add .<br />git commit -am 'Deploy breakthrough script'<br />git push heroku master<br />
现在你的 app 已经传到 Heroku,但是它还不可以干任何事。由于这个小脚本无法接受 HTTP 请求,访问.没法做任何事,但是这并不是一个问题。
为了让这个脚本全天候执行,我们需要使用一个简单的 Heroku 插件 Heroku Scheduler。在你的 app 看板页点击 Configure Add-ons 来安装插件。
接下来在搜索框输入 Heroku Scheduler:
点击搜索结果,并且按下 Provision 按钮。
如果你返回到你的 app 看板页,你将会看到 Heroku Scheduler:
点击 Heroku Scheduler链接去配置一个任务,点击 Create Job 按钮,在这里选择 10 minutes 的选项,之后选择执行命令python <name_of_your_script>.py,最后点击 Save job 按钮。
虽然到目前为止我们在 Heroku 上使用的所有东西都是免费的,但是 Heroku Scheduler 将会花费 25 美元每个月。而我们的程序是要秒级执行的。因为该脚本需要每 3 秒执行一次,所以每 10 分钟运行该项目,一个月下来将会花费 12 美分。
建议
我希望你喜欢这个项目,并且喜欢自己动手操作的过程。我们通过这不到 30 行代码实现了很多功能。不过这个项目还不够完美,这里我有一些改善的建议:
如果你有其他好主意,欢迎留言告诉我。
总结
我希望你喜欢上这篇文章并且通过这篇文章学到东西。我相信这样的项目是学习新工具和新概念的最好方式。最近我做了 Web scraping API,在做的过程中我也学到很多。如果你喜欢这种学习方式并且你想要做更多的事情,请在评论区留言。
我有许多别的想法,希望你将会喜欢上它们。如果你使用这段代码实现了别的东西,请一定分享给我啊。我相信这段代码有很多可能性。
Happy Coding!
非营利组织自 2014 年成立以来,以“帮助人们免费学习编程”为使命,创建了大量免费的编程教程,包括交互式课程、视频课程、文章等。线下开发者社区遍布 160 多个国家、2000 多个城市。我们正在帮助全球数百万人学习编程,希望让世界上每个人都有机会获得免费的优质的编程教育资源,成为开发者或者运用编程去解决问题。
你也想成为 freeCodeCamp 社区的贡献者吗?欢迎了解。
不会写代码也能实现赏金自动化
网站优化 • 优采云 发表了文章 • 0 个评论 • 58 次浏览 • 2022-06-01 14:41
最近一直在研究自动化漏洞发现的技术,github 也有非常多优秀的集成工具,本着学习研究的心态,对这些工具进行了学习,今天来分享其中的一个,通过 bash 脚本将各种工具集成到一起,实现无需自己实现相关功能也能自动化漏洞发现。项目地址:
reconFTW 是一款优秀的信息收集工具,可以尽可能的收集子域名以及各种漏洞的检查,说的这么溜,到底是如何实现的?这是我比较感兴趣的,首先看一下安装脚本,看看都用到了哪些工具,如图:
数了数,大概用了三十个工具集成在一起,接下来我想知道这些工具都是干什么用的,那么就需要一个一个的看,为了节省大家的时间,我来给大家一一介绍一下。
0x01 gf (替代 grep,更方便提取结果中的关键内容)
项目地址:
其实 grep 能做非常多的事儿,但是由于参数复杂,每次使用可能都要去查询怎么用,如何组合参数,而这个工具就是为了能够在其他工具输出结果之后,通过这个工具来对结果进行整理,从而输出不同工具所需的参数内容,实现不同工具之间的数据共享来实现自动化的流程。
0x02 qsreplace (url 去重、参数替换)
项目地址:
在抓取到大量 URL 时,需要对这些 URL 进行去重,去掉相同接口、相同参数但是参数值不同的 URL,该工具可以替换 URL 中的参数值为某个指定字符串,替换之后在进行整体去重,就能实现相同接口、相同参数名的 URL 只保留一个,减少测试的目标,从而提升测试效率,实现效果:
0x03 Amass(子域名收集工具)
项目地址:
子域名收集的方式无非集中方式:dns 枚举、网络空间搜索、dns 反解析、搜索引擎、历史 dns 记录等,这款工具集成非常多的数据源,一个图就能一目了然:
0x04 ffuf (网页 fuzz 工具)
项目地址:
FUZZ 的目的就通过大量测试来发现隐藏的问题,比如隐藏的功能,隐藏的漏洞等,这个工具可以对网站的请求进行模糊测试,无论是 GET 参数还是 POST 参数,还能测试 header 中的关键字段以及目录等。比如 fuzz 目录:
0x05 github-subdomains(从 github 上发现子域名)
项目地址:
github 是程序员的聚集地,程序员的共享精神是一直存在的,他们也会时不时把自己在企业写的代码分享出去,从而给了我们一个信息收集的途径,那么这个工具就是通过 github 的代码搜索功能来实现子域名的收集。
0x06 waybackurls (从第三方平台获取目标网页内容)
项目地址:
这个工具之前的文章介绍过,主要从多个第三方平台来获取目标网页内容,有些平台通过自己实现爬虫的方式,抓取全网网站内容,然后提供给一些人使用,从而省去了直接访问目标的操作,在不接触目标的情况下也能获取目标网页内容。该工具主要获取的网页中的 URL 列表,为后续的漏洞测试做准备。
0x07 nuclei (poc 扫描工具)
项目地址:
nuclei 是国外知名的 POC 检测工具,它所收录的 POC 非常丰富,而且更新非常快,网络上一旦出现任何漏洞,其 POC 可能几个小时就能获得更新,而且完全免费开源,用过的都说好。
0x08 anew (内容去重工具)
项目地址:
类似于tee -a去掉文件中的重复行,没啥可介绍的,直接看案例吧:
0x09 notify (自动通报工具)
项目地址:
该工具可以将消息推送到多个平台(Slack / Discord / Telegram / Pushover / Email / Teams 等),主要方便在发现漏洞时,能够及时得知,然后提交,防止发现漏洞而被他人捷足先登的情况,当然怎么用,还要看自己的需求。
0x0A unfurl(快速提取 url 中的关键字段)
项目地址:
当我们获取到大量的 URL 时,我们需要提取 URL 中的主域名或者子域名输出列表,怎么办?你可以自己编写脚本实现,当然也可以使用这个工具,直接看效果吧:
0x0B 阶段性总结
这个项目集成了三十个工具,今天先分享十个吧,不知道大家是否有兴趣了解更多,如果还想要了解剩下的二十个工具分别是什么,有什么用,以及项目流程如何将所有工具集成自动化的,还请你点个再看,根据大家的意愿来决定是否更新下一篇完整介绍以及程序的流程讲解。
查看全部
不会写代码也能实现赏金自动化
最近一直在研究自动化漏洞发现的技术,github 也有非常多优秀的集成工具,本着学习研究的心态,对这些工具进行了学习,今天来分享其中的一个,通过 bash 脚本将各种工具集成到一起,实现无需自己实现相关功能也能自动化漏洞发现。项目地址:
reconFTW 是一款优秀的信息收集工具,可以尽可能的收集子域名以及各种漏洞的检查,说的这么溜,到底是如何实现的?这是我比较感兴趣的,首先看一下安装脚本,看看都用到了哪些工具,如图:
数了数,大概用了三十个工具集成在一起,接下来我想知道这些工具都是干什么用的,那么就需要一个一个的看,为了节省大家的时间,我来给大家一一介绍一下。
0x01 gf (替代 grep,更方便提取结果中的关键内容)
项目地址:
其实 grep 能做非常多的事儿,但是由于参数复杂,每次使用可能都要去查询怎么用,如何组合参数,而这个工具就是为了能够在其他工具输出结果之后,通过这个工具来对结果进行整理,从而输出不同工具所需的参数内容,实现不同工具之间的数据共享来实现自动化的流程。
0x02 qsreplace (url 去重、参数替换)
项目地址:
在抓取到大量 URL 时,需要对这些 URL 进行去重,去掉相同接口、相同参数但是参数值不同的 URL,该工具可以替换 URL 中的参数值为某个指定字符串,替换之后在进行整体去重,就能实现相同接口、相同参数名的 URL 只保留一个,减少测试的目标,从而提升测试效率,实现效果:
0x03 Amass(子域名收集工具)
项目地址:
子域名收集的方式无非集中方式:dns 枚举、网络空间搜索、dns 反解析、搜索引擎、历史 dns 记录等,这款工具集成非常多的数据源,一个图就能一目了然:
0x04 ffuf (网页 fuzz 工具)
项目地址:
FUZZ 的目的就通过大量测试来发现隐藏的问题,比如隐藏的功能,隐藏的漏洞等,这个工具可以对网站的请求进行模糊测试,无论是 GET 参数还是 POST 参数,还能测试 header 中的关键字段以及目录等。比如 fuzz 目录:
0x05 github-subdomains(从 github 上发现子域名)
项目地址:
github 是程序员的聚集地,程序员的共享精神是一直存在的,他们也会时不时把自己在企业写的代码分享出去,从而给了我们一个信息收集的途径,那么这个工具就是通过 github 的代码搜索功能来实现子域名的收集。
0x06 waybackurls (从第三方平台获取目标网页内容)
项目地址:
这个工具之前的文章介绍过,主要从多个第三方平台来获取目标网页内容,有些平台通过自己实现爬虫的方式,抓取全网网站内容,然后提供给一些人使用,从而省去了直接访问目标的操作,在不接触目标的情况下也能获取目标网页内容。该工具主要获取的网页中的 URL 列表,为后续的漏洞测试做准备。
0x07 nuclei (poc 扫描工具)
项目地址:
nuclei 是国外知名的 POC 检测工具,它所收录的 POC 非常丰富,而且更新非常快,网络上一旦出现任何漏洞,其 POC 可能几个小时就能获得更新,而且完全免费开源,用过的都说好。
0x08 anew (内容去重工具)
项目地址:
类似于tee -a去掉文件中的重复行,没啥可介绍的,直接看案例吧:
0x09 notify (自动通报工具)
项目地址:
该工具可以将消息推送到多个平台(Slack / Discord / Telegram / Pushover / Email / Teams 等),主要方便在发现漏洞时,能够及时得知,然后提交,防止发现漏洞而被他人捷足先登的情况,当然怎么用,还要看自己的需求。
0x0A unfurl(快速提取 url 中的关键字段)
项目地址:
当我们获取到大量的 URL 时,我们需要提取 URL 中的主域名或者子域名输出列表,怎么办?你可以自己编写脚本实现,当然也可以使用这个工具,直接看效果吧:
0x0B 阶段性总结
这个项目集成了三十个工具,今天先分享十个吧,不知道大家是否有兴趣了解更多,如果还想要了解剩下的二十个工具分别是什么,有什么用,以及项目流程如何将所有工具集成自动化的,还请你点个再看,根据大家的意愿来决定是否更新下一篇完整介绍以及程序的流程讲解。