爬虫攻守道 - 2023最新 - 正则表达式勇猛精进 - 爬取某天气网站历史数据

news2025/1/14 1:07:59

前言

在 正则表达式 - 匹配开头、结尾、中间 - 某天气网站网页源代码分析 这篇文章里,我们介绍了如何用正则表达式匹配包含特定样式的Table标签,也就是同时匹配开头、结尾、以及中间。

当你能真正理解这个写法,就会觉得不过是柳暗花明罢了。是的,现实往往如此 —— 屡败屡战的过程最难将书,结果最易轻描淡写,只剩下予取予求的招数变化。

所以接下来,我们要做的就是复制这个成功经验,把之前那些“2步走方案”实现的匹配替换,全部改为“1步到位”方案 —— 性能且不说,我们先追求代码简洁度,开搞。

正文

限定范围 精准打击

regex101 网站有提供1个对理解正则表达式运作过程非常有用的工具:Regex Debugger。我简单录了个视频,有兴趣的同学可以自己去研究。

正则表达式运行过程

这里想表达的是,我们可能不需要理解正则的原理,就向开车没必要懂车构造一样。但有一点需要清楚的是:匹配就是寻找,在小范围内寻找一定比在大范围内寻找要快。

基于这个铁律,看下我们的代码逻辑,即使是在剔除“假Table”后,几乎每个正则也还是在做全文匹配,寻找 td,寻找 th 等等等等 —— 而我们的目标范围其实很明确,就是在剩余的这个 “真Table” 中,所以其实没有必要对 html 内 table 外 的其他任何内容去做任何匹配了。

我们用1个正则将“真Table” —— <table></table> 标签内的东西提取,保存,将后续所有的匹配限定在这个范围。(其实可以进一步缩小到  <tbody></tbody>,甚至贪婪匹配从第1个 <tr> 到最后1个 </tr>之间). 得到这样的代码。让 tbm_table 而不是 page_source, 成为我们的最新目标。

# limit match scope, table to be matched
regex_tbm_table = r"<tbody>([\s\S]*?)<\/tbody>"
tbm_table = re.findall(regex_tbm_table, page_source)[0]

###
# 中间是对这个 “真table” 的进一步处理
###

# 将处理好的 table 再填充回 html 代码
page_source = re.sub(regex_tbm_table, tbm_table, page_source)

鸟枪换炮 重装出击

这篇 爬虫攻守道 - 2023最新 - Python Selenium 实现 - 数据去伪存真,正则表达式谁与争锋 - 爬取某天气网站历史数据 留下的第1条 Todo ,是我们的第1个目标。

之前为了替换掉包含4种隐藏样式的td,我们先是正则得到td,然后for 循环全部 td,再次正则找出匹配然后 replace 为空。忽略打印测试,我们用了21行代码来达到这个目标。

# 在保留的 table 中匹配出所有的 td
regex_td = r"<td.*?\/td>"
tds = re.findall(regex_td, page_source)
print('{}_{}: total {} tds been found'.format(city, month, len(tds)))


# 将包含 display:none 隐藏样式的 td 其实就是假数据,替换为空
# display:none 是最终效果,实际在代码里是随机字符串,而且好几个,所以先找到 css 定义中内容为 display:none 的样式名称
regex_td_css1 = r"(.*?)\s*{\n\s*display: none;"
class_ndisplays = re.findall(regex_td_css1, page_source)
# print('class_ndisplays', len(class_ndisplays))

# 将包含这些样式的 td 替换为空
for class_ndisplay in class_ndisplays:
	class_ndisplay = class_ndisplay.replace('.', '').strip()

	for td in tds:
		if re.findall(class_ndisplay, td):
			page_source = page_source.replace(td, '')


# 将包含明文 display:none 样式的 td 替换为空
regex_td_css2 = r"display:none"
for td in tds:
	if re.findall(regex_td_css2, td):
		page_source = page_source.replace(td, '')

# 将包含 hidden-lg 隐藏样式的 td 其实就是假数据,替换为空
regex_td_css3 = "hidden-lg"
for td in tds:
	if re.findall(regex_td_css3, td):
		page_source = page_source.replace(td, '')

regex_td_css4 = r"\"hidden\""
for td in tds:
	if re.findall(regex_td_css4, td):
		page_source = page_source.replace(td, '')

让我们看看 21行最终能简化到几行。

第1步:先打出头鸟

比其他3个样式都复杂的 regex_td_css1,原先的处理逻辑是先根据 css定义的形式 —— {}包裹,以及内容为 display: none 的 —— 反向提取得到名称。然后再到td 范围内匹配 css引用名称,进而替换。

以我目前极为有限的前端知识 —— 提取名称这步是绕不开的,但是css 引用都是定义在 <style></style> 标签内的。所以按照缩小范围精准打击的思路,我们先得到<style></style> 标签内的东西,代号为 tbm_style,然后在这个小范围内得到被引用的 css样式名称,依然是1个随机字符串列表。

再下来,我们用1次 for 循环,在 tbm_table 范围内把这些名称全部替换为 和 regex_td_css2 一致的明文 display:none。 经过这样“合并”处理后,即使还需要 for 循环全部 td,我们也只需要3次 (针对 regex_td_css2,regex_td_css3,regex_td_css4)。于是得到这样的代码:

# 处理 regex_td_css1
# css 引用样式在 <style></style> 中定义
regex_tbm_style = r"<style([\s\S]*?)<\/style>"
tbm_style = re.findall(regex_tbm_style, page_source)[0]

regex_td_css1 = r"(.*?)\s*{\n\s*display: none;"
class_ndisplays = re.findall(regex_td_css1, tbm_style)
# print('class_ndisplays', len(class_ndisplays))

# 将 table 内对这些样式的引用。替换为明文 display:none
# 加1个 total_count 用作调试辅助,观察次数
total_count = 0
for class_ndisplay in class_ndisplays:
	class_ndisplay = class_ndisplay.replace('.', '').strip()
	tbm_table, count = re.subn(class_ndisplay, r'display:none', tbm_table)
	total_count = total_count + count
print('{}_{}: total {} css quotations been replaced.'.format(city, month, total_count))

第2步,鸟枪换炮。

此鸟非彼鸟。直接在全文种寻找满足条件的td,然后re.sub,干掉 for 循环。前提就是写出这种一般人没耐心真看不懂的正则表达式,当然还需要得心应手的测试和验证工具

 

于是得到这样的代码。不算打印的话,原先每个样式4行代码,现在变成了2行。我们使用13行代码完成了原先21行代码才能完成的工作。

# 形式:class="display:none"
regex_css2_td = r"(?=<td)(?:(?!<\/td>)[\s\S])*?display:none[\s\S]*?<\/td>"
tbm_table, count_css2 = re.subn(regex_css2_td, '', tbm_table)
print('{}_{}: total {} css2:display:none been replaced. {} left.'.format(city, month, count_css2, len(tds)-count_css2))

# 形式:class="hidden-xs hidden-sm hidden-lg hidden-md" —— hidden-lg 是其中之一
regex_css3_td = r"(?=<td)(?:(?!<\/td>)[\s\S])*?hidden-lg[\s\S]*?<\/td>"
tbm_table, count_css3 = re.subn(regex_css3_td, '', tbm_table)
print('{}_{}: total {} css3:hidden-lg been replaced. {} left.'.format(city, month, count_css3, len(tds)-count_css2-count_css3))

# 形式:class="hidden" —— hidden 就1个,所以加上前后引号,否则和 css1 无法区分
regex_css4_td = r"(?=<td)(?:(?!<\/td>)[\s\S])*?\"hidden\"[\s\S]*?<\/td>"
tbm_table, count_css4 = re.subn(regex_css4_td, '', tbm_table)
print('{}_{}: total {} css4:hidden been replaced. {} left.'.format(city, month, count_css4, len(tds)-count_css2-count_css3-count_css4))

更进一步,还有1种更为牛逼、更为疯狂的写法,re.subn 的第1个参数支持多种不同的匹配规则,中间以 | 分隔,这样最终的有效代码行数来到了12行。

# 形式:class="display:none"
regex_css_rule2 = r"(?=<td)(?:(?!<\/td>)[\s\S])*?display:none[\s\S]*?<\/td>"

# 形式:class="hidden-xs hidden-sm hidden-lg hidden-md" —— hidden-lg 是其中之一
regex_css_rule3 = r"(?=<td)(?:(?!<\/td>)[\s\S])*?hidden-lg[\s\S]*?<\/td>"

# 形式:class="hidden" —— hidden 就1个,所以加上前后引号,否则和 css1 无法区分
regex_css_rule4 = r"(?=<td)(?:(?!<\/td>)[\s\S])*?\"hidden\"[\s\S]*?<\/td>"

regex_css_invincible = regex_css_rule2 + '|' + regex_css_rule3 + '|' + regex_css_rule4
tbm_table, total_count = re.subn(regex_css_invincible, '', tbm_table)
print('{}_{}: {} hidden css been replaced.'.format(city, month, total_count))

其他诸如 th 替换成 td,span 替换的正常逻辑保留不变。

为了调试需要,我们还是保留了对 td 的匹配,但是现在只是为了统计处理前后的数量变化,不再参与替换。最终用于处理 table 的终极代码如下 —— 记忆力不好,写了很多注释,大家谅解。

start_time = time.time()
print("{}_{} html page source get successfully,process start".format(city, month))

# 为了便于对照,根据拿到 page_source 的时间,先保留1份原始文本,
response_time = time.strftime("%Y%m%d%H%M%S")
with open("{}_{}_{}_original.html".format(city, month, response_time), 'w', encoding='utf-8') as f:
	f.write(page_source)

# 可以先移除Script 中的eval,否则太长了,移除后 size 会减少很多
# 正则匹配也是运算,应该尽可能减小匹配范围
regex_script = r"eval\(.*"
page_source = re.sub(regex_script, '', page_source)

# 样式中没有设置 position 的Table,就是包含真实数据的,保留
# 另外2个 替换为空,使用一步到位的正则表达式
re_one_step = r"(?=<table)(?:(?!>)[\s\S])*?position[\s\S]*?(?<=<\/table>)"
page_source, count = re.subn(re_one_step, '', page_source)
print('{}_{}: {} fake tables been removed.'.format(city, month, count))

with open("{}_{}_{}_removefaketable.html".format(city, month, response_time), 'w', encoding='utf-8') as f:
	f.write(page_source)

# limit match scope, table to be matched
regex_tbm_table = r"<tbody>([\s\S]*?)<\/tbody>"
tbm_table = re.findall(regex_tbm_table, page_source)[0]

###
# 中间是对这个 “真table” 的进一步处理
###
# 表头也是包含了伪造数据的,也需要剔除 表头通过2种形式提供,
# 一种是放在 td 里的,可以和数据一起处理
# 另一种是放在 th 里,所以还需要增加 th 的匹配判断
# 保留下来的 Table 中, td 是一定有的,th 不一定有
# 最简单的方式就是把 th 替换为 td,这样就不用在 th 里依次判断4种隐藏样式
regex_th = r"<th(.*?)th>"

# 无论最终是否执行替换,正则匹配都会执行。所以可以直接执行 re.subn
# 正则匹配后得到match,如果其中有闭合的(),就会生成 group
# 正则将<thth>之间的内容匹配出来,得到分组,用\1表示
# 将匹配结果,保留分组内容,两端替换为td的开合标签
tbm_table, count = re.subn(regex_th, r"<td\1td>", tbm_table)
if count > 0:
	print(
		'{}_{}: field names been found in <th></th>, {} ths been replaced.'.format(city, month, count))

# 在保留的 table 中匹配出所有的 td
# 在经过终极优化的代码中,这部分已经没有必要了,这里保留为了调试观察
regex_td = r"<td.*?\/td>"
tds = re.findall(regex_td, tbm_table)
print('{}_{}: {} tds been found'.format(city, month, len(tds)))

# 处理 regex_td_css1
# css 引用样式在 <style></style> 中定义
regex_tbm_style = r"<style([\s\S]*?)<\/style>"
tbm_style = re.findall(regex_tbm_style, page_source)[0]

regex_css_rule1 = r"(.*?)\s*{\n\s*display: none;"
class_ndisplays = re.findall(regex_css_rule1, tbm_style)
# print('class_ndisplays', len(class_ndisplays))

# 将 table 内对这些样式的引用。替换为明文 display:none
total_count = 0
for class_ndisplay in class_ndisplays:
	class_ndisplay = class_ndisplay.replace('.', '').strip()
	tbm_table, count = re.subn(class_ndisplay, r'display:none', tbm_table)
	total_count = total_count + count
print('{}_{}: {} css1:quotation been replaced to css2.'.format(city, month, total_count))

# 形式:class="display:none"
regex_css_rule2 = r"(?=<td)(?:(?!<\/td>)[\s\S])*?display:none[\s\S]*?<\/td>"
# tbm_table, count_css2 = re.subn(regex_css_rule2, '', tbm_table)
# print('{}_{}: {} css2:display:none been replaced to empty. {} left.'.format(city, month, count_css2, len(tds)-count_css2))

# 形式:class="hidden-xs hidden-sm hidden-lg hidden-md" —— hidden-lg 是其中之一
regex_css_rule3 = r"(?=<td)(?:(?!<\/td>)[\s\S])*?hidden-lg[\s\S]*?<\/td>"
# tbm_table, count_css3 = re.subn(regex_css_rule3, '', tbm_table)
# print('{}_{}: {} css3:hidden-lg been replaced to empty. {} left.'.format(city, month, count_css3, len(tds)-count_css2-count_css3))

# 形式:class="hidden" —— hidden 就1个,所以加上前后引号,否则和 css1 无法区分
regex_css_rule4 = r"(?=<td)(?:(?!<\/td>)[\s\S])*?\"hidden\"[\s\S]*?<\/td>"
# tbm_table, count_css4 = re.subn(regex_css_rule4, '', tbm_table)
# print('{}_{}: {} css4:hidden been replaced to empty. {} left.'.format(city, month, count_css4, len(tds)-count_css2-count_css3-count_css4))

regex_css_invincible = regex_css_rule2 + '|' + regex_css_rule3 + '|' + regex_css_rule4
tbm_table, total_count = re.subn(regex_css_invincible, '', tbm_table)
print('{}_{}: {} hidden css been replaced.'.format(city, month, total_count))

# 质量等级 是在 td 内部又包了1层 span 标签,为了和其他数据统一逻辑,
# 我们把包含span 的 td 找出来,直接替换成 <td>质量等级</td>
# 正则会得到3个分组,质量等级文本是第3个分组
regex_span = r"(?=<td)((?!<\/td>)[\s\S])*(<span[\s\S]*?>(.*?)<\/span>)<\/td>"
tbm_table, count = re.subn(regex_span, r'<td>\3</td>', tbm_table)
print('{}_{}: {} spans been processed'.format(city, month, count))

# 可选,再次匹配得到的是包含真实数据的 td 数量
# 经过前面的处理,得到的有效td 数量应该固定为 (天数 + 表头)* 9列
# (31 + 1)*9 = 288 或者 (30+1)*9 = 279
regex_td = r"<td.*?\/td>"
tds_real = re.findall(regex_td, tbm_table)
print('{}_{}: {} tds which contain real data been left'.format(city, month, len(tds_real)))

# 将处理好的 table 再填充回 html 代码
page_source = re.sub(regex_tbm_table, tbm_table, page_source)

# 把 处理好后的 page_source 写入本地文件
with open("{}_{}_{}_purified.html".format(city, month, response_time), 'w', encoding='utf-8') as f:
	f.write(page_source)

end_time = time.time()
print("{}_{}: html page source get successfully,process finished within {}s".format(city, month, end_time-start_time))

终章

现在代码和逻辑是简洁了,但效率和性能呢?片面地追求简洁,其实是外强中干臭美花瓶。还记得优化前的效果吗?在 Python Selenium 实现 这篇文章的末尾,我放了1个视频 —— 对,是剪辑掉中间50s文本处理时间之后的视频。

现在呢?话不多说,让代码跑起来。

看到了吗?哈哈哈哈哈哈,让我仰天长啸五分钟。优化后的代码,几乎是立刻就拿到了数据,爬取效果足以和 js逆向 +  ajax请求 方案 媲美了。

最后的最后,还是再次感谢这个天气网站的程序员,硬生生把1个爬虫菜鸟逼成了爬虫票友。哈哈哈哈哈,我出门大笑去了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/162726.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何把拍摄视频中多余的人或物去除?

大家应该都有这样一个烦恼吧&#xff1f;就是拍摄的一段视频中有多余的人物出现&#xff0c;想要把里面的人物去除掉&#xff0c;或者是自己拍摄的一段视频&#xff0c;视频里出现了多余的人物&#xff0c;但是又不能重启拍摄的情况下&#xff0c;想要把视频中的人物去除掉应该…

Spring Security笔记

创建个项目 引入Spring Web和Spring Security 即可 写个Controller接收请求 转发重定向都可以 static下定义两个页面 login.html页面 用来登录 main.html如果可以跳到这里,说明登录成功 启动运行程序 我们访问登录接口 或者是访问静态资源都会重定向到这个页面 这个页面说…

并发编程(多线程)

一、进程与线程 多进程编程已经能够解决并发编程的问题了(已经可以利用cpu多核资源了).但是仍然存在这缺陷. 就是,进程太重了(消耗资源多,速度慢),线程应运而生被称为"轻量级编程",解决并发编程的各种问题的同时,让IO速度大大提升. 线程"轻"主要"轻…

SOFAEnclave:蚂蚁金服新一代可信编程环境,让机密计算为金融业务保驾护航102年

引言 互联网金融本质上是对大量敏感数据的处理以及由此沉淀的关键业务智能。近年来涌现出来的新业态更是将数据处理的范畴从单方数据扩展到了涉及合作方的多方数据。 另一方面&#xff0c;从 GDPR 到 HIPAA&#xff0c;数据隐私监管保护的范围愈加扩大&#xff0c;力度日益增…

app逆向 || x动

声明 本文仅供学习参考&#xff0c;如有侵权可私信本人删除&#xff0c;请勿用于其他途径&#xff0c;违者后果自负&#xff01; 如果觉得文章对你有所帮助&#xff0c;可以给博主点击关注和收藏哦&#xff01; 本文适用于对安卓开发和Java有了解的同学! 文中涉及的app均放在…

运行Dlinknet提取道路和水体(总结帖)——全流程步骤总结

之前写了很多制作样本然后跑代码的帖子 但由于我也是第一次跑 记录一下自己摸索的过程 因此导致 每一篇的内容很碎 每次我想自己去回顾一下的时候 都有太多摸索尝试的过程了 因此我在这里总结一下我摸索的整个过程的详细步骤 大家可以先看这篇再去我的对应博客里面看具体的细节…

【C++逆向】虚表(Virtual table)

什么是多态 定义一个虚基类ISpeaker class ISpeaker{ protected:size_t b; public:ISpeaker( size_t _v ): b(_v) {}virtual void speak() 0; };有两个子类&#xff0c;都实现了虚函数speak()&#xff1a; class Dog : public ISpeaker { public:Dog(): ISpeaker(0){}//vir…

Gin操作MySQLd的增加修改删除的Restful风格的API

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文 目录 一、gin是什么? 二、gin- mysql 1.gin-mysql 2.CRUD的gin的mysql 通过jsontool

Win10忘记开机密码无法进入桌面怎么办?

Win10忘记开机密码无法进入桌面怎么办&#xff1f;有用户设置了电脑的开机密码之后&#xff0c;因为一段时间没有去开机使用电脑了&#xff0c;导致将开机的密码忘记了。那么这个情况下我们怎么去进行电脑的开机呢&#xff1f;接下来我们来看看详细的解决方法分享吧。 解决方法…

SpringCore RCE 1day漏洞复现(NSSCTF Spring Core RCE)

漏洞描述&#xff1a; 作为目前全球最受欢迎的Java轻量级开源框架&#xff0c;Spring允许开发人员专注于业务逻辑&#xff0c;简化Java企业级应用的开发周期。 但在Spring框架的JDK9版本(及以上版本)中&#xff0c;远程攻击者可在满足特定条件的基础上&#xff0c;通过框架的…

【学习笔记】【Pytorch】一、卷积层

【学习笔记】【Pytorch】一、卷积层学习地址主要内容一、卷积操作示例二、Tensor&#xff08;张量&#xff09;是什么&#xff1f;三、functional.conv2d函数的使用1.使用说明2.代码实现四、torch.Tensor与torch.tensor区别五、nn.Conv2d类的使用1.使用说明2.代码实现六、卷积公…

基于servlet+mysql+jsp实现鞋子商城系统

基于servletmysqljsp实现鞋子商城系统一、系统介绍1、系统主要功能&#xff1a;2、环境配置二、功能展示1.主页(客户)2.用户登陆、个人中心&#xff08;客户&#xff09;3.商品分类&#xff08;客户&#xff09;3.我的购物车(客户)4.我的订单&#xff08;客户&#xff09;5.订单…

微信小程序页面导航、编程式导航、页面事件、生命周期和WXS脚本

文章目录页面导航1.导航到tarBar页面2.导航到非 tabBar 页面3.后退导航编程式导航1.导航到tabBar页面2.导航到非 tabBar 页面3.后退导航导航传参1. 声明式导航传参2. 编程式导航传参3. 在 onLoad 中接收导航参数页面事件下拉刷新上拉触底数据请求获取中添加loading效果,请求完毕…

一本修炼秘籍,带你打穿文件上传的21层妖塔(1)

目录 前言 引子 第一层&#xff1a;JS限制——你在玩一种很新的防御 第二层&#xff1a;Content-Type限制——我好像在哪见过你 第三层&#xff1a;黑名单绕过——让我康康&#xff01; 前言 &#x1f340;作者简介&#xff1a;被吉师散养、喜欢前端、学过后端、练过CTF、…

Jetpack Compose中的副作用Api

Compose的生命周期 每个Composable函数最终会对应LayoutNode节点树中的一个LayoutNode节点&#xff0c;可简单的为其定义生命周期&#xff1a; onActive: 进入重组作用域&#xff0c; Composable对应的LayoutNode节点被挂接到节点树上onUpdate&#xff1a;触发重组&#xff0c…

Dolphin scheduler在Windows环境下的部署与开发

这里写自定义目录标题环境介绍WSL2工程下载修改POM文件java版本mysql驱动修改mysql密码IDEA配置JDK8模块导出运行配置环境介绍 MySql&#xff1a;8.0.31 JDK&#xff1a;17 需要安装windows的wsl2 WSL2 首先安装好WSL2&#xff0c;并且通过 sudo apt-get install openjdk-17…

类模板与模板类

#include <stdio.h>#include <iostream>using namespace std;//注意必须将类的声明和定义写在同一个.h文件中 未来把它包含进来//写上关键字template 和模板参数列表template<typename T, int KSize, int KVal>class MyArray{public:MyArray();//当在类内定义…

正点原子STM32(基于HAL库)2

目录STM32 基础知识入门寄存器基础知识STM32F103 系统架构Cortex M3 内核& 芯片STM32 系统架构存储器映射寄存器映射新建寄存器版本MDK 工程STM32 基础知识入门 寄存器基础知识 寄存器&#xff08;Register&#xff09;是单片机内部一种特殊的内存&#xff0c;它可以实现…

【自学Docker】Docker HelloWorld

Docker HelloWorld Docker服务 查看Docker服务状态 使用 systemctl status docker 命令查看 Docker 服务的状态。 haicoder(www.haicoder.net)# systemctl status docker我们使用 systemctl status docker 命令查看 Docker 服务的状态&#xff0c;显示结果如下图所示&#…

HotPDF Delphi PDF编译器形成PDF文档

HotPDF Delphi PDF编译器形成PDF文档 HotPDF Delphi PDF编译器支持通过内部和外部链接完全形成PDF文档。计算机还完全支持Unicode。此外&#xff0c;在您的产品和软件中使用此计算机的最新功能&#xff0c;您可以指定加密、打印和编辑PDF文档的能力。当您加密PDF文档时&#xf…